Skip to content

Layer 5: 上下文窗口管理 —— 有限空间里的信息保鲜

上下文窗口有限(200K / 1M tokens),一个长对话可能产生数百轮工具调用。如何在不丢失关键信息的前提下,把对话控制在窗口内?

这是 Agent 系统从「demo」到「生产」的分水岭。简单的 Agent 可以忽略上下文管理(对话不长),但生产级 Agent 必须解决这个问题。


Claude Code 不是「一刀切」的压缩,而是设计了四级渐进式压缩策略,从最轻量到最重量:

开销由低到高:
┌──────────────────────────────────────────────────┐
│ Level 1: Snip │
│ 最早的历史块直接切除(最快,信息丢失最多) │
├──────────────────────────────────────────────────┤
│ Level 2: Microcompact │
│ 在 prompt cache 内编辑,移除旧 tool_use blocks │
│ (快速,保持 cache 有效,信息损失可控) │
├──────────────────────────────────────────────────┤
│ Level 3: Context Collapse │
│ 语义区间折叠(将一段对话折叠为摘要) │
│ (中等开销,需要 API 调用生成摘要) │
├──────────────────────────────────────────────────┤
│ Level 4: Autocompact │
│ 全量对话摘要(用 Claude 总结全部历史) │
│ (开销最大,但压缩比最高) │
└──────────────────────────────────────────────────┘
queryLoop 每次迭代的 Phase 1:
1. Tool result 预算检查 → 限制单轮聚合大小
2. Snip 压缩 → 切除最早的块
3. Microcompact → cache 内编辑
4. Context Collapse → 语义折叠
5. Autocompact → 达阈值时全量压缩
6. Blocking limit → 硬性上限检查

每一级只在必要时触发——如果 Level 1 就够了,不会进入 Level 4。


估算 token 数 > autoCompactThreshold(约上下文窗口的 80-90%)
autoCompactIfNeeded() 触发
┌──────────────────────────────────────────────┐
│ 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(如果使用了技能) │
└──────────────────────────────────────────────┘
问题:压缩请求本身可能触发 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 次都失败 → 抛出错误
POST_COMPACT_MAX_FILES_TO_RESTORE = 5 // 最多恢复 5 个文件
POST_COMPACT_TOKEN_BUDGET = 50,000 // 总预算 50K tokens
POST_COMPACT_MAX_TOKENS_PER_FILE = 5,000 // 单文件最多 5K tokens
POST_COMPACT_MAX_TOKENS_PER_SKILL = 5,000 // 单技能最多 5K
POST_COMPACT_SKILLS_TOKEN_BUDGET = 25,000 // 技能总预算 25K

★ 设计洞察:压缩后自动恢复最近读取的文件内容是一个关键细节。如果用户正在编辑 3 个文件,压缩后这些文件的内容会从上下文中消失。通过恢复最近 5 个文件,模型可以继续工作而不需要重新读取。


一次 Grep 搜索可能返回 100KB+ 的结果
一次文件读取可能返回 50KB+ 的内容
这些结果如果全部保存在消息历史中 → 上下文迅速膨胀
utils/toolResultStorage.ts
持久化决策:
result.length > getPersistenceThreshold(toolName) ?
→ 持久化到磁盘 + 消息中只保留预览
: → 正常保存在消息中
阈值计算:
threshold = min(tool.maxResultSizeChars, 50_000) // 默认上限 50K 字符
// GrowthBook 可覆盖特定工具的阈值
工具返回 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)
// 为什么用 flag: 'wx'?
// wx = 独占创建,如果文件已存在则失败(EEXIST)
场景:对话恢复(/resume)时重放消息
1 次: 写入成功
2 次: EEXIST → 跳过写入 → 使用已有文件
→ 保证同一个 toolUseId 的结果只写入一次
→ 保证预览内容始终一致(prompt cache 稳定)

★ 设计洞察flag: 'wx' 是一个精妙的细节。在分布式系统中常用 CAS(Compare-And-Swap)保证幂等性,这里用文件系统的 O_EXCL 标志实现了同样的效果——如果文件已存在,操作是空操作。


query/tokenBudget.ts
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. 否则停止
轮次 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:错误驱动的压缩”
正常对话中 → API 返回 prompt_too_long 错误
Phase 4 (错误恢复):
1. 尝试 Context Collapse Drain → 快速但有限
2. 尝试 Reactive Compact → 完整压缩
Autocompact (预防性):
- 在 API 调用前触发(Phase 1)
- 基于 token 估算
- 不紧急,可以从容处理
Reactive Compact (反应性):
- 在 API 返回错误后触发(Phase 4)
- 基于实际错误
- 紧急,必须立即处理
- 利用 withholding 机制对用户透明
正常流程: Withholding 流程:
API → error → 用户看到错误 API → error → 扣住
尝试 reactive compact
成功 → 用户无感知,重试
失败 → 释放错误,用户看到

6. Session Memory Compact:压缩时的记忆保留

Section titled “6. Session Memory Compact:压缩时的记忆保留”
type SessionMemoryCompactConfig = {
minTokens: 10_000 // 至少保留 10K tokens
minTextBlockMessages: 5 // 至少保留 5 条有文本的消息
maxTokens: 40_000 // 最多保留 40K tokens
}
保留:
✓ 有文本块的 Assistant 消息(对话历史核心)
✓ 有内容的 User 消息(用户输入)
✓ tool_use blocks(行动历史)
✓ 与保留 tool_use 配对的 tool_result
丢弃:
✗ 孤立的 tool_result(对应的 tool_use 已删除)
✗ 图片/文档块
✗ 系统消息(除了 compact boundary)
如果保留 tool_result → 必须保留对应的 tool_use
如果保留 assistant 消息中的 thinking → 保留该消息的所有块
不在同一个 message.id 内拆分

普通压缩: 每次压缩请求都是全新的系统提示词 → cache 未命中
优化后: Fork 子 Agent 的压缩复用主 Agent 的 prompt cache prefix
效果:
cache 命中率: ~98% vs ~2%(无优化)
日均节省: ~380 亿 tokens(全平台统计)
Microcompact 的设计目标:
在 prompt cache 的有效范围内做编辑
→ 移除旧的 tool_use blocks(减少 token)
→ 但不改变消息结构(cache 仍然有效)
效果:
减少 token 数 + 保持 cache 命中 = 最优的成本控制

★ 设计洞察:Prompt Cache 是 Claude Code 成本控制的核心武器。压缩策略不仅要考虑「删什么」,还要考虑「怎么删才不会破坏 cache」。Microcompact 就是在这两个约束之间找到的平衡点。


~/.claude/sessions/{sessionId}/
├── transcript ← 完整对话记录(JSON)
├── agents/
│ └── {agentId}/
│ └── transcript ← 子 Agent 对话记录
└── tool-results/
└── {toolUseId}.json ← 大型工具结果
/resume 或 claude --resume {sessionId}
loadConversationForResume(sessionId)
deserializeMessages(transcript)
恢复状态: messages, usage, metadata
继续对话(从最后一条消息继续)

为什么不直接 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 的关键特征
没有外置:
50 次 Grep 搜索 × 50KB/次 = 2.5MB 在上下文中
→ 快速达到上下文上限 → 频繁压缩 → 高成本
有外置:
50 次 Grep 搜索 × 2KB 预览/次 = 100KB 在上下文中
+ 2.5MB 在磁盘上(零 token 成本)
→ 上下文增长慢 → 压缩频率低 → 成本可控

交互方向说明
← 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 允许用户自定义压缩行为