核心机制

上下文压缩管理

很多人第一次接触 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,
  ...
)

这里最值得注意的,不是函数名本身,而是它们的执行顺序:

  1. 先裁掉过大的工具结果
  2. 再做 snip
  3. 再做 microcompact
  4. 再投影 context collapse
  5. 最后才尝试 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).

这句话说明两件事:

  1. snipmicrocompact 不是互斥关系
  2. snip 更靠前,属于更轻量的局部瘦身

对应代码:

const snipResult = snipModule!.snipCompactIfNeeded(messagesForQuery)
messagesForQuery = snipResult.messages
snipTokensFreed = snipResult.tokensFreed
if (snipResult.boundaryMessage) {
  yield snipResult.boundaryMessage
}

从返回值也能看出它的作用:

  • 产出新的消息数组
  • 记录释放了多少 token
  • 必要时生成边界消息

你可以把 snip 理解成:

在不破坏主要会话结构的前提下,先对低价值部分做局部裁剪。

第 3 层:microcompact

microcompactsnip 更进一步,但还没有进入“整段历史总结”的阶段。

源码里有一句很关键的注释:

// 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 里的 contextCollapseCommitscontextCollapseSnapshot 正好对上:

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.

它表达的是一个很重要的设计:

  1. 压缩后不是所有旧消息都消失
  2. 有些片段会被保留
  3. 会话恢复时要把这些片段重新接回链上

这也是为什么 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 恢复正确性

用户真正感知到的效果是什么

这套机制最终会带来几个实际体验:

  1. 长任务不会因为读了太多文件立刻崩掉
  2. 对话可以持续很多轮,而不是越来越迟钝
  3. Claude Code 在超限时有自救能力
  4. /resume 恢复出来的会话仍然能接上前文
  5. 它会优先保留结构,而不是一上来就把历史压成一坨摘要

最后一句话总结

Claude Code 的上下文压缩,不是“接近上限时做一次摘要”这么简单。
从源码看,它更像一套分层内存管理系统:

  • 前面几层尽量局部瘦身
  • 中间一层尽量折叠视图保留结构
  • 后面才做全局摘要
  • 再后面还有真实报错后的恢复压缩

所以如果你把 Claude Code 当成“模型 + 工具”来看,会低估它。
真正让它能持续完成复杂工程任务的,是这种运行时级别的上下文管理能力。