基于 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 用一套五层防御体系来解决这两个问题。
架构总览:五层防线
用户输入 → [L1 源头截断] → [L2 去重] → [L3 微压缩] → [L4 自动压缩] → [L5 兜底] → API
每一层解决不同粒度的问题,从"不让大数据进来"到"实在不行就全量摘要",层层递进。
L1:源头截断 — 在工具执行阶段就控制大小
这是最前置的防线。在工具结果返回给模型之前,就把过大的内容拦截住。
工具结果大小限制
核心常量定义在 src/constants/toolLimits.ts:
// 单个工具结果的默认上限
export const DEFAULT_MAX_RESULT_SIZE_CHARS = 50_000 // 50K 字符
// 单个工具结果的 token 上限
export const MAX_TOOL_RESULT_TOKENS = 100_000 // 100K tokens ≈ 400KB
// 单条用户消息中所有工具结果的聚合上限
export const MAX_TOOL_RESULTS_PER_MESSAGE_CHARS = 200_000 // 200K 字符
当工具结果超过阈值时,maybePersistLargeToolResult()(位于 src/utils/toolResultStorage.ts)会执行"落盘 + 预览"策略:
async function maybePersistLargeToolResult(
toolResultBlock, toolName, persistenceThreshold
) {
const size = contentSize(content)
const threshold = persistenceThreshold ?? MAX_TOOL_RESULT_BYTES
if (size <= threshold) {
return toolResultBlock // 没超,原样返回
}
// 超了:完整内容写入磁盘临时文件
const result = await persistToolResult(content, toolResultBlock.tool_use_id)
// 只给模型一个预览 + 文件路径
const message = buildLargeToolResultMessage(result)
return { ...toolResultBlock, content: message }
}
模型看到的是类似这样的内容:
Tool output too large (523KB). Full output saved to: /tmp/.claude/tool-results/abc123.txt
Preview (first 2000 chars):
...前2000字符的预览...
[Use Read tool to see the full output]
并行工具的聚合限制
Claude Code 支持并行工具调用。如果一轮有 10 个并行 grep 搜索,每个返回 40K 字符,总共 400K 会超过单消息 200K 的聚合限制。系统会把最大的几个结果落盘,直到总量降到预算内。
其他源头限制
| 限制项 | 阈值 | 位置 |
|---|---|---|
| Git status | 2,000 字符 | context.ts |
| CLAUDE.md 单文件 | 40,000 字符 | claudemd.ts |
| FileRead 单次 | 2,000 行 | FileReadTool/prompt.ts |
L2:去重 — 不重复放相同内容
文件读取去重
当模型再次读取一个已经在上下文中且未修改的文件时,不会重复放入完整内容。FileReadTool 通过 readFileState(一个 FileStateCache)追踪已读文件的内容哈希:
// src/tools/FileReadTool/prompt.ts
export const FILE_UNCHANGED_STUB =
'File unchanged since last read. The content from the earlier Read ' +
'tool_result in this conversation is still current — refer to that ' +
'instead of re-reading.'
一个 5000 行的文件如果被读了 3 次但没变,只有第一次会占用上下文空间,后两次只占一行 stub 的 token。
图片/文档剥离
压缩时,stripImagesFromMessages() 把所有 image/document block 替换为文本标记:
if (block.type === 'image') {
return [{ type: 'text', text: '[image]' }]
}
if (block.type === 'document') {
return [{ type: 'text', text: '[document]' }]
}
图片对生成对话摘要没有价值,但一张图可能占 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 调用超过一定时间(服务端缓存已过期),直接在本地清空旧工具结果内容。反正缓存已经冷了,不存在"破坏缓存"的问题。
export async function microcompactMessages(messages, toolUseContext, querySource) {
// 时间触发优先:缓存已冷,直接清理
const timeBasedResult = maybeTimeBasedMicrocompact(messages, querySource)
if (timeBasedResult) return timeBasedResult
// 缓存编辑路径:缓存还热,用 cache_edits API 精确删除
if (feature('CACHED_MICROCOMPACT')) {
// ...
}
return { messages } // 都不满足,不做处理
}
L4:自动压缩 (Auto-Compact) — 接近上限时全量摘要
当 token 用量接近上下文窗口时,触发完整的对话压缩。这是最重量级的防线。
阈值计算
定义在 src/services/compact/autoCompact.ts:
// 预留给摘要输出的 token
const MAX_OUTPUT_TOKENS_FOR_SUMMARY = 20_000
// 自动压缩的缓冲区
export const AUTOCOMPACT_BUFFER_TOKENS = 13_000
// 有效窗口 = 上下文窗口 - 输出预留
function getEffectiveContextWindowSize(model) {
return getContextWindowForModel(model) - reservedTokensForSummary
}
// 自动压缩阈值 = 有效窗口 - 缓冲区
function getAutoCompactThreshold(model) {
return getEffectiveContextWindowSize(model) - AUTOCOMPACT_BUFFER_TOKENS
}
以 200K 上下文窗口为例:
有效窗口: 180,000 tokens
自动压缩触发: 167,000 tokens (~93%)
警告显示: 160,000 tokens (~89%)
阻塞(必须压缩): 177,000 tokens (~98%)
压缩过程
compactConversation()(位于 src/services/compact/compact.ts)的完整流程:
第一步:执行 PreCompact hooks
允许用户通过 hooks 注入自定义的压缩指令(比如"压缩时重点保留测试输出")。
第二步:生成摘要
把整个对话历史发给模型,用一个精心设计的 prompt 要求按 9 个维度生成结构化摘要。
摘要 prompt 的核心结构(src/services/compact/prompt.ts):
1. Primary Request and Intent — 用户的所有显式请求和意图
2. Key Technical Concepts — 技术概念、框架
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 — 下一步计划
第三步:防止意图漂移
这是压缩设计中最关键的部分。prompt 中有明确指令防止模型"跑偏":
9. Optional Next Step: ...IMPORTANT: ensure that this step is DIRECTLY
in line with the user's most recent explicit requests... Do not start
on tangential requests or really old requests that were already
completed without confirming with the user first.
If there is a next step, include direct quotes from the most recent
conversation showing exactly what task you were working on and where
you left off. This should be verbatim to ensure there's no drift in
task interpretation.
要求摘要中包含最近对话的原文引用,确保任务解释不会在压缩过程中发生偏移。
第四步:格式化摘要
formatCompactSummary() 剥离 <analysis> 思考过程(这是给模型的"草稿纸",提高摘要质量但不需要保留),只保留 <summary> 部分。
第五步:构建压缩后的消息
function buildPostCompactMessages(result) {
return [
result.boundaryMarker, // 分界标记(system 消息,不发给 API)
...result.summaryMessages, // 摘要(伪用户消息)
...(result.messagesToKeep ?? []), // 保留的原始消息(部分压缩时)
...result.attachments, // 恢复的关键附件
...result.hookResults, // SessionStart hooks 输出
]
}
第六步:恢复关键上下文
压缩后会重新注入:
- 最近读过的文件内容(最多 5 个,每个最多 5000 token)
- 当前 plan(如果有)
- 已调用的 skills 内容
- MCP 服务器指令
- 延迟加载的工具 schema
第七步:替换消息
在 query.ts 中:
const postCompactMessages = buildPostCompactMessages(compactionResult)
for (const message of postCompactMessages) {
yield message // 发给 REPL 更新 UI
}
// 关键:后续 API 调用用压缩后的消息
messagesForQuery = postCompactMessages
在 REPL.tsx 中,收到 compact_boundary 消息时:
if (isCompactBoundaryMessage(newMessage)) {
if (isFullscreenEnvEnabled()) {
// 全屏模式:保留上一次压缩后的消息用于滚动回看
setMessages(old => [
...getMessagesAfterCompactBoundary(old),
newMessage
])
} else {
// 普通模式:直接清空,只留 boundary
setMessages(() => [newMessage])
}
}
压缩后的续接指令
摘要消息的末尾会附加续接指令:
Continue the conversation from where it left off without asking the user
any further questions. Resume directly — do not acknowledge the summary,
do not recap what was happening, do not preface with "I'll continue" or
similar. Pick up the last task as if the break never happened.
这确保模型不会在压缩后说"好的,让我继续之前的工作..."这种废话,而是直接无缝衔接。
熔断机制
const MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3
// 连续失败 3 次后停止重试
if (tracking?.consecutiveFailures >= MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES) {
return { wasCompacted: false }
}
避免在上下文不可恢复的情况下无限循环浪费 API 调用。源码注释提到:曾经有 1,279 个会话出现 50+ 次连续失败(最多 3,272 次),每天浪费约 25 万次 API 调用。
L5:兜底机制 — 当压缩本身也爆了
Prompt Too Long 重试
如果压缩请求本身也超了上下文窗口(对话实在太长),truncateHeadForPTLRetry() 会按 API 轮次分组,从最旧的开始丢弃:
function truncateHeadForPTLRetry(messages, ptlResponse) {
const groups = groupMessagesByApiRound(messages)
// 根据 API 返回的 token gap 计算需要丢弃多少组
const tokenGap = getPromptTooLongTokenGap(ptlResponse)
let dropCount
if (tokenGap !== undefined) {
// 精确丢弃:累加最旧的组直到覆盖 gap
let acc = 0
dropCount = 0
for (const g of groups) {
acc += roughTokenCountEstimationForMessages(g)
dropCount++
if (acc >= tokenGap) break
}
} else {
// 模糊丢弃:丢 20%
dropCount = Math.max(1, Math.floor(groups.length * 0.2))
}
return groups.slice(dropCount).flat()
}
最多重试 3 次(MAX_PTL_RETRIES = 3)。
Transcript 保底
压缩后,完整的原始对话会写入 transcript 文件(JSONL 格式)。摘要消息里会告诉模型:
If you need specific details from before compaction (like exact code
snippets, error messages, or content you generated), read the full
transcript at: /Users/xxx/.claude/sessions/abc123.jsonl
模型可以用 Read 工具去读原始记录。这相当于一个"外部记忆"——上下文窗口里放不下的信息,按需从磁盘加载。
部分压缩
用户可以通过 UI 选择只压缩某条消息之前或之后的部分:
up_to方向:压缩旧的,保留新的(保留最近的工作上下文)from方向:压缩新的,保留旧的(保留 prompt cache)
Token 估算:快速但保守
贯穿整个体系的是 token 计数。Claude Code 用两种方式估算:
精确计数:调用 Anthropic 的 countTokens API(或 Bedrock 的 CountTokensCommand)。准确但有网络开销。
粗略估算:roughTokenCountEstimation(),按字节数除以 4 估算(JSON 文件除以 2,因为 {、}、:、, 等单字符 token 密度更高)。快速但保守,乘以 4/3 的安全系数:
function roughTokenCountEstimation(content, bytesPerToken = 4) {
return Math.round(content.length / bytesPerToken)
}
function estimateMessageTokens(messages) {
// ...计算...
return Math.ceil(totalTokens * (4 / 3)) // 保守上浮 33%
}
保守估算确保不会因为低估 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 用两级预算来防御:
第一级:单工具限制
每个工具结果独立检查。超过阈值就落盘:
工具执行 → mapToolResultToToolResultBlockParam() → processToolResultBlock()
↓
getPersistenceThreshold()
↓
maybePersistLargeToolResult()
↓ ↓
size ≤ 阈值 size > 阈值
↓ ↓
原样返回 persistToolResult()
↓
写入磁盘 + 返回预览
阈值不是写死的。getPersistenceThreshold() 有三层优先级:
- GrowthBook 远程配置(
tengu_satin_quoll):可以按工具名动态调整阈值,不需要发版 - 工具自声明的
maxResultSizeChars:每个工具可以声明自己的上限 - 全局默认 50K 字符:兜底值
特殊情况:Read 工具声明 maxResultSizeChars: Infinity,永远不会被落盘。因为 Read 的结果如果被落盘,模型需要再用 Read 去读落盘文件——形成循环。Read 有自己的行数限制(2000 行)来控制大小。
落盘文件用 tool_use_id 命名,写入时用 flag: 'wx'(排他创建)。如果文件已存在(上一轮已经落盘过同一个结果),直接跳过写入,只生成预览。这避免了微压缩重放消息时的重复 I/O。
第二级:单消息聚合限制
这是更精妙的一层。即使每个工具结果都在 50K 以内,8 个并行结果合在一条用户消息里仍然可能超标。
enforceToolResultBudget() 在每次 API 调用前检查每条用户消息的聚合大小:
消息 A: [grep结果1: 30K] [grep结果2: 35K] [grep结果3: 40K] = 105K
消息 B: [read结果: 20K] = 20K
聚合限制: 200K
消息 A 没超 200K,不处理。但如果再多几个结果超了,系统会把最大的几个落盘,直到总量降到预算内。
关键设计:决策冻结
这里有一个非常精妙的设计——ContentReplacementState:
type ContentReplacementState = {
seenIds: Set<string> // 见过的所有 tool_use_id
replacements: Map<string, string> // 被替换的 id → 替换后的内容
}
每个 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 轮: [user] [asst] [user: grep结果A] [asst] [user: read结果B] [asst]
↑ 缓存前缀到这里
第 5 轮: 微压缩判断 grep结果A 已经足够旧
↓
不修改本地消息(保持 messagesForQuery 不变)
↓
在 API 请求中附加 cache_edits: [{ delete: toolUseId_A }]
↓
服务端从缓存中删除 grep结果A 的内容
↓
缓存前缀仍然有效(只是变短了),不需要重新计算
触发条件
微压缩有两条触发路径:
路径一:缓存编辑(热缓存)
当缓存还"热"(距离上次 API 调用时间短)时,用 cache_edits API。cachedMicrocompactPath() 追踪每个工具结果的 tool_use_id,按照配置的 count-based 阈值决定哪些该清理。
路径二:时间触发(冷缓存)
evaluateTimeBasedTrigger() 检查距离上次 API 调用的时间间隔。如果超过阈值(服务端缓存 TTL 已过期),直接在本地清空旧工具结果内容。因为缓存已经冷了,修改消息内容不会有额外的缓存失效成本。
function maybeTimeBasedMicrocompact(messages, querySource) {
// 时间触发优先检查
const timeBasedResult = evaluateTimeBasedTrigger(messages, querySource)
if (timeBasedResult) {
// 缓存已冷,直接清空旧工具结果
return timeBasedResult
}
return null // 缓存还热,走缓存编辑路径
}
哪些工具结果会被清理
COMPACTABLE_TOOLS 集合定义了可清理的工具:
- Read、Bash、Grep、Glob 等读取类工具 → 可清理(结果是快照,可重新获取)
- Edit、Write 等写入类工具 → 不可清理(记录了实际变更历史)
- AgentTool 等复合工具 → 不可清理(子 agent 的完整交互记录)
这个区分很关键:如果清理了 Edit 的结果,模型就不知道自己之前改了什么,可能重复修改或产生冲突。
与自动压缩的协作
微压缩和自动压缩不是互斥的,而是协作关系:
每轮 API 调用前:
1. 微压缩先跑 → 清理旧工具结果,释放一些 token
2. 自动压缩再检查 → 如果微压缩释放的不够,触发全量摘要
snipTokensFreed 参数把微压缩释放的 token 数传给自动压缩的阈值检查,避免误触发:
const tokenCount = tokenCountWithEstimation(messages) - snipTokensFreed
const isAboveAutoCompactThreshold = tokenCount >= autoCompactThreshold
深度三:压缩摘要的意图保持 — 9 维结构化摘要 + 原文引用
这个机制解决的核心问题是:全量压缩后,模型如何不"忘记"自己在做什么。
这是最难的问题。一个 10 万 token 的对话被压缩成 5000 token 的摘要,信息损失是必然的。关键是损失什么、保留什么。
摘要的双阶段生成
压缩 prompt 要求模型先在 <analysis> 中思考,再在 <summary> 中输出:
<analysis>
[模型的思考过程:逐条分析每条消息,确保没有遗漏]
</analysis>
<summary>
[结构化的 9 维摘要]
</summary>
<analysis> 是"草稿纸"——它提高了摘要质量(让模型先梳理再总结),但不会进入最终上下文。formatCompactSummary() 会把它剥离:
function formatCompactSummary(summary) {
// 剥离 analysis(草稿纸,不需要保留)
formattedSummary = formattedSummary.replace(
/<analysis>[\s\S]*?<\/analysis>/, ''
)
// 提取 summary 内容
const summaryMatch = formattedSummary.match(/<summary>([\s\S]*?)<\/summary>/)
// ...
}
这个设计很聪明:用 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 中有一段关键指令:
If there is a next step, include direct quotes from the most recent
conversation showing exactly what task you were working on and where
you left off. This should be verbatim to ensure there's no drift in
task interpretation.
要求摘要中包含最近对话的逐字引用。这不是修辞——它是一个工程约束。每次压缩都会引入一定程度的信息损失,如果摘要用模型自己的话重新表述任务,经过多次压缩后,任务描述可能会逐渐偏离用户的原始意图(类似"传话游戏"效应)。逐字引用打断了这个漂移链。
压缩后的续接指令
摘要消息的末尾有严格的续接指令:
Continue the conversation from where it left off without asking the user
any further questions. Resume directly — do not acknowledge the summary,
do not recap what was happening, do not preface with "I'll continue" or
similar. Pick up the last task as if the break never happened.
这防止了一个常见问题:模型在压缩后说"好的,根据之前的对话,我们在做 X,让我继续..."这种废话不仅浪费 token,还可能在复述中引入偏差。直接续接,不给模型"重新解释"任务的机会。
压缩后的上下文恢复
摘要只是文字描述,但模型继续工作还需要"活的"上下文。compactConversation() 在摘要之后注入:
最近读过的文件(最多 5 个,每个最多 5000 token):
const POST_COMPACT_MAX_FILES_TO_RESTORE = 5
const POST_COMPACT_MAX_TOKENS_PER_FILE = 5_000
从压缩前的 readFileState 缓存中取出最近读过的文件,重新作为附件注入。这样模型不需要重新读取就能继续编辑。
已调用的 Skills(最多 25K token,每个最多 5K):
const POST_COMPACT_MAX_TOKENS_PER_SKILL = 5_000
const POST_COMPACT_SKILLS_TOKEN_BUDGET = 25_000
Skills 可能很大(verify=18.7KB, claude-api=20.1KB),但指令的关键部分通常在文件开头。截断比丢弃好。
Plan 文件:如果当前有 plan(/plan 命令创建的),重新注入,确保模型知道整体计划。
MCP 指令和工具 schema:压缩吃掉了之前的 delta 附件,需要重新从当前状态生成完整的 delta,让模型知道有哪些 MCP 工具和指令可用。
Transcript 路径:
If you need specific details from before compaction (like exact code
snippets, error messages, or content you generated), read the full
transcript at: /path/to/transcript.jsonl
这是最后的保底——如果摘要中遗漏了某个细节,模型可以用 Read 工具去读完整的原始对话记录。
多次压缩的累积效应
一个长会话可能经历多次压缩。每次压缩都是在上一次的摘要基础上再压缩,信息损失会累积。Claude Code 通过几个机制缓解:
- turnCounter 追踪:记录距离上次压缩过了多少轮,用于遥测分析压缩频率
- recompactionInfo:传递上次压缩的元数据,帮助诊断"压缩后立即又触发压缩"的问题
- truePostCompactTokenCount:精确计算压缩后的实际 token 数,预判是否会在下一轮立即重新触发
- transcript 保底:无论压缩多少次,磁盘上的完整记录始终可用
这三个机制分别守护了上下文管理的三个关键环节:入口控制(不让大数据进来)、存量优化(在不破坏缓存的前提下回收空间)、信息保真(压缩时不丢失用户意图)。它们的协作构成了 Claude Code 在长对话中保持稳定工作能力的基础。