Claude Code 上下文压缩系统深度解析

基于源码分析,揭秘 Claude Code 如何通过五层机制让对话”永不超限”。


目录

  1. 系统全景
  2. 上下文窗口检测
  3. Token 计数机制
  4. 第一层:时间触发微压缩
  5. 第二层:缓存微压缩(ant 专属)
  6. 第三层:自动压缩
  7. 第四层:会话记忆压缩
  8. 第五层:完全压缩
  9. 压缩后的上下文重建
  10. 技能内容的跨压缩存活
  11. 警告与阻断系统
  12. 查询循环中的触发顺序
  13. 所有关键常量速查表

系统全景

所有 LLM 都有上下文窗口限制。Claude Code 的解法不是单一机制,而是五层递进式压缩架构,按照代价从低到高依次尝试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Token 压力上升


[层一] 时间触发微压缩 ← 检测服务端缓存已冷,清空旧工具结果


[层二] 缓存微压缩 ← 通过 API cache_edits 在服务端静默删除(ant 专属)


[层三] 自动压缩 ← Token 超过阈值时触发,带熔断器


[层四] 会话记忆压缩 ← 利用已有运行时摘要,无需额外 API 调用


[层五] 完全压缩 ← fork 子 Agent,让 AI 总结整段对话

相关文件分布在 src/services/compact/ 目录下:

文件 职责
compact.ts 核心压缩逻辑(~1700 行)
autoCompact.ts 自动压缩阈值、熔断器
microCompact.ts 微压缩(时间触发 + 缓存路径)
apiMicrocompact.ts API 原生上下文管理(ant 专属)
sessionMemoryCompact.ts 会话记忆压缩
prompt.ts NO_TOOLS_PREAMBLE、总结提示词
grouping.ts API 轮次分组(用于 PTL 重试)
compactWarningState.ts 警告抑制状态
postCompactCleanup.ts 压缩后缓存重置

上下文窗口检测

文件:src/utils/context.ts

Claude Code 通过多重检测确定当前模型的上下文窗口大小(第 51–98 行):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
export const MODEL_CONTEXT_WINDOW_DEFAULT = 200_000  // 默认 200K

export function getContextWindowForModel(model: string, betas?: string[]): number {
// 1. ant 内部环境变量覆盖(最高优先级)
if (process.env.USER_TYPE === 'ant' && process.env.CLAUDE_CODE_MAX_CONTEXT_TOKENS) {
const override = parseInt(process.env.CLAUDE_CODE_MAX_CONTEXT_TOKENS, 10)
if (!isNaN(override) && override > 0) return override
}

// 2. 模型名称含 [1m] 后缀 → 1M 上下文
if (has1mContext(model)) return 1_000_000

// 3. 模型能力注册表
const cap = getModelCapability(model)
if (cap?.max_input_tokens && cap.max_input_tokens >= 100_000) {
if (cap.max_input_tokens > MODEL_CONTEXT_WINDOW_DEFAULT && is1mContextDisabled())
return MODEL_CONTEXT_WINDOW_DEFAULT
return cap.max_input_tokens
}

// 4. 1M Beta Header + 支持的模型
if (betas?.includes(CONTEXT_1M_BETA_HEADER) && modelSupports1M(model))
return 1_000_000

// 5. GrowthBook 实验:Sonnet 4.6 获得 1M
if (getSonnet1mExpTreatmentEnabled(model)) return 1_000_000

// 6. ant 内部模型注册表
if (process.env.USER_TYPE === 'ant') {
const antModel = resolveAntModel(model)
if (antModel?.contextWindow) return antModel.contextWindow
}

return MODEL_CONTEXT_WINDOW_DEFAULT // 兜底:200K
}

压缩系统使用的是有效上下文窗口,需减去为总结输出预留的 token(autoCompact.ts 第 28–49 行):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const MAX_OUTPUT_TOKENS_FOR_SUMMARY = 20_000  // p99.99 实测为 17,387 tokens

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
// 标准 200K 模型:180,000
}

Token 计数机制

文件:src/utils/tokens.ts

系统使用两种计数策略:

精确计数(基于 API 返回值)

tokenCountWithEstimation()(第 226–261 行):从最近一次 API 响应的 usage 字段读取精确值,再加上此后新增消息的估算值:

1
2
3
4
5
6
7
8
9
// 从 usage 中读取的字段
getTokenCountFromUsage(usage) =
input_tokens +
cache_creation_input_tokens + // 缓存创建消耗
cache_read_input_tokens + // 缓存读取消耗
output_tokens // 输出消耗

// 此后新增消息的粗估
+ roughTokenCountEstimationForMessages(messages.slice(i + 1))

注意:token 计数包含缓存消耗,这是正确的——缓存内容虽然便宜,但确实占据了上下文窗口。

粗略估算(无 API 调用)

roughTokenCountEstimation()src/services/tokenEstimation.ts 第 203 行):

1
Math.round(content.length / 4)  // 每 4 字节约等于 1 个 token

更精确时可调用 countMessagesTokensWithAPI,使用 anthropic.beta.messages.countTokens(...)。若主路径失败,兜底为 countTokensViaHaikuFallback——真实发起一个 max_tokens: 1 的 Haiku API 调用,从返回的 usage 中读取实际 token 数。


第一层:时间触发微压缩

文件:src/services/compact/microCompact.ts

设计动机

Anthropic API 的 Prompt Cache TTL 约为 60 分钟。超时后,服务端缓存失效,所有历史工具结果都需要重新发送。此时已有大量 token 被”无效”内容占用——何不趁机清空?

触发条件

1
2
3
4
5
// 第 446–530 行
function maybeTimeBasedMicrocompact(messages, querySource): MicrocompactResult | null

// 触发:距上次 assistant 消息 > gapThresholdMinutes(默认 60 分钟)
// 配置来源:GrowthBook 标志 tengu_slate_heron

执行动作

  1. 保留最近 keepRecent(默认 5 个)可压缩工具结果
  2. 其余旧工具结果内容替换为:
1
2
3
export const TIME_BASED_MC_CLEARED_MESSAGE = '[Old tool result content cleared]'
// 文件:microCompact.ts 第 36 行
// 也定义于:src/utils/toolResultStorage.ts 第 34 行(两处内容保持一致,由测试保障)
  1. 调用 resetMicrocompactState() 清空缓存-MC 状态

可被压缩的工具类型file_read、Bash/shell 工具、grepglobweb_searchweb_fetchfile_editfile_write

关键细节:时间触发微压缩直接修改本地消息数组。因为缓存已冷,不存在服务端缓存失效问题。


第二层:缓存微压缩(ant 专属)

文件:src/services/compact/microCompact.ts,第 305–399 行

功能标志:CACHED_MICROCOMPACT(仅 Anthropic 内部)

与第一层的本质区别

时间触发微压缩修改本地消息;缓存微压缩发送 cache_edits 指令到服务端,在不破坏缓存前缀的前提下静默删除旧内容。

1
2
普通清空:本地改 → 下次发送时服务端缓存全部失效(昂贵)
缓存编辑:发送 cache_edits → 服务端删除指定条目 → 缓存前缀完好(便宜)

consumePendingCacheEdits() 在每次 API 请求前被 claude.ts 调用,将待处理的编辑指令附加到请求中。

外部用户的降级

1
2
3
4
5
6
// 第 288–292 行注释
// Legacy microcompact path removed — tengu_cache_plum_violet is always true.
// For contexts where cached microcompact is not available (external builds,
// non-ant users, unsupported models, sub-agents), no compaction happens here;
// autocompact handles context pressure instead.
return { messages }

对外部用户而言,前两层微压缩均不生效,上下文压力完全由第三层自动压缩处理。


第三层:自动压缩

文件:src/services/compact/autoCompact.ts

触发阈值的精确计算

“87%”并非代码中的字面常量,而是从 buffer 减法推导而来:

1
2
3
4
5
6
7
export const AUTOCOMPACT_BUFFER_TOKENS = 13_000   // 第 62 行
export const MAX_OUTPUT_TOKENS_FOR_SUMMARY = 20_000

// 对于标准 200K 模型:
effectiveWindow = 200,000 - 20,000 = 180,000
threshold = 180,000 - 13,000 = 167,000
// 占原始窗口的 83.5%,占有效窗口的 92.8%

可通过环境变量 CLAUDE_AUTOCOMPACT_PCT_OVERRIDE(百分比)覆盖阈值。

熔断器:三次失败停止

1
2
3
4
5
6
// 第 70 行
const MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3

// 来自代码注释(BigQuery 数据,2026-03-10):
// "1,279 个会话出现了 50+ 次连续失败(最多 3,272 次),
// 每天全局浪费约 250K 次 API 调用。"
1
2
3
4
// 第 257–265 行
if (tracking?.consecutiveFailures >= MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES) {
return { wasCompacted: false } // 本 session 永久停止尝试
}

成功时 consecutiveFailures 重置为 0;失败时递增。三次失败后该 session 不再尝试自动压缩。

完整触发决策链

自动压缩在以下情况下跳过autoCompact.ts 第 160–239 行):

  1. querySource === 'session_memory''compact'(递归防护)
  2. querySource === 'marble_origami'(上下文折叠 Agent,CONTEXT_COLLAPSE 特性开启时)
  3. 设置了 DISABLE_COMPACTDISABLE_AUTO_COMPACT 环境变量
  4. 用户配置 autoCompactEnabled === false
  5. REACTIVE_COMPACT 特性 + tengu_cobalt_raccoon 标志为 true(响应式专属模式)
  6. CONTEXT_COLLAPSE 特性开启且 isContextCollapseEnabled() 为 true
  7. 最终检查:tokenCountWithEstimation(messages) - snipTokensFreed >= threshold

第四层:会话记忆压缩

文件:src/services/compact/sessionMemoryCompact.ts

这是完全压缩的轻量替代方案:利用后台维护的运行时摘要,无需额外的 API 调用来生成总结。

默认配置

1
2
3
4
5
6
export const DEFAULT_SM_COMPACT_CONFIG = {
minTokens: 10_000, // 保留消息的最小 token 数
minTextBlockMessages: 5, // 保留包含文本的最少消息数
maxTokens: 40_000, // 保留消息的硬上限
}
// 可通过 GrowthBook tengu_sm_compact_config 远程调整

工作原理

  1. 找到 lastSummarizedMessageId(会话记忆系统最近已总结到的消息)
  2. 保留从该消息点到当前的消息(必要时向前扩展以满足最小阈值)
  3. 使用已有的会话记忆内容作为历史摘要
  4. 无需调用 API 生成摘要

安全门控

如果压缩后(摘要 + 保留消息)的 token 数仍超过自动压缩阈值,则返回 null,回退到完全压缩。

启用条件:GrowthBook 标志 tengu_session_memorytengu_sm_compact 同时为 true,或设置了 ENABLE_CLAUDE_CODE_SM_COMPACT 环境变量。


第五层:完全压缩

文件:src/services/compact/compact.ts(~1700 行)

NO_TOOLS_PREAMBLE:为什么这么严厉

文件:src/services/compact/prompt.ts,第 19–26 行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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.

`

// 还有结尾重申(第 269–272 行)
const NO_TOOLS_TRAILER =
'\n\nREMINDER: Do NOT call any tools. Respond with plain text only — ' +
'an <analysis> block followed by a <summary> block. ' +
'Tool calls will be rejected and you will fail the task.'

代码注释(第 12–18 行)解释了为什么措辞如此强硬:

“在 Sonnet 4.6+ 自适应思考模型上,即使有较弱的指令,模型有时仍会尝试工具调用。使用 maxTurns: 1 的缓存共享 fork 路径时,被拒绝的工具调用意味着没有文本输出 → 触发流式降级(4.6 上失败率 2.79%,4.5 上仅 0.01%)。”

API 层同样强制执行(compact.ts 第 1125–1134 行):

1
2
3
4
5
6
7
8
9
10
export function createCompactCanUseTool(): CanUseToolFn {
return async () => ({
behavior: 'deny' as const,
message: 'Tool use is not allowed during compaction',
decisionReason: {
type: 'other' as const,
reason: 'compaction agent should only produce text summary',
},
})
}

<analysis> 草稿模式

压缩提示词要求模型先写 <analysis> 块(思考草稿,提升总结质量),再写 <summary> 块。生成完成后,formatCompactSummary() 完全删除 analysis 块,只保留 summary 注入回上下文。

9 节总结提示词

压缩提示词要求总结以下 9 个方面:

  1. 主要请求与意图
  2. 关键技术概念
  3. 文件和代码段(含完整代码片段)
  4. 错误及修复方案
  5. 问题解决过程
  6. 所有用户消息
  7. 待处理任务
  8. 当前工作状态
  9. 可选的下一步(含最近对话的直接引用)

双路径流式架构

路径 A:缓存共享 fork(默认)

由 GrowthBook 标志 tengu_compact_cache_prefix(默认 true)控制,通过 runForkedAgent 运行,继承主对话的缓存前缀。

  • 不设置 maxOutputTokens(设置会导致 thinking 配置分歧,破坏缓存键)
  • skipCacheWrite: true,防止总结过程污染缓存

路径 B:直接流式降级

路径 A 失败时使用。设置 maxOutputTokensOverride: Math.min(20_000, getMaxOutputTokensForModel(...))。工具仅提供 [FileReadTool](若启用工具搜索则额外提供 ToolSearchTool 和 MCP 工具)。

PTL(Prompt Too Long)重试

压缩调用本身也可能超限。遇到 prompt_too_long 错误时:

1
2
3
4
5
6
const MAX_PTL_RETRIES = 3  // compact.ts 第 227 行

// truncateHeadForPTLRetry:
// 按 API 轮次分组(groupMessagesByApiRound),
// 从最旧的组开始删除,直到腾出足够空间
// 若错误中含 tokenGap,精确删除;否则删除约 20% 的消息

摘要注入回上下文的格式

getCompactUserSummaryMessage()prompt.ts 第 337 行):

1
2
3
4
5
6
7
8
This session is being continued from a previous conversation that ran out of context.
The summary below covers the earlier portion of the conversation.

Summary:
[格式化后的总结内容]

If you need specific details from before compaction (like exact code snippets, error messages,
or content you generated), read the full transcript at: [transcriptPath]

若为自动压缩(非用户手动触发),还会追加:

1
2
3
4
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.

压缩后的上下文重建

压缩完成后,buildPostCompactMessages() 按严格顺序重建上下文(第 330–338 行):

1
2
3
4
boundaryMarker
→ summaryMessages(AI 生成的历史摘要)
→ messagesToKeep(压缩边界后保留的最近消息)
→ attachments(按以下顺序)

Attachments 注入顺序compactConversation 函数):

  1. 文件重读(最多 5 个,总计 ≤50K tokens,每个 ≤5K)
  2. 异步 Agent 状态消息
  3. Plan 文件 attachment(若处于 plan 模式)
  4. Plan 模式指令
  5. 已调用技能内容(若本 session 使用过技能)
  6. 工具集 deltacallSite: 'compact_full',从头重新声明全部工具)
  7. Agent 列表 delta
  8. MCP 指令 delta
  9. SessionStart hook 结果(CLAUDE.md 重新注入等)

技能内容的跨压缩存活

文件:src/bootstrap/state.ts,第 1500–1540 行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export type InvokedSkillInfo = {
skillName: string
skillPath: string
content: string // 完整处理后的技能内容(含基础目录头 + 变量替换)
invokedAt: number // 时间戳(用于按近期排序)
agentId: string | null
}

export function addInvokedSkill(
skillName: string, skillPath: string, content: string, agentId: string | null = null,
): void {
const key = `${agentId ?? ''}:${skillName}`
STATE.invokedSkills.set(key, { skillName, skillPath, content, invokedAt: Date.now(), agentId })
}

压缩时的技能恢复createSkillAttachmentIfNeededcompact.ts 第 1494–1534 行):

  • invokedAt 降序排列(最近使用的优先保留)
  • 每个技能最多 POST_COMPACT_MAX_TOKENS_PER_SKILL = 5,000 tokens,超出部分用 marker 替换:
    1
    [... skill content truncated for compaction; use Read on the skill path if you need the full text]
  • 总技能预算 POST_COMPACT_SKILLS_TOKEN_BUDGET = 25,000 tokens

postCompactCleanup.ts 的关键注释(第 17–20 行):

1
2
3
4
// Note: We intentionally do NOT clear invoked skill content here.
// Skill content must survive across multiple compactions so that
// createSkillAttachmentIfNeeded() can include the full skill text
// in subsequent compaction attachments.

技能内容会跨越多次压缩持久保留——这是刻意设计,不是遗漏。


警告与阻断系统

文件:src/services/compact/autoCompact.ts,第 93–145 行

系统维护四个阶梯式阈值:

1
2
3
4
├─ WARNING_THRESHOLD(threshold - 20,000 tokens):显示上下文窗口使用警告
├─ ERROR_THRESHOLD(与 WARNING 相同值):显示红色警告
├─ AUTO_COMPACT_THRESHOLD(effectiveWindow - 13,000 tokens):触发自动压缩
└─ BLOCKING_LIMIT(actualWindow - 3,000 tokens):阻断用户输入
1
2
3
4
5
6
7
8
9
10
11
12
13
export function calculateTokenWarningState(tokenUsage, model) {
const threshold = isAutoCompactEnabled()
? getAutoCompactThreshold(model)
: getEffectiveContextWindowSize(model)

const percentLeft = Math.max(0, Math.round(((threshold - tokenUsage) / threshold) * 100))

const isAboveWarningThreshold = tokenUsage >= threshold - 20_000
const isAboveAutoCompactThreshold = isAutoCompactEnabled() && tokenUsage >= threshold
const isAtBlockingLimit = tokenUsage >= actualContextWindow - 3_000 // 硬上限前 3K

return { percentLeft, isAboveWarningThreshold, isAboveAutoCompactThreshold, isAtBlockingLimit }
}

isAtBlockingLimit 阻止新用户输入。可通过 CLAUDE_CODE_BLOCKING_LIMIT_OVERRIDE 环境变量覆盖。


查询循环中的触发顺序

文件:src/query.ts,第 393–468 行

每次用户发送消息,压缩层按固定顺序依次检查:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1. HISTORY_SNIP(feature('HISTORY_SNIP'))
└─ 从对话中间删除消息
└─ 返回 snipTokensFreed(传给 autocompact 校正阈值计算)

2. microcompactMessages(始终运行)
├─ maybeTimeBasedMicrocompact() ← 时间触发,有结果则短路返回
└─ cachedMicrocompactPath() ← 缓存路径(CACHED_MICROCOMPACT 特性)
└─ 外部用户:直接返回 { messages }(不做任何处理)

3. applyCollapsesIfNeeded(feature('CONTEXT_COLLAPSE'))
└─ 基于 commit log 的粒度上下文管理
└─ 若触发,跳过 autocompact

4. autoCompactIfNeeded(最终防线)
├─ 检查熔断器
├─ 检查全部跳过条件
├─ 检查 tokenCount - snipTokensFreed >= threshold
├─ 优先尝试 sessionMemoryCompact(无需 API 调用)
└─ 回退到完全压缩(fork 子 Agent 生成总结)

所有关键常量速查表

常量 文件 行号
MODEL_CONTEXT_WINDOW_DEFAULT 200,000 context.ts 9
MAX_OUTPUT_TOKENS_FOR_SUMMARY 20,000 autoCompact.ts 30
AUTOCOMPACT_BUFFER_TOKENS 13,000 autoCompact.ts 62
WARNING_THRESHOLD_BUFFER_TOKENS 20,000 autoCompact.ts 63
ERROR_THRESHOLD_BUFFER_TOKENS 20,000 autoCompact.ts 64
MANUAL_COMPACT_BUFFER_TOKENS 3,000 autoCompact.ts 65
MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES 3 autoCompact.ts 70
POST_COMPACT_MAX_FILES_TO_RESTORE 5 compact.ts 122
POST_COMPACT_TOKEN_BUDGET 50,000 compact.ts 123
POST_COMPACT_MAX_TOKENS_PER_FILE 5,000 compact.ts 124
POST_COMPACT_MAX_TOKENS_PER_SKILL 5,000 compact.ts 129
POST_COMPACT_SKILLS_TOKEN_BUDGET 25,000 compact.ts 130
MAX_COMPACT_STREAMING_RETRIES 2 compact.ts 131
MAX_PTL_RETRIES 3 compact.ts 227
DEFAULT_SM_COMPACT_CONFIG.minTokens 10,000 sessionMemoryCompact.ts 57
DEFAULT_SM_COMPACT_CONFIG.minTextBlockMessages 5 sessionMemoryCompact.ts 58
DEFAULT_SM_COMPACT_CONFIG.maxTokens 40,000 sessionMemoryCompact.ts 59
TIME_BASED_MC_CONFIG.gapThresholdMinutes 60 分钟 timeBasedMCConfig.ts 31
TIME_BASED_MC_CONFIG.keepRecent 5 timeBasedMCConfig.ts 33
TOOL_RESULT_CLEARED_MESSAGE '[Old tool result content cleared]' toolResultStorage.ts 34
TIME_BASED_MC_CLEARED_MESSAGE '[Old tool result content cleared]' microCompact.ts 36
roughTokenCountEstimation 精度 4 字节/token tokenEstimation.ts 205
API native MC trigger 180,000 tokens apiMicrocompact.ts
API native MC target 40,000 tokens apiMicrocompact.ts

设计哲学总结

这套系统体现了几个精妙的工程取舍:

1. 代价递进:从”直接清空旧工具结果”(近乎零代价)到”fork 子 Agent 生成摘要”(一次额外 API 调用),代价逐层提升,每层只在必要时触发。

2. 缓存感知:时间触发微压缩专门利用缓存冷却窗口,在”反正要重发”的时机顺手清理,不额外增加成本。

3. 熔断器防护:BQ 数据显示没有熔断器时每天全局浪费 25 万次 API 调用——用 3 次失败上限解决了一个真实的生产问题。

4. 技能内容持久化:技能内容刻意跨越多次压缩保留,确保 AI 始终能”记住”本 session 使用过哪些技能的完整指令,而不是在每次压缩后茫然。

5. 摘要与工具硬隔离NO_TOOLS_PREAMBLE + API 层 deny 双重保险,源于真实的 2.79% 失败率数据,不是过度防御。