一文拆解Claude Code中上下文压缩机制
先说一个核心判断:在真实的编码会话中,token 总量轻松飙到 400K 甚至更高,而模型的上下文窗口只有 200K。Claude Code 究竟是怎么把“无限增长的对话”塞进“固定大小的窗口”,还能一路保持思路连贯的?
答案不是“撑满了就总结一下”这么简单。其背后是一条
5 级渐进式压缩流水线
文章核心要点:
- Claude Code 用 管理上下文,核心哲学是
5 层手段
。渐进式压缩:cheapest first, hea viest last
- 前 4 层(工具结果落盘、历史裁剪、微压缩、上下文折叠)。
几乎零成本、且基本无损/可回滚
- 只有最后一层 会真正调用一次 LLM 做摘要——
AutoCompact
,而这一层才是“有损不可逆”的
。绝大多数对话根本走不到这里
- “有损不可逆”的根本局限,由两件事兜底:(CLAUDE.md / auto memory)+
跨会话的记忆系统
。完整保留原始对话的 transcript
- 整条压缩流水线位于源码
src/services/compact/,约。3,960 行 TypeScript / 5 个文件
适合谁读

一、先理解问题:窗口有限,对话无限
不妨把上下文窗口想象成模型的
工作台面
- :约 20–25K token,是雷打不动的固定开销。
系统提示词 + 工具定义
- :每轮再加 200–2,000 token,视情况而定。
系统提醒(system reminders)
- 剩下大约 。
175K token 才是“对话历史”能用的空间
问题在于:
历史会无限增长,窗口却不会
grep、让它改几轮代码——每次工具调用都往台面上堆 2K–8K token。不处理的话,迟早撑爆。
更微妙的是:
撑爆之前,质量就已经开始下滑了
它是模型“思考”的地方
所以 Claude Code 的目标不是“用满”,而是
在台面变乱的过程中持续清理,始终给推理留出余量

二、核心设计哲学:渐进式压缩
整条流水线只有一句话原则:
最便宜的手段先上,最重的手段最后用。
每往下一层,要么消耗更多算力,要么丢掉更多细节。于是系统能做到:“能用零成本手段解决的,绝不动用花钱又有损的 LLM 摘要。” 实测结论也印证了这一点:
绝大多数对话根本走不到最后一层。
下面是完整的 5 级全景。
- 消息历史持续增长
- L1 · 工具结果预算 单块 >50K 字符 → 落盘 + 2KB 预览 成本:零 · 可恢复
- L2 · 历史裁剪 Snip 回收陈旧的对话脚手架 成本:零
- L3 · 微压缩 MicroCompact 回收旧工具输出 · 时间型/缓存型双路径 成本:零 API 调用
- L4 · 上下文折叠 Context Collapse ~90% 触发 · 投影式折叠 · 可回滚 成本:零 · 非破坏性
- L5 · 自动压缩 AutoCompact 分叉子 Agent 生成 LLM 摘要 成本:一次 API 调用 · 不可逆
- 组装最终 API 请求
三、逐层拆解
L1 · 工具结果预算(Tool Result Budget)
问题
做法
DEFAULT_MAX_RESULT_SIZE_CHARS,约 50,000 字符
不会粗暴截断
落盘
2KB 的预览
Output too large (2.3 MB). Full output sa ved to: /tmp/.claude/session-xxx/tool-results/toolu_abc123.txt Preview (first 2.0 KB): [前 2000 字节内容] ...
为什么是“落盘”而不是“截断”?
永久丢失
Read 工具从磁盘把完整文件取回来。2KB 预览则刚好够它判断“要不要去取”。
这是对很多笔记里“Snip 就是直接截断”说法的修正:第一层的本质是
可恢复的落盘
L2 · 历史裁剪(History Snip)
可以把这一层理解成
对话脚手架的垃圾回收
L3 · 微压缩(MicroCompact)—— 回收旧工具输出
这是最关键、也最精妙的一层。一句话定性:
MicroCompact 不是“总结历史”,而是“对旧工具输出做垃圾回收”。

它专门清理哪些东西
微压缩只回收“旧工具调用返回的大块原始内容”,因为这些内容有个共同点——
丢了还能再拿回来
| 工具类型 | 为什么适合清理 |
|---|---|
| Read | 文件之后可以重新读取 |
| Bash / Shell | 旧日志通常很大,而且可能已经过时 |
| Grep | 搜索结果可以重新生成 |
| Glob | 文件列表可以重新扫描 |
| WebSearch / WebFetch | 网页内容可以重新获取 |
| Edit / Write | 修改结果通常已经落到文件系统里 |
两条互斥的路径:时间型 vs 缓存型
微压缩内部分两条路,
互斥
microcompactMessages()
|
+-------------+--------------+
| |
长时间未交互? 缓存仍然有效?
| |
是 是
| |
Time-based Microcompact Cached Microcompact
(直接改本地消息内容) (改用 cache_edits)
| |
冷缓存场景 热缓存场景
| 对比项 | Time-based Microcompact | Cached Microcompact |
|---|---|---|
| 使用场景 | 长时间没继续对话,缓存大概率已过期 | 对话持续进行,缓存仍然有效 |
| 是否修改本地消息 | 修改 | 不修改 |
| 如何删除内容 | 把旧结果替换为固定占位符 | 给 API 发送 cache_edits |
| 是否保留提示缓存 | 不需要(缓存已经冷了) | 尽量保留缓存前缀 |
| 触发依据 | 时间间隔 | 工具数量阈值 |
| 优先级 | 最高 | 时间路径未触发时才执行 |
Cached 路径的巧思
cache_edits 指令,让服务端在原缓存块里“就地标记删除”
已有缓存: A B C D E F G
↓ 发送 cache_edits:基于原缓存,把 C 标记为删除
有效视图: A B D E F G
类比
视图(View)
一个容易踩坑的追问:cache 里到底还在不在?
微压缩后,被标记删除的旧工具结果,cache 里还缓存着吗?会暂时同时存在于“服务端原始缓存块”和“本地消息历史”里,但在
模型当前使用的有效缓存视图中已被排除
cache 里仍包含它,但模型有效上下文不包含它。
| 所在位置 | 是否还在 |
|---|---|
Claude Code 本地 messages | 还在 |
| 本地会话记录 / transcript | 通常还在 |
| 服务端原始缓存块 | 很可能暂时还在,直到 TTL 过期 |
| 模型当前有效上下文 | 不在 |
| 后续请求的有效 token 统计 | 不再计入活动上下文 |
L4 · 上下文折叠(Context Collapse)—— 增量、可回滚
如果前几层还不够,进入折叠层。它的定位和“自动压缩”有本质区别:
Context Collapse 是持续、增量式的上下文管理;AutoCompact 是达到阈值后对整段会话的一次集中式重写。
折叠层最大的优点是
非破坏性、可回滚
从不删除
独立的 collapse store
projectView() 在请求时把摘要“叠加”到原始消息之上。换句话说,模型看到的是折叠后的视图,但底层数据还在——需要时能还原。
它大约在
~90% 利用率
~95%
| 对比项 | Context Collapse | AutoCompact(自动压缩) |
|---|---|---|
| 工作方式 | 持续、增量式管理 | 达到阈值后一次性执行 |
| 处理粒度 | 更细,逐步提交可保留信息 | 更粗,把旧上下文整体摘要化 |
| 触发阶段 | 约 90% 开始 commit,95% 进入阻塞处理 | 有效窗口剩约 13K token 时触发 |
| 是否调用模型 | 通常由独立 context agent 整理、提交 | 会调用模型生成摘要 |
| 对原消息的影响 | 维护独立的 committed log,逐步折叠活动上下文 | 直接用“摘要 + 保留的近期消息”替换旧消息 |
| 中断程度 | 偏增量、后台式整理 | 类似一次 stop-the-world 压缩 |
| 信息保留 | 倾向保存更细粒度的结构化信息 | 主要依赖摘要质量 |
| 能否与自动压缩并存 | — | 开启 Collapse 后,主动 AutoCompact 被禁用 |
一句话记住差异:
- :以“对话段落”为压缩单位 → “上下文快满了,把过去整体总结一次。”
AutoCompact
- :以“事实、决定、状态、产物”为提交单位 → “在上下文变满的过程中,持续把有价值的信息提交出去,再逐步折叠已处理的活动上下文。”
Context Collapse
L5 · 自动压缩(AutoCompact)—— 唯一真正“有损”的一层
走到这里,才会真的
调用一次 LLM 做摘要
触发与阈值(以 200K 窗口为例)
- 给“写摘要”本身用;
保留约 20,000 token
- 作为缓冲——所以当有效窗口只剩约 13K 时触发;
保留约 13,000 token
- 另有 的手动压缩缓冲:只剩这么多时,新请求会被阻塞,提示你手动
3,000 token
/compact。

调用链:一张图看懂“该不该压、怎么压”
用户发一条消息
│
▼
主对话循环(query loop)
│
▼
autoCompactIfNeeded(messages, ...) ← 总指挥
│
▼
先查熔断器:连续失败 ≥ 3 次? ──是──▶ 直接放弃(防死循环)
│否
▼
shouldAutoCompact(...) ← 只负责判断「该不该」
│ 一连串"直接返回 false"的守卫(递归 / 实验开关 / 功能未开)
│ 都过了 → 数 token → calculateTokenWarningState → 返回 true/false
▼
返回 false → 不压,结束
返回 true → 继续:
│
▼
trySessionMemoryCompaction(...) ← ① 优先用「会话记忆」压
│ 成功 → 清理 + 返回 wasCompacted:true
│ 失败 / 不可用 ↓
▼
compactConversation(...) ← ② 退而用传统 LLM 摘要
│ 成功 → 清理 + consecutiveFailures:0 + 返回
└ 抛错 → 失败次数 +1 + 返回 wasCompacted:false
几个值得注意的设计:
- :连续失败 3 次就停止再试,避免在异常情况下反复烧钱、卡死。
熔断器(circuit breaker)
- :在真正花钱做完整摘要之前,先尝试用后台预先攒好的“会话记忆”来压——能省掉那次昂贵的模型调用。
会话记忆优先
- 强制保留三类信息:
摘要的提示词
。压缩后完成了什么、当前状态、做过的关键决策
messages只剩一条,但 Agent 知道“之前发生过什么”,能接着干活。
还有一层“兜底中的兜底”:反应式压缩(Reactive Compact)
再周密的估算也有失手的时候——某个工具结果意外巨大、多个系统提醒同时注入、token 估算偏低……一旦 API 直接返回
413(Prompt Too Long)
Reactive Compact
只保留最后 4 条消息、其余全部摘要
hasAttemptedReactiveCompact 守卫保证它只尝试一次
这一层的哲学很值得玩味:
与其追求完美的 token 计数,不如接受估算的不精确,并提供一条稳健的恢复路径。
以及:手动 / Agent 主动压缩
除了自动触发,压缩也能被主动调用:你随时可以 /compact(还能附带指令,比如“重点保留认证相关的工作”);Agent 自己也可能在
即将切换到一个完全不同的任务
compact 清空当前上下文,为新任务腾地方。
四、那个绕不开的问题:有损不可逆,怎么破?
回到最初的灵魂拷问。先把结论说清楚:
这条流水线里,前四层基本都是无损或可回滚的
Read 回来、折叠可还原、微压缩只是“视图”过滤。真正不可逆的,只有第 5 层那次 LLM 摘要。
让对话尽量止步于前几层,把“有损摘要”压到最少发生
但只要它会发生,信息论上就一定有损耗。Claude Code 用两件事来兜底这条根本局限:
- :压缩管的是“当前会话的台面整洁度”,而
跨会话的记忆系统
CLAUDE.md/ auto memory 管的是“跨会话的长期知识”。每个会话从干净上下文开始,记忆文件在开头被读入;auto memory 还能让 Claude 根据你的纠正自动记笔记。两者配合,Agent 既不被当前对话淹没,又不丢失重要的长期信息。 - :
完整的原始档案(transcript)
.transcripts/里保存了。它是事后取证 / 回溯用的备份——压缩前的全部原始对话
,但只要你需要,一切都在。Agent 平时不会主动去翻
五、一个少有人提的隐患:注入指令会“穿透”压缩
这是整套设计里一个容易被忽视、却很值得警惕的点:
压缩流水线对所有内容一视同仁。
摘要器(summarizer)会用同一条流水线处理“用户指令”和“工具结果”。如果攻击者在某个项目文件里埋了恶意指令,而模型恰好读了那个文件——这些指令会
一起被卷进摘要
再也无法区分
草稿区,也会忠实地把注入指令一并保留下来。流水线里没有一个环节去区分“这是用户说的”还是“这是模型读到的文件里写的”。
换句话说:
prompt injection 不仅能影响当前回答,还可能“固化”进被压缩后的长期上下文里。
六、写在最后:从这套设计能学到什么
抛开 Claude Code 本身,这套压缩流水线其实是一份很好的
上下文工程范本
- 不要一上来就动用最贵、最有损的手段;先穷尽零成本、可恢复的清理。
分层降级,便宜的先上。
- 落盘而非截断、折叠而非删除、视图过滤而非物理移除——把“不可逆操作”压到最后、最少。
能可逆就别不可逆。
- 与其追求完美的 token 估算,不如做好 413 兜底。这是工程上的成熟,而非妥协。
接受不完美,但准备好恢复路径。
- 摘要强制保留“做了什么 / 当前状态 / 关键决策”,而不是自由发挥——固定模板能显著降低“丢掉要紧细节”的概率。
结构化地保留关键信息。
- 会话内的整洁度和跨会话的长期知识,是两套互补的系统,别用一个去硬扛另一个的活。
压缩 ≠ 记忆。
想自己摸一遍?在真实会话里跑一次 /context,对照本文的阈值,看看 system prompt、tools、memory、messages 和那块 autocompact 缓冲各占多少 token——把“理论”和“实测”对上号,比只读源码更有体感。
附:技术坐标与说明
- 压缩流水线源码位于
src/services/compact/,约。3,960 行 TypeScript,横跨 5 个文件
- 关键函数:
autoCompactIfNeeded/shouldAutoCompact/calculateTokenWarningState/trySessionMemoryCompaction/compactConversation/microcompactMessages/projectView。
