Claude Code 的上下文压缩
这一章要解决的问题很具体:Claude Code 的对话越来越长时,系统什么时候决定压缩?压缩请求怎么发?失败了怎么兜底?压缩之后,模型又靠什么接着干活?
源码里的答案不是“把历史总结一下”这么简单。压缩链路至少包含三层动作:
autoCompactIfNeeded()判断是否该压缩。compactConversation()把旧消息变成结构化摘要,并重建压缩后的消息数组。createPostCompactFileAttachments()、技能附件、计划附件、delta 附件把必要现场重新挂回去。
核心直觉
Claude Code 的压缩不是记忆管理的装饰功能,而是长任务状态机的一次换轨:旧消息被摘要替换,文件、计划、技能和工具声明通过附件重新进入上下文。压缩成功的标准不是“少了多少 token”,而是下一轮还能不能继续执行。
先看源码入口
先把源码定位点放在桌面上。读这一章不需要先记住所有函数,但要知道每段逻辑大概在哪个文件里。
| 源码定位 | 关键符号 | 负责什么 |
|---|---|---|
services/compact/autoCompact.ts | autoCompactIfNeeded() | query loop 中的自动压缩入口,负责阈值、熔断、Session Memory 优先级和失败计数 |
services/compact/autoCompact.ts | getEffectiveContextWindowSize()、getAutoCompactThreshold()、shouldAutoCompact() | 计算有效窗口和触发条件 |
services/compact/compact.ts | compactConversation() | 执行压缩请求、处理 prompt too long 重试、组装 CompactionResult |
services/compact/prompt.ts | BASE_COMPACT_PROMPT、PARTIAL_COMPACT_PROMPT、formatCompactSummary() | 定义压缩提示词模板,并剥离 @@INLINE_0@@ 草稿块 |
services/compact/compact.ts | buildPostCompactMessages() | 把边界、摘要、保留消息、附件、hook 结果合成压缩后的消息序列 |
services/compact/compact.ts | createPostCompactFileAttachments()、createSkillAttachmentIfNeeded() | 恢复压缩后仍然要继续使用的文件和技能 |
services/compact/microCompact.ts | microcompactMessages()、maybeTimeBasedMicrocompact()、cachedMicrocompactPath() | 在全量压缩前先修剪工具结果,降低上下文增长速度 |
这些入口共同说明一件事:压缩不是一个函数,而是一条从预算判断到现场重建的链路。
总流程图
下面的流程图只画源码里真实出现的行为节点,不画抽象概念。
这张图有两个关键点:
- 微压缩发生在自动压缩之前,它不生成摘要,主要处理工具结果占用。
- 自动压缩成功后还没有结束,必须把压缩后的第一轮上下文重新拼出来。
自动压缩入口:先算窗口,再决定是否压缩
自动压缩不是等到 API 报错才开始。autoCompact.ts 先根据模型上下文窗口计算一个提前触发点,再由 shouldAutoCompact() 做一串守卫判断。
有效窗口的计算
源码里第一步不是直接用模型窗口,而是先扣掉压缩摘要需要的输出空间。
const MAX_OUTPUT_TOKENS_FOR_SUMMARY = 20_000
export function getEffectiveContextWindowSize(model: string): number {
const reservedTokensForSummary = Math.min(
getMaxOutputTokensForModel(model),
MAX_OUTPUT_TOKENS_FOR_SUMMARY,
)
let contextWindow = getContextWindowForModel(model, getSdkBetas())
const autoCompactWindow = process.env.CLAUDE_CODE_AUTO_COMPACT_WINDOW
if (autoCompactWindow) {
const parsed = parseInt(autoCompactWindow, 10)
if (!isNaN(parsed) && parsed > 0) {
contextWindow = Math.min(contextWindow, parsed)
}
}
return contextWindow - reservedTokensForSummary
}
这段代码证明了两件事。
第一,压缩摘要本身也要占输出 token,所以有效窗口不是原始窗口。MAX_OUTPUT_TOKENS_FOR_SUMMARY = 20_000 来自线上摘要长度统计,原文记录 p99.99 摘要长度是 17,387 tokens,20K 是安全余量。
第二,CLAUDE_CODE_AUTO_COMPACT_WINDOW 只能通过 Math.min() 缩小窗口,不能扩大模型真实窗口。这是典型的 fail-closed 设计:环境变量可以让压缩更保守,不能让系统越过模型限制。
触发阈值的计算
有效窗口之后,还要再减一层 buffer。
export const AUTOCOMPACT_BUFFER_TOKENS = 13_000
export function getAutoCompactThreshold(model: string): number {
const effectiveContextWindow = getEffectiveContextWindowSize(model)
const autocompactThreshold =
effectiveContextWindow - AUTOCOMPACT_BUFFER_TOKENS
const envPercent = process.env.CLAUDE_AUTOCOMPACT_PCT_OVERRIDE
if (envPercent) {
const parsed = parseFloat(envPercent)
if (!isNaN(parsed) && parsed > 0 && parsed <= 100) {
const percentageThreshold = Math.floor(
effectiveContextWindow * (parsed / 100),
)
return Math.min(percentageThreshold, autocompactThreshold)
}
}
return autocompactThreshold
}
这段代码证明:自动压缩阈值是一个提前量,不是窗口边界。AUTOCOMPACT_BUFFER_TOKENS = 13_000 给当前轮可能新增的工具结果、系统附件和压缩过程留空间。
以 200K 窗口为例:
| 步骤 | 公式 | 结果 |
|---|---|---|
| 原始上下文窗口 | contextWindow | 200,000 |
| 摘要输出预留 | MAX_OUTPUT_TOKENS_FOR_SUMMARY | 20,000 |
| 有效窗口 | 200,000 - 20,000 | 180,000 |
| 自动压缩缓冲 | AUTOCOMPACT_BUFFER_TOKENS | 13,000 |
| 自动压缩阈值 | 180,000 - 13,000 | 167,000 |
CLAUDE_AUTOCOMPACT_PCT_OVERRIDE 同样只能让压缩更早发生,因为返回值也是 Math.min(percentageThreshold, autocompactThreshold)。源码没有给用户一个“冒险晚压缩”的入口。
shouldAutoCompact() 的前置守卫
阈值只是最后一步。真正进入压缩前,shouldAutoCompact() 还会过滤掉几类不该压缩的调用。
if (querySource === 'session_memory' || querySource === 'compact') {
return false
}
if (querySource === 'marble_origami') {
return false
}
if (!isAutoCompactEnabled()) {
return false
}
if (isReactiveCompactEnabled()) {
return false
}
if (isContextCollapseEnabled()) {
return false
}
这段代码证明,自动压缩的触发条件不是单纯的 tokenCount >= threshold。它先排除递归来源,再排除 ctx-agent 共享状态污染,再让位给 reactive compact 和 Context Collapse。
容易误解
自动压缩不是“上下文越满越应该马上压缩”。当 Context Collapse 开启时,autocompact 会被禁用,因为 Collapse 在 90% 左右开始提交、95% 左右阻塞;autocompact 如果抢先执行,会破坏 Collapse 正准备保存的细粒度上下文。
熔断器:连续失败三次后停止自动尝试
压缩失败不是小概率噪音。原文引用的源码注释记录过一次线上事故:2026-03-10 的 BQ 数据显示,1,279 个 session 出现 50 次以上连续失败,单个 session 最高 3,272 次,全局每天浪费约 250K 次 API 调用。
源码里的修复非常硬:
const MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3
export type AutoCompactTrackingState = {
compacted: boolean
turnCounter: number
turnId: string
consecutiveFailures?: number
}
执行入口会先看失败次数。
if (
tracking?.consecutiveFailures !== undefined &&
tracking.consecutiveFailures >= MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES
) {
return { wasCompacted: false }
}
这段代码证明:连续三次自动压缩失败后,当前会话不再自动重试。成功路径会把 consecutiveFailures 重置为 0,失败路径递增计数。熔断只拦自动压缩,不等于用户不能手动 /compact。
这里的设计判断很清楚:宁可让会话暴露出“需要人工处理”的状态,也不要让系统在后台用确定失败的请求烧预算。
压缩提示词:摘要不是自由发挥,而是固定结构
触发自动压缩后,系统要向模型发送一个专门的压缩请求。prompt.ts 里不是一句“请总结对话”,而是三套变体和一个九段式输出结构。
三种提示词变体
| 变体 | 常量 | 摘要范围 | 典型用途 |
|---|---|---|---|
| 完整压缩 | BASE_COMPACT_PROMPT | 到目前为止的整个对话 | 首次自动压缩、手动 /compact |
| 近期压缩 | PARTIAL_COMPACT_PROMPT | 已保留上下文之后的最近消息 | 保留前缀,只压缩新增部分 |
| 前缀压缩 | PARTIAL_COMPACT_UP_TO_PROMPT | 新消息之前的历史前缀 | 摘要放在开头,后面继续接保留消息 |
这三个变体的差异不是文案差异,而是压缩视野不同。PARTIAL 不能假装自己看过完整对话,PARTIAL_UP_TO 也不能写“下一步”,因为更新的消息会接在它后面。
九段式摘要模板
BASE_COMPACT_PROMPT 要求摘要覆盖这些段落:
| 段落 | 保留的信息 | 为什么重要 |
|---|---|---|
Primary Request and Intent | 用户显式请求和意图 | 压缩后避免跑题 |
Key Technical Concepts | 技术、框架、模式、约束 | 保留判断语境 |
Files and Code Sections | 文件名、代码片段、修改点 | 让后续编辑有源码锚点 |
Errors and fixes | 错误、修复、用户反馈 | 防止重复犯错 |
Problem Solving | 已解决问题和仍在排查的问题 | 保留推理路径 |
All user messages | 非工具结果的用户消息 | 最大化保留用户约束 |
Pending Tasks | 明确未完成任务 | 下一轮继续推进 |
Current Work | 压缩前正在做什么 | 接住当前现场 |
Optional Next Step | 直接符合最近请求的下一步 | 给下一轮启动抓手 |
这说明 Claude Code 对压缩摘要的要求是“可继续执行”,不是“可读摘要”。尤其是 Files and Code Sections、Errors and fixes、Current Work 三段,都是面向下一轮工具调用的。
@@INLINE_0@@ 先写草稿,再从结果中剥离
提示词要求模型先写 @@INLINE_0@@,再写 @@INLINE_1@@。但最终上下文里不会留下 @@INLINE_2@@。
formattedSummary = formattedSummary.replace(
/<analysis>[\s\S]*?<\/analysis>/,
'',
)
这段代码证明:@@INLINE_0@@ 是一次性的整理空间,用来提高摘要覆盖率;它不是压缩后的长期记忆。Claude Code 用模型输出的一部分 token 换更稳定的摘要结构,但不把草稿继续带进后续上下文。
禁止工具调用
压缩请求还会在前后加入禁止工具调用的硬提示。
const NO_TOOLS_PREAMBLE = `CRITICAL: Respond with TEXT ONLY. Do NOT call any tools.
- Do NOT use Read, Bash, Grep, Glob, Edit, Write, or ANY other tool.
- You already have all the context you need in the conversation above.
- Tool calls will be REJECTED and will waste your only turn; you will fail the task.
- Your entire response must be plain text: an <analysis> block followed by a <summary> block.
`
这段代码证明:压缩调用不是普通 agent loop。它使用 maxTurns: 1,工具调用会被拒绝。一旦模型在唯一一轮里调用工具,压缩就没有文本摘要可用,所以提示词必须把“不要调用工具”放到最高优先级。
compactConversation():真正执行压缩的编排器
compactConversation() 负责把“需要压缩”变成一个新的消息序列。它不只调用模型,还要处理 prompt too long、清理文件状态、生成附件、触发 hook、记录 telemetry。
压缩后的消息结构
压缩结果不是一条 summary message 单独返回,而是按固定顺序拼成消息数组。
return [
boundaryMarker,
...summaryMessages,
...messagesToKeep,
...attachments,
...hookResults,
]
这段代码证明:压缩后的上下文有明确层次。
| 部分 | 含义 | 后续影响 |
|---|---|---|
boundaryMarker | SystemCompactBoundaryMessage,标记压缩边界 | 让系统知道这里发生过压缩 |
summaryMessages | 模型生成的结构化摘要 | 取代旧历史主线 |
messagesToKeep | 部分压缩时保留的近端消息 | 防止刚发生的交互被摘要化丢细节 |
attachments | 文件、计划、技能、工具声明等 | 重建工作现场 |
hookResults | SessionStart hooks 结果 | 让压缩后的会话像新 session 一样补齐环境信号 |
核心直觉
buildPostCompactMessages() 的顺序很重要。先有边界,再有摘要,再接保留消息,最后追加附件和 hook。模型看到的是“上一段会话的总结 + 近期原文 + 当前环境补丁”,不是孤零零的一份摘要。
压缩请求过长:Prompt too long 的重试路径
长会话需要压缩,但压缩请求也要把旧历史发给模型。如果旧历史已经超过输入限制,就会出现递归问题:想压缩,但压缩 prompt 自己太长。
源码用 truncateHeadForPTLRetry() 处理这类失败。
const groups = groupMessagesByApiRound(input)
const tokenGap = getPromptTooLongTokenGap(ptlResponse)
let dropCount: number
if (tokenGap !== undefined) {
let acc = 0
dropCount = 0
for (const g of groups) {
acc += roughTokenCountEstimationForMessages(g)
dropCount++
if (acc >= tokenGap) break
}
} else {
dropCount = Math.max(1, Math.floor(groups.length * 0.2))
}
这段代码证明 PTL 重试不是随便裁剪字符串,而是按 API round 分组丢弃旧内容。groupMessagesByApiRound() 的作用是避免把一个 tool_use 和对应的 tool_result 拆开。
如果错误响应能解析出 tokenGap,就从最旧的组开始累加,直到丢掉足够 token。如果不能解析,就丢掉 20% 的轮次组。最多重试 MAX_PTL_RETRIES = 3 次。
还有一个小但关键的修复:如果上次重试已经插入过 PTL_RETRY_MARKER,下一次分组前会先去掉它。
const input =
messages[0]?.type === 'user' &&
messages[0].isMeta &&
messages[0].message.content === PTL_RETRY_MARKER
? messages.slice(1)
: messages
这段代码证明了一个边界处理:避免每次重试都只丢掉上一次插入的 marker,导致没有实际进展。
压缩后恢复:先清空缓存,再选择性接回现场
压缩摘要生成之后,模型丢失了旧消息里的文件内容、技能内容、计划状态和延迟工具声明。compactConversation() 不会假设摘要能保住所有东西,而是用附件重新注入关键现场。
文件状态:快照、清空、恢复
压缩前先快照当前读过的文件状态,然后清空缓存。
const preCompactReadFileState = cacheToObject(context.readFileState)
context.readFileState.clear()
context.loadedNestedMemoryPaths?.clear()
这段代码证明:压缩后系统不再认为模型“已经知道”那些文件内容。否则后续去重逻辑会误判模型仍持有旧文件状态。清空之后,系统再把真正值得恢复的文件作为附件发给模型。
恢复预算由几个常量控制:
export const POST_COMPACT_MAX_FILES_TO_RESTORE = 5
export const POST_COMPACT_TOKEN_BUDGET = 50_000
export const POST_COMPACT_MAX_TOKENS_PER_FILE = 5_000
export const POST_COMPACT_MAX_TOKENS_PER_SKILL = 5_000
export const POST_COMPACT_SKILLS_TOKEN_BUDGET = 25_000
这些常量说明:压缩后的恢复是预算系统,不是全量回填。
| 预算 | 数值 | 影响 |
|---|---|---|
POST_COMPACT_MAX_FILES_TO_RESTORE | 5 | 最多恢复最近 5 个文件 |
POST_COMPACT_TOKEN_BUDGET | 50K | 文件附件总预算 |
POST_COMPACT_MAX_TOKENS_PER_FILE | 5K | 单文件恢复上限 |
POST_COMPACT_MAX_TOKENS_PER_SKILL | 5K | 单个技能内容上限 |
POST_COMPACT_SKILLS_TOKEN_BUDGET | 25K | 技能附件总预算 |
文件恢复过滤链
createPostCompactFileAttachments() 会从压缩前的 readFileState 里挑文件。
const preservedReadPaths = collectReadToolFilePaths(preservedMessages)
const recentFiles = Object.entries(readFileState)
.map(([filename, state]) => ({ filename, ...state }))
.filter(
file =>
!shouldExcludeFromPostCompactRestore(
file.filename,
toolUseContext.agentId,
) && !preservedReadPaths.has(expandPath(file.filename)),
)
.sort((a, b) => b.timestamp - a.timestamp)
.slice(0, maxFiles)
这段代码证明了文件恢复的优先级:
- 排除 plan 文件和记忆文件,因为它们有独立注入路径。
- 排除仍保留在近端消息里的 Read 文件,避免重复注入。
- 按
timestamp降序,只取最近的maxFiles。
最后还有总预算过滤。
let usedTokens = 0
return results.filter((result): result is AttachmentMessage => {
if (result === null) {
return false
}
const attachmentTokens = roughTokenCountEstimation(jsonStringify(result))
if (usedTokens + attachmentTokens <= POST_COMPACT_TOKEN_BUDGET) {
usedTokens += attachmentTokens
return true
}
return false
})
这段代码证明,即使最多只有 5 个文件,也不是一定全进上下文。恢复附件按顺序累加 token,超过 50K 的文件会被丢弃。
容易误解
文件恢复读取的是压缩后的当前磁盘内容,不是 readFileState 快照里的旧文本。快照提供“哪些文件最近重要”的排序依据,真正注入时会重新生成文件附件。
技能、计划和工具声明的恢复
技能恢复走 createSkillAttachmentIfNeeded()。它按调用时间排序,每个技能截断到 5K token,总预算 25K。
const skills = Array.from(invokedSkills.values())
.sort((a, b) => b.invokedAt - a.invokedAt)
.map(skill => ({
name: skill.skillName,
path: skill.skillPath,
content: truncateToTokens(
skill.content,
POST_COMPACT_MAX_TOKENS_PER_SKILL,
),
}))
这段代码证明:技能恢复不是恢复技能列表,而是恢复已经调用过的技能内容。源码注释里还特别说明,技能文件开头通常最关键,所以“按技能截断”比“整个丢弃”更合理。
计划和计划模式分别恢复:
| 附件 | 函数 | 恢复什么 |
|---|---|---|
plan_file_reference | createPlanAttachmentIfNeeded() | 当前 agent 的计划文件内容 |
plan_mode | createPlanModeAttachmentIfNeeded() | 当前是否仍处于 plan mode |
invoked_skills | createSkillAttachmentIfNeeded() | 已调用技能内容 |
task_status | createAsyncAgentAttachmentsIfNeeded() | 后台 async agent 状态 |
压缩还会吞掉之前的 delta 附件,所以 compactConversation() 会重新宣告工具和指令上下文。
for (const att of getDeferredToolsDeltaAttachment(
context.options.tools,
context.options.mainLoopModel,
[],
{ callSite: 'compact_full' },
)) {
postCompactFileAttachments.push(createAttachmentMessage(att))
}
for (const att of getAgentListingDeltaAttachment(context, [])) {
postCompactFileAttachments.push(createAttachmentMessage(att))
}
for (const att of getMcpInstructionsDeltaAttachment(
context.options.mcpClients,
context.options.tools,
context.options.mainLoopModel,
[],
)) {
postCompactFileAttachments.push(createAttachmentMessage(att))
}
这段代码里最关键的是空数组 []。正常 delta 附件会和历史消息做 diff,只发送新增部分;压缩后传空历史,等于“和空基线比较”,因此重新发送完整声明。callSite: 'compact_full' 也说明这是压缩后的完整重播路径。
微压缩:不是摘要,而是提前修剪工具结果
第 9 章主线是自动压缩,但源码里还有一层容易混淆的机制:microcompact。它不是 compactConversation() 的小号版本。它不调用模型生成摘要,而是直接处理旧的 tool_result。
时间型微压缩
maybeTimeBasedMicrocompact() 先根据时间间隔判断是否触发。
export function evaluateTimeBasedTrigger(
messages: Message[],
querySource: QuerySource | undefined,
): { gapMinutes: number; config: TimeBasedMCConfig } | null {
const config = getTimeBasedMCConfig()
if (!config.enabled || !querySource || !isMainThreadSource(querySource)) {
return null
}
const lastAssistant = messages.findLast(m => m.type === 'assistant')
if (!lastAssistant) {
return null
}
const gapMinutes =
(Date.now() - new Date(lastAssistant.timestamp).getTime()) / 60_000
if (!Number.isFinite(gapMinutes) || gapMinutes < config.gapThresholdMinutes) {
return null
}
return { gapMinutes, config }
}
这段代码证明,时间型微压缩只在主线程、明确 query source、并且距离上次 assistant 足够久时触发。默认配置来自 TimeBasedMCConfig:enabled: false、gapThresholdMinutes: 60、keepRecent: 5。
触发后,它会把旧 tool_result.content 替换成占位文本,同时保留最近若干个工具结果。
const result: Message[] = messages.map(message => {
if (message.type !== 'user' || !Array.isArray(message.message.content)) {
return message
}
const newContent = message.message.content.map(block => {
if (
block.type === 'tool_result' &&
clearSet.has(block.tool_use_id) &&
block.content !== TIME_BASED_MC_CLEARED_MESSAGE
) {
return { ...block, content: TIME_BASED_MC_CLEARED_MESSAGE }
}
return block
})
return { ...message, message: { ...message.message, content: newContent } }
})
这段代码证明:时间型微压缩会修改本地消息内容。它适合冷缓存场景,因为距离上次 assistant 很久,服务端缓存大概率已经过期,下一次本来就要重写前缀。
缓存型微压缩
实时会话不能轻易改本地消息,因为会破坏 prompt cache。cachedMicrocompactPath() 的做法是扫描可压缩工具结果,然后生成 cache_edits。
const compactableToolIds = new Set(collectCompactableToolIds(messages))
for (const message of messages) {
if (message.type === 'user' && Array.isArray(message.message.content)) {
const groupIds: string[] = []
for (const block of message.message.content) {
if (
block.type === 'tool_result' &&
compactableToolIds.has(block.tool_use_id) &&
!state.registeredTools.has(block.tool_use_id)
) {
mod.registerToolResult(state, block.tool_use_id)
groupIds.push(block.tool_use_id)
}
}
mod.registerToolMessage(state, groupIds)
}
}
这段代码证明:缓存微压缩按 tool_use_id 追踪工具结果,而不是按文本内容盲删。后续 createCacheEditsBlock() 会把要删除的工具结果变成 API 可以理解的编辑块。
type CachedMCEditsBlock = {
type: 'cache_edits'
edits: { type: 'delete'; cache_reference: string }[]
}
缓存型微压缩的特点是:本地消息不变,删除动作交给服务端缓存层执行。时间型微压缩一旦触发,会直接短路缓存型路径,因为冷缓存场景不需要再维护 warm cache 的连续性。
状态和数据结构
| 状态/结构 | 所在模块 | 关键字段 | 影响 |
|---|---|---|---|
AutoCompactTrackingState | autoCompact.ts | compacted、turnCounter、turnId、consecutiveFailures | 在 query loop 轮次之间追踪压缩状态和失败熔断 |
CompactionResult | compact.ts | messages、summary、wasCompacted 等 | 返回压缩后的消息序列和状态 |
readFileState | ToolUseContext | 文件路径、内容、时间戳 | 决定压缩后哪些文件有资格恢复 |
invokedSkills | skills 状态 | skillName、skillPath、content、invokedAt | 决定压缩后恢复哪些技能内容 |
pendingCacheEdits | microCompact.ts | cache_edits block | 缓存型微压缩等待 API 层发送的删除指令 |
pinnedEdits | API 层 | user message index、edit block | 确保已经发送过的 cache edits 在后续请求中稳定复现 |
这些状态共同构成压缩系统的“记忆外骨骼”:摘要负责主线,状态表负责恢复工作集,cache edits 负责在压缩前减缓上下文增长。
设计取舍
| 取舍 | 源码证据 | 工程判断 |
|---|---|---|
| 提前压缩,而不是撞到窗口再压缩 | MAX_OUTPUT_TOKENS_FOR_SUMMARY、AUTOCOMPACT_BUFFER_TOKENS | 压缩调用本身也需要输入输出空间,必须预留安全区 |
| 环境变量只能更保守 | Math.min(contextWindow, parsed)、Math.min(percentageThreshold, autocompactThreshold) | 用户可以降低风险,不能绕过模型限制 |
| Context Collapse 开启时禁用 autocompact | shouldAutoCompact() 的 Collapse 守卫 | 避免两个上下文管理器抢同一段历史 |
| 连续失败三次熔断 | MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3 | 停止确定失败的自动重试,保护 API 预算 |
压缩后清空 readFileState 再恢复 | cacheToObject() 后 clear() | 防止系统误以为模型仍知道旧文件内容 |
| 文件恢复有数量、单文件、总预算 | POST_COMPACT_* 常量 | 恢复工作集,不恢复全集 |
| 技能按头部截断 | truncateToTokens(skill.content, 5K) | 技能文件开头通常是核心指令,截断比丢弃更好 |
| delta 附件传空历史完整重播 | getDeferredToolsDeltaAttachment(..., []) | 复用增量机制,在压缩后恢复完整能力声明 |
| 微压缩分冷缓存和热缓存路径 | maybeTimeBasedMicrocompact() 短路 cachedMicrocompactPath() | 冷缓存直接改消息,热缓存用 cache_edits 保护缓存连续性 |
读源码抓手
读源码路线
不要从压缩提示词开始读。更好的路线是先看触发,再看压缩结果怎么回写。
- 先读
autoCompactIfNeeded(),看 query loop 每轮如何决定是否压缩。 - 再读
getEffectiveContextWindowSize()和getAutoCompactThreshold(),把 20K、13K、环境变量覆盖算清楚。 - 接着读
shouldAutoCompact(),重点看querySource、REACTIVE_COMPACT、Context Collapse 的守卫。 - 然后读
compactConversation(),追踪压缩请求、PTL retry、summary formatting、hook 和 telemetry。 - 再看
buildPostCompactMessages(),确认压缩后消息顺序。 - 最后追
createPostCompactFileAttachments()、createSkillAttachmentIfNeeded()、delta attachment 重播,理解“压缩后为什么还能继续干活”。 - 如果还想理解压缩前的减压机制,再读
microcompactMessages(),区分时间型和缓存型微压缩。
小结
小结
Claude Code 的上下文压缩是一个预算驱动的状态重建系统。
- 自动压缩阈值来自
有效窗口 - 13K buffer,有效窗口还会先扣掉最多 20K 的摘要输出空间。 shouldAutoCompact()不只看 token,还会排除递归来源、ctx-agent、reactive compact 和 Context Collapse。- 压缩摘要由固定提示词生成,
@@INLINE_0@@用来提升覆盖率,但会在formatCompactSummary()中剥离。 compactConversation()的核心产物是新的消息序列:边界、摘要、保留消息、附件和 hook 结果。- 压缩后的文件、技能、计划、工具声明都走选择性恢复,源码通过预算和过滤规则保证“接回工作集”,而不是把压缩省下来的空间立刻填满。