一、引言:上下文是 Agentic AI 的生命线
在 前文 中,我们拆解了 Claude Code 的 Agentic Loop 和工具系统。但有一个核心问题始终悬而未决——当对话越来越长,上下文窗口逐渐耗尽时,Claude Code 如何做到"无限对话"?
答案藏在一个精密的上下文管理系统中。这个系统涉及 Token 计数、五层递进压缩、提示缓存优化、Session Memory 持久化 等多个子系统的协同工作。Claude Code 的 system prompt 中有这样一句看似轻描淡写的话:
“The system will automatically compress prior messages in your conversation as it approaches context limits. This means your conversation with the user is not limited by the context window.”
这句话背后,是数千行精心设计的代码。本文基于 2026 年 3 月 31 日泄露的源码快照,从源码层面深入分析上下文管理的完整实现。
全文架构
| 章节 | 主题 | 核心文件 |
|---|---|---|
| 二 | 消息结构与类型系统 | types/message.js, utils/messages.ts |
| 三 | Token 计数与上下文窗口 | utils/tokens.ts, utils/context.ts, services/tokenEstimation.ts |
| 四 | 系统提示词组装 | constants/prompts.ts, utils/systemPrompt.ts, context.ts |
| 五 | 五层递进压缩策略 | services/compact/*.ts |
| 六 | 提示缓存 (Prompt Cache) | utils/api.ts, services/api/promptCacheBreakDetection.ts |
| 七 | Session Memory 持久化 | services/SessionMemory/sessionMemory.ts |
| 八 | 主查询循环的上下文编排 | query.ts |
| 九 | 实现原理深度剖析 | utils/messages.ts, utils/toolResultStorage.ts, services/compact/compact.ts |
二、消息结构与类型系统
2.1 核心消息类型
Claude Code 的消息系统远比简单的 user/assistant 交替复杂。源码中定义了丰富的消息类型层次:
// types/message.js 中定义的消息类型(从 utils/messages.ts 的 import 推断)
type UserMessage = {
type: 'user'
message: { role: 'user'; content: string | ContentBlockParam[] }
isMeta?: true // 系统元消息,不显示给用户
isCompactSummary?: true // 压缩摘要消息
isVirtual?: true // 虚拟消息,不发送到 API
toolUseResult?: unknown // 工具结果数据
uuid: UUID
timestamp: string
}
type AssistantMessage = {
type: 'assistant'
message: { role: 'assistant'; content: ContentBlock[]; usage: Usage }
isApiErrorMessage?: boolean
apiError?: string
uuid: UUID
timestamp: string
}
type SystemMessage = {
type: 'system'
subtype: 'compact_boundary' | 'microcompact_boundary' | ...
content: string
}
type AttachmentMessage = {
type: 'attachment'
attachment: Attachment // 文件变更、计划、技能等
}
几个关键设计点:
isMeta: 系统内部注入的消息(如用户上下文、恢复提示),参与 API 调用但不在 UI 显示isCompactSummary: 标记压缩后的摘要消息,用于 UI 特殊渲染isVirtual: 仅用于显示(如 REPL 内部工具调用),normalizeMessagesForAPI时被过滤compact_boundary: 压缩边界标记,getMessagesAfterCompactBoundary只发送边界之后的消息
2.2 消息规范化(发送到 API 前)
normalizeMessagesForAPI 是消息发送前的最后一道关卡:
// utils/messages.ts
export function normalizeMessagesForAPI(
messages: Message[], tools: Tools = [],
): (UserMessage | AssistantMessage)[] {
const availableToolNames = new Set(tools.map(t => t.name))
// 1. 重排附件消息(向上冒泡直到遇到 tool_result 或 assistant 消息)
// 2. 过滤虚拟消息(isVirtual = true)
// 3. 处理 tool_use/tool_result 配对完整性
// 4. 合并相同 message.id 的消息(并行工具调用的拆分记录)
// 5. 过滤 system / progress 类型消息(API 不接受)
}
2.3 压缩边界消息
压缩后,所有旧消息被一个边界标记替代:
export function createCompactBoundaryMessage(
trigger: 'manual' | 'auto',
preTokens: number,
lastPreCompactMessageUuid?: UUID,
): SystemCompactBoundaryMessage {
return {
type: 'system',
subtype: 'compact_boundary',
content: 'Conversation compacted',
compactMetadata: { trigger, preTokens, userContext, messagesSummarized },
}
}
export function getMessagesAfterCompactBoundary(messages) {
const boundaryIndex = findLastCompactBoundaryIndex(messages)
return boundaryIndex === -1 ? messages : messages.slice(boundaryIndex)
}
这意味着每次 API 调用,只发送最后一次压缩之后的消息,加上压缩摘要。
三、Token 计数与上下文窗口
3.1 上下文窗口大小
// utils/context.ts
export const MODEL_CONTEXT_WINDOW_DEFAULT = 200_000
export function getContextWindowForModel(model: string, betas?: string[]): number {
// 优先级:
// 1. 环境变量覆盖 (CLAUDE_CODE_MAX_CONTEXT_TOKENS, ant-only)
// 2. [1m] 后缀 → 1,000,000
// 3. 模型能力查询 (max_input_tokens)
// 4. beta 头检查 (CONTEXT_1M_BETA_HEADER)
// 5. 默认 200,000
}
模型最大输出 token 配置:
| 模型 | 默认输出 | 上限 |
|---|---|---|
| Opus 4.6 | 64K | 128K |
| Sonnet 4.6 | 32K | 128K |
| Sonnet 4 / Haiku 4 | 32K | 64K |
3.2 Token 计数核心函数
tokenCountWithEstimation 是所有阈值检查(autocompact、session memory、blocking limit)的统一度量:
// utils/tokens.ts
export function tokenCountWithEstimation(messages: readonly Message[]): number {
// 从后往前找到最后一个有 API usage 数据的助手消息
let i = messages.length - 1
while (i >= 0) {
const usage = getTokenUsage(messages[i])
if (usage) {
// 关键:处理并行工具调用的拆分消息(相同 message.id)
// 向前找到第一个相同 id 的消息,确保所有交错的 tool_result 都被估算
const responseId = getAssistantMessageId(messages[i])
if (responseId) {
let j = i - 1
while (j >= 0) {
if (getAssistantMessageId(messages[j]) === responseId) i = j
else if (getAssistantMessageId(messages[j]) !== undefined) break
j--
}
}
return getTokenCountFromUsage(usage) +
roughTokenCountEstimationForMessages(messages.slice(i + 1))
}
i--
}
return roughTokenCountEstimationForMessages(messages)
}
核心思路:最后一次 API 返回的 usage(精确值)+ 之后新增消息的粗略估算。
3.3 粗略 Token 估算
// services/tokenEstimation.ts
export function roughTokenCountEstimation(content: string, bytesPerToken = 4): number {
return Math.round(content.length / bytesPerToken)
}
// JSON 更密集,每 2 个字符约 1 个 token
export function bytesPerTokenForFileType(fileExtension: string): number {
switch (fileExtension) {
case 'json': case 'jsonl': case 'jsonc': return 2
default: return 4
}
}
这个 4 chars/token 的经验值贯穿了整个系统。Claude Code 还支持通过 API 进行精确计数:
export async function countMessagesTokensWithAPI(messages, tools): Promise<number | null> {
// 使用 anthropic.beta.messages.countTokens API
// 支持 Bedrock 和 Vertex 后端
}
3.4 上下文使用百分比
export function calculateContextPercentages(currentUsage, contextWindowSize) {
const totalInputTokens =
currentUsage.input_tokens +
currentUsage.cache_creation_input_tokens +
currentUsage.cache_read_input_tokens
const usedPercentage = Math.round((totalInputTokens / contextWindowSize) * 100)
return { used: clampedUsed, remaining: 100 - clampedUsed }
}
四、系统提示词组装
4.1 双层上下文注入
Claude Code 的上下文注入分为两层:
// context.ts
export const getSystemContext = memoize(async () => ({
gitStatus, // git 状态快照(会话级别缓存)
cacheBreaker, // 缓存破坏注入(调试用)
}))
export const getUserContext = memoize(async () => ({
claudeMd, // CLAUDE.md 记忆文件内容
currentDate, // 当前日期
}))
注入方式不同:
// utils/api.ts
// 系统上下文 → 追加到系统提示词末尾
export function appendSystemContext(systemPrompt, context) {
return [...systemPrompt, Object.entries(context).map(...)].filter(Boolean)
}
// 用户上下文 → 作为首条用户消息注入
export function prependUserContext(messages, context) {
return [
createUserMessage({
content: `<system-reminder>..${context}..</system-reminder>`,
isMeta: true, // 不显示给用户
}),
...messages,
]
}
4.2 系统提示词分区缓存
系统提示词被精心分为可缓存和不可缓存两类:
// constants/systemPromptSections.ts
// 缓存型:计算一次,整个会话不变
export function systemPromptSection(name: string, compute: ComputeFn)
// 危险型:每轮重新计算,会破坏提示缓存
export function DANGEROUS_uncachedSystemPromptSection(name, compute, reason)
在 getSystemPrompt 中的组装顺序:
// constants/prompts.ts
export async function getSystemPrompt(tools, model, ...): Promise<string[]> {
return [
// --- 静态内容(可跨 org 全局缓存)---
getSimpleIntroSection(outputStyleConfig), // 身份定义
getSimpleSystemSection(), // 系统规则
getSimpleDoingTasksSection(), // 任务指令
getActionsSection(), // 操作规范
getUsingYourToolsSection(enabledTools), // 工具使用指南
getSimpleToneAndStyleSection(), // 语调风格
getOutputEfficiencySection(), // 输出效率
// === 边界标记 ===
...(shouldUseGlobalCacheScope() ? [SYSTEM_PROMPT_DYNAMIC_BOUNDARY] : []),
// --- 动态内容(每会话变化)---
...resolvedDynamicSections, // memory, env_info, language, MCP 等
].filter(s => s !== null)
}
SYSTEM_PROMPT_DYNAMIC_BOUNDARY 是全局缓存的关键——边界之前的内容可以在所有用户之间共享缓存。
4.3 有效系统提示词构建优先级
// utils/systemPrompt.ts
export function buildEffectiveSystemPrompt({...}) {
// 优先级: override > coordinator > agent > custom > default
// appendSystemPrompt 总是追加在最后
if (overrideSystemPrompt) return asSystemPrompt([overrideSystemPrompt])
if (coordinatorMode) return getCoordinatorSystemPrompt()
if (agentSystemPrompt) return [agentSystemPrompt]
if (customSystemPrompt) return [customSystemPrompt]
return defaultSystemPrompt
}
五、五层递进压缩策略
这是 Claude Code 上下文管理最精妙的部分。当上下文逐渐膨胀,系统会按照从轻到重的顺序逐层尝试压缩。
5.1 Layer 1: History Snip(历史裁剪)
最轻量的策略,在每次 query 循环入口执行:
// query.ts
if (feature('HISTORY_SNIP')) {
const snipResult = snipModule.snipCompactIfNeeded(messagesForQuery)
messagesForQuery = snipResult.messages
snipTokensFreed = snipResult.tokensFreed
}
直接裁剪最老的 API 轮次组,零 token 成本,不调用 LLM。
5.2 Layer 2: MicroCompact(微压缩)
清理旧的工具结果内容,有两种路径:
// services/compact/microCompact.ts
const COMPACTABLE_TOOLS = new Set([
'Read', 'Bash', 'Grep', 'Glob', 'WebSearch', 'WebFetch', 'Edit', 'Write'
])
// 路径 1: 缓存微压缩(cache_edits API)
// 不修改本地消息内容,通过 API 的 cache_edits 删除旧工具结果
// 关键优势:不破坏缓存前缀
async function cachedMicrocompactPath(messages, querySource) {
const toolsToDelete = mod.getToolResultsToDelete(state)
if (toolsToDelete.length > 0) {
pendingCacheEdits = mod.createCacheEditsBlock(state, toolsToDelete)
return { messages, compactionInfo: { pendingCacheEdits: {...} } }
}
}
// 路径 2: 基于时间的微压缩
// 当空闲超过阈值时,缓存已冷,直接清空旧工具结果
function maybeTimeBasedMicrocompact(messages, querySource) {
const gapMinutes = (Date.now() - lastAssistant.timestamp) / 60_000
if (gapMinutes >= config.gapThresholdMinutes) {
// 保留最近 N 个,清空其余
// 替换为 '[Old tool result content cleared]'
}
}
5.3 Layer 3: Context Collapse(上下文折叠)
上下文折叠是一种读时投影策略——不修改原始消息,而是在读取时将旧的对话片段折叠为摘要视图:
// query.ts
if (feature('CONTEXT_COLLAPSE') && contextCollapse) {
const collapseResult = await contextCollapse.applyCollapsesIfNeeded(
messagesForQuery, toolUseContext, querySource,
)
messagesForQuery = collapseResult.messages
}
运行在 autocompact 之前——如果折叠就足以将 token 降到阈值以下,则避免全量摘要压缩,保持更细粒度的上下文。
触发阈值:90% 上下文使用率开始折叠,95% 阻止新的 spawn。当 API 返回 413 时可以 drain 已暂存的折叠。
5.4 Layer 4: Session Memory Compact(SM 压缩)
这是传统压缩的替代方案,使用预先提取的 Session Memory 文件:
// services/compact/sessionMemoryCompact.ts
export const DEFAULT_SM_COMPACT_CONFIG = {
minTokens: 10_000, // 最少保留 10K token 的原始消息
minTextBlockMessages: 5, // 最少保留 5 条文本消息
maxTokens: 40_000, // 最多保留 40K token
}
export async function trySessionMemoryCompaction(messages, agentId?, autoCompactThreshold?) {
// 1. 检查 session memory 文件是否存在且非空
const sessionMemory = await getSessionMemoryContent()
if (!sessionMemory || await isSessionMemoryEmpty(sessionMemory)) return null
// 2. 计算保留消息的起始索引
const startIndex = calculateMessagesToKeepIndex(messages, lastSummarizedIndex)
// 3. 确保不拆分 tool_use/tool_result 对
adjustIndexToPreserveAPIInvariants(messages, startIndex)
// 4. 用 session memory 内容替代传统 LLM 摘要
return createCompactionResultFromSessionMemory(
messages, sessionMemory, messagesToKeep, hookResults, ...
)
}
优势:零 LLM 调用成本(摘要已在后台提前准备好),且保留了最近的原始消息。
5.5 Layer 5: Full Compact(传统 LLM 摘要压缩)
最重量级的策略,使用 Forked Agent 生成结构化摘要:
// services/compact/compact.ts
export async function compactConversation(messages, context, ...): Promise<CompactionResult> {
// 1. 执行 PreCompact hooks
const hookResult = await executePreCompactHooks(...)
// 2. 构建压缩提示词(9 个结构化部分)
const compactPrompt = getCompactPrompt(customInstructions)
// 3. 使用 forked agent 生成摘要(共享主线程的 prompt cache)
summaryResponse = await streamCompactSummary({...})
// 4. 处理 prompt-too-long 重试(截断最旧消息,最多 3 次)
if (summary?.startsWith(PROMPT_TOO_LONG_ERROR_MESSAGE)) {
const truncated = truncateHeadForPTLRetry(messagesToSummarize, summaryResponse)
}
// 5. 清除文件状态缓存
context.readFileState.clear()
// 6. 恢复最近读取的文件(最多 5 个,50K token 预算)
const fileAttachments = await createPostCompactFileAttachments(
preCompactReadFileState, context, POST_COMPACT_MAX_FILES_TO_RESTORE
)
// 7. 重新注入计划、技能、工具信息
// 8. 执行 SessionStart hooks 和 PostCompact hooks
// 9. 返回 CompactionResult
}
压缩提示词要求生成包含 9 个部分的结构化摘要:
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 // 可选的下一步
此外还有一个 <analysis> 思考区域,在最终摘要中会被剥离——这是一种"先深度思考再精简输出"的技巧。
5.6 Reactive Compact(反应式压缩)
当 API 返回 prompt-too-long (413) 错误时的最后兜底:
// query.ts
if ((isWithheld413 || isWithheldMedia) && reactiveCompact) {
const compacted = await reactiveCompact.tryReactiveCompact({
hasAttempted: hasAttemptedReactiveCompact,
messages: messagesForQuery,
cacheSafeParams: {...},
})
if (compacted) {
const postCompactMessages = buildPostCompactMessages(compacted)
// 用压缩后的消息替换并重试
state = { messages: postCompactMessages, ... }
continue
}
}
5.7 压缩后的状态清理
// services/compact/postCompactCleanup.ts
export function runPostCompactCleanup(querySource?: QuerySource): void {
resetMicrocompactState() // 重置微压缩状态
resetContextCollapse() // 重置上下文折叠(仅主线程)
getUserContext.cache.clear() // 清除用户上下文缓存
resetGetMemoryFilesCache() // 重置记忆文件缓存
clearSystemPromptSections() // 清除系统提示词分区缓存
clearClassifierApprovals() // 清除分类器审批
clearSessionMessagesCache() // 清除会话消息缓存
}
六、提示缓存 (Prompt Cache) 策略
Prompt Cache 对 Claude Code 的成本和延迟至关重要。系统设计了精细的缓存分区和破坏检测机制。
6.1 系统提示词缓存分区
// utils/api.ts
export function splitSysPromptPrefix(systemPrompt, options?) {
// 三种模式:
// 1. MCP 工具存在 → org 级缓存(避免 MCP 指令变化破坏全局缓存)
// 2. 全局缓存 + 边界标记 → 静态=global, 动态=无缓存
// 3. 默认 → org 级缓存
//
// 返回 SystemPromptBlock[] 带有 cacheScope: 'global' | 'org' | null
}
cache_control 支持的配置:
cache_control: {
type: 'ephemeral',
scope: 'global' | 'org', // global 跨组织共享,org 组织内共享
ttl: '5m' | '1h' // 缓存生存时间
}
6.2 Forked Agent 的缓存共享
压缩和 Session Memory 提取使用 runForkedAgent,关键设计是共享主线程的缓存前缀:
// compact.ts
const result = await runForkedAgent({
promptMessages: [summaryRequest],
cacheSafeParams, // 传入主线程的 system/tools/context
canUseTool: createCompactCanUseTool(),
querySource: 'compact',
forkLabel: 'compact',
maxTurns: 1,
skipCacheWrite: true,
})
cacheSafeParams 包含 systemPrompt、userContext、systemContext、toolUseContext、forkContextMessages,这些与主线程完全相同,确保缓存命中。
6.3 缓存破坏检测
// services/api/promptCacheBreakDetection.ts
// Phase 1: 记录提示状态(pre-call)
export function recordPromptState(snapshot: PromptStateSnapshot) {
// 追踪: systemHash, toolsHash, cacheControlHash,
// model, fastMode, betas, effortValue, extraBodyHash
// perToolHashes (精确到每个工具的 schema)
}
// Phase 2: 检查响应中的缓存破坏(post-call)
export async function checkResponseForCacheBreak(
querySource, cacheReadTokens, cacheCreationTokens, messages, ...
) {
// 检测条件: cache_read_tokens 下降 > 5% 且绝对值 > 2K
// 排除: TTL 过期 / cacheDeletion / compaction
// 输出: 详细的变化分析 + diff 文件
}
防误报措施:
// 压缩后重置基线
export function notifyCompaction(querySource, agentId?) {
state.prevCacheReadTokens = null // 下次不比较
}
// 微压缩后标记
export function notifyCacheDeletion(querySource, agentId?) {
state.cacheDeletionsPending = true // 下次跳过检查
}
七、Session Memory 持久化
Session Memory 是 Claude Code 的"工作笔记本"——自动维护一个 markdown 文件,记录当前对话的关键信息。
7.1 提取时机
// services/SessionMemory/sessionMemory.ts
export function shouldExtractMemory(messages: Message[]): boolean {
// 初始化阈值:达到一定 token 数后开始
if (!hasMetInitializationThreshold(currentTokenCount)) return false
// 更新阈值:token 增长 + 工具调用次数双重门控
const hasMetTokenThreshold = hasMetUpdateThreshold(currentTokenCount)
const hasMetToolCallThreshold =
toolCallsSinceLastUpdate >= getToolCallsBetweenUpdates()
// 在自然对话间歇时触发(最后一轮没有工具调用)
return (hasMetTokenThreshold && hasMetToolCallThreshold) ||
(hasMetTokenThreshold && !hasToolCallsInLastTurn)
}
7.2 提取过程
export function initSessionMemory(): void {
if (!isAutoCompactEnabled()) return
// 注册为 post-sampling hook
registerPostSamplingHook(extractSessionMemory)
}
const extractSessionMemory = sequential(async function(context) {
// 1. 只在主 REPL 线程运行
if (querySource !== 'repl_main_thread') return
// 2. 创建/读取 session memory 文件
const { memoryPath, currentMemory } = await setupSessionMemoryFile(setupContext)
// 3. 使用 forked agent 运行提取(共享主线程的 prompt cache)
await runForkedAgent({
promptMessages: [createUserMessage({ content: userPrompt })],
cacheSafeParams: createCacheSafeParams(context),
canUseTool: createMemoryFileCanUseTool(memoryPath),
querySource: 'session_memory',
forkLabel: 'session_memory',
})
// 4. 记录 lastSummarizedMessageId(用于 SM-compact 确定边界)
updateLastSummarizedMessageIdIfSafe(messages)
})
7.3 与压缩的协同
Session Memory 与压缩系统紧密耦合:
- SM-Compact 优先:
autoCompactIfNeeded先尝试trySessionMemoryCompaction,成功则跳过传统压缩 - 边界追踪:
lastSummarizedMessageId标记 SM 已覆盖到哪条消息,压缩时只需保留之后的原始消息 - 成本优势: SM-Compact 不调用 LLM 生成摘要(摘要已在后台准备好),传统压缩需要 ~20K output tokens
八、主查询循环的上下文编排
所有上述子系统在 query.ts 的主循环中协同工作:
// query.ts
async function* queryLoop(params) {
while (true) {
let messagesForQuery = [...getMessagesAfterCompactBoundary(messages)]
// ① 工具结果预算控制
messagesForQuery = await applyToolResultBudget(messagesForQuery, ...)
// ② Snip Compact(裁剪最老历史)
const snipResult = snipModule.snipCompactIfNeeded(messagesForQuery)
snipTokensFreed = snipResult.tokensFreed
// ③ MicroCompact(清理旧工具结果)
const microcompactResult = await deps.microcompact(messagesForQuery, ...)
// ④ Context Collapse(折叠上下文视图)
const collapseResult = await contextCollapse.applyCollapsesIfNeeded(...)
// ⑤ 系统提示词组装
const fullSystemPrompt = appendSystemContext(systemPrompt, systemContext)
// ⑥ Auto Compact(SM 压缩 → 传统压缩回退)
const { compactionResult } = await deps.autocompact(messagesForQuery, ...)
// ⑦ Blocking Limit 检查
const { isAtBlockingLimit } = calculateTokenWarningState(
tokenCountWithEstimation(messagesForQuery) - snipTokensFreed, model
)
// ⑧ API 调用(带 prompt cache)
for await (const message of deps.callModel({
messages: prependUserContext(messagesForQuery, userContext),
systemPrompt: fullSystemPrompt,
...
})) {
// 处理流式响应、tool_use 块、错误恢复...
}
// ⑨ 工具执行
for await (const update of toolUpdates) { ... }
// ⑩ 附件注入(文件变更、技能发现、记忆预取)
for await (const attachment of getAttachmentMessages(...)) { ... }
// ⑪ 循环继续
state = { messages: [...messagesForQuery, ...assistantMessages, ...toolResults], ... }
}
}
8.1 Token 阈值体系
// services/compact/autoCompact.ts
export function calculateTokenWarningState(tokenUsage, model) {
const effectiveWindow = getEffectiveContextWindowSize(model)
// = contextWindow - min(maxOutputTokens, 20K)
const autoCompactThreshold = effectiveWindow - 13_000 // ~93%
const warningThreshold = autoCompactThreshold - 20_000
const errorThreshold = autoCompactThreshold - 20_000
const blockingLimit = effectiveWindow - 3_000 // ~98%
return {
isAboveWarningThreshold, // UI 显示黄色警告
isAboveErrorThreshold, // UI 显示红色警告
isAboveAutoCompactThreshold, // 触发自动压缩
isAtBlockingLimit, // 阻止 API 调用
}
}
8.2 断路器机制
防止无限重试:
const MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3
if (tracking?.consecutiveFailures >= MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES) {
return { wasCompacted: false } // 停止重试
}
8.3 图片剥离
压缩前自动剥离图片以减少 token 消耗:
export function stripImagesFromMessages(messages: Message[]): Message[] {
// image → [image]
// document → [document]
// 包括嵌套在 tool_result 中的图片
}
九、实现原理深度剖析
前面几章介绍了各个子系统的功能和使用方式,本章深入到源码层面,剖析几个关键机制的内部实现原理。
9.1 消息规范化管线的 7 步处理流程
normalizeMessagesForAPI 是所有消息发送到 API 之前的最终规范化管线,实现了一个精密的 7 步处理流程:
// utils/messages.ts 第 1989-2370 行
export function normalizeMessagesForAPI(messages, tools) {
// === 步骤 1: 附件重排序 + 虚拟消息过滤 ===
const reorderedMessages = reorderAttachmentsForAPI(messages)
.filter(m => !(m.isVirtual)) // 过滤 display-only 消息
// === 步骤 2: 构建错误→剥离映射 ===
// PDF/image 过大错误后,从前一个 isMeta 消息中清理对应的 document/image 块
const stripTargets = new Map<string, Set<string>>()
// 向后扫描匹配 API 错误与其触发的用户消息
// === 步骤 3: 类型过滤 ===
// 只保留: user / assistant / attachment / system(local_command)
// 过滤: progress / system(非 local_command) / 合成 API 错误
// === 步骤 4: 逐消息类型处理 ===
.forEach(message => {
case 'user':
// - 剥离 tool_reference 块(tool search 未启用时)
// - 清理触发错误的 document/image 块
// 关键: 连续 user 消息合并(Bedrock 兼容性要求)
if (lastMessage?.type === 'user') {
result[i] = mergeUserMessages(lastMessage, normalizedMessage)
}
case 'assistant':
// - normalizeToolInputForAPI 标准化工具输入
// - 规范化工具名(alias → canonical)
// 关键: 向后扫描合并同 message.id 的 assistant(并行工具调用拆分记录)
for (let i = result.length - 1; i >= 0; i--) {
if (msg.message.id === normalizedMessage.message.id) {
result[i] = mergeAssistantMessages(msg, normalizedMessage)
return
}
}
case 'attachment':
// 转为 user 消息,合并到前一个 user
})
// === 步骤 5-7: 后处理管道 ===
const relocated = relocateToolReferenceSiblings(result) // tool_reference 重定位
const filtered = filterOrphanedThinkingOnlyMessages(relocated) // 清理孤立 thinking
const cleaned = filterTrailingThinkingFromLastAssistant(filtered) // 尾部 thinking
const nonEmpty = ensureNonEmptyAssistantContent(cleaned) // 确保非空
const smooshed = smooshSystemReminderSiblings(nonEmpty) // SR 合并
const sanitized = sanitizeErrorToolResultContent(smooshed) // 错误内容清理
validateImagesForAPI(sanitized) // 验证图片大小
return sanitized
}
设计洞察:这个管线看似复杂,但每一步都解决了实际的兼容性问题。例如连续 user 消息合并是为了 Bedrock 兼容(Bedrock 不支持连续同 role 消息),同
message.id的 assistant 合并是因为并行工具调用时流式响应会为每个 content block 生成独立的AssistantMessage记录。
9.2 Tool Result Budget 的冻结语义
enforceToolResultBudget 实现了一个核心不变量——一旦一个 tool_result 被"看到",它的命运就被永远冻结:
// utils/toolResultStorage.ts 第 769-909 行
export async function enforceToolResultBudget(messages, state, skipToolNames) {
// 按 API 级别用户消息分组(模拟 normalizeMessagesForAPI 的合并逻辑)
const candidatesByMessage = collectCandidatesByMessage(messages)
const limit = getPerMessageBudgetLimit()
for (const candidates of candidatesByMessage) {
// 三路分区:
// - mustReapply: 之前已替换 → 重新应用缓存的替换(零 I/O,字节相同)
// - frozen: 之前已看到但未替换 → 永远不替换(保护 prompt cache 前缀)
// - fresh: 新的 → 可能需要替换
const { mustReapply, frozen, fresh } = partitionByPriorDecision(candidates, state)
mustReapply.forEach(c => replacementMap.set(c.toolUseId, c.replacement))
if (fresh.length === 0) continue // 之前处理过的消息
// 跳过 maxResultSizeChars=Infinity 的工具(如 Read)
const eligible = fresh.filter(c => !shouldSkip(c.toolUseId))
// 如果 frozen + fresh 总大小超过预算,按大小降序选择最大的进行替换
const selected = frozenSize + freshSize > limit
? selectFreshToReplace(eligible, frozenSize, limit)
: []
// 非替换候选同步标记为已看到(保持缓存稳定)
// 替换候选在 await 后原子化标记
}
// 并发持久化到磁盘
const freshReplacements = await Promise.all(
toPersist.map(async c => [c, await buildReplacement(c)] as const)
)
}
这个"冻结"语义的核心意义在于:prompt cache 的前缀一旦被服务端缓存,任何对它的修改都会导致缓存失效。所以已经发送给模型的 tool_result 内容必须在后续轮次中保持完全一致。
9.3 压缩摘要的双路径执行
streamCompactSummary 实现了一个精妙的双路径策略来最大化缓存命中率:
// services/compact/compact.ts 第 1136-1396 行
async function streamCompactSummary({messages, summaryRequest, cacheSafeParams, ...}) {
// === 路径 1: Forked Agent(缓存共享)===
// 关键约束: 不设置 maxOutputTokens!
// 因为 fork 通过发送完全相同的 cache-key 参数复用主线程的缓存前缀。
// 设置 maxOutputTokens 会通过 Math.min(budget, maxOutputTokens-1)
// 改变 thinking config → 缓存键不匹配 → 缓存失效。
if (promptCacheSharingEnabled) {
try {
const result = await runForkedAgent({
promptMessages: [summaryRequest],
cacheSafeParams, // 包含主线程的 system/tools/context
querySource: 'compact',
maxTurns: 1, // 只生成一轮
skipCacheWrite: true, // 不写入缓存(会被压缩后的内容替代)
})
if (assistantMsg && !assistantMsg.isApiErrorMessage) {
return assistantMsg // 成功:缓存命中
}
// 失败:降级到流式路径
} catch (error) {
// 异常:降级到流式路径
}
}
// === 路径 2: 独立流式 API 调用(回退)===
// 这里可以安全设置 maxOutputTokensOverride(不共享缓存)
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
const streamingGen = queryModelWithStreaming({
systemPrompt: asSystemPrompt(['You are a helpful AI assistant...']),
thinkingConfig: { type: 'disabled' }, // 压缩不需要 thinking
tools: [FileReadTool, ...optionalToolSearch],
options: {
maxOutputTokensOverride: Math.min(COMPACT_MAX_OUTPUT_TOKENS, ...),
},
})
// ... 消费流式响应 ...
if (!response && attempt < maxAttempts) {
await sleep(getRetryDelay(attempt)) // 退避重试
}
}
}
9.4 Session Memory 提取的时机控制
Session Memory 的提取使用双阈值门控 + 自然间歇检测:
// services/SessionMemory/sessionMemory.ts 第 134-181 行
export function shouldExtractMemory(messages) {
const currentTokenCount = tokenCountWithEstimation(messages)
// 门控 1: 初始化阈值(只触发一次)
if (!isSessionMemoryInitialized()) {
if (!hasMetInitializationThreshold(currentTokenCount)) return false
markSessionMemoryInitialized()
}
// 门控 2: 更新间隔阈值
// 两个条件必须同时满足:token 增长 AND 工具调用次数
const hasMetTokenThreshold = hasMetUpdateThreshold(currentTokenCount)
const hasMetToolCallThreshold =
toolCallsSinceLastUpdate >= getToolCallsBetweenUpdates()
// 门控 3: 自然间歇检测
// 最后一轮没有工具调用 = 对话的自然暂停点
const hasToolCallsInLastTurn = hasToolCallsInLastAssistantTurn(messages)
// 组合逻辑:
// A: token阈值 + 工具调用阈值 → 强制提取
// B: token阈值 + 无工具调用 → 在自然间歇时提取
// 重要: token 阈值始终必须满足(防止过于频繁的提取)
return (hasMetTokenThreshold && hasMetToolCallThreshold) ||
(hasMetTokenThreshold && !hasToolCallsInLastTurn)
}
Session Memory 的模板定义了 9 个结构化部分:
# Session Title → 5-10 词的信息密集标题
# Current State → 当前正在做什么、待完成任务、下一步
# Task specification → 用户要求构建什么
# Files and Functions → 重要文件及其内容
# Workflow → 常用命令及其执行顺序
# Errors & Corrections → 错误和修复方法
# Codebase and System Documentation → 系统组件及其关系
# Learnings → 有效/无效的方法
# Key results → 用户要求的具体输出
# Worklog → 逐步操作日志
每个部分有 2K token 的限制,总量不超过 12K token。超限时会附加 CRITICAL 级别的凝缩提醒。
9.5 API 不变量保护:tool_use/tool_result 配对完整性
adjustIndexToPreserveAPIInvariants 解决了一个微妙但致命的 bug——并行工具调用时,流式响应会为每个 content block 生成独立的 AssistantMessage 记录(共享 message.id),如果压缩切割点落在这些记录之间,normalizeMessagesForAPI 合并后会产生孤立的 tool_result,导致 API 400 错误:
压缩切割前的消息序列:
Index N: assistant, id=X, [thinking]
Index N+1: assistant, id=X, [tool_use: ORPHAN_ID]
Index N+2: assistant, id=X, [tool_use: VALID_ID]
Index N+3: user, [tool_result: ORPHAN_ID, tool_result: VALID_ID]
如果 startIndex = N+2:
→ normalizeMessagesForAPI 合并同 id 的 assistant
→ 但 N+1 的 tool_use 被排除!
→ 结果: assistant=[tool_use: VALID_ID], user=[tool_result: ORPHAN_ID, VALID_ID]
→ API 400: orphan tool_result references non-existent tool_use
修复方法:两步向前扫描——先找缺失的 tool_use,再找共享 message.id 的 thinking 块,扩展保留范围。
9.6 Reactive Compact 的三级恢复瀑布
当 API 返回 413 (prompt-too-long) 时,query.ts 实现了一个三级恢复瀑布,每级用完后才降级到下一级:
Level 1: Context Collapse Drain
→ 提交所有暂存的折叠(便宜,保留细粒度上下文)
→ 条件: 之前的 transition 不是 collapse_drain_retry(防止循环)
→ 成功 → continue 重试
Level 2: Reactive Compact
→ 触发完整的 compactConversation + buildPostCompactMessages
→ 条件: hasAttemptedReactiveCompact === false(单次尝试)
→ 成功 → 替换消息数组 → continue 重试
Level 3: Surface Error
→ 显示被扣留的错误消息
→ 调用 executeStopFailureHooks(不调用正常 stop hooks——防止死循环)
→ return { reason: 'prompt_too_long' }
max_output_tokens 恢复同样是级联式的:
Phase 1: Escalating Retry (8K → 64K)
→ 同一请求以 64K 限制重试(无恢复消息、无多轮往返)
→ 条件: maxOutputTokensOverride === undefined(只触发一次)
Phase 2: Multi-Turn Recovery (最多 3 次)
→ 注入恢复消息: "Output token limit hit. Resume directly — no apology,
no recap. Pick up mid-thought. Break remaining work into smaller pieces."
→ 标记为 isMeta(不显示给用户)
→ 每次将 assistantMessages 追加到 messages 中,让模型继续
Phase 3: Surface Error
→ 恢复次数用尽 → yield 被扣留的错误消息
十、总结与设计洞察
10.1 架构设计哲学
Claude Code 的上下文管理体现了几个核心设计原则:
- 渐进式降级: 从零成本的 Snip → 本地清理的 MicroCompact → 无 LLM 成本的 SM-Compact → 最终的 Full Compact
- 缓存第一: 每个子系统都考虑了对 prompt cache 的影响,
SYSTEM_PROMPT_DYNAMIC_BOUNDARY和cache_editsAPI 都是为了最大化缓存命中率 - 冻结不变量: Tool Result Budget 的"一旦看到就冻结"确保了 prompt cache 前缀的字节级稳定性
- 异步后台: Session Memory 提取在后台异步进行,不阻塞主对话流
- 防御性设计: 断路器、PTL 重试、Reactive Compact 三级恢复瀑布构成多层兜底
- API 不变量保护:
adjustIndexToPreserveAPIInvariants等机制确保了 tool_use/tool_result 配对和 thinking 块的完整性
10.2 关键数据指标
| 指标 | 值 |
|---|---|
| 默认上下文窗口 | 200K tokens |
| 1M 上下文支持 | Opus 4.6, Sonnet 4.6 |
| AutoCompact 缓冲区 | 13K tokens |
| Blocking 保留区 | 3K tokens |
| 压缩最大输出 | 20K tokens |
| Post-Compact 文件恢复 | 最多 5 个,50K token 预算 |
| 每个文件最大 token | 5K |
| 每个技能最大 token | 5K (25K 总预算) |
| SM-Compact 保留范围 | 10K-40K tokens |
| SM 每部分最大 token | 2K (12K 总量) |
| 断路器阈值 | 连续 3 次失败 |
| PTL 重试次数 | 最多 3 次 |
| max_output_tokens 恢复 | 最多 3 次 |
| Token 估算精度 | ~4 chars/token (JSON ~2 chars/token) |
| 缓存破坏检测阈值 | cache_read 下降 >5% 且 >2K tokens |
10.3 与其他 AI 编码工具的对比
大多数 AI 编码工具在上下文耗尽时简单地截断历史或要求用户开启新会话。Claude Code 的多层压缩策略使其能够在数小时甚至数天的连续会话中保持上下文连贯性,这是其在复杂工程任务中表现突出的核心原因之一。
特别值得注意的是 Session Memory + SM-Compact 的组合——它让压缩从"事后补救"变成了"提前准备"。后台持续提取会话笔记,在压缩触发时直接使用已有摘要,既节省了 token 成本,又保留了最近的原始对话上下文。
另一个值得借鉴的设计是 Tool Result Budget 的冻结语义——它从根本上解决了"修改历史消息导致缓存失效"的矛盾:已发送的内容永远不变,新内容按需替换,两者互不干扰。这种"只增不改"的设计模式值得在其他需要缓存稳定性的系统中推广。
本文基于 2026 年 3 月 31 日泄露的 Claude Code 源码快照分析。代码经过反编译和整理,可能与实际实现存在细微差异。
「真诚赞赏,手留余香」
真诚赞赏,手留余香
使用微信扫描二维码完成支付