200K 的窗口,跑完 400K 的任务:Claude Code 上下文压缩机制全拆解

一次真实的编码会话,token 总量可能轻松冲到 400K 甚至更高------可模型的上下文窗口只有 200K。Claude Code 是怎么把"无限增长的对话"塞进"固定大小的窗口",还能一路保持思路连贯的?

答案不是"撑满了就总结一下"这么简单。它背后是一条 5 级渐进式压缩流水线:最便宜、最无损的手段先上,最贵、最有损的手段压到最后才用。本文做一次源码级拆解。

文章内容:

  • Claude Code 用 5 层手段 管理上下文,核心哲学是渐进式压缩:cheapest first, heaviest 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, heaviest last)

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

下面是完整的 5 级全景。

scss 复制代码
消息历史持续增长

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 的预览

lua 复制代码
<persisted-output>
Output too large (2.3 MB). Full output saved to:
/tmp/.claude/session-xxx/tool-results/toolu_abc123.txt
Preview (first 2.0 KB):
[前 2000 字节内容] ...
</persisted-output>

为什么是"落盘"而不是"截断"? 截断意味着永久丢失 ------万一 bug 恰好藏在第 500 行呢?落盘后,模型如果后续真需要那段内容,可以用 Read 工具从磁盘把完整文件取回来。2KB 预览则刚好够它判断"要不要去取"。

✍️ 这是对很多笔记里"Snip 就是直接截断"说法的修正:第一层的本质是可恢复的落盘,而不是丢弃。这恰恰呼应了整套系统"尽量不做不可逆操作"的取向。

L2 · 历史裁剪(History Snip)

可以把这一层理解成对话脚手架的垃圾回收。会话里那些重复的 assistant 包装、冗余的记账信息、早就不影响下一步决策的旧片段,会在更重的压缩启动之前先被裁掉。它同样是零成本,且只动"明显没用"的部分。

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

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

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

3.1 它专门清理哪些东西

微压缩只回收"旧工具调用返回的大块原始内容",因为这些内容有个共同点------丢了还能再拿回来

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

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

微压缩内部分两条路,互斥,按场景择一:

scss 复制代码
                microcompactMessages()
                          |
            +-------------+--------------+
            |                            |
      长时间未交互?                 缓存仍然有效?
            |                            |
           是                           是
            |                            |
   Time-based Microcompact        Cached Microcompact
     (直接改本地消息内容)         (改用 cache_edits)
            |                            |
        冷缓存场景                    热缓存场景
对比项 Time-based Microcompact Cached Microcompact
使用场景 长时间没继续对话,缓存大概率已过期 对话持续进行,缓存仍然有效
是否修改本地消息 修改 不修改
如何删除内容 把旧结果替换为固定占位符 给 API 发送 cache_edits
是否保留提示缓存 不需要(缓存已经冷了) 尽量保留缓存前缀
触发依据 时间间隔 工具数量阈值
优先级 最高,触发后直接返回 时间路径未触发时才执行

Cached 路径的巧思 :它不改本地消息,而是给服务端发一条 cache_edits 指令,让服务端在原缓存块里"就地标记删除" ,从而不破坏宝贵的缓存前缀:

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

🧠 类比 :把它想成数据库里的视图(View) ------底层的消息数组(表)原封不动,但每次 API 请求看到的是一个被过滤、被精简后的"投影"。

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

Q:微压缩后,被标记删除的旧工具结果,cache 里还缓存着吗?

A:会暂时同时存在于"服务端原始缓存块"和"本地消息历史"里,但在模型当前使用的有效缓存视图中已被排除 ,不再占用上下文。一句话------cache 里仍包含它,但模型有效上下文不包含它。

所在位置 是否还在
Claude Code 本地 messages 还在
本地会话记录 / transcript 通常还在
服务端原始缓存块 很可能暂时还在,直到 TTL 过期
模型当前有效上下文 不在
后续请求的有效 token 统计 不再计入活动上下文

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

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

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

折叠层最大的优点是非破坏性、可回滚 :原始消息从不删除 ,生成的摘要存放在一个独立的 collapse store ,由 projectView() 在请求时把摘要"叠加"到原始消息之上。换句话说,模型看到的是折叠后的视图,但底层数据还在------需要时能还原。

它大约在 ~90% 利用率 开始提交(commit), ~95% 进入更强的阻塞式处理。和自动压缩的完整对比:

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

一句话记住差异:

  • AutoCompact :以"对话段落"为压缩单位 → "上下文快满了,把过去整体总结一次。"
  • Context Collapse :以"事实、决定、状态、产物"为提交单位 → "在上下文变满的过程中,持续把有价值的信息提交出去,再逐步折叠已处理的活动上下文。"

L5 · 自动压缩(AutoCompact)------ 唯一真正"有损"的一层

走到这里,才会真的调用一次 LLM 做摘要。这也是幻灯片上"有损不可逆"画框的那一层。

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

  • 保留约 20,000 token 给"写摘要"本身用;
  • 保留约 13,000 token 作为缓冲------所以当有效窗口只剩约 13K 时触发;
  • 另有 3,000 token 的手动压缩缓冲:只剩这么多时,新请求会被阻塞,提示你手动 /compact

5.2 调用链:一张图看懂"该不该压、怎么压"

arduino 复制代码
用户发一条消息
        │
        ▼
主对话循环(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 知道"之前发生过什么",能接着干活。

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

再周密的估算也有失手的时候------某个工具结果意外巨大、多个系统提醒同时注入、token 估算偏低......一旦 API 直接返回 413(Prompt Too Long)Reactive Compact 会立刻触发:只保留最后 4 条消息、其余全部摘要 ,然后重试。一个 hasAttemptedReactiveCompact 守卫保证它只尝试一次,不会陷入重试死循环;若一次仍不够,错误才上抛给用户。

这一层的哲学很值得玩味:与其追求完美的 token 计数,不如接受估算的不精确,并提供一条稳健的恢复路径。

5.4 以及:手动 / Agent 主动压缩

除了自动触发,压缩也能被主动调用:你随时可以 /compact(还能附带指令,比如"重点保留认证相关的工作");Agent 自己也可能在即将切换到一个完全不同的任务 时,主动 compact 清空当前上下文,为新任务腾地方。


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

回到最初的灵魂拷问。先把结论说清楚:这条流水线里,前四层基本都是无损或可回滚的 ------落盘可 Read 回来、折叠可还原、微压缩只是"视图"过滤。真正不可逆的,只有第 5 层那次 LLM 摘要。 系统的全部努力,就是让对话尽量止步于前几层,把"有损摘要"压到最少发生

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

  1. 跨会话的记忆系统 :压缩管的是"当前会话的台面整洁度",而 CLAUDE.md / auto memory 管的是"跨会话的长期知识"。每个会话从干净上下文开始,记忆文件在开头被读入;auto memory 还能让 Claude 根据你的纠正自动记笔记。两者配合,Agent 既不被当前对话淹没,又不丢失重要的长期信息。
  2. 完整的原始档案(transcript).transcripts/ 里保存了压缩前的全部原始对话 。它是事后取证 / 回溯用的备份------Agent 平时不会主动去翻,但只要你需要,一切都在。

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

这是整套设计里一个容易被忽视、却很值得警惕的点:压缩流水线对所有内容一视同仁。

摘要器(summarizer)会用同一条流水线处理"用户指令"和"工具结果"。如果攻击者在某个项目文件里埋了恶意指令,而模型恰好读了那个文件------这些指令会一起被卷进摘要 ,在压缩后与正常上下文再也无法区分 。那个让摘要质量很高的 <analysis> 草稿区,也会忠实地把注入指令一并保留下来。流水线里没有一个环节去区分"这是用户说的"还是"这是模型读到的文件里写的"。

换句话说: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

相关推荐
小鼻子的猫4 小时前
独立开发 30 天:2.5 万行代码,23 个 Bug,5 次重构——一个 AI 社区的诞生
架构
咖啡八杯4 小时前
GoF设计模式——命令模式
java·设计模式·架构
candyTong5 小时前
阿里开源 AI Code Review 工具:ocr review 的执行链路解析
javascript·后端·架构
doiito19 小时前
【Agent Harness】TPS的“自工程完结”教会了我一件事:别把Bug留给下一道工序
架构·rust
烬羽19 小时前
中英文 token 数量差一倍?两段 JS 代码搞懂 LLM 底层是怎么"读"文字的
javascript·程序员·架构
白鲸开源1 天前
一文读懂DolphinScheduler插件机制:如何轻松扩展任务类型与数据源
java·架构·github
棒槌开发师1 天前
动态组件设计(elpis)
架构
得物技术1 天前
从表单到 Agent:得物社区活动搭建的 AI 实践之路
人工智能·架构·agent
Ausra无忧1 天前
记录在公司把单服务器升级成多服务器架构流程
前端·后端·架构