第五章:上下文管理
学习时间: 3 小时 | 难度: ⭐⭐⭐ | 前置: 第二章
学习目标
完成本章后,学员将能够:
- 解释 System Prompt 的 Static/Dynamic Boundary 设计
- 描述 CLAUDE.md 4 层加载机制
- 理解上下文压缩的触发条件和执行流程
- 应用 Memoized Singleton 和 Checkpoint/Restore 模式
1. System Prompt 构建
prompts.ts (~900行) 负责构建发送给 Claude API 的 System Prompt。这是整个系统中最关键的上下文组件。
1.1 Static/Dynamic Boundary 设计
System Prompt 被分为两个区域,用特殊标记分隔:
┌─────────────────────────────────────────────┐
│ [静态内容 - cacheScope:'global'] │
│ │
│ • 角色定义(你是一个 AI 编程助手...) │
│ • 安全指令(不要执行危险操作...) │
│ • 编码风格(使用 TypeScript...) │
│ • 工具指引(如何使用各个工具...) │
│ • 错误处理策略 │
│ • 输出格式要求 │
│ • 通用约束 │
│ │
│ __SYSTEM_PROMPT_DYNAMIC_BOUNDARY__ │
│ │
│ [动态内容 - 注册表模式,按需计算] │
│ │
│ • 当前工作目录信息 │
│ • Git 状态 │
│ • CLAUDE.md 内容 │
│ • 可用工具列表 │
│ • 当前日期时间 │
│ • 用户偏好设置 │
└─────────────────────────────────────────────┘
1.2 为什么这样设计
核心目标: 最大化 prompt cache 命中率。
Claude API 的 prompt cache 基于前缀匹配。如果 System Prompt 的前缀保持稳定,后续请求可以复用缓存,显著降低 API 成本和延迟。
| 区域 | 变化频率 | Cache 策略 |
|---|---|---|
| 静态区 | 几乎不变 | cacheScope:'global',跨组织缓存 |
| 动态区 | 每会话/每轮 | 不缓存,每次都重新生成 |
1.3 注册表模式
动态内容使用注册表模式,每个 section 独立注册:
// 伪代码:注册表模式
const promptSections = new Map<string, () => string>()
// 注册 section
promptSections.set('git-status', () => getGitStatus())
promptSections.set('cwd-info', () => getCwdInfo())
promptSections.set('claude-md', () => getClaudeMdContent())
promptSections.set('tools', () => getToolDescriptions())
// 构建时按需计算
function buildDynamicPrompt(): string {
return Array.from(promptSections.entries())
.map(([name, generator]) => generator())
.join('\n')
}
2. CLAUDE.md 4 层加载
claudemd.ts 实现了 Claude Code 的配置文件加载机制。CLAUDE.md 是项目级的配置文件,类似于 .editorconfig 或 .eslintrc。
2.1 4 层优先级
从低到高,后加载的覆盖先加载的:
Layer 1 (最低): /etc/claude-code/CLAUDE.md
→ 企业策略,由 IT 管理员维护
→ 所有用户共享
Layer 2: ~/.claude/CLAUDE.md
→ 用户全局配置
→ 该用户的所有项目共享
Layer 3: 项目层级 CLAUDE.md
→ 从 CWD 向上遍历目录树
→ 支持 monorepo 的多级配置
→ 例如: /project/CLAUDE.md, /project/packages/CLAUDE.md
Layer 4 (最高): CLAUDE.local.md
→ 本地私有配置
→ 通常在 .gitignore 中
→ 个人偏好,不提交到版本控制
2.2 加载算法
// claudemd.ts 加载流程(概念还原)
async function loadClaudeMd(cwd: string): Promise<string> {
const layers = []
// Layer 1: 企业策略
layers.push(await readFile('/etc/claude-code/CLAUDE.md'))
// Layer 2: 用户全局
layers.push(await readFile('~/.claude/CLAUDE.md'))
// Layer 3: 项目层级(从 CWD 向上遍历)
let dir = cwd
while (dir !== '/') {
const path = join(dir, 'CLAUDE.md')
if (await exists(path)) {
layers.push(await readFile(path))
}
dir = dirname(dir)
}
// Layer 4: 本地私有
layers.push(await readFile(join(cwd, 'CLAUDE.local.md')))
// 合并(后面的覆盖前面的)
return mergeLayers(layers)
}
2.3 高级特性
| 特性 | 说明 |
|---|---|
@include 指令 | 引用其他文件的内容 |
| frontmatter 条件匹配 | 根据条件激活/禁用配置段 |
| 循环引用检测 | 防止 A include B、B include A 的死循环 |
| 变量替换 | 支持 ${workspaceFolder} 等变量 |
# CLAUDE.md 示例
---
when: "file:*.py"
---
Python 项目使用 type hints,遵循 PEP 8。
---
when: "file:*.ts"
---
TypeScript 项目使用 strict mode,避免 any。
@include shared-rules.md
3. 上下文压缩机制
compact.ts (~1700行) 实现了当上下文接近 token 限制时的自动压缩机制。
3.1 触发条件
token 计数接近限制
↓
QueryEngine 检测到阈值
↓
触发自动压缩
3.2 压缩流程
步骤 1: 执行 PreCompact hooks
→ 通知外部系统即将压缩
→ 允许外部系统保存状态
步骤 2: fork 子 agent 生成摘要
→ 创建一个子 Agent
→ 将需要压缩的消息发送给子 Agent
→ 子 Agent 生成结构化摘要
→ 复用 prompt cache(Fork 模式)
步骤 3: PTL 重试(Prompt Too Long)
→ 如果摘要生成也触发 prompt_too_long
→ 截断最旧的消息重试
→ 最多尝试 3 次
步骤 4: 清空 readFileState 缓存
→ 压缩后文件内容可能已变化
→ 清空缓存避免使用过期数据
步骤 5: 生成 post-compact 附件
→ 文件快照(当前打开的文件状态)
→ plan(当前任务计划)
→ skill(学到的模式)
3.3 压缩策略
| 策略 | 优先级 | 效果 |
|---|---|---|
| 截断最旧消息 | 最高 | 快速释放空间 |
| LLM 摘要压缩 | 中 | 保留关键信息 |
| 升级 max_tokens | 低 | 临时方案 |
3.4 Checkpoint/Restore 模式
压缩前后的状态管理遵循 Checkpoint/Restore 模式:
压缩前:
checkpoint = {
messages: [...currentMessages],
readFileState: {...currentFileState},
plan: currentPlan,
skill: currentSkill
}
压缩后:
restore(checkpoint)
// 但消息列表已替换为摘要版本
// readFileState 已清空
// plan 和 skill 作为附件注入
4. 上下文收集
context.ts 负责收集所有需要注入到上下文中的信息。它使用 Memoized Singleton 模式,确保每个信息只收集一次。
4.1 收集的信息
| 信息 | 来源 | 变化频率 |
|---|---|---|
| 当前工作目录 | process.cwd() | 会话级 |
| Git 状态 | git status | 每轮 |
| 日期时间 | new Date() | 每轮 |
| CLAUDE.md 内容 | 文件系统 | 会话级 |
| 可用工具列表 | tools.ts | 会话级 |
| 用户偏好 | 配置文件 | 会话级 |
4.2 Memoized Singleton 模式
// 伪代码:Memoized Singleton
const memoizedGitStatus = memoize(async () => {
return await exec('git status --porcelain')
}, { key: 'git-status', ttl: 'turn' }) // 每轮刷新
const memoizedClaudeMd = memoize(async () => {
return await loadClaudeMd(process.cwd())
}, { key: 'claude-md', ttl: 'session' }) // 会话级缓存
设计意图: I/O 操作(特别是文件系统和 git 命令)是昂贵的。通过 memoize 缓存,避免在同一会话中重复执行相同的 I/O 操作。
5. 上下文管理架构图
┌─────────────────────────────────────────────────────────────┐
│ 上下文管理系统 │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ System Prompt │ │ CLAUDE.md │ │ context.ts │ │
│ │ 构建器 │ │ 加载器 │ │ 收集器 │ │
│ │ │ │ │ │ │ │
│ │ 静态区(缓存) │ │ 4层优先级 │ │ Memoized │ │
│ │ 动态区(实时) │ │ @include │ │ Singleton │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ └─────────────────┼─────────────────┘ │
│ │ │
│ ┌──────▼───────┐ │
│ │ compact.ts │ │
│ │ 压缩引擎 │ │
│ │ │ │
│ │ 触发检测 │ │
│ │ 子Agent摘要 │ │
│ │ PTL重试 │ │
│ │ 状态恢复 │ │
│ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
设计模式
本章涉及的模式:
| 模式 | 定义 | 应用位置 |
|---|---|---|
| Static/Dynamic Boundary | 分离可缓存和动态内容 | System Prompt 构建 |
| Memoized Singleton | 函数级缓存,首次调用执行 I/O | context.ts 信息收集 |
| Layered Configuration with Merge | 多层配置叠加,后加载覆盖先加载 | CLAUDE.md 4层加载 |
| Checkpoint/Restore | 压缩前保存状态,压缩后恢复 | compact.ts 上下文压缩 |
| Registry Pattern | 动态注册和按需计算 | Prompt section 注册 |
| Retry with Truncation | 逐步截断旧消息重试 | PTL 重试策略 |
源码验证
- ✅ System Prompt ~900行:prompts.ts 行数确认
- ✅ Static/Dynamic Boundary:
__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__标记确认 - ✅ CLAUDE.md 4层加载:claudemd.ts 加载逻辑确认
- ✅ 上下文压缩 ~1700行:compact.ts 行数确认
- ✅ Memoized Singleton:context.ts 缓存模式确认
思考题
-
Static/Dynamic Boundary 的前提是「静态内容不变」。如果需要根据用户角色(如管理员/普通用户)动态调整静态区,该如何设计?
-
CLAUDE.md 的 4 层加载中,企业策略(Layer 1)优先级最低,这意味着用户可以通过 Layer 2-4 覆盖企业策略。这在安全敏感的环境中是否合理?
-
上下文压缩使用子 Agent 生成摘要,这本身也消耗 token。如果压缩过程本身也触发 prompt_too_long,PTL 重试最多 3 次够吗?
-
Memoized Singleton 的 TTL 设置为「会话级」或「轮级」,如果用户在一个长会话中修改了 CLAUDE.md,缓存不会更新。这是否是一个 bug?