Claude Code 的五级压缩流水线:由轻到重的上下文管理艺术
引言:每个 AI Agent 都绕不开的“桌面困境”
想象你有一张固定大小的办公桌(上下文窗口),随着工作时间拉长,各种文件、资料、草稿纸会不断堆上来,直到桌面完全被占满。这时,你再想放新东西就放不下了——对应的现实是,AI 会遗忘最早的内容,甚至开始在有限的记忆里自相矛盾。
Claude 的上下文窗口高达100 万个 token,听起来很充裕。但一次真实的编程会话可以轻松产生数倍于此的数据:几十次文件读取、数百次工具调用、数千行npm test输出、反复的grep搜索结果。更棘手的是另一个现象:上下文越长,模型越蠢(官方称之为“上下文腐烂”)。那些 2 小时前读的配置文件、1 小时前调试失败的日志,全都挤在窗口里,像噪音一样持续干扰当前的注意力。
为了应对这个问题,Claude Code 在源码的src/services/compact/目录下(超过 3,960 行 TypeScript 代码)构建了一套精密的五级渐进式压缩流水线。它不是等到“快满了”才做一次暴力摘要,而是把多种上下文管理策略串成一条前置压缩链,再补一条调用后的兜底恢复链。
这套系统的核心设计哲学只有一句话:能用轻量手段解决的,绝不动用重武器。
五级全景图
在讲解每一级之前,先看整体结构:
┌─────────────────────────────────────────────────────────────────────┐ │ 前置压缩链(每次 API 调用前评估) │ │ │ │ 第一级:Snip(历史剪除)—— 删“脚手架”,最轻量 │ │ ↓ 不够就进入下一级 │ │ 第二级:Microcompact(微压缩)—— 清理“旧工具结果”,轻量 │ │ ↓ 不够就进入下一级 │ │ 第三级:Context Collapse(上下文折叠)—— 分段摘要,中等 │ │ ↓ 不够就进入下一级 │ │ 第四级:Autocompact(自动压缩)—— 全量摘要,重量级 │ │ │ │ ════════════════════════════════════════════════════════════════ │ │ 被动恢复链(API 报错后才触发) │ │ │ │ 第五级:Reactive Compact(响应式压缩)—— 紧急救援,最后防线 │ └─────────────────────────────────────────────────────────────────────┘前四级像漏斗一样层层过滤,第五级是安全网。下面逐级深入。
前置压缩链(主动预防)
第一级:Snip(历史剪除)
成本:零(纯内存数组操作,不调用模型,不做摘要)
它解决什么问题?
对话进行中,消息数组里会积累大量“对话的脚手架”——比如重复出现的 assistant 回复框架、系统内部记账用的元数据、早期已经完成的任务标记等。这些东西就像施工结束后留在现场的脚手架,除了占地方,毫无价值。
它具体怎么做?
直接从消息数组中移除那些陈旧的、不再影响后续决策的特定消息片段。它不涉及任何字符串截断,不做摘要生成,只是纯粹地从数组中删除冗余条目。
一个关键的微妙之处在于:REPL(交互界面)侧仍保留这些消息用于 UI 滚动展示,但发给 API 的消息载荷已经被剪除了。用户看到的聊天记录是完整的,但发给模型的上下文是剪裁过的。
设计思维:账本必须对得上
这一级真正的技术难点不在“删除”,而在记账准确性。当 Snip 删除了某些消息后,它会把snipTokensFreed(本次剪除释放的 token 数)记录下来,传递给后续层级的阈值计算。
为什么这么重要?因为如果没有这个校正,AutoCompact(第四级)在计算“当前上下文用了多少 token”时,拿到的会是剪除之前的旧 usage 数据——它可能错误地认为上下文仍然很满,从而在已经释放了空间的情况下仍然强行触发全量摘要,白白浪费一次 LLM 调用和费用。
这是一个极其容易被忽略的细节:压缩不仅仅是“删东西”,还要确保后续的决策建立在正确的数据之上。
第二级:Microcompact(微压缩)
成本:极低(纯本地字符串替换 / 服务端 cache_edits,不调用模型)
它解决什么问题?
在 AI 编程过程中,工具调用(Bash、Read、Grep 等)会产生海量的tool_result。许多结果——比如一次性的find搜索结果、已经执行完的ls -la输出——用过一次后就再也没有价值了,但依然死死占着上下文空间。
它具体怎么做?有两种工作模式:
模式一:时间窗模式(Time-window)
检查每条tool_result的时间戳。如果某条tool_result产生的时间距离当前对话超过 60 分钟,系统就把它替换成一个简短的占位符:[old tool result cleared]。
模式二:Cache 编辑模式(Cache edits)
Anthropic API 支持Prompt Cache机制——如果请求的开头部分相同,这部分 token 可以享受高达 90% 的计费折扣。粗暴地删除消息可能会打碎缓存前缀,导致缓存命中率暴跌——省了 token 空间,却亏了钱。
在 Cache 编辑模式下,系统通过 API 侧的cache_edits功能,在服务端精准删除旧工具结果,同时小心翼翼地保留 prompt cache 前缀的结构完整性。这是一种“微创手术”——不改变缓存前缀的哈希特征,只切除内部冗余的旧结果。
设计思维:缓存感知的“微创”压缩
为什么区分两种模式?因为缓存是钱。如果用户在对话中频繁调用工具且间隔很长,时间窗模式更简单直接;如果用户处于高密度工具调用中且缓存命中率很高,Cache 编辑模式就更经济。这个设计表明:压缩决策不仅要看 token 数,还要看成本结构。
第三级:Context Collapse(上下文折叠)
成本:中等(后台异步处理,不阻塞主流程)
它解决什么问题?
前两级解决的是“删除垃圾”,但当上下文真正由“有效历史对话”组成时,就不能再删了——需要对对话本身进行结构化重组。
它具体怎么做?
将历史消息切分成多个片段,每个片段独立生成一份摘要,然后用这些摘要替换掉原始消息。与“一次性全量摘要”不同,Context Collapse 保留了比全量摘要更细粒度的上下文结构——每段都保留了自己的主题标记,而不是把所有东西搅成一大锅粥。
整个过程在后台异步执行,用户完全感知不到压缩的发生。
如果折叠后 token 已经达标(降到了安全水位以下),第四级的 AutoCompact 就不会被触发。换句话说,第三级是第四级的“缓冲垫”——能通过分段摘要解决的问题,就不走全量摘要。
设计思维:把“同步阻塞”变成“异步保洁”
传统的全文摘要有一个致命缺点:压缩那一刻会阻塞用户——你可能要等 3~5 秒才能继续对话。而 Context Collapse 把这个过程拆解成多个小任务在后台排队执行。这就像五星级酒店的保洁:你不会看到保洁员推着车在你门口站着等你出门,她是在你离店期间悄悄打扫的。
第四级:Autocompact(自动摘要压缩)
成本:高(调用 LLM 生成全量摘要,消耗真实 token 和费用)
它解决什么问题?
当前三级都无法把上下文降到安全水位以下时,意味着“垃圾”已经清理干净、“碎片”已经整理完毕,但桌面还是满的——唯一的办法就是把整个对话浓缩成一篇精华摘要。
它具体怎么做?
- 触发阈值:有效上下文窗口 -13,000 个 token(即上下文使用超过80%时触发)。预留 13,000 个 token 是为了给模型的回复留出充足的生成空间。
- 执行方式:fork 一个独立的子 Agent,让它通读整个对话历史,生成一份完整的对话摘要。
- 替换逻辑:用这份摘要替换掉全部历史消息——对话记录清零,只留下摘要作为新的“历史上下文”。
摘要生成的优化路径:
- 优先路径(
tengu_compact_cache_prefix特性开启时):复用主 Agent 的 prompt cache,摘要生成近乎零额外的 token 开销。 - 降级路径:优先路径失败时,直接直连 API 生成摘要(成本相对更高)。
熔断机制(Circuit Breaker):连续失败3 次后自动熔断,不再重试。这是为了防止在异常情况(如 API 持续超时、模型持续报错)下反复浪费资源和费用,陷入无限重试的死亡循环。
设计思维:把 LLM 调用放在最后一步
在整条流水线中,前三级都是“免费”的本地操作,只有这一级才开始真正花钱。LLM 调用是最昂贵的资源,把它放在“万不得已”的最后一步——这就是整条流水线成本优先哲学的终极体现。
被动恢复链(响应式压缩)
第五级:Reactive Compact(响应式压缩)
成本:最高(紧急状态下调用 LLM + 强制数据处理)
触发条件:前置压缩链(第一级到第四级)全部未能阻止上下文超限,API真的返回了prompt_too_long(413)错误。
它具体怎么做?分三步执行,呈“阶梯式救援”:
第一步:尝试 Context Collapse drain(排空暂存折叠)
什么叫“drain”?在前三级中,Context Collapse 可能只是打了标记、生成了摘要草稿、但尚未正式提交到消息数组中。这一强制把所有待提交的折叠操作一次性全部落实——就像开闸放水,把蓄水池里所有的水一次性排空。
第二步:还不够则紧急触发完整对话摘要
如果第一步释放的空间仍然不够,立即强制执行一次完整对话摘要(相当于强行再跑一遍第四级 AutoCompact)。
第三步:都失败则报错退出
如果连摘要都生成不了(或压不下去),系统放弃本次请求,明确向用户报错:“上下文太长了,无法处理。”
额外细节:prompt_too_long的重试机制
当收到prompt_too_long错误时,系统会尝试通过丢弃最旧的消息组来恢复:
- 如果能从错误响应中解析出
tokenGap(超出了多少 token),就精确计算需要丢弃多少条最旧的消息。 - 如果无法解析(比如错误信息不完整),则按比例丢弃最老的 20% 消息组。
设计思维:诚实失败优于悄悄丢数据
第五级的设计非常克制——它不会无限重试,也不会为了“把请求发出去”而偷偷丢弃用户的关键指令。当它确认自己救不了的时候,选择诚实告知用户。这在工程上是一种优雅的降级:宁可报错让用户手动处理(比如开启新对话),也不要自作主张地篡改用户的原始意图。
总结:一条精打细算的防御体系
将这五级和应急链放在一起看,整个系统的结构就清晰了:
| 层级 | 名称 | 成本 | 核心操作 | 触发时机 |
|---|---|---|---|---|
| 第一级 | Snip(历史剪除) | 零 | 删除冗余“脚手架”消息 | 每次 API 调用前 |
| 第二级 | Microcompact(微压缩) | 极低 | 时间窗 / Cache 编辑,清理旧工具结果 | 每次 API 调用前 |
| 第三级 | Context Collapse(上下文折叠) | 中等 | 后台异步多段摘要 | 每次 API 调用前 |
| 第四级 | Autocompact(自动压缩) | 高 | fork 子 Agent 生成全量摘要 | 上下文超 80% 阈值 |
| 第五级 | Reactive Compact(响应式压缩) | 最高 | Drain + 紧急摘要 + 报错退出 | API 返回 413 错误后 |
这套机制真正的精妙之处在于四个设计维度:
成本排序:把上下文管理变成一条严格按成本递增排列的流水线——能本地解决的不调 API,能字符串替换的不跑模型。
缓存感知:整个设计都围绕 Anthropic 的 Prompt Cache 展开,第二级的 Cache 编辑模式和第四级的缓存复用路径,都是在“释放 token”和“保住缓存命中率”之间做精细权衡。
记账正确性:第一级释放的 token 会一路传递到第四级的阈值计算,防止“空间已经释放了却还要触发全量摘要”的逻辑乌龙。
异步非阻塞:第三级在后台默默工作,用户几乎感知不到压缩正在进行。
正如 Claude Code 团队所说:你在管理会话、上下文和压缩时的方式,对最终结果的影响比你预期的要大。这五级压缩流水线,就是 Claude Code 在这个维度上交出的答卷——不是简单粗暴的截断,也不是凌乱的、全量摘要,而是一条精打细算、层层过滤的智能流水线。