Layer 5: 上下文窗口管理 —— 有限空间里的信息保鲜
上下文窗口有限(200K / 1M tokens),一个长对话可能产生数百轮工具调用。如何在不丢失关键信息的前提下,把对话控制在窗口内?
这是 Agent 系统从「demo」到「生产」的分水岭。简单的 Agent 可以忽略上下文管理(对话不长),但生产级 Agent 必须解决这个问题。
1. 多级压缩策略
Section titled “1. 多级压缩策略”Claude Code 不是「一刀切」的压缩,而是设计了四级渐进式压缩策略,从最轻量到最重量:
开销由低到高:┌──────────────────────────────────────────────────┐│ Level 1: Snip ││ 最早的历史块直接切除(最快,信息丢失最多) │├──────────────────────────────────────────────────┤│ Level 2: Microcompact ││ 在 prompt cache 内编辑,移除旧 tool_use blocks ││ (快速,保持 cache 有效,信息损失可控) │├──────────────────────────────────────────────────┤│ Level 3: Context Collapse ││ 语义区间折叠(将一段对话折叠为摘要) ││ (中等开销,需要 API 调用生成摘要) │├──────────────────────────────────────────────────┤│ Level 4: Autocompact ││ 全量对话摘要(用 Claude 总结全部历史) ││ (开销最大,但压缩比最高) │└──────────────────────────────────────────────────┘1.1 在 Agent Loop 中的位置
Section titled “1.1 在 Agent Loop 中的位置”queryLoop 每次迭代的 Phase 1: 1. Tool result 预算检查 → 限制单轮聚合大小 2. Snip 压缩 → 切除最早的块 3. Microcompact → cache 内编辑 4. Context Collapse → 语义折叠 5. Autocompact → 达阈值时全量压缩 6. Blocking limit → 硬性上限检查每一级只在必要时触发——如果 Level 1 就够了,不会进入 Level 4。
2. Autocompact:全量对话压缩
Section titled “2. Autocompact:全量对话压缩”2.1 触发条件
Section titled “2.1 触发条件”估算 token 数 > autoCompactThreshold(约上下文窗口的 80-90%) ↓autoCompactIfNeeded() 触发2.2 完整五阶段流程
Section titled “2.2 完整五阶段流程”┌──────────────────────────────────────────────┐│ Autocompact 流程 ││ ││ Phase 1: Pre-Compact Hooks ││ ├─ 执行 executePreCompactHooks() ││ ├─ Hook 可以修改 custom instructions ││ └─ Hook 可以添加 user-facing message ││ ││ Phase 2: 消息准备 ││ ├─ 剥离图片/文档块(替换为 [image] 标记) ││ ├─ 剥离可重注入的 attachments ││ └─ Prompt-too-long 重试循环(最多 3 次) ││ ││ Phase 3: 摘要生成 ││ ├─ 构建 compact prompt ││ ├─ 调用 Claude API 生成摘要 ││ └─ 流式获取摘要结果 ││ ││ Phase 4: 缓存清理 & 上下文恢复 ││ ├─ 清除 readFileState 缓存 ││ ├─ 恢复最近读取的文件(最多 5 个,50K tokens) ││ └─ 恢复异步 Agent 附件(如果有) ││ ││ Phase 5: Post-Compact Hooks & 重注入 ││ ├─ 执行 processSessionStartHooks('compact') ││ ├─ 重注入 Tool delta(完整工具列表) ││ ├─ 重注入 Agent listing ││ ├─ 重注入 MCP instructions ││ ├─ 重注入 Plan attachment(如果在计划模式) ││ └─ 重注入 Skill attachment(如果使用了技能) │└──────────────────────────────────────────────┘2.3 Prompt-too-long 重试循环
Section titled “2.3 Prompt-too-long 重试循环”问题:压缩请求本身可能触发 prompt-too-long(消息太多连压缩都放不下)
解决方案:for attempt = 1 to 3: 1. 发送压缩请求给 Claude 2. 如果返回 PROMPT_TOO_LONG: a. 解析 tokenGap(需要删除多少 token) b. 从最早的 API-round groups 开始删除 c. 如果无法删除更多 → 回退到删除 20% d. 用截断后的消息重试 3. 否则 → 成功,跳出循环
如果 3 次都失败 → 抛出错误2.4 文件恢复预算
Section titled “2.4 文件恢复预算”POST_COMPACT_MAX_FILES_TO_RESTORE = 5 // 最多恢复 5 个文件POST_COMPACT_TOKEN_BUDGET = 50,000 // 总预算 50K tokensPOST_COMPACT_MAX_TOKENS_PER_FILE = 5,000 // 单文件最多 5K tokensPOST_COMPACT_MAX_TOKENS_PER_SKILL = 5,000 // 单技能最多 5KPOST_COMPACT_SKILLS_TOKEN_BUDGET = 25,000 // 技能总预算 25K★ 设计洞察:压缩后自动恢复最近读取的文件内容是一个关键细节。如果用户正在编辑 3 个文件,压缩后这些文件的内容会从上下文中消失。通过恢复最近 5 个文件,模型可以继续工作而不需要重新读取。
3. 大结果外置
Section titled “3. 大结果外置”3.1 问题
Section titled “3.1 问题”一次 Grep 搜索可能返回 100KB+ 的结果一次文件读取可能返回 50KB+ 的内容这些结果如果全部保存在消息历史中 → 上下文迅速膨胀3.2 解决方案
Section titled “3.2 解决方案”持久化决策: result.length > getPersistenceThreshold(toolName) ? → 持久化到磁盘 + 消息中只保留预览 : → 正常保存在消息中
阈值计算: threshold = min(tool.maxResultSizeChars, 50_000) // 默认上限 50K 字符 // GrowthBook 可覆盖特定工具的阈值3.3 持久化流程
Section titled “3.3 持久化流程”工具返回 result (> threshold) ↓检查是否有图片块 → 有 → 跳过持久化(图片无法预览) ↓写入文件: projectDir/sessionId/tool-results/{toolUseId}.{json|txt} → 使用 flag: 'wx'(独占创建,避免覆盖) ↓生成 2KB 预览: → 截断到 2048 字符 → 尽量在换行符处截断(保持可读性) ↓替换消息内容: 原始: { type: 'text', text: '...100KB...' } 替换: { type: 'text', text: '...2KB预览...\n\n[Full output: /path/to/file]' } ↓记录到 ContentReplacementState: seenIds.add(toolUseId) replacements.set(toolUseId, previewText)3.4 幂等性保证
Section titled “3.4 幂等性保证”// 为什么用 flag: 'wx'?// wx = 独占创建,如果文件已存在则失败(EEXIST)
场景:对话恢复(/resume)时重放消息 第 1 次: 写入成功 第 2 次: EEXIST → 跳过写入 → 使用已有文件
→ 保证同一个 toolUseId 的结果只写入一次→ 保证预览内容始终一致(prompt cache 稳定)★ 设计洞察:
flag: 'wx'是一个精妙的细节。在分布式系统中常用 CAS(Compare-And-Swap)保证幂等性,这里用文件系统的O_EXCL标志实现了同样的效果——如果文件已存在,操作是空操作。
4. Token 预算系统
Section titled “4. Token 预算系统”4.1 预算检查逻辑
Section titled “4.1 预算检查逻辑”function checkTokenBudget( tracker: BudgetTracker, agentId: string | undefined, budget: number | null, globalTurnTokens: number,): TokenBudgetDecision
决策流程: 1. 子 Agent 或无预算 → 自动停止 2. turnTokens < budget * 90% (COMPLETION_THRESHOLD) → 继续(注入 nudge 消息提醒模型注意预算) 3. 检测递减收益: continuationCount >= 3 AND deltaSinceLastCheck < 500 tokens AND lastDeltaTokens < 500 tokens → 判定为递减,停止 4. 否则 → 停止4.2 递减收益检测
Section titled “4.2 递减收益检测”轮次 1: 输出 2000 tokens → 正常轮次 2: 输出 1500 tokens → 正常轮次 3: 输出 300 tokens → 连续 1 次低产出轮次 4: 输出 200 tokens → 连续 2 次低产出轮次 5: 输出 100 tokens → 连续 3 次低产出 → 停止!
判定逻辑: 如果连续 3 次每轮输出 < 500 tokens,说明模型已经没有新的内容要生产,继续循环是浪费。5. Reactive Compact:错误驱动的压缩
Section titled “5. Reactive Compact:错误驱动的压缩”5.1 触发场景
Section titled “5.1 触发场景”正常对话中 → API 返回 prompt_too_long 错误 ↓Phase 4 (错误恢复): 1. 尝试 Context Collapse Drain → 快速但有限 2. 尝试 Reactive Compact → 完整压缩5.2 与 Autocompact 的区别
Section titled “5.2 与 Autocompact 的区别”Autocompact (预防性): - 在 API 调用前触发(Phase 1) - 基于 token 估算 - 不紧急,可以从容处理
Reactive Compact (反应性): - 在 API 返回错误后触发(Phase 4) - 基于实际错误 - 紧急,必须立即处理 - 利用 withholding 机制对用户透明5.3 与 Withholding 的协作
Section titled “5.3 与 Withholding 的协作”正常流程: Withholding 流程:API → error → 用户看到错误 API → error → 扣住 ↓ 尝试 reactive compact ↓ 成功 → 用户无感知,重试 失败 → 释放错误,用户看到6. Session Memory Compact:压缩时的记忆保留
Section titled “6. Session Memory Compact:压缩时的记忆保留”6.1 保留策略
Section titled “6.1 保留策略”type SessionMemoryCompactConfig = { minTokens: 10_000 // 至少保留 10K tokens minTextBlockMessages: 5 // 至少保留 5 条有文本的消息 maxTokens: 40_000 // 最多保留 40K tokens}6.2 保留规则
Section titled “6.2 保留规则”保留: ✓ 有文本块的 Assistant 消息(对话历史核心) ✓ 有内容的 User 消息(用户输入) ✓ tool_use blocks(行动历史) ✓ 与保留 tool_use 配对的 tool_result
丢弃: ✗ 孤立的 tool_result(对应的 tool_use 已删除) ✗ 图片/文档块 ✗ 系统消息(除了 compact boundary)6.3 配对保留逻辑
Section titled “6.3 配对保留逻辑”如果保留 tool_result → 必须保留对应的 tool_use如果保留 assistant 消息中的 thinking → 保留该消息的所有块不在同一个 message.id 内拆分7. Prompt Cache 在压缩中的作用
Section titled “7. Prompt Cache 在压缩中的作用”7.1 压缩时的 Cache 共享
Section titled “7.1 压缩时的 Cache 共享”普通压缩: 每次压缩请求都是全新的系统提示词 → cache 未命中优化后: Fork 子 Agent 的压缩复用主 Agent 的 prompt cache prefix
效果: cache 命中率: ~98% vs ~2%(无优化) 日均节省: ~380 亿 tokens(全平台统计)7.2 Microcompact 的 Cache 保护
Section titled “7.2 Microcompact 的 Cache 保护”Microcompact 的设计目标: 在 prompt cache 的有效范围内做编辑 → 移除旧的 tool_use blocks(减少 token) → 但不改变消息结构(cache 仍然有效)
效果: 减少 token 数 + 保持 cache 命中 = 最优的成本控制★ 设计洞察:Prompt Cache 是 Claude Code 成本控制的核心武器。压缩策略不仅要考虑「删什么」,还要考虑「怎么删才不会破坏 cache」。Microcompact 就是在这两个约束之间找到的平衡点。
8. 会话持久化与恢复
Section titled “8. 会话持久化与恢复”8.1 存储结构
Section titled “8.1 存储结构”~/.claude/sessions/{sessionId}/├── transcript ← 完整对话记录(JSON)├── agents/│ └── {agentId}/│ └── transcript ← 子 Agent 对话记录└── tool-results/ └── {toolUseId}.json ← 大型工具结果8.2 恢复流程
Section titled “8.2 恢复流程”/resume 或 claude --resume {sessionId} ↓loadConversationForResume(sessionId) ↓deserializeMessages(transcript) ↓恢复状态: messages, usage, metadata ↓继续对话(从最后一条消息继续)9. 设计洞察总结
Section titled “9. 设计洞察总结”四级压缩的渐进式策略
Section titled “四级压缩的渐进式策略”为什么不直接 Autocompact? - Autocompact 需要 API 调用(慢 + 费钱) - 很多情况下 Snip 或 Microcompact 就够了
为什么不只用 Snip? - Snip 丢弃信息最多,早期对话可能包含关键上下文 - Autocompact 通过摘要保留关键信息
渐进式策略的优势: 90% 的情况 → Level 1-2 处理(快速、低成本) 9% 的情况 → Level 3 处理(中等开销) 1% 的情况 → Level 4 处理(高开销但必要)Withholding + Reactive Compact 的用户体验
Section titled “Withholding + Reactive Compact 的用户体验”用户视角: "我一直在对话,偶尔会停顿一下(压缩中),但从没看到过错误"
实际发生的事: 1. API 返回 prompt-too-long 2. 错误被扣住 3. 自动压缩 4. 重试成功 5. 用户继续对话
→ 透明的错误恢复是生产级 Agent 的关键特征大结果外置的经济学
Section titled “大结果外置的经济学”没有外置: 50 次 Grep 搜索 × 50KB/次 = 2.5MB 在上下文中 → 快速达到上下文上限 → 频繁压缩 → 高成本
有外置: 50 次 Grep 搜索 × 2KB 预览/次 = 100KB 在上下文中 + 2.5MB 在磁盘上(零 token 成本) → 上下文增长慢 → 压缩频率低 → 成本可控与其他层的交互
Section titled “与其他层的交互”| 交互方向 | 说明 |
|---|---|
| ← L1 (Agent Loop) | Phase 1 触发多级压缩,Phase 4 触发 Reactive Compact |
| ← L2 (Tool 系统) | maxResultSizeChars 决定大结果是否外置 |
| ← L6 (子 Agent) | Fork 子 Agent 的压缩利用 Prompt Cache 共享 |
| → API 层 | Prompt Cache scope 控制、压缩请求本身也是 API 调用 |
| → 磁盘 | Tool results、session transcripts 的持久化 |
| ← Hook 系统 | PreCompact / PostCompact hooks 允许用户自定义压缩行为 |