Skip to content

Layer 5: 上下文管理 —— 长对话不爆窗口的工程

模型的上下文窗口是有限的(100K-1M tokens),但 Agent 的对话可以无限长。如何在有限的窗口中维持无限的对话?

这是 Agent 工程中被严重低估的问题。Claude Code 用了四层防御来解决它。


Layer D: Token Budget(预算控制)
→ 每轮对话的 token 预算,超过 90% 就停止
Layer C: Auto-Compact(自动压缩)
→ 接近窗口上限时自动触发对话摘要
Layer B: Tool Result Persistence(大结果外置)
→ 超过阈值的工具输出存磁盘,消息中只保留摘要
Layer A: Prompt Cache(提示词缓存)
→ 系统提示词跨轮次复用,避免重复消耗 token

每一层解决不同的问题,层层叠加形成完整的上下文管理策略。


2. Layer A: Prompt Cache(系统提示词缓存)

Section titled “2. Layer A: Prompt Cache(系统提示词缓存)”

系统提示词(包含工具描述、CLAUDE.md 内容、Git 状态等)通常有 10K-50K tokens。如果每轮对话都重新发送,成本和延迟都很高。

// query.ts:449-451
const fullSystemPrompt = asSystemPrompt(
appendSystemContext(systemPrompt, systemContext),
)

系统提示词在 query 循环中是不可变的——它在循环开始前构建一次,之后所有迭代都复用相同的对象。这保证了 Anthropic API 的 prompt cache 可以跨轮次命中。

Fork 子 Agent 的 Cache 共享

// forkSubagent.ts — 子 Agent 复用父上下文的系统提示词
// 生成字节级相同的前缀,最大化 cache 命中率
context.renderedSystemPrompt // 父的冻结快照传给子

★ 设计洞察renderedSystemPrompt 是在 turn 开始时冻结的快照。为什么不在 fork 时重新生成?因为 GrowthBook 的 feature flag 可能在运行期间从 cold→warm 发生变化,导致生成的系统提示词出现差异,从而打破 cache。冻结快照保证了缓存一致性。


3. Layer B: Tool Result Persistence(大结果外置)

Section titled “3. Layer B: Tool Result Persistence(大结果外置)”

一个 Bash("cat large_file.py") 可能返回 100K 字符的输出。如果全部留在消息历史中,几轮对话就会撑爆上下文窗口。

// toolResultStorage.ts — 两级控制
// Level 1: 每个工具的阈值
function getPersistenceThreshold(toolName, declaredMax): number {
// 工具自己声明的 maxResultSizeChars
// 与全局默认值 50K 取最小值
// GrowthBook 可以按工具名覆盖
return Math.min(declaredMaxResultSizeChars, DEFAULT_MAX_RESULT_SIZE_CHARS)
}
// Level 2: 每条消息的聚合预算
// 一条 user message 中所有 tool_result 的总大小预算
// 由 ContentReplacementState 跟踪

持久化流程

工具执行完毕,返回 result
├── 结果为空?→ 注入 "(toolName completed with no output)" 防止模型误判
├── 结果 ≤ 阈值?→ 正常返回
└── 结果 > 阈值?
├── 1. 写入磁盘:~/.claude/tool-results/{sessionId}/{toolUseId}.json
├── 2. 生成预览(前 N 字符)
└── 3. 消息中替换为:文件路径 + 预览 + 大小信息

ContentReplacementState 的精妙设计

// toolResultStorage.ts:390-412
type ContentReplacementState = {
decisions: Map<string, 'keep' | 'replace'> // toolUseId → 决策
totalKeptSize: number // 已保留的总大小
budget: number // 每条消息的预算
}

为什么需要 ContentReplacementState?因为 prompt cache。

如果轮次 A 中某个工具结果被保留(keep),但轮次 B 中同一个结果因为预算不足被替换(replace),那么轮次 B 的消息内容就与轮次 A 不同了——prompt cache 就失效了。所以替换决策必须是稳定的:一旦决定 keep 或 replace,后续轮次的决策不能变。

★ 设计洞察:这是一个非常容易被忽略的一致性问题。大多数 Agent 框架不会注意到 tool result truncation 会破坏 prompt cache。Claude Code 通过持久化 ContentReplacementState 并在轮次间传递来保证一致性。


4. Layer C: Auto-Compact(自动对话压缩)

Section titled “4. Layer C: Auto-Compact(自动对话压缩)”
// autoCompact.ts:225-238
const tokenCount = tokenCountWithEstimation(messages) - snipTokensFreed
const threshold = getAutoCompactThreshold(model)
// threshold = effectiveContextWindow - 13K (AUTOCOMPACT_BUFFER_TOKENS)
return tokenCount >= threshold // tokens 接近窗口上限

即:当消息 token 数 ≥ (上下文窗口 - 13K 缓冲区) 时触发。

1. 触发条件满足
├── 2. PreCompact Hook(可阻止压缩)
│ └── exit 2 → 跳过本次压缩
├── 3. 剥离图片和可重注入的附件
│ └── 避免压缩 API 自身 prompt-too-long
├── 4. 调用 Claude 生成对话摘要
│ ├── 成功 → 摘要替换旧消息
│ └── prompt-too-long?→ 渐进式丢弃最旧的消息轮次,重试(最多 3 次)
├── 5. 后处理
│ ├── 重注入最近编辑的文件(最多 5 个,50K token 预算)
│ ├── 重注入活跃的 Skill
│ └── 重注入当前 Plan
├── 6. SessionStart Hook(触发 source='compact')
│ └── 让 Hook 知道这是压缩后的「新会话」
└── 7. PostCompact Hook
└── 可选的后续处理

PTL 重试循环

压缩请求本身超出上下文 → 丢弃最旧的消息轮次 → 重试
最多重试 3 次(MAX_PTL_RETRIES)
如果还是超出 → 报错给用户

记忆提取

sessionMemoryCompact.ts
// 压缩时自动提取关键信息到持久记忆
// 避免压缩后丢失重要的上下文

熔断器

// autoCompact.ts:257-265
const MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3
// 连续 3 次压缩失败 → 停止尝试
// 避免在不可恢复的场景下反复浪费 API 调用

★ 设计洞察:压缩后「重注入」最近编辑的文件是一个关键细节。压缩摘要只能记住「做了什么」,但模型经常需要「看到」正在编辑的文件的当前内容。重注入机制解决了这个「压缩后失忆」问题。


5. Layer D: Token Budget(预算控制)

Section titled “5. Layer D: Token Budget(预算控制)”
// tokenBudget.ts:45-93
function checkTokenBudget(tracker, agentId, budget, globalTurnTokens): Decision {
// 子 Agent 不参与预算控制(由父控制)
if (agentId || budget === null) return { action: 'stop' }
const pct = Math.round((turnTokens / budget) * 100)
const delta = globalTurnTokens - tracker.lastGlobalTurnTokens
// 「边际递减」检测:连续 3+ 次续写,每次 < 500 tokens
const isDiminishing =
tracker.continuationCount >= 3 &&
delta < 500 && tracker.lastDelta < 500
// 未达 90% 且有效产出 → 继续
if (!isDiminishing && turnTokens < budget * 0.9) {
return { action: 'continue', nudgeMessage: "..." }
}
// 达到 90% 或边际递减 → 停止
return { action: 'stop' }
}
  1. 硬上限:token 使用量达到预算的 90%
  2. 边际递减:连续 3 次续写每次都不到 500 tokens(说明模型没什么实质内容产出了)

★ 设计洞察:「边际递减」检测是一个巧妙的启发式。模型有时会进入一种「空转」状态——每次续写都只产出少量无意义的 token。这个检测可以尽早终止这种空转,节省 token。


当 API 返回 413(prompt-too-long)时,query 循环有一套多层恢复策略:

API 返回 413
├── Step 1: Collapse Drain(上下文折叠排水)
│ └── 如果启用了上下文折叠,先尝试释放折叠区域
├── Step 2: Reactive Compact(响应式压缩)
│ └── 立即触发一次完整的对话压缩
└── Step 3: Surface Error(暴露错误)
└── 前两步都失败 → 告诉用户和模型
// query.ts:1062-1183 — 错误恢复逻辑
if (withheldError && is413Error(withheldError)) {
// Step 1: Collapse drain
const drained = await collapseDrain(messages)
if (drained) {
state = { ..., transition: { reason: 'collapse_drain_retry' } }
continue // 重试
}
// Step 2: Reactive compact
if (!hasAttemptedReactiveCompact) {
await compactConversation(messages)
state = { ..., transition: { reason: 'reactive_compact_retry' } }
continue // 重试
}
// Step 3: Surface error
yield errorMessage
}

当模型因为 max_output_tokens 限制被截断时:

模型被截断(stop_reason: 'max_tokens')
├── 第 1 次:尝试升级 max_output_tokens(8K → 64K)
│ └── 用更大的限制重试
└── 第 2+ 次:多轮恢复
└── 注入 recovery message:"你的回复被截断了,请从断点继续"
→ 模型接着上次的输出继续
// query.ts:1185-1256
if (stopReason === 'max_tokens') {
if (maxOutputTokensRecoveryCount < 1) {
// 升级 max_output_tokens
state = { ..., maxOutputTokensOverride: 64000,
transition: { reason: 'max_output_tokens_escalate' } }
continue
}
// 多轮恢复
const recoveryMessage = createRecoveryMessage("Continue from where you left off...")
state = { ..., transition: { reason: 'max_output_tokens_recovery' } }
continue
}

会话存储位置:~/.claude/sessions/{sessionId}/
├── transcript # 完整消息历史(JSON)
├── agents/
│ └── {agentId}/
│ └── transcript # 子 Agent 的消息历史
└── metadata # 会话元数据(标题、成本、指标)
工具结果存储:~/.claude/tool-results/{sessionId}/
└── {toolUseId}.json # 大型工具结果
会话恢复:
claude -r # 恢复最近的会话
claude --resume {id} # 恢复指定会话
/resume [search] # 搜索并恢复

恢复流程deserializeMessages() 重建完整的消息历史,包括从磁盘加载大型工具结果。


单一策略不够。需要:

  • 预防(大结果外置、prompt cache)
  • 检测(token 计数、阈值监控)
  • 恢复(自动压缩、413 恢复)
  • 终止(token budget、边际递减检测)

任何可能改变消息内容的操作(工具结果截断、压缩、附件注入)都可能破坏 prompt cache。需要通过持久化决策状态(ContentReplacementState)来保证跨轮次一致性。

好的压缩需要:

  1. 生成摘要(记住做了什么)
  2. 重注入关键文件(看到当前状态)
  3. 提取记忆(持久化重要信息)
  4. 触发 Hook(让外部系统知道)

任何自动触发的恢复操作(压缩、重试)都需要熔断器。连续 3 次压缩失败就停止尝试,避免浪费 API 调用。


  • → Tool 系统(L2):工具通过 maxResultSizeChars 声明自己的结果大小,超限结果被持久化
  • → Hook 系统(L4):PreCompact/PostCompact Hook 控制压缩行为,SessionStart(compact) 通知压缩发生
  • → Agent Loop(L1):413 恢复和 token budget 检查是 query 循环的一部分
  • → 权限系统(L3):压缩不影响权限状态,ToolPermissionContext 是不可变的