乐于分享
好东西不私藏

Claude Code 源码拆解第四篇:为什么聊久了它还没失忆?自动压缩与上下文续命机制

Claude Code 源码拆解第四篇:为什么聊久了它还没失忆?自动压缩与上下文续命机制

为什么聊久了它还没失忆?自动压缩与上下文续命机制

不是把历史删掉,而是在上下文快炸的时候,把任务接力棒稳稳传下去。

前两天,一个朋友给我发来一段很典型的 Claude Code 对话。

前面四十分钟都很顺:读文件、跑命令、改代码、查报错,节奏一点问题没有。

结果到后面,味道开始变了:

  • 明明刚看过的文件,它又想再读一遍。
  • 刚定下来的计划,它开始答非所问。
  • 回答没有完全错,但越来越飘,越来越慢。

是不是模型状态掉了?

我说,不是模型状态掉了,是上下文快满了。

第三篇我们刚讲完:工具多不一定更强,上下文脏了,agent 就会越来越笨。

第四篇更值得讲的是另一半问题:

那 Claude Code 为什么没有像很多 AI 工具那样,聊久了就直接失忆?

答案不是“它的模型更聪明”,而是它在源码里专门做了一整套自动压缩与上下文续命机制

Claude Code 不是把历史一刀切掉,而是先压缩、再补状态、再继续跑

这篇你会看到什么

  • 为什么上下文不能简单截断
  • 自动压缩在什么阈值触发
  • 为什么 compact 本质上是“续命”而不是“摘要”
  • 连 compact 请求自己都超长时,Claude Code 怎么救火
  • 压缩后文件、计划、技能、工具状态怎么补回去

一、上下文快炸的时候,为什么不能简单截断

很多人对“压缩”的直觉是:把最早的对话删掉,只留最近几轮继续聊。

这个想法看起来很合理,但对 agent 来说其实很危险。因为长任务里真正重要的,不只是“最近一句话”,还包括文件状态、plan mode、skill 状态、deferred tools、后台 agent 状态这些工作现场。

所以 Claude Code 在 src/services/compact/compact.ts 里干的事,不是简单“删旧消息”,而是:先总结历史,再把继续工作所必需的状态重新补回来。

二、自动压缩不是等爆了再救火,而是提前留缓冲区

在 src/services/compact/autoCompact.ts 里,Claude Code 先算的是 effective context window,也就是模型标称上下文扣掉预留给压缩摘要输出的空间之后,真正还能用的预算。

export function getEffectiveContextWindowSize(model: string): number {
  const reservedTokensForSummary = Math.min(
    getMaxOutputTokensForModel(model),
    MAX_OUTPUT_TOKENS_FOR_SUMMARY,
  )

  return contextWindow - reservedTokensForSummary
}

源码里默认给 summary 预留了 20_000 token。原因很直接:compact 本身也是一次模型调用,如果你把窗口吃到满格,再去请求 compact,compact 请求自己都有可能先炸。

Claude Code 不是等上下文见底才处理,而是提前留出压缩缓冲区

接着它会继续算自动压缩阈值:

export const AUTOCOMPACT_BUFFER_TOKENS = 13_000

export function getAutoCompactThreshold(model: string): number {
  const effectiveContextWindow = getEffectiveContextWindowSize(model)
  return effectiveContextWindow - AUTOCOMPACT_BUFFER_TOKENS
}

翻译成人话就是:Claude Code 会故意在真正极限之前,提前空出一块缓冲区。

为什么这一步很关键

agent 运行是动态的。你刚算完 token,下一轮 assistant、多段 tool_result、用户新贴的长日志,都可能立刻把窗口打满。系统如果总等到最后一格才处理,基本注定来不及。

三、它不只算一个阈值,而是分成 warning、error、blocking 三层

在 calculateTokenWarningState() 里,Claude Code 不是只判断“要不要 compact”,而是把上下文状态拆成 warningerrorauto compact threshold 和 blocking limit 四层。

这意味着它在处理上下文时,更像一套分级预警系统,而不是一个二元开关。很多 AI 工具的痛点就在这里:平时没预算意识,真超长了才突然丢给你一句 prompt too long,任务直接断掉。

四、自动压缩到底什么时候触发

触发判断在 shouldAutoCompact()

const tokenCount = tokenCountWithEstimation(messages) - snipTokensFreed
const threshold = getAutoCompactThreshold(model)

const { isAboveAutoCompactThreshold } = calculateTokenWarningState(
  tokenCount,
  model,
)

return isAboveAutoCompactThreshold

这里最重要的两个点是:

  1. 它用的是估算 token,而不是每次等 API 的精确统计回来。
  2. 它有递归保护,避免 compactsession_memory、某些 context collapse / reactive compact 模式里再触发 compact,防止系统死循环。

五、真正开始压缩时,它不是本线程硬做,而是 fork 一个 agent 去总结

在 compactConversation() 里,Claude Code 不是在当前主线程里直接把历史改写掉,而是发起一次专门的压缩流程,生成 summary,再用新的消息结构替换旧上下文。

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

// forked-agent path reuses main conversation's prompt cache.

这表示 compact 不是完全额外再开一份陌生请求,而是尽量复用主对话已有的 prompt cache。这样不仅更省 token,也能避免 compact 自己变成新的成本黑洞。

compact 的关键不是生成摘要,而是把一个还能继续工作的上下文重新搭出来

六、压缩前先做减法:图片、文档、重复注入的附件先剥掉

在真正发起 compact 请求之前,Claude Code 会先做减法。

stripImagesFromMessages() 会把图片和文档替换成 [image][document] 这样的标记;stripReinjectedAttachments() 会剥掉一些后面本来就会重新注入的 attachment。

这里非常能看出工程思路:不让 summarizer 背不该背的负担。 要压缩的是任务历史,不是把所有上下文原封不动再跑一遍。

七、最危险的情况:连压缩请求自己都超长了,怎么办

很多系统做到“自动压缩”这一步就停了,但 Claude Code 继续往前想了一层:如果上下文已经脏到一种程度,连 compact 请求自己都发不出去怎么办?

在 compact.ts 里,答案是 truncateHeadForPTLRetry()

它会先按 API round 分组,再从最老的一批 group 开始裁掉,然后重试 compact。配合的分组函数在 src/services/compact/grouping.ts

export function groupMessagesByApiRound(messages: Message[]): Message[][]

这一步的关键不是“裁掉最早的东西”这么简单,而是要保证切分点依然对 tool_use / tool_result 链是安全的,不把一个未闭合的调用链硬拆开。

这里最值得学的不是 happy path

最值钱的是 Claude Code 连“救火流程自己都着火了”这件事都提前想到了。成熟 agent 系统的上限,很多时候就藏在这些坏路径里。

八、压缩完成后,真正难的不是“生成摘要”,而是“怎么继续干活”

从 buildPostCompactMessages() 的返回结构就能看出来,compact 之后留下来的不只是 summary:

return [
  result.boundaryMarker,
  ...result.summaryMessages,
  ...(result.messagesToKeep ?? []),
  ...result.attachments,
  ...result.hookResults,
]

它真正干的是:压缩不是清空上下文,而是重建一个“还能工作的上下文”。

文件、计划、skill、deferred tools、后台任务状态,都是 compact 后要补回的工作现场

九、Claude Code 压缩后会主动补哪些状态

  1. 文件状态:恢复关键文件附件,但有单文件和总 token 预算。
  2. plan 状态:如果有 plan 文件或处于 plan mode,会重新注入对应 attachment。
  3. skill 状态:已调用 skill 会做截断后重新注入,避免行为规范丢失。
  4. 工具和指令状态:deferred tools delta、agent listing delta、MCP instructions delta 都会重新广播。
  5. 后台 agent 状态:仍在执行或尚未取回结果的 agent,会重新补 task status。

这套动作本质上都在做同一件事:让模型继续知道自己当前处在什么工作现场里。

十、自动压缩还自带熔断器,避免失败后反复空转

在 autoCompactIfNeeded() 里,还有一个很像分布式系统的设计:连续失败熔断器。

const MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3

如果自动压缩连续失败,Claude Code 不会在每一轮都继续发起注定失败的 compact 请求,而是到阈值后停掉后续自动尝试,避免把会话拖进更坏的状态。

十一、那 micro compact 是什么

这篇主讲自动压缩,但 src/services/compact/microCompact.ts 也值得顺手提一下。

它更像“边跑边清理桌面”:对文件读取结果、shell 输出、grep / glob / web fetch 结果、部分旧 tool_result 做轻量止血。full compact 更像一次阶段总结,micro compact 更像过程中的即时整理。

十二、这套设计对我们自己写 agent,有什么直接启发

如果你自己也在做 agent,这 4 条最值得带走

  • 不要把 compact 理解成摘要功能,它真正解决的是任务连续性。
  • 压缩一定要带状态恢复,不然只是在高级删历史。
  • 一定要给 compact 自己留缓冲区,不要吃到 99% 才想起处理。
  • 一定要设计坏路径,真正能跑长任务的 agent 拼的是故障时还能不能稳住。

总结

第三篇讲的是:上下文为什么会脏。

第四篇补上的,是另一半:上下文脏了以后,Claude Code 怎么不让 agent 当场失忆。

它的答案不是简单粗暴地删历史,而是提前算预算、在阈值附近自动 compact、用 forked agent 做总结、最坏情况下继续裁老消息重试、压完以后把文件、计划、skill、工具和后台任务状态重新补回去。

所以你会发现,Claude Code 的 compact 其实不是一个“省 token 小技巧”,而是一套真正的上下文续命系统

长任务里,真正重要的不是“模型记住了多少历史”,而是“系统有没有能力把必要状态带着活下去”。

很多人做 agent,精力都花在“多接几个工具”“多写几段 prompt”“多堆几个 workflow”。但 Claude Code 这段源码真正提醒我们的,是另一件更朴素、也更关键的事:

真正决定 agent 上限的,往往不是它最能打的时候,而是它快撑不住的时候,系统有没有办法把它救回来。

关键源码位置

  • src/services/compact/autoCompact.ts:33-49
  • src/services/compact/autoCompact.ts:62-91
  • src/services/compact/autoCompact.ts:93-145
  • src/services/compact/autoCompact.ts:160-239
  • src/services/compact/autoCompact.ts:241-351
  • src/services/compact/compact.ts:145-223
  • src/services/compact/compact.ts:243-260
  • src/services/compact/compact.ts:330-338
  • src/services/compact/compact.ts:387-760
  • src/services/compact/compact.ts:1470-1599
  • src/services/compact/grouping.ts:1-63
  • src/services/compact/microCompact.ts:40-50, 253-260