上下文压缩管理
上下文压缩管理
很多人第一次接触 Claude Code,都会以为它的“长上下文”能力主要来自模型本身。
但从源码看,真正撑住长任务的,不只是上下文窗口,而是一整套分级压缩机制。
Claude Code 并不是等消息塞满之后,简单做一次摘要。它在主查询链路里准备了多层处理:
- 工具结果预算裁剪
snip细粒度裁剪microcompact微压缩context collapse折叠视图autocompact自动摘要压缩reactive compact出错后的兜底压缩
这意味着 Claude Code 的“上下文管理”本质上是一个多阶段管线,而不是单点能力。
为什么 Claude Code 必须做压缩
Claude Code 的任务不是一次问答,而是持续执行工程任务:
- 读取多个文件
- 搜索代码库
- 运行 Bash 命令
- 写文件和补丁
- 调用子 Agent
- 与 MCP / LSP 交换结果
这些行为会不断把新消息和工具结果追加进会话历史。
如果没有压缩,模型很快就会被旧消息、长工具输出和附件塞满。
Anthropic 在系统提示词里甚至直接提醒了这一点:
1 | function getSystemRemindersSection(): string { |
对应中文可以理解成:
这段对话会通过自动摘要获得“近似无限”的上下文。
更明确的一句还出现在 getSimpleSystemSection() 里:
1 | `The system will automatically compress prior messages in your conversation as it approaches context limits.` |
中文就是:
当对话接近上下文限制时,系统会自动压缩更早的消息。
所以从产品承诺到运行时实现,Claude Code 都把“自动压缩”当成基础设施,而不是补丁逻辑。
主入口在 query.ts
真正的压缩主链在 /Users/xuanyuan/Downloads/claude-code-src/query.ts。
从源码顺序可以看出,它不是只做一次 compact(),而是多层串联:
1 | messagesForQuery = await applyToolResultBudget(...) |
这里最值得注意的,不是函数名本身,而是它们的执行顺序:
- 先裁掉过大的工具结果
- 再做
snip - 再做
microcompact - 再投影
context collapse - 最后才尝试
autocompact
也就是说,Claude Code 并不急着把旧历史粗暴压成一段摘要,而是优先尝试保留更多细节。
第 1 层:工具结果预算裁剪
最先运行的是 applyToolResultBudget(...)。
1 | messagesForQuery = await applyToolResultBudget( |
这一层的目标很直接:
在进入真正的上下文压缩之前,先把明显过大的工具结果做替换或裁剪。
这很重要,因为很多时候占空间的不是用户消息,而是:
BashTool打出来的大段终端输出- 搜索工具返回的长结果
- 文件读取工具读到的大文件片段
如果这类结果不先处理,后面的压缩就会被低价值大文本拖累。
第 2 层:snip
源码注释已经把它的定位写得很清楚:
1 | // Apply snip before microcompact (both may run — they are not mutually exclusive). |
这句话说明两件事:
snip和microcompact不是互斥关系snip更靠前,属于更轻量的局部瘦身
对应代码:
1 | const snipResult = snipModule!.snipCompactIfNeeded(messagesForQuery) |
从返回值也能看出它的作用:
- 产出新的消息数组
- 记录释放了多少 token
- 必要时生成边界消息
你可以把 snip 理解成:
在不破坏主要会话结构的前提下,先对低价值部分做局部裁剪。
第 3 层:microcompact
microcompact 比 snip 更进一步,但还没有进入“整段历史总结”的阶段。
源码里有一句很关键的注释:
1 | // Apply microcompact before autocompact |
而且前面还有一句更有信息量:
1 | // cached MC operates purely by tool_use_id |
这说明 microcompact 很大一部分设计目标,是围绕工具调用记录做细粒度压缩,尤其适合:
- 工具调用链很多
- 某些工具结果内容很长
- 但工具调用本身的结构信息仍然值得保留
它和 snip 的区别可以粗略理解为:
snip:先削掉一部分内容microcompact:对局部消息块做更结构化的微压缩
这一层体现了 Claude Code 一个很强的工程判断:
只要还能保留结构化上下文,就不要急着把它们全变成一段摘要。
第 4 层:context collapse
这是源码里很容易被忽视,但实际上非常聪明的一层。
源码注释原文非常值得看:
1 | // Project the collapsed context view and maybe commit more collapses. |
对应中文意思是:
在进入自动压缩前,先投影一个折叠后的上下文视图。
如果折叠之后已经回到阈值以下,那么自动压缩就不需要执行,这样就能保留更细粒度的上下文,而不是把它们合成一段大摘要。
对应调用:
1 | const collapseResult = await contextCollapse.applyCollapsesIfNeeded( |
这里的关键思想不是“删除历史”,而是“重新投影视图”。
也就是说,底层日志未必被彻底抹掉,但当前喂给模型的视图被折叠了。
这和后面 sessionStorage.ts 里的 contextCollapseCommits、contextCollapseSnapshot 正好对上:
1 | const contextCollapseCommits: ContextCollapseCommitEntry[] = [] |
这说明 Claude Code 对 collapse 的处理,不只是临时内存操作,而是有提交记录和快照概念的。
第 5 层:autocompact
真正大家通常理解的“自动摘要压缩”,在源码里对应 deps.autocompact(...)。
1 | const { compactionResult, consecutiveFailures } = await deps.autocompact( |
如果成功,会返回 compactionResult,然后马上构建压缩后的消息链:
1 | const postCompactMessages = buildPostCompactMessages(compactionResult) |
这一步有几个关键点:
- 不只是生成摘要,还会重建 post-compact 消息序列
- 压缩结果会真正回写进当前对话执行链
- 后续模型请求会基于压缩后的消息继续进行
换句话说,autocompact 不是旁路日志,而是会改变主循环继续执行时看到的上下文。
第 6 层:reactive compact
如果前面的主动压缩还不够,Claude Code 还有一条兜底链路:reactive compact。
这段逻辑在 query.ts 的流式返回后半段:
1 | if ((isWithheld413 || isWithheldMedia) && reactiveCompact) { |
这里处理两种常见失败:
prompt too long- 媒体内容过大,例如图片 / PDF / 多图输入
也就是说,reactive compact 不是日常首选路径,而是:
当真实 API 调用已经报错时,再用一次恢复性压缩把任务救回来。
这一点很能体现 Claude Code 的工程成熟度。
它不是假设“主动压缩一定成功”,而是把失败恢复也纳入主循环设计。
compact_boundary 才是压缩真正落盘的边界
压缩不仅发生在内存里,还会影响会话持久化和恢复。
QueryEngine.ts 里有专门的 compact_boundary 处理逻辑:
1 | if ( |
同时在回放时它也被当作一种需要确认的系统消息:
1 | (msg.type === 'system' && msg.subtype === 'compact_boundary') |
这说明 compact_boundary 的作用不是展示 UI 提示,而是:
- 标记一次压缩在会话链条中的边界
- 告诉 transcript 哪一段历史已经被总结
- 给恢复逻辑一个重新拼接 preserved segment 的锚点
sessionStorage.ts 里为什么这么复杂
如果只做“摘要替换”,会话恢复其实很简单。
但 Claude Code 不是这样,所以 sessionStorage.ts 里有很多专门处理压缩边界和保留段的逻辑。
最典型的是这段注释:
1 | /** |
以及 applyPreservedSegmentRelinks(...) 里的这段解释:
1 | // Only the LAST seg-boundary is relinked — earlier segs were summarized |
它表达的是一个很重要的设计:
- 压缩后不是所有旧消息都消失
- 有些片段会被保留
- 会话恢复时要把这些片段重新接回链上
这也是为什么 Claude Code 的上下文压缩,不是普通聊天产品里那种“把前文总结成一段文字”那么简单。
它其实是“分级压缩”,不是“统一摘要”
现在可以把整套机制总结成一个更准确的图:
这是一个明显的“层层升级”设计:
- 能局部处理,就不做全局摘要
- 能折叠视图,就不立即合并成摘要
- 主动压缩失败后,再走恢复性压缩
这种分级处理的目标很明确:
尽量延后信息损失,尽量保留结构,最后才牺牲细节。
它还和 Prompt Cache 有关系
在 constants/prompts.ts 里有一个很关键的常量:
1 | export const SYSTEM_PROMPT_DYNAMIC_BOUNDARY = |
源码注释说得很直白:
1 | * Everything BEFORE this marker in the system prompt array can use scope: 'global'. |
中文就是:
这个边界之前的 system prompt 可以进入全局缓存;之后的部分是用户或会话相关内容,不应该缓存。
这和上下文压缩放在一起看,会更容易理解 Claude Code 的总体策略:
- 静态 system prompt 尽量缓存
- 动态上下文尽量分层压缩
- 历史消息通过 boundary 与 snapshot 管理
所以它不是单纯在“缩消息”,而是在同时管理:
- token 成本
- 上下文可持续性
- prompt cache 命中
- resume 恢复正确性
用户真正感知到的效果是什么
这套机制最终会带来几个实际体验:
- 长任务不会因为读了太多文件立刻崩掉
- 对话可以持续很多轮,而不是越来越迟钝
- Claude Code 在超限时有自救能力
/resume恢复出来的会话仍然能接上前文- 它会优先保留结构,而不是一上来就把历史压成一坨摘要
最后一句话总结
Claude Code 的上下文压缩,不是“接近上限时做一次摘要”这么简单。
从源码看,它更像一套分层内存管理系统:
- 前面几层尽量局部瘦身
- 中间一层尽量折叠视图保留结构
- 后面才做全局摘要
- 再后面还有真实报错后的恢复压缩
所以如果你把 Claude Code 当成“模型 + 工具”来看,会低估它。
真正让它能持续完成复杂工程任务的,是这种运行时级别的上下文管理能力。