Claude Code 防上下文爆炸:源码级深度解析

作者:lizhongxuan日期:2026/4/2

基于 Claude Code v2.1.88 源码还原分析。本文从源码层面拆解 Claude Code 如何在长对话中管理上下文窗口,防止 token 爆炸,同时保持用户意图不被稀释。

问题:为什么上下文会爆炸?

Claude Code 是一个 agentic coding 工具。一次典型的编码会话中,模型会:

  • 读取十几个文件(每个几百到几千行)
  • 执行 shell 命令并获取输出
  • 搜索代码库(grep/glob 结果可能很大)
  • 编辑文件并查看 diff
  • 调用子 agent 处理子任务

每一步都会往对话历史里塞入工具调用和工具结果。一个中等复杂度的任务,对话可能膨胀到几十万 token。而 Claude 的上下文窗口是有限的(通常 200K token),超了就会报 prompt_too_long 错误,对话直接中断。

更隐蔽的问题是"意图稀释"——当上下文中充斥着大量工具结果时,用户最初的请求和最近的反馈会被淹没在海量的代码片段中,模型可能"忘记"自己在做什么。

Claude Code 用一套五层防御体系来解决这两个问题。

架构总览:五层防线

1用户输入  [L1 源头截断]  [L2 去重]  [L3 微压缩]  [L4 自动压缩]  [L5 兜底]  API
2

每一层解决不同粒度的问题,从"不让大数据进来"到"实在不行就全量摘要",层层递进。

L1:源头截断 — 在工具执行阶段就控制大小

这是最前置的防线。在工具结果返回给模型之前,就把过大的内容拦截住。

工具结果大小限制

核心常量定义在 src/constants/toolLimits.ts

1// 单个工具结果的默认上限
2export const DEFAULT_MAX_RESULT_SIZE_CHARS = 50_000    // 50K 字符
3
4// 单个工具结果的 token 上限
5export const MAX_TOOL_RESULT_TOKENS = 100_000          // 100K tokens  400KB
6
7// 单条用户消息中所有工具结果的聚合上限
8export const MAX_TOOL_RESULTS_PER_MESSAGE_CHARS = 200_000  // 200K 字符
9

当工具结果超过阈值时,maybePersistLargeToolResult()(位于 src/utils/toolResultStorage.ts)会执行"落盘 + 预览"策略:

1async function maybePersistLargeToolResult(
2  toolResultBlock, toolName, persistenceThreshold
3) {
4  const size = contentSize(content)
5  const threshold = persistenceThreshold ?? MAX_TOOL_RESULT_BYTES
6  
7  if (size <= threshold) {
8    return toolResultBlock  // 没超,原样返回
9  }
10
11  // 超了:完整内容写入磁盘临时文件
12  const result = await persistToolResult(content, toolResultBlock.tool_use_id)
13  // 只给模型一个预览 + 文件路径
14  const message = buildLargeToolResultMessage(result)
15  return { ...toolResultBlock, content: message }
16}
17

模型看到的是类似这样的内容:

1Tool output too large (523KB). Full output saved to: /tmp/.claude/tool-results/abc123.txt
2Preview (first 2000 chars):
3...2000字符的预览...
4[Use Read tool to see the full output]
5

并行工具的聚合限制

Claude Code 支持并行工具调用。如果一轮有 10 个并行 grep 搜索,每个返回 40K 字符,总共 400K 会超过单消息 200K 的聚合限制。系统会把最大的几个结果落盘,直到总量降到预算内。

其他源头限制

限制项阈值位置
Git status2,000 字符context.ts
CLAUDE.md 单文件40,000 字符claudemd.ts
FileRead 单次2,000 行FileReadTool/prompt.ts

L2:去重 — 不重复放相同内容

文件读取去重

当模型再次读取一个已经在上下文中且未修改的文件时,不会重复放入完整内容。FileReadTool 通过 readFileState(一个 FileStateCache)追踪已读文件的内容哈希:

1// src/tools/FileReadTool/prompt.ts
2export const FILE_UNCHANGED_STUB =
3  'File unchanged since last read. The content from the earlier Read ' +
4  'tool_result in this conversation is still current  refer to that ' +
5  'instead of re-reading.'
6

一个 5000 行的文件如果被读了 3 次但没变,只有第一次会占用上下文空间,后两次只占一行 stub 的 token。

图片/文档剥离

压缩时,stripImagesFromMessages() 把所有 image/document block 替换为文本标记:

1if (block.type === 'image') {
2  return [{ type: 'text', text: '[image]' }]
3}
4if (block.type === 'document') {
5  return [{ type: 'text', text: '[document]' }]
6}
7

图片对生成对话摘要没有价值,但一张图可能占 2000+ token。

L3:微压缩 (Microcompact) — 每轮清理旧工具结果

这是 Claude Code 最精妙的上下文管理机制。在每次 API 调用前,microcompactMessages()(位于 src/services/compact/microCompact.ts)会清理旧的工具结果。

核心思路

模型已经"看过"并处理过的旧工具结果,对后续推理的价值递减。比如 10 轮前读的一个文件内容,模型早已基于它做了决策,继续保留在上下文中只是浪费 token。

微压缩把这些旧结果的内容清空或替换为占位符,只保留最近几轮的完整结果。

可压缩的工具

不是所有工具结果都会被清理:

  • 会被清理:Read、Bash、Grep、Glob 等读取类工具 — 它们的结果是"快照",可以重新获取
  • 不会被清理:Edit、Write 等写入类工具 — 它们记录了实际的代码变更历史,丢失会导致模型不知道自己改了什么

两条路径

缓存编辑路径 (Cached Microcompact):利用 Anthropic API 的 cache_edits 能力,在服务端直接删除旧工具结果的缓存内容,不破坏 prompt cache 前缀。这是最优路径——既省了 token,又不用重新计算缓存。

时间触发路径:如果距离上次 API 调用超过一定时间(服务端缓存已过期),直接在本地清空旧工具结果内容。反正缓存已经冷了,不存在"破坏缓存"的问题。

1export async function microcompactMessages(messages, toolUseContext, querySource) {
2  // 时间触发优先:缓存已冷,直接清理
3  const timeBasedResult = maybeTimeBasedMicrocompact(messages, querySource)
4  if (timeBasedResult) return timeBasedResult
5
6  // 缓存编辑路径:缓存还热,用 cache_edits API 精确删除
7  if (feature('CACHED_MICROCOMPACT')) {
8    // ...
9  }
10
11  return { messages }  // 都不满足,不做处理
12}
13

L4:自动压缩 (Auto-Compact) — 接近上限时全量摘要

当 token 用量接近上下文窗口时,触发完整的对话压缩。这是最重量级的防线。

阈值计算

定义在 src/services/compact/autoCompact.ts

1// 预留给摘要输出的 token
2const MAX_OUTPUT_TOKENS_FOR_SUMMARY = 20_000
3
4// 自动压缩的缓冲区
5export const AUTOCOMPACT_BUFFER_TOKENS = 13_000
6
7// 有效窗口 = 上下文窗口 - 输出预留
8function getEffectiveContextWindowSize(model) {
9  return getContextWindowForModel(model) - reservedTokensForSummary
10}
11
12// 自动压缩阈值 = 有效窗口 - 缓冲区
13function getAutoCompactThreshold(model) {
14  return getEffectiveContextWindowSize(model) - AUTOCOMPACT_BUFFER_TOKENS
15}
16

以 200K 上下文窗口为例:

1有效窗口:         180,000 tokens
2自动压缩触发:     167,000 tokens (~93%)
3警告显示:         160,000 tokens (~89%)
4阻塞(必须压缩):   177,000 tokens (~98%)
5

压缩过程

compactConversation()(位于 src/services/compact/compact.ts)的完整流程:

第一步:执行 PreCompact hooks

允许用户通过 hooks 注入自定义的压缩指令(比如"压缩时重点保留测试输出")。

第二步:生成摘要

把整个对话历史发给模型,用一个精心设计的 prompt 要求按 9 个维度生成结构化摘要。

摘要 prompt 的核心结构(src/services/compact/prompt.ts):

11. Primary Request and Intent  用户的所有显式请求和意图
22. Key Technical Concepts  技术概念、框架
33. Files and Code Sections  文件和代码片段(含完整代码)
44. Errors and fixes  遇到的错误和修复方式
55. Problem Solving  问题解决过程
66. All user messages  所有非工具结果的用户消息(原文列出)
77. Pending Tasks  待办任务
88. Current Work  当前正在做的工作(精确描述)
99. Optional Next Step  下一步计划
10

第三步:防止意图漂移

这是压缩设计中最关键的部分。prompt 中有明确指令防止模型"跑偏":

19. Optional Next Step: ...IMPORTANT: ensure that this step is DIRECTLY 
2   in line with the user's most recent explicit requests... Do not start 
3   on tangential requests or really old requests that were already 
4   completed without confirming with the user first.
5   
6   If there is a next step, include direct quotes from the most recent 
7   conversation showing exactly what task you were working on and where 
8   you left off. This should be verbatim to ensure there's no drift in 
9   task interpretation.
10

要求摘要中包含最近对话的原文引用,确保任务解释不会在压缩过程中发生偏移。

第四步:格式化摘要

formatCompactSummary() 剥离 <analysis> 思考过程(这是给模型的"草稿纸",提高摘要质量但不需要保留),只保留 <summary> 部分。

第五步:构建压缩后的消息

1function buildPostCompactMessages(result) {
2  return [
3    result.boundaryMarker,       // 分界标记(system 消息,不发给 API)
4    ...result.summaryMessages,   // 摘要(伪用户消息)
5    ...(result.messagesToKeep ?? []),  // 保留的原始消息(部分压缩时)
6    ...result.attachments,       // 恢复的关键附件
7    ...result.hookResults,       // SessionStart hooks 输出
8  ]
9}
10

第六步:恢复关键上下文

压缩后会重新注入:

  • 最近读过的文件内容(最多 5 个,每个最多 5000 token)
  • 当前 plan(如果有)
  • 已调用的 skills 内容
  • MCP 服务器指令
  • 延迟加载的工具 schema

第七步:替换消息

query.ts 中:

1const postCompactMessages = buildPostCompactMessages(compactionResult)
2
3for (const message of postCompactMessages) {
4  yield message  // 发给 REPL 更新 UI
5}
6
7// 关键:后续 API 调用用压缩后的消息
8messagesForQuery = postCompactMessages
9

REPL.tsx 中,收到 compact_boundary 消息时:

1if (isCompactBoundaryMessage(newMessage)) {
2  if (isFullscreenEnvEnabled()) {
3    // 全屏模式:保留上一次压缩后的消息用于滚动回看
4    setMessages(old => [
5      ...getMessagesAfterCompactBoundary(old),
6      newMessage
7    ])
8  } else {
9    // 普通模式:直接清空,只留 boundary
10    setMessages(() => [newMessage])
11  }
12}
13

压缩后的续接指令

摘要消息的末尾会附加续接指令:

1Continue the conversation from where it left off without asking the user 
2any further questions. Resume directly  do not acknowledge the summary, 
3do not recap what was happening, do not preface with "I'll continue" or 
4similar. Pick up the last task as if the break never happened.
5

这确保模型不会在压缩后说"好的,让我继续之前的工作..."这种废话,而是直接无缝衔接。

熔断机制

1const MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3
2
3// 连续失败 3 次后停止重试
4if (tracking?.consecutiveFailures >= MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES) {
5  return { wasCompacted: false }
6}
7

避免在上下文不可恢复的情况下无限循环浪费 API 调用。源码注释提到:曾经有 1,279 个会话出现 50+ 次连续失败(最多 3,272 次),每天浪费约 25 万次 API 调用。

L5:兜底机制 — 当压缩本身也爆了

Prompt Too Long 重试

如果压缩请求本身也超了上下文窗口(对话实在太长),truncateHeadForPTLRetry() 会按 API 轮次分组,从最旧的开始丢弃:

1function truncateHeadForPTLRetry(messages, ptlResponse) {
2  const groups = groupMessagesByApiRound(messages)
3  
4  // 根据 API 返回的 token gap 计算需要丢弃多少组
5  const tokenGap = getPromptTooLongTokenGap(ptlResponse)
6  let dropCount
7  if (tokenGap !== undefined) {
8    // 精确丢弃:累加最旧的组直到覆盖 gap
9    let acc = 0
10    dropCount = 0
11    for (const g of groups) {
12      acc += roughTokenCountEstimationForMessages(g)
13      dropCount++
14      if (acc >= tokenGap) break
15    }
16  } else {
17    // 模糊丢弃:丢 20%
18    dropCount = Math.max(1, Math.floor(groups.length * 0.2))
19  }
20  
21  return groups.slice(dropCount).flat()
22}
23

最多重试 3 次(MAX_PTL_RETRIES = 3)。

Transcript 保底

压缩后,完整的原始对话会写入 transcript 文件(JSONL 格式)。摘要消息里会告诉模型:

1If you need specific details from before compaction (like exact code 
2snippets, error messages, or content you generated), read the full 
3transcript at: /Users/xxx/.claude/sessions/abc123.jsonl
4

模型可以用 Read 工具去读原始记录。这相当于一个"外部记忆"——上下文窗口里放不下的信息,按需从磁盘加载。

部分压缩

用户可以通过 UI 选择只压缩某条消息之前或之后的部分:

  • up_to 方向:压缩旧的,保留新的(保留最近的工作上下文)
  • from 方向:压缩新的,保留旧的(保留 prompt cache)

Token 估算:快速但保守

贯穿整个体系的是 token 计数。Claude Code 用两种方式估算:

精确计数:调用 Anthropic 的 countTokens API(或 Bedrock 的 CountTokensCommand)。准确但有网络开销。

粗略估算roughTokenCountEstimation(),按字节数除以 4 估算(JSON 文件除以 2,因为 {}:, 等单字符 token 密度更高)。快速但保守,乘以 4/3 的安全系数:

1function roughTokenCountEstimation(content, bytesPerToken = 4) {
2  return Math.round(content.length / bytesPerToken)
3}
4
5function estimateMessageTokens(messages) {
6  // ...计算...
7  return Math.ceil(totalTokens * (4 / 3))  // 保守上浮 33%
8}
9

保守估算确保不会因为低估 token 数而错过压缩时机。

设计哲学总结

Claude Code 的上下文管理体现了几个核心设计原则:

1. 分层防御,逐级加重

从源头截断(零成本)到微压缩(低成本)到全量摘要(高成本),每一层只在前一层不够用时才触发。大多数对话只会用到 L1-L3,永远不需要全量压缩。

2. 保留意图,丢弃数据

压缩摘要的 9 个维度设计确保用户的请求、反馈、当前工作状态和下一步计划被完整保留。丢弃的是旧的工具输出——这些数据可以通过重新执行工具来恢复。

3. 可恢复性

落盘的大工具结果、transcript 文件、文件读取去重的 stub——所有被"丢弃"的内容都有恢复路径。模型可以通过 Read 工具按需加载。

4. 缓存友好

微压缩的缓存编辑路径、system prompt 的静态/动态分区、userContext 放在用户消息而非 system prompt 中——这些设计都在尽量减少 prompt cache 失效,降低 API 成本。

5. 防御性编程

熔断机制(3 次失败停止)、PTL 重试(压缩请求本身也可能爆)、空结果注入(防止模型误判 turn boundary)——每个边界条件都有处理。


本文基于 Claude Code v2.1.88 源码分析。源码从 npm 包的 source map 中还原,包含 4,756 个源文件。文中引用的代码路径均为实际源码位置。


深度展开:三个最关键的机制

深度一:工具结果落盘 — 两级预算体系

这个机制解决的核心问题是:并行工具调用可以在一轮内产生巨量输出

比如模型同时发起 8 个 grep 搜索,每个返回 40K 字符,一轮就是 320K 字符(约 80K token),直接吃掉近一半上下文窗口。

Claude Code 用两级预算来防御:

第一级:单工具限制

每个工具结果独立检查。超过阈值就落盘:

1工具执行  mapToolResultToToolResultBlockParam()  processToolResultBlock()
2                                                          
3                                              getPersistenceThreshold()
4                                                          
5                                              maybePersistLargeToolResult()
6                                                               
7                                              size  阈值    size > 阈值
8                                                               
9                                            原样返回      persistToolResult()
10                                                              
11                                                    写入磁盘 + 返回预览
12

阈值不是写死的。getPersistenceThreshold() 有三层优先级:

  1. GrowthBook 远程配置tengu_satin_quoll):可以按工具名动态调整阈值,不需要发版
  2. 工具自声明的 maxResultSizeChars:每个工具可以声明自己的上限
  3. 全局默认 50K 字符:兜底值

特殊情况:Read 工具声明 maxResultSizeChars: Infinity,永远不会被落盘。因为 Read 的结果如果被落盘,模型需要再用 Read 去读落盘文件——形成循环。Read 有自己的行数限制(2000 行)来控制大小。

落盘文件用 tool_use_id 命名,写入时用 flag: 'wx'(排他创建)。如果文件已存在(上一轮已经落盘过同一个结果),直接跳过写入,只生成预览。这避免了微压缩重放消息时的重复 I/O。

第二级:单消息聚合限制

这是更精妙的一层。即使每个工具结果都在 50K 以内,8 个并行结果合在一条用户消息里仍然可能超标。

enforceToolResultBudget() 在每次 API 调用前检查每条用户消息的聚合大小:

1消息 A: [grep结果1: 30K] [grep结果2: 35K] [grep结果3: 40K] = 105K
2消息 B: [read结果: 20K]                                     = 20K
3                                                    聚合限制: 200K
4

消息 A 没超 200K,不处理。但如果再多几个结果超了,系统会把最大的几个落盘,直到总量降到预算内。

关键设计:决策冻结

这里有一个非常精妙的设计——ContentReplacementState

1type ContentReplacementState = {
2  seenIds: Set<string>           // 见过的所有 tool_use_id
3  replacements: Map<string, string>  // 被替换的 id  替换后的内容
4}
5

每个 tool_result 的命运在第一次被看到时就被"冻结"了:

  • 如果第一次见到时被替换了 → 之后每轮都用缓存的替换内容重新应用(mustReapply
  • 如果第一次见到时没被替换 → 之后永远不会被替换(frozen
  • 只有从未见过的结果才有资格被新替换(fresh

为什么要冻结?为了 prompt cache。Anthropic API 的 prompt cache 是基于前缀匹配的。如果第 5 轮的一个工具结果在第 5 轮没被替换,但在第 8 轮突然被替换了,那第 5-7 轮积累的缓存前缀就全部失效了。冻结决策确保消息内容在整个会话中保持稳定。

被替换的结果,每轮都用 Map 查找重新应用完全相同的字符串——零 I/O,字节级一致,不可能失败。

深度二:微压缩的缓存编辑路径 — 不破坏缓存的上下文回收

微压缩解决的核心问题是:旧工具结果占着上下文但价值递减,清理它们又会破坏 prompt cache

传统做法是直接修改消息内容(把旧工具结果替换为空),但这会改变发给 API 的消息前缀,导致缓存失效。缓存失效意味着每次 API 调用都要重新处理整个前缀,成本翻倍。

Claude Code 的缓存编辑路径利用了 Anthropic API 的 cache_edits 能力,在服务端直接删除缓存中的指定内容,而不修改本地消息。

工作流程

1 1 轮: [user] [asst] [user: grep结果A] [asst] [user: read结果B] [asst]
2                                                                     缓存前缀到这里
3
4 5 轮: 微压缩判断 grep结果A 已经足够旧
5         
6         不修改本地消息(保持 messagesForQuery 不变)
7         
8          API 请求中附加 cache_edits: [{ delete: toolUseId_A }]
9         
10         服务端从缓存中删除 grep结果A 的内容
11         
12         缓存前缀仍然有效(只是变短了),不需要重新计算
13

触发条件

微压缩有两条触发路径:

路径一:缓存编辑(热缓存)

当缓存还"热"(距离上次 API 调用时间短)时,用 cache_edits API。cachedMicrocompactPath() 追踪每个工具结果的 tool_use_id,按照配置的 count-based 阈值决定哪些该清理。

路径二:时间触发(冷缓存)

evaluateTimeBasedTrigger() 检查距离上次 API 调用的时间间隔。如果超过阈值(服务端缓存 TTL 已过期),直接在本地清空旧工具结果内容。因为缓存已经冷了,修改消息内容不会有额外的缓存失效成本。

1function maybeTimeBasedMicrocompact(messages, querySource) {
2  // 时间触发优先检查
3  const timeBasedResult = evaluateTimeBasedTrigger(messages, querySource)
4  if (timeBasedResult) {
5    // 缓存已冷,直接清空旧工具结果
6    return timeBasedResult
7  }
8  return null  // 缓存还热,走缓存编辑路径
9}
10

哪些工具结果会被清理

COMPACTABLE_TOOLS 集合定义了可清理的工具:

  • Read、Bash、Grep、Glob 等读取类工具 → 可清理(结果是快照,可重新获取)
  • Edit、Write 等写入类工具 → 不可清理(记录了实际变更历史)
  • AgentTool 等复合工具 → 不可清理(子 agent 的完整交互记录)

这个区分很关键:如果清理了 Edit 的结果,模型就不知道自己之前改了什么,可能重复修改或产生冲突。

与自动压缩的协作

微压缩和自动压缩不是互斥的,而是协作关系:

1每轮 API 调用前:
2  1. 微压缩先跑  清理旧工具结果,释放一些 token
3  2. 自动压缩再检查  如果微压缩释放的不够,触发全量摘要
4

snipTokensFreed 参数把微压缩释放的 token 数传给自动压缩的阈值检查,避免误触发:

1const tokenCount = tokenCountWithEstimation(messages) - snipTokensFreed
2const isAboveAutoCompactThreshold = tokenCount >= autoCompactThreshold
3

深度三:压缩摘要的意图保持 — 9 维结构化摘要 + 原文引用

这个机制解决的核心问题是:全量压缩后,模型如何不"忘记"自己在做什么

这是最难的问题。一个 10 万 token 的对话被压缩成 5000 token 的摘要,信息损失是必然的。关键是损失什么、保留什么。

摘要的双阶段生成

压缩 prompt 要求模型先在 <analysis> 中思考,再在 <summary> 中输出:

1<analysis>
2[模型的思考过程:逐条分析每条消息,确保没有遗漏]
3</analysis>
4
5<summary>
6[结构化的 9 维摘要]
7</summary>
8

<analysis> 是"草稿纸"——它提高了摘要质量(让模型先梳理再总结),但不会进入最终上下文。formatCompactSummary() 会把它剥离:

1function formatCompactSummary(summary) {
2  // 剥离 analysis(草稿纸,不需要保留)
3  formattedSummary = formattedSummary.replace(
4    /<analysis>[\s\S]*?<\/analysis>/, ''
5  )
6  // 提取 summary 内容
7  const summaryMatch = formattedSummary.match(/<summary>([\s\S]*?)<\/summary>/)
8  // ...
9}
10

这个设计很聪明:用 analysis 提高质量,但不让它占用压缩后的宝贵上下文空间。

9 个维度的设计逻辑

每个维度解决一个特定的"遗忘"风险:

维度解决的问题
1. Primary Request and Intent防止模型忘记用户最初要做什么
2. Key Technical Concepts保留技术决策的上下文(比如"我们选了 JWT 而不是 session")
3. Files and Code Sections保留代码级细节,含完整代码片段
4. Errors and fixes防止模型重复犯同样的错误
5. Problem Solving保留问题解决的推理链
6. All user messages最关键——原文列出所有用户消息,防止用户反馈被丢失
7. Pending Tasks防止模型忘记还有什么没做
8. Current Work精确描述压缩前正在做的事
9. Optional Next Step带原文引用的下一步计划

第 6 维"All user messages"特别重要。用户的消息往往很短("改一下这个函数"、"不对,用另一种方式"),但它们是理解意图变化的关键。如果只保留摘要而丢失了用户的原始反馈,模型可能会回到用户已经否定的方案。

防漂移的原文引用要求

第 9 维的 prompt 中有一段关键指令:

1If there is a next step, include direct quotes from the most recent 
2conversation showing exactly what task you were working on and where 
3you left off. This should be verbatim to ensure there's no drift in 
4task interpretation.
5

要求摘要中包含最近对话的逐字引用。这不是修辞——它是一个工程约束。每次压缩都会引入一定程度的信息损失,如果摘要用模型自己的话重新表述任务,经过多次压缩后,任务描述可能会逐渐偏离用户的原始意图(类似"传话游戏"效应)。逐字引用打断了这个漂移链。

压缩后的续接指令

摘要消息的末尾有严格的续接指令:

1Continue the conversation from where it left off without asking the user 
2any further questions. Resume directly  do not acknowledge the summary, 
3do not recap what was happening, do not preface with "I'll continue" or 
4similar. Pick up the last task as if the break never happened.
5

这防止了一个常见问题:模型在压缩后说"好的,根据之前的对话,我们在做 X,让我继续..."这种废话不仅浪费 token,还可能在复述中引入偏差。直接续接,不给模型"重新解释"任务的机会。

压缩后的上下文恢复

摘要只是文字描述,但模型继续工作还需要"活的"上下文。compactConversation() 在摘要之后注入:

最近读过的文件(最多 5 个,每个最多 5000 token):

1const POST_COMPACT_MAX_FILES_TO_RESTORE = 5
2const POST_COMPACT_MAX_TOKENS_PER_FILE = 5_000
3

从压缩前的 readFileState 缓存中取出最近读过的文件,重新作为附件注入。这样模型不需要重新读取就能继续编辑。

已调用的 Skills(最多 25K token,每个最多 5K):

1const POST_COMPACT_MAX_TOKENS_PER_SKILL = 5_000
2const POST_COMPACT_SKILLS_TOKEN_BUDGET = 25_000
3

Skills 可能很大(verify=18.7KB, claude-api=20.1KB),但指令的关键部分通常在文件开头。截断比丢弃好。

Plan 文件:如果当前有 plan(/plan 命令创建的),重新注入,确保模型知道整体计划。

MCP 指令和工具 schema:压缩吃掉了之前的 delta 附件,需要重新从当前状态生成完整的 delta,让模型知道有哪些 MCP 工具和指令可用。

Transcript 路径

1If you need specific details from before compaction (like exact code 
2snippets, error messages, or content you generated), read the full 
3transcript at: /path/to/transcript.jsonl
4

这是最后的保底——如果摘要中遗漏了某个细节,模型可以用 Read 工具去读完整的原始对话记录。

多次压缩的累积效应

一个长会话可能经历多次压缩。每次压缩都是在上一次的摘要基础上再压缩,信息损失会累积。Claude Code 通过几个机制缓解:

  1. turnCounter 追踪:记录距离上次压缩过了多少轮,用于遥测分析压缩频率
  2. recompactionInfo:传递上次压缩的元数据,帮助诊断"压缩后立即又触发压缩"的问题
  3. truePostCompactTokenCount:精确计算压缩后的实际 token 数,预判是否会在下一轮立即重新触发
  4. transcript 保底:无论压缩多少次,磁盘上的完整记录始终可用

这三个机制分别守护了上下文管理的三个关键环节:入口控制(不让大数据进来)、存量优化(在不破坏缓存的前提下回收空间)、信息保真(压缩时不丢失用户意图)。它们的协作构成了 Claude Code 在长对话中保持稳定工作能力的基础。


Claude Code 防上下文爆炸:源码级深度解析》 是转载文章,点击查看原文


相关推荐


【万字长文】从 AI SDK 到 mini-opencode:一次很巧的 Go Agent 架构实践
mCell2026/3/25

同步更新至个人站点:从 AI SDK 到 mini-opencode:一次很巧的 Go Agent 架构实践 相关链接: 从零构建 Mini Claude Code:stack.mcell.top/blog/2026/a… 本次 Mini OpenCode 仓库地址:github.com/minorcell/m… Memo Code:memo.mcell.top/ 前阵子,我写过一篇 从零构建 Mini Claude Code 的 Agent 开发入门教程。 那次基本是顺着 AI SDK


Rust宏编程完全指南:用元编程解锁Rust的终极力量
土豆12502026/3/17

"宏就像是编译器的魔法棒,挥一挥,重复的代码就消失了。" —— 某位深夜 debug 的 Rustacean 目录 Why:为什么需要宏? What:宏是什么? How:如何使用宏? 声明宏 (macro_rules!) 派生宏 (Derive Macros) 属性宏 (Attribute Macros) 函数式宏 (Function-like Macros) 最佳实践 常见误区 总结 Why:为什么需要宏? 想象一下,你正在写一个 Web 框架,需要为 50 个不同的结构体实现相


【毕设】前后端(无模型训练)
2301_815389372026/3/8

后端 第一步,先建一个项目文件夹。 打开你电脑上任意一个地方,新建一个文件夹,就叫 ebike-detection,然后把你的 best.pt 复制进去。 第二步,安装Flask和相关依赖。 打开命令提示符(按 Win+R,输入 cmd,回车),然后把下面这行命令复制进去运行: pip install flask flask-cors ultralytics pillow 好,第三步,创建Flask后端文件。 在你的 ebike-detection


Node.js 安装与配置完全指南:从零开始搭建开发环境
张3蜂2026/2/28

目录 引言 第一部分:Node.js 简介与版本选择 1.1 什么是 Node.js? 1.2 Node.js 版本介绍 第二部分:Node.js 安装方式详解 2.1 方式一:官方安装包(最简单,适合初学者) Windows/macOS 安装步骤: 2.2 方式二:包管理器安装(适合 Linux 用户) Ubuntu/Debian 系统安装步骤: CentOS/RHEL 系统安装步骤: macOS 使用 Homebrew 安装: 2.3 方式三:使用 NVM 安装(最推


ThreadForge v1.1.0 发布:让 Java 并发更接近 Go 的开发体验
一只叫煤球的猫2026/2/20

正好春节放假,自驾去了陕西、河南、安徽,一路上走走停停。 白天基本在路上,晚上在酒店或者服务区休息时,抽一些时间继续打磨 ThreadForge。 一点点补了个 v1.1.0 出来。 仍然保持 ThreadForge 的目标: 让 Java 能写出更简单、更可推理、更可观测的并发代码。 这次版本,重点补齐了并发开发里几个还算常见的能力。 v1.1.0 核心更新 Retry Policy(失败重试) 支持 scope 级默认重试,也支持任务级覆盖,不再到处手写 while/try-catch


git pull拉取的时候碰到报错:error: 您对下列文件的本地修改将被合并操作覆盖 请在合并前提交或贮藏您的修改。
skywalk81632026/2/11

git pull拉取的时候碰到报错: error: 您对下列文件的本地修改将被合并操作覆盖:         data/processed/acnes_related_data.csv         data/processed/activity_data.csv         data/processed/hemolytic_data.csv         data/raw/active_peptides.csv         data/raw/hemolytic.csv      


自己搭邮件服务器有多难?我用 Mailu 跑通了整套企业邮箱
GetcharZp2026/2/3

从“为什么要自建邮箱”讲起,拆解 Mailu 的架构、优缺点,以及普通人也能照着做的安装实战。 Github:github.com/Mailu/Mailu 官网:mailu.io/ 这几年,越来越多团队开始重新审视一件事:邮箱,到底要不要掌握在自己手里? 用第三方企业邮箱当然省事,但账号封禁、功能限制、隐私不可控的问题,一旦遇到,几乎没有回旋余地。于是,自建邮件服务器这件事,又被不少技术团队重新捡了起来。 而在一堆方案里,Mailu 是被频繁提到的一个名字。 Mailu 是什么?一句话先讲明


VS code 类产物中 win11 终端字体内容和颜色 加粗不匹配问题
小兵张健2026/1/24

我尝试了各种方式,换字体,改配置,结果还是乱的,有人知道怎么搞吗?跪求,Mac好像天然就没问题,急急急大佬们


Django 踩坑记:OceanBase 4012 Timeout 两条红线,语句超时 vs 事务超时一次讲透
哈里谢顿2026/1/16

一、4012 是谁抛的? Django 本身没有 4012 错误码,它是 OceanBase 的“杀手”信号: 当前 SQL 或 当前事务累计执行时间 ≥ 系统阈值,直接返回 4012。 二、两条红线长啥样? 变量名默认阈值计时对象触发后果ob_query_timeout10 000 000 µs = 10 s单条 SQL 执行时长这条语句被杀,事务可继续ob_trx_timeout100 000 000 µs = 100 s事务 begin→


HarmonyOS一杯冰美式的时间 -- @Env
猫猫头啊2026/1/8

一、前言 该系列依旧会带着大家,了解,开阔一些不怎么热门的API,也可能是偷偷被更新的API,也可以是好玩的,藏在官方文档的边边角角~当然也会有一些API,之前是我们辛辛苦苦的手撸代码,现在有一个API能帮我们快速实现的,希望大家能找宝藏。 如果您有任何疑问、对文章写的不满意、发现错误或者有更好的方法,欢迎在评论、私信或邮件中提出,非常感谢您的支持。🙏 二、@Env的诞生背景 OK,步入正题把,在多设备开发的场景中,我们经常需要根据不同的设备环境(比如窗口大小、横竖屏等)来调整UI布局。以前我

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 XYZ博客