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