AgentHarness 课程

第五章:上下文管理

8.1K字·21分钟·
System Prompt构建、CLAUDE.md 4层加载、上下文压缩

学习时间: 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/Ocontext.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 缓存模式确认

思考题

  1. Static/Dynamic Boundary 的前提是「静态内容不变」。如果需要根据用户角色(如管理员/普通用户)动态调整静态区,该如何设计?

  2. CLAUDE.md 的 4 层加载中,企业策略(Layer 1)优先级最低,这意味着用户可以通过 Layer 2-4 覆盖企业策略。这在安全敏感的环境中是否合理?

  3. 上下文压缩使用子 Agent 生成摘要,这本身也消耗 token。如果压缩过程本身也触发 prompt_too_long,PTL 重试最多 3 次够吗?

  4. Memoized Singleton 的 TTL 设置为「会话级」或「轮级」,如果用户在一个长会话中修改了 CLAUDE.md,缓存不会更新。这是否是一个 bug?


04-权限系统 | 06-MCP与扩展