首页 > 教程攻略 > ai教程 >一文拆解Claude Code中上下文压缩机制

一文拆解Claude Code中上下文压缩机制

来源:互联网 时间:2026-06-29 07:11:46

先说一个核心判断:在真实的编码会话中,token 总量轻松飙到 400K 甚至更高,而模型的上下文窗口只有 200K。Claude Code 究竟是怎么把“无限增长的对话”塞进“固定大小的窗口”,还能一路保持思路连贯的?

答案不是“撑满了就总结一下”这么简单。其背后是一条

5 级渐进式压缩流水线

:最便宜、最无损的手段先上,最贵、最有损的手段压到最后才用。本文就来一次源码级的拆解。

文章核心要点:

  • Claude Code 用

    5 层手段

    管理上下文,核心哲学是

    渐进式压缩:cheapest first, hea viest last

  • 前 4 层(工具结果落盘、历史裁剪、微压缩、上下文折叠)

    几乎零成本、且基本无损/可回滚

  • 只有最后一层

    AutoCompact

    会真正调用一次 LLM 做摘要——

    这一层才是“有损不可逆”的

    ,而

    绝大多数对话根本走不到这里

  • “有损不可逆”的根本局限,由两件事兜底:

    跨会话的记忆系统

    (CLAUDE.md / auto memory)+

    完整保留原始对话的 transcript

  • 整条压缩流水线位于源码 src/services/compact/,约

    3,960 行 TypeScript / 5 个文件

适合谁读

:用 Claude Code 但好奇它“内部怎么转”的开发者;做 Agent / LLM 应用、需要自己设计上下文工程的工程师。

一、先理解问题:窗口有限,对话无限

不妨把上下文窗口想象成模型的

工作台面

。对一个 200K token 的模型来说,这块台面在你发第一条消息之前就已经被占掉一部分了:

  • 系统提示词 + 工具定义

    :约 20–25K token,是雷打不动的固定开销。
  • 系统提醒(system reminders)

    :每轮再加 200–2,000 token,视情况而定。
  • 剩下大约

    175K token 才是“对话历史”能用的空间

问题在于:

历史会无限增长,窗口却不会

。你读一个 3,000 行的文件、跑几条 grep、让它改几轮代码——每次工具调用都往台面上堆 2K–8K token。不处理的话,迟早撑爆。

更微妙的是:

撑爆之前,质量就已经开始下滑了

。这和电脑内存很像——你可以把内存用到 95%,但最后那点空间会被换页、GC、系统开销吃掉,程序反而卡死。LLM 同理:那块“空着”的上下文不是浪费,

它是模型“思考”的地方

。窗口越满,模型用来规划、权衡、评估代码改动的余地就越小。

所以 Claude Code 的目标不是“用满”,而是

在台面变乱的过程中持续清理,始终给推理留出余量

二、核心设计哲学:渐进式压缩

整条流水线只有一句话原则:

最便宜的手段先上,最重的手段最后用。

(cheapest first, hea viest last)

每往下一层,要么消耗更多算力,要么丢掉更多细节。于是系统能做到:“能用零成本手段解决的,绝不动用花钱又有损的 LLM 摘要。” 实测结论也印证了这一点:

绝大多数对话根本走不到最后一层。

下面是完整的 5 级全景。

  • 消息历史持续增长
  • L1 · 工具结果预算 单块 >50K 字符 → 落盘 + 2KB 预览 成本:零 · 可恢复
  • L2 · 历史裁剪 Snip 回收陈旧的对话脚手架 成本:零
  • L3 · 微压缩 MicroCompact 回收旧工具输出 · 时间型/缓存型双路径 成本:零 API 调用
  • L4 · 上下文折叠 Context Collapse ~90% 触发 · 投影式折叠 · 可回滚 成本:零 · 非破坏性
  • L5 · 自动压缩 AutoCompact 分叉子 Agent 生成 LLM 摘要 成本:一次 API 调用 · 不可逆
  • 组装最终 API 请求

三、逐层拆解

L1 · 工具结果预算(Tool Result Budget)

问题

:某个工具一次就吐出一个巨大的内容块——比如读了个几 MB 的文件、跑了条刷屏的命令。

做法

:当单个工具结果超过阈值(DEFAULT_MAX_RESULT_SIZE_CHARS,约

50,000 字符

)时,Claude Code

不会粗暴截断

,而是把完整输出

落盘

,只在上下文里留一个约

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 字节内容] ...

为什么是“落盘”而不是“截断”?

截断意味着

永久丢失

——万一 bug 恰好藏在第 500 行呢?落盘后,模型如果后续真需要那段内容,可以用 Read 工具从磁盘把完整文件取回来。2KB 预览则刚好够它判断“要不要去取”。

这是对很多笔记里“Snip 就是直接截断”说法的修正:第一层的本质是

可恢复的落盘

,而不是丢弃。这恰恰呼应了整套系统“尽量不做不可逆操作”的取向。

L2 · 历史裁剪(History Snip)

可以把这一层理解成

对话脚手架的垃圾回收

。会话里那些重复的 assistant 包装、冗余的记账信息、早就不影响下一步决策的旧片段,会在更重的压缩启动之前先被裁掉。它同样是零成本,且只动“明显没用”的部分。

L3 · 微压缩(MicroCompact)—— 回收旧工具输出

这是最关键、也最精妙的一层。一句话定性:

MicroCompact 不是“总结历史”,而是“对旧工具输出做垃圾回收”。

它专门清理哪些东西

微压缩只回收“旧工具调用返回的大块原始内容”,因为这些内容有个共同点——

丢了还能再拿回来

工具类型为什么适合清理
Read文件之后可以重新读取
Bash / Shell旧日志通常很大,而且可能已经过时
Grep搜索结果可以重新生成
Glob文件列表可以重新扫描
WebSearch / WebFetch网页内容可以重新获取
Edit / Write修改结果通常已经落到文件系统里

两条互斥的路径:时间型 vs 缓存型

微压缩内部分两条路,

互斥

,按场景择一:

microcompactMessages()
|
+-------------+--------------+
| |
长时间未交互? 缓存仍然有效?
| |
是 是
| |
Time-based Microcompact Cached Microcompact
(直接改本地消息内容) (改用 cache_edits)
| |
冷缓存场景 热缓存场景

对比项Time-based MicrocompactCached Microcompact
使用场景长时间没继续对话,缓存大概率已过期对话持续进行,缓存仍然有效
是否修改本地消息修改

不修改

如何删除内容把旧结果替换为固定占位符给 API 发送 cache_edits
是否保留提示缓存不需要(缓存已经冷了)

尽量保留缓存前缀

触发依据时间间隔工具数量阈值
优先级

最高

,触发后直接返回
时间路径未触发时才执行

Cached 路径的巧思

:它不改本地消息,而是给服务端发一条 cache_edits 指令,让服务端

在原缓存块里“就地标记删除”

,从而不破坏宝贵的缓存前缀:

已有缓存: A B C D E F G
↓ 发送 cache_edits:基于原缓存,把 C 标记为删除
有效视图: A B D E F G

类比

:把它想成数据库里的

视图(View)

——底层的消息数组(表)原封不动,但每次 API 请求看到的是一个被过滤、被精简后的“投影”。

一个容易踩坑的追问:cache 里到底还在不在?

微压缩后,被标记删除的旧工具结果,cache 里还缓存着吗?会暂时同时存在于“服务端原始缓存块”和“本地消息历史”里,但在

模型当前使用的有效缓存视图中已被排除

,不再占用上下文。一句话——

cache 里仍包含它,但模型有效上下文不包含它。

所在位置是否还在
Claude Code 本地 messages

还在

本地会话记录 / transcript通常

还在

服务端原始缓存块很可能暂时

还在,直到 TTL 过期

模型当前有效上下文

不在

后续请求的有效 token 统计

不再计入活动上下文

L4 · 上下文折叠(Context Collapse)—— 增量、可回滚

如果前几层还不够,进入折叠层。它的定位和“自动压缩”有本质区别:

Context Collapse 是持续、增量式的上下文管理;AutoCompact 是达到阈值后对整段会话的一次集中式重写。

折叠层最大的优点是

非破坏性、可回滚

:原始消息

从不删除

,生成的摘要存放在一个

独立的 collapse store

,由 projectView() 在请求时把摘要“叠加”到原始消息之上。换句话说,模型看到的是折叠后的视图,但底层数据还在——需要时能还原。

它大约在

~90% 利用率

开始提交(commit),

~95%

进入更强的阻塞式处理。和自动压缩的完整对比:

对比项Context CollapseAutoCompact(自动压缩)
工作方式持续、增量式管理达到阈值后一次性执行
处理粒度更细,逐步提交可保留信息更粗,把旧上下文整体摘要化
触发阶段约 90% 开始 commit,95% 进入阻塞处理有效窗口剩约 13K token 时触发
是否调用模型通常由独立 context agent 整理、提交会调用模型生成摘要
对原消息的影响维护独立的 committed log,逐步折叠活动上下文直接用“摘要 + 保留的近期消息”替换旧消息
中断程度偏增量、后台式整理类似一次 stop-the-world 压缩
信息保留倾向保存更细粒度的结构化信息主要依赖摘要质量
能否与自动压缩并存开启 Collapse 后,主动 AutoCompact 被禁用

一句话记住差异:

  • AutoCompact

    :以“对话段落”为压缩单位 → “上下文快满了,把过去整体总结一次。”
  • Context Collapse

    :以“事实、决定、状态、产物”为提交单位 → “在上下文变满的过程中,持续把有价值的信息提交出去,再逐步折叠已处理的活动上下文。”

L5 · 自动压缩(AutoCompact)—— 唯一真正“有损”的一层

走到这里,才会真的

调用一次 LLM 做摘要

。这也是“有损不可逆”的那一层。

触发与阈值(以 200K 窗口为例)

  • 保留约 20,000 token

    给“写摘要”本身用;
  • 保留约 13,000 token

    作为缓冲——所以当有效窗口只剩约 13K 时触发;
  • 另有

    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

几个值得注意的设计:

  • 熔断器(circuit breaker)

    :连续失败 3 次就停止再试,避免在异常情况下反复烧钱、卡死。
  • 会话记忆优先

    :在真正花钱做完整摘要之前,先尝试用后台预先攒好的“会话记忆”来压——能省掉那次昂贵的模型调用。
  • 摘要的提示词

    强制保留三类信息:

    完成了什么、当前状态、做过的关键决策

    。压缩后 messages 只剩一条,但 Agent 知道“之前发生过什么”,能接着干活。

还有一层“兜底中的兜底”:反应式压缩(Reactive Compact)

再周密的估算也有失手的时候——某个工具结果意外巨大、多个系统提醒同时注入、token 估算偏低……一旦 API 直接返回

413(Prompt Too Long)

Reactive Compact

会立刻触发:

只保留最后 4 条消息、其余全部摘要

,然后重试。一个 hasAttemptedReactiveCompact 守卫保证它

只尝试一次

,不会陷入重试死循环;若一次仍不够,错误才上抛给用户。

这一层的哲学很值得玩味:

与其追求完美的 token 计数,不如接受估算的不精确,并提供一条稳健的恢复路径。

以及:手动 / Agent 主动压缩

除了自动触发,压缩也能被主动调用:你随时可以 /compact(还能附带指令,比如“重点保留认证相关的工作”);Agent 自己也可能在

即将切换到一个完全不同的任务

时,主动 compact 清空当前上下文,为新任务腾地方。

四、那个绕不开的问题:有损不可逆,怎么破?

回到最初的灵魂拷问。先把结论说清楚:

这条流水线里,前四层基本都是无损或可回滚的

——落盘可 Read 回来、折叠可还原、微压缩只是“视图”过滤。

真正不可逆的,只有第 5 层那次 LLM 摘要。

系统的全部努力,就是

让对话尽量止步于前几层,把“有损摘要”压到最少发生

但只要它会发生,信息论上就一定有损耗。Claude Code 用两件事来兜底这条根本局限:

  1. 跨会话的记忆系统

    :压缩管的是“当前会话的台面整洁度”,而 CLAUDE.md / auto memory 管的是“跨会话的长期知识”。每个会话从干净上下文开始,记忆文件在开头被读入;auto memory 还能让 Claude 根据你的纠正自动记笔记。两者配合,Agent 既不被当前对话淹没,又不丢失重要的长期信息。
  2. 完整的原始档案(transcript)

    .transcripts/ 里保存了

    压缩前的全部原始对话

    。它是事后取证 / 回溯用的备份——

    Agent 平时不会主动去翻

    ,但只要你需要,一切都在。

五、一个少有人提的隐患:注入指令会“穿透”压缩

这是整套设计里一个容易被忽视、却很值得警惕的点:

压缩流水线对所有内容一视同仁。

摘要器(summarizer)会用同一条流水线处理“用户指令”和“工具结果”。如果攻击者在某个项目文件里埋了恶意指令,而模型恰好读了那个文件——这些指令会

一起被卷进摘要

,在压缩后与正常上下文

再也无法区分

。那个让摘要质量很高的 草稿区,也会忠实地把注入指令一并保留下来。

流水线里没有一个环节去区分“这是用户说的”还是“这是模型读到的文件里写的”。

换句话说:

prompt injection 不仅能影响当前回答,还可能“固化”进被压缩后的长期上下文里。

做 Agent 安全的同学,这是个值得单独深挖的攻击面。

六、写在最后:从这套设计能学到什么

抛开 Claude Code 本身,这套压缩流水线其实是一份很好的

上下文工程范本

  1. 分层降级,便宜的先上。

    不要一上来就动用最贵、最有损的手段;先穷尽零成本、可恢复的清理。
  2. 能可逆就别不可逆。

    落盘而非截断、折叠而非删除、视图过滤而非物理移除——把“不可逆操作”压到最后、最少。
  3. 接受不完美,但准备好恢复路径。

    与其追求完美的 token 估算,不如做好 413 兜底。这是工程上的成熟,而非妥协。
  4. 结构化地保留关键信息。

    摘要强制保留“做了什么 / 当前状态 / 关键决策”,而不是自由发挥——固定模板能显著降低“丢掉要紧细节”的概率。
  5. 压缩 ≠ 记忆。

    会话内的整洁度和跨会话的长期知识,是两套互补的系统,别用一个去硬扛另一个的活。

想自己摸一遍?在真实会话里跑一次 /context,对照本文的阈值,看看 system prompt、tools、memory、messages 和那块 autocompact 缓冲各占多少 token——把“理论”和“实测”对上号,比只读源码更有体感。

附:技术坐标与说明

  • 压缩流水线源码位于 src/services/compact/,约

    3,960 行 TypeScript,横跨 5 个文件

  • 关键函数:autoCompactIfNeeded / shouldAutoCompact / calculateTokenWarningState / trySessionMemoryCompaction / compactConversation / microcompactMessages / projectView