本文基于 Claude Code 源码深度分析,揭秘其上下文压缩(Compaction)的完整实现原理。如果你曾好奇"为什么 Claude Code 能在超长对话中依然保持上下文连贯",这篇文章会给你答案。
概述
Claude Code 实现了一个多层上下文压缩系统,用于在模型上下文窗口限制内优雅地处理无限长度的对话。系统核心包括:
- 4 层压缩策略,各有不同的成本/效果权衡
- 会话稳定的状态管理,防止缓存键在会话中间失效
- 工具配对不变量保持,确保 API 合约在截断时不被破坏
- 多级错误恢复路径,含回退链
- 5 级 token 预算层级,协调不同优化阶段
关键数字:
- 4 层压缩策略(基于时间、缓存编辑、会话记忆、完整压缩)
- 5 个 token 阈值
- 最多 3 次连续失败后熔断
- 60 分钟空隙触发(服务器缓存 TTL)
- 13,000 token 安全缓冲
- 压缩后清理 40+ 种缓存
- 完整压缩失败率约 0.1%
系统架构(6 层)
Claude Code 的压缩系统分为 6 层,每层职责清晰:
- Layer 1: 触发检测 — 评估当前 token 使用量 vs 阈值,选择压缩策略,与特性门控协调
- Layer 2: 策略实现 — 基于时间 MC(字符串替换旧 tool results)、缓存 MC(排队 cache_edits 到 API 层)、会话记忆(使用已提取的摘要替代 API 调用)、完整压缩(调用 Claude 生成摘要)
- Layer 3: 状态管理 — 会话稳定锁存器、微压缩状态、会话记忆提取状态、压缩后清理
- Layer 4: 缓存一致性 — 通知缓存中断检测器预期的变化,跨请求维持稳定的缓存键
- Layer 5: API 成 — 注入 context_management 头,在原始位置包含 cache_edits
- Layer 6: 压缩后清理 — 清除会话状态、context collapse、memory cache
1. 触发条件:何时触发压缩
1.1 自动压缩决策树
入口:shouldAutoCompact(messages, model, querySource)
决策流程:
- 递归保护检查:querySource === 'session_memory' 或 'compact' → FALSE(防止循环)
- 启用标志检查:DISABLE_COMPACT=true 或 DISABLE_AUTO_COMPACT=true → FALSE
- 竞争性上下文管理检查:feature('REACTIVE_COMPACT') 或 feature('CONTEXT_COLLAPSE') → FALSE
- Token 阈值检查:tokens >= threshold → TRUE(需要压缩)
1.2 Token 预算层级(5 级)
| 层级 | 说明 | Sonnet 3.5 | Haiku 3 |
|---|---|---|---|
| TIER 1 | 模型原始上下文窗口 | 200,000 | 128,000 |
| TIER 2 | 有效窗口(预留 20K 输出) | 180,000 | 108,000 |
| TIER 3 | 自动压缩阈值(-13K 缓冲) | 167,000 | 95,000 |
| TIER 4 | 警告阈值(黄灯) | 147,000 | 75,000 |
| TIER 5 | 错误阈值/阻塞限制(红区) | 127,000 | 55,000 |
2. 四层压缩策略
Layer 1:基于时间的微压缩(缓存冷)
触发条件:距上次助手消息 > 60 分钟(服务器缓存 TTL 过期)
策略:把旧的 tool results 替换为占位符 "[Old tool result content cleared]",只保留最近的 5 个。无需任何 API 请求,延迟 <10ms。
Layer 2:缓存微压缩(缓存热)
触发条件:Tool result 数量超过阈值,且缓存仍然有效
策略:不直接修改本地消息,而是通过 cache_edits 告诉 API "请删掉这几个 tool results",从而在不使缓存前缀失效的情况下完成压缩。延迟仅 10-50ms。
示例请求结构:
{
"context_management": {
"type": "auto",
"cache_control": { "type": "ephemeral" },
"cache_edits": [
{ "type": "delete", "index": 5 },
{ "type": "delete", "index": 8 }
]
}
}
Layer 3:会话记忆压缩(实验性)
触发条件:会话记忆中存在已提取的对话摘要
策略:无需调用模型 API,直接读取磁盘上已有的会话摘要替换历史消息。压缩效果 40-60%,延迟 100-500ms。
关键步骤包括:
- 轮询等待会话记忆提取完成(最多 15s)
- 计算保留消息的起始索引
- 调整工具配对不变量,确保 tool_use/tool_result 对完整
Layer 4:完整压缩(兜底)
触发条件:Token 超过阈值且前三层都无法处理
策略:直接调用 Claude API,让模型对整个对话历史生成一份摘要,然后用摘要 + 关键附件 + 最近消息重建对话。压缩效果高达 60-80%,但需要 3-10s。
内置 PTL(Prompt Too Long)重试循环:当摘要生成请求本身也超长时,按 API-round 组删除最旧的消息组,然后重试。
3. 四层策略性能对比
| 指标 | 基于时间 MC | 缓存 MC | 会话记忆 | 完整压缩 |
|---|---|---|---|---|
| Token 节省 | 10-30% tools | 5-15% tools | 40-60% 上下文 | 60-80% 上下文 |
| 延迟 | 10-50ms | 100-500ms | 100-500ms | 3-10s |
| 缓存影响 | 冷(无收益) | 热(100-200 tokens) | 5-10K 创建 | 30-50K 创建 |
| 失败率 | 0% | ~1% | 5-15% | 0.1% |
| API 调用 | 0 | 0 | 0 | 1 |
4. 工程亮点
会话稳定锁存器
缓存键 = hash(系统提示 + 消息 + 请求参数)。如果中途改变 context_management 头的存在与否,缓存键会翻转,之前的缓存全部失效。
解决方案:首次请求时决定配置并锁存,后续请求无条件复用锁存值,直到会话结束。
cacheEditingHeaderLatched: null | boolean
// null → 首次请求,计算并写入锁存
// true/false → 复用,忽略运行时配置变化
工具配对不变量
API 要求每个 tool_use 必须在 tool_result 之前存在。截断消息时若不注意,很容易把 tool_use 切掉但留下孤立的 tool_result,导致 API 报错。
Claude Code 用 adjustIndexToPreserveAPIInvariants() 专门处理这个问题:向前扩展 startIndex 直到所有 tool 配对完整。
有意不清理 skill 名称
压缩后清理代码中有一行显眼的注释:
sentSkillNames 不清理 — 原因: 重新注入 skill_listing (~4K tokens) 纯属 cache_creation 浪费
这是个精心的权衡:已发送的 skill 列表不重新注入,但具体调用过的 skill 内容(invoked_skills)仍然作为附件保留,确保模型压缩后仍能看到完整指令。
5. 错误恢复路径(3 级)
- Stage 1: Context-Collapse 排空 — 排出暂存队列,用更小的上下文重试
- Stage 2: 反应式压缩 — 413/媒体错误时触发,异步压缩后重试
- Stage 3: 显示错误 — 提示用户手动 /compact
6. 关键文件索引
| 文件 | 职责 |
|---|---|
| src/services/compact/autoCompact.ts | 自动压缩触发逻辑、阈值计算 |
| src/services/compact/compact.ts | 完整压缩实现、PTL 重试 |
| src/services/compact/sessionMemoryCompact.ts | 会话记忆压缩 |
| src/services/compact/microCompact.ts | 基于时间/缓存微压缩 |
| src/services/compact/postCompactCleanup.ts | 压缩后清理策略 |
| src/services/query.ts | 查询状态机、错误恢复路径 |
总结
Claude Code 的上下文压缩机制是一套精心设计的多层系统,从轻量级的时间微压缩到重量级的完整压缩,各有适用场景。关键亮点包括会话稳定锁存器、工具配对不变量保持、以及有意不清理 skill 名称的权衡。这套机制让 Claude Code 能在长时间任务中保持上下文连贯,是 Agent 系统设计的重要参考。