Claude Code 压缩机制#
调研文档(非设计文档)。基于源码逆向分析,记录 Claude Code 的压缩机制。
可靠度说明:5 级级联的描述来自第三方逆向分析,不是 Anthropic 官方文档确认的。 官方 API 文档(platform.claude.com)只描述了单一的 compact 机制(超 token 阈值 → LLM 摘要), 内部的 Microcompact / Snip / Context Collapse 分层是 Claude Code 客户端的实现细节。 不同逆向分析来源对执行顺序的描述有细微差异。以下采用 Inside Claude Code 的版本(分析最详细)。 阈值和参数在不同版本中有调整,以下数字可能不完全准确。
来源(按可靠度排序):
- Compaction - Claude Platform Docs(官方,但只描述 API 层面)
- Context editing - Claude API Docs(官方)
- Inside Claude Code - Context Compaction(逆向分析,最详细)
- Dive into Claude Code(arXiv 2604.14228,VILA-Lab 源码逆向分析)
- DeepWiki - Context Window & Compaction(社区 wiki)
- Claude Code VS OpenCode §5.3(对比分析)
1. 总览#
Claude Code 有两类压缩机制:
常规操作(每轮 LLM 调用前都做,和上下文长度无关):
- Budget Reduction:截断超大的单个工具输出
级联压缩(按 Tier 1→2→3→4→5 的顺序升级,成本低的先做):
- Tier 1 Microcompact:清理旧工具输出
- Tier 2 Snip:删最旧的几轮对话
- Tier 3 Context Collapse:分段 LLM 摘要
- Tier 4 Auto-Compact:全量 LLM 摘要
- Tier 5 Reactive:API 返回 413 时紧急压缩
| 名称 | 做什么 | 触发条件 | 调 LLM | 破缓存 | 信息损失 | |
|---|---|---|---|---|---|---|
| 常规 | Budget Reduction | 截断超大单个输出 | 单个 tool_result > 4000 字符 | 否 | 否 | 中间内容丢 |
| Tier 1 | Microcompact | 旧工具输出清理 | 工具调用 ≥ 50 次(之后每 25 次)/ 空闲 90 分钟 | 否 | cache-aware 不破 | 旧输出丢 |
| Tier 2 | Snip | 删最旧几轮 | Tier 1 后仍超预算 | 否 | 是 | 整轮丢 |
| Tier 3 | Context Collapse | 分段摘要 | Tier 2 后仍超,~90% 占用 | 是 | 是 | 细节丢,要点留 |
| Tier 4 | Auto-Compact | 全量摘要 | Tier 3 后仍超 / 用户 /compact |
是 | 是 | 大量丢,只留概要 |
| Tier 5 | Reactive | 紧急压缩 | API 返回 413 prompt_too_long | 是 | 是 | 同 Tier 4 |
级联逻辑:每轮 LLM 调用前,从 Tier 1 开始检查。满足条件就执行,执行完重新检查是否还超。还超就进入下一个 Tier。前面的 Tier 成本低(不调 LLM、不破缓存),尽量在前面解决。
注意:Tier 3 Context Collapse 和 Tier 4 Auto-Compact 的关系,逆向分析来源描述为 "overlapping strategies"(重叠策略),不一定是严格的"3 不够才上 4"。可能某些配置下 跳过 Tier 3 直接走 Tier 4。
/compact手动命令直接触发 Tier 4,跳过 Tier 1-3。
2. 常规操作:Budget Reduction#
和级联无关,每轮 LLM 调用前都做。
做什么:检查每个工具输出的大小,超过阈值就截断。
参数:
- 截断阈值:4000 字符(约 1000 tokens)
- 截断方式:超过 4000 字符的输出,只保留前 2400 字符(60%)+ 后 1600 字符(40%),中间丢弃
- 只处理工具输出(tool_result),不处理 user/assistant 消息
示例:
截断前(50000 字符):
[tool_result] "src/a.py:12: TODO fix this\nsrc/b.py:34: ..."
截断后(4000 字符):
[tool_result] "src/a.py:12: TODO fix this\n...[46000 chars removed]...\nsrc/z.py:99: TODO cleanup"
3. 级联压缩#
每轮 LLM 调用前,Budget Reduction 之后,按 Tier 顺序检查和执行。
Tier 1:Microcompact#
做什么:清理旧的工具输出,释放空间。
触发条件:
- Cache-aware path:50 次工具调用后首次触发,之后每 25 次再触发
- Time-based path:距上次 assistant 消息 ~90 分钟
只处理特定工具:FileRead、Shell、Grep、Glob、WebSearch、WebFetch、FileEdit、FileWrite
两条路径:
| Cache-aware(主力) | Time-based(备用) | |
|---|---|---|
| 做什么 | 通过 Context Editing API 让服务端清除旧 tool_result | 客户端把旧 tool_result 替换为 sentinel 字符串 |
| 破缓存 | 不破(服务端操作,客户端消息不变) | 破(客户端改了消息) |
| 触发 | 50 次工具调用后,每 25 次 | 空闲 ~90 分钟 |
| 可恢复 | 否 | 否 |
| 依赖 | Anthropic API 的 Context Editing(其他 provider 不支持) | 无依赖 |
压缩效果:每个被清理的工具输出从原始大小(几百到几千 tokens)→ 0 tokens(cache-aware)或 ~20 tokens(time-based)。例:一次 Microcompact 清理了 10 个旧工具结果,每个平均 500 tokens → 释放约 4800 tokens。
示例:
清理前:
[tool_result] "import os\nimport sys\n\nclass Config:\n ..."(2000 tokens)
清理后(cache-aware):
[tool_result] ""(服务端清空,0 tokens,缓存不破)
清理后(time-based):
[tool_result] "[content no longer available]"(~20 tokens)
Cache-aware path 细节:sentinel 字符串做了字节级归一化(byte-stable canonical form)——重复 microcompact 不改变已缓存内容。核心原则:已进缓存的内容,客户端不修改(修改会破前缀匹配)。要清旧内容时:
- 已在缓存 → cache-aware path,通过 Context Editing API 让服务端清
- 没在缓存 → time-based path,客户端直接替换为 sentinel
Context Editing API(cache_edits)
Microcompact cache-aware path 的底层实现。Anthropic API 的公开 beta(header: context-management-2025-06-27),不是 Claude Code 专有,普通开发者可用。
工作原理:客户端发送完整消息历史(不做任何修改),同时在 API 请求中传一个 cache_edits 参数。服务端收到后:
- 在缓存内部找到旧的 tool_result 块
- 把它们替换为空或占位文本
- 缓存前缀不变(因为客户端发的消息没变,服务端只改了缓存内部的数据)
- 客户端完全不需要知道哪些被清了
和正常 LLM 调用合并在一起,不是额外请求——发消息的同时顺便告诉服务端"清掉旧的 tool_result"。
| 策略 | 做什么 |
|---|---|
clear_tool_uses |
清除旧的 tool_result,只保留最近 N 个(N 的具体值未从源码确认,逆向分析未给出) |
clear_thinking |
清除旧的 thinking blocks |
clear_at_least |
控制最少清多少 token |
和其他 Tier 的关系:Context Editing 不是独立的 Tier,它是 Tier 1 Microcompact cache-aware path 的底层实现。Tier 2-5 都不用这个 API——它们直接修改客户端的消息列表。
限制:只有 Anthropic API 支持。OpenAI / Google / 其他 provider 没有类似能力,只能走 time-based path(客户端替换,会破缓存)。
Tier 2:Snip#
做什么:直接删除最旧的几轮对话。不做摘要、不存磁盘,直接丢。
触发条件:Tier 1 做完后仍超预算(逆向分析中出现 ~13K tokens buffer 的说法,但具体阈值可能随版本变化)
参数:
- 删除粒度:整轮(user + assistant + 该轮所有工具调用一起删),不会出现"user 在但 assistant 被删"的不一致
- 删多少:从最旧的开始逐轮删,每删一轮重新算 token 数,删到阈值以下为止(不是固定删 N 轮)
- 删除的内容彻底消失,模型不知道之前聊过什么
- 没有删除标记(不会在上下文里留"已删除 5 轮"之类的提示)
代价:信息完全丢失,缓存前缀被破坏。但免费(不调 LLM),执行速度快。
Tier 3:Context Collapse#
做什么:把旧对话分成若干段(5-10 轮一段),每段用 LLM 生成摘要。
触发条件:Tier 2 Snip 做完后仍超阈值(约 90%,非阻塞触发;95% 时阻塞强制触发)
关键特性:原始消息保留在 collapse store 中。这是读时投影(read-time projection)——类似数据库 View,底表不动,查询看到的是摘要视图。原始历史保留着,理论上可以重建/回滚。
参数:
- 分段标准:5-10 轮一段(具体值未从源码确认)
- 每段独立用 LLM 摘要,每段摘要约 100-300 tokens(估算,未确认)
- 最近 N 轮完整保留,只摘要更旧的段(N 值未确认)
- LLM 调用次数 = 段数(例:30 轮分 3 段 = 3 次 LLM 调用)
和 Tier 4 的区别:
- Context Collapse 是分段摘要,每段独立,保留分段边界和时间线结构
- Auto-Compact 是全量摘要,一把压成一段,结构丢失
示例:
压缩前(35 轮):
[轮 6-10] 读代码、找 bug、修复、测试
[轮 11-15] 重构 config 模块
[轮 16-35] 改 API 模块...
压缩后:
[摘要] "Turns 6-10: 修了 utils.py 的 bug,添加了回滚脚本"
[摘要] "Turns 11-15: 重构了 config 模块,抽出了 Settings 类"
[轮 16-35] ...(最近的完整保留)
原始的 turn 6-15 仍然保存在 collapse store 中,模型看到的是折叠版本。
Tier 4:Auto-Compact#
做什么:把整个对话历史一次性发给 LLM,生成一个摘要块,替换所有旧消息。
触发条件:Tier 3 做完后仍超阈值。或用户手动 /compact。
关键特性:原始消息被替换,不可回滚。信息损失最大。
参数:
- 摘要块大小:约 2000-5000 tokens(取决于对话复杂度,由 LLM 自行决定)
- 保留最近 1-2 轮完整对话(不被摘要)
- LLM 调用:1 次(全量摘要)
- 压缩后上下文从原来的 70-90% 降到约 15-25%
- 可通过
CLAUDE_AUTOCOMPACT_PCT_OVERRIDE环境变量调整触发百分比
具体过程:
- 执行 PreCompact hooks(通知系统即将压缩)
- 用
getCompactPrompt()构造压缩提示(类似"请把以下对话历史压缩成关键事实") - 把整个对话历史发给 LLM,生成摘要
- 用
buildPostCompactMessages()构建压缩后的消息 - system prompt 从 CLAUDE.md 重新加载(不是从摘要来的,所以 CLAUDE.md 的内容永远不丢)
/compact 手动命令:
/compact或/compact 保留关于数据库迁移的讨论- 直接触发 Auto-Compact,不经过 Tier 1-3
- 带提示词时摘要质量更好(用户指导保留什么)
- 官方建议在 60% 占用时手动
/compact
丢失的:早期指令、设计讨论、推理过程、50+ 轮前的代码片段、风格偏好。 保留的:当前任务、最近修改的文件名、最近的错误和解决方案、CLAUDE.md 内容。
示例:
压缩前(35 轮,150K tokens):
[system prompt]
[35 轮完整对话历史]
压缩后(~30K tokens):
[system prompt](从 CLAUDE.md 重新加载)
[compaction block]
"会话摘要:用户在做 Python 项目重构。
已完成:修了 utils.py 的 bug、重构了 config 模块、添加了 3 个测试。
当前状态:所有测试通过。用户正在处理 API 模块。
关键决定:使用 FastAPI 替换 Flask、数据库用 PostgreSQL。
修改过的文件:src/utils.py, src/config.py, tests/test_utils.py"
[最近 1-2 轮完整保留]
链式压缩:压缩后继续聊,再次满了再压。每次在上一次摘要基础上再压,信息逐层衰减——第一次丢早期细节,第二次丢中期细节。长会话可能压缩 3-5 次。
Tier 5:Reactive#
做什么:API 返回 413(prompt too long)错误时的紧急压缩。
触发条件:LLM 调用失败,返回 prompt_too_long / 413 错误。
做法:尝试 context-collapse overflow recovery,失败则做紧急 compact。每轮至多触发一次。都失败则终止会话。
4. 执行流程#
每轮 LLM 调用前的完整流程:
1. Budget Reduction → 截断超大工具输出(每轮都做)
2. 级联检查(按 Tier 顺序):
Tier 1: Microcompact 条件满足?→ 清旧工具输出
Tier 2: 仍超阈值?→ Snip 删旧轮
Tier 3: 仍超阈值?→ Context Collapse 分段摘要
Tier 4: 仍超阈值?→ Auto-Compact 全量摘要
3. 调 LLM
4. 如果 API 返回 413:
Tier 5: Reactive → 紧急压缩 → 重试
→ 失败则终止
5. 完整运行示例#
场景:200K context,持续工作几小时。
阶段 1(0-30%):正常对话。Budget Reduction 检查工具输出大小,没超的跳过。级联都不触发。
阶段 2(30-50%):工具调用超过 50 次。Tier 1 Microcompact cache-aware 首次触发,清理前 30 次工具调用的输出,释放约 15K tokens。之后每 25 次再清一轮。级联到 Tier 1 就够了,Tier 2-4 不触发。
阶段 3(超阈值):对话消息累积超阈值。Tier 1 Microcompact 做了,还是超。Tier 2 Snip 触发,删最旧的 5 轮,释放约 25K tokens。降到阈值以下,Tier 3-4 不触发。
阶段 4:继续使用。再次超阈值时重复阶段 3(Microcompact + Snip)。
阶段 5(极端):反复 Snip 后可删的旧轮不多了,Snip 不够用。Tier 3 Context Collapse 触发,分段摘要。还不够则 Tier 4 Auto-Compact。这种情况很少——因为 Microcompact 日常控制住了工具输出的增长,Snip 通常够用。
6. 关键设计原则#
-
Lazy degradation:成本低的先做。Microcompact(免费不破缓存)→ Snip(免费破缓存)→ Context Collapse(调 LLM)→ Auto-Compact(调 LLM,最激进)。
-
工具输出优先清理:旧的 grep / read_file / bash 输出是最大且最不重要的消耗者。先清它们,对话消息尽量保留。
-
日常靠 Microcompact:cache-aware path 持续释放空间且不破缓存。这使得 Snip/Compact 极少触发。
-
缓存保护:Microcompact cache-aware 通过 Context Editing API 服务端清除,缓存不破。Budget Reduction 截断后内容固定。Snip/Compact 会破缓存但触发频率低。
-
对话消息不单独截断:user/assistant 消息要么整轮删(Snip),要么 LLM 摘要(Compact)。不对单条消息截断。
-
CLAUDE.md 不参与压缩:从磁盘重新加载,永远不丢。
-
用户控制:
/compact+/context让用户主导。自动是兜底。