上下文预算管理(Context Budget Management)深度源码解析
本文基于 Claude Code 生产级源码,深度剖析 Agent 主循环中"上下文预算管理"这一核心机制的完整实现。从工具结果裁剪到五层压缩管线,再到错误恢复,逐层拆解每个环节的设计动机、核心算法和关键代码。
一、为什么需要上下文预算管理
LLM 的上下文窗口是有限的(如 200K tokens)。在 Agent 持续决策循环中,每一轮工具调用都会产生大量输出(一个 grep 可能返回上万行、一个 Read 可能读取数千行代码),如果不加控制,几轮交互后上下文就会被撑满。
上下文预算管理要解决三个问题:
- 空间问题:在有限窗口内尽可能保留有价值的信息
- 成本问题:token 就是钱,无效 token 浪费 API 调用费用
- 质量问题:过多的历史噪音会干扰模型决策("迷失在中间"效应)
二、五层压缩管线总览
Claude Code 设计了渐进式的五层压缩策略,每层在不同的时机、以不同的粒度释放 token 空间:
原始消息序列
│
▼
┌─────────────────────────────────────┐
│ 第1层:Tool Result Budget │ 每轮调用前,裁剪过大的工具返回结果
│ (applyToolResultBudget) │ 粒度:单个 tool_result block
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ 第2层:Snip Compact │ 剪断式压缩,移除旧的已完成步骤
│ (snipCompactIfNeeded) │ 粒度:API-round 级别
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ 第3层:Microcompact │ 清理旧工具结果内容 / API 原生清理
│ (microcompact) │ 粒度:单个工具调用结果
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ 第4层:Context Collapse │ 折叠已完成步骤为结构化摘要
│ (applyCollapsesIfNeeded) │ 粒度:逻辑步骤组
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ 第5层:AutoCompact │ 调用模型生成全量摘要替换历史
│ (autoCompactIfNeeded) │ 粒度:全部历史消息
└─────────────────────────────────────┘
│
▼
发送给模型
设计哲学:从轻到重、从局部到全局。前几层尽量保留信息粒度(不丢失原始内容),只有当空间仍然不足时才动用更"暴力"的压缩手段。
三、第1层:Tool Result Budget(工具结果预算裁剪)
3.1 设计动机
工具返回的结果大小不可控——一个 Bash 命令可能输出 10 万字符,一个 Read 可能读取几千行文件。如果不加限制,一次工具调用就可能吃掉大量上下文空间。
3.2 核心数据结构:ContentReplacementState
// src/utils/toolResultStorage.ts
type ContentReplacementState = {
seenIds: Set<string> // 已处理过的 tool_use_id 集合
replacements: Map<string, string> // 被替换的 id → 预览文本(用于重放)
}
这个结构是per-conversation级别的,在整个会话生命周期中维护。它有两个核心作用:
- 去重:通过
seenIds避免对同一个 tool_result 重复处理 - 缓存稳定性:通过
replacements保证 prompt cache 不会因替换内容变化而失效——一旦某个 tool_result 被替换为预览文本,后续所有请求都使用相同的预览文本
3.3 核心算法:enforceToolResultBudget
// src/utils/toolResultStorage.ts
export async function applyToolResultBudget(
messages: Message[],
contentReplacementState: ContentReplacementState,
persistCallback?: (records: ContentReplacementRecord[]) => void,
exemptTools?: Set<string>,
): Promise<Message[]>
处理流程分为三步:
第一步:分区(partitionByPriorDecision)
将所有 tool_result 分为三类:
mustReapply — 之前已经被替换过的(从 replacements Map 中查找),必须重放相同的替换
frozen — 已冻结的(如被引用的),不可替换
fresh — 新的结果,需要做大小决策
这个分区设计是关键——它保证了 prompt cache 的稳定性。如果每次请求都对同一个 tool_result 做不同的处理,API 的 prompt cache 就会不断失效,导致成本飙升。
第二步:大小检查与持久化
对每个 fresh 的 tool_result,检查其大小是否超过预算(由工具的 maxResultSizeChars 或全局默认值决定)。超出的结果会被持久化到磁盘:
// src/utils/toolResultStorage.ts
function persistToolResult(
toolUseId: string,
content: string,
sessionId: string,
): { preview: string; filePath: string }
持久化路径:<session>/tool-results/<toolUseId>.txt
预览生成:截取前约 2KB 的内容作为预览。
第三步:替换
将原始的大 tool_result 替换为包含预览和文件路径的结构化消息:
// src/utils/toolResultStorage.ts
function buildLargeToolResultMessage(
toolUseId: string,
preview: string,
filePath: string,
totalChars: number,
): string
替换后的消息格式:
<persisted-output>
<content_preview>
[前2KB的内容预览]
</content_preview>
<file_path>/path/to/session/tool-results/toolu_xxx.txt</file_path>
<total_chars>123456</total_chars>
</persisted-output>
模型看到预览后,如果认为需要完整内容,可以通过 Read 工具读取磁盘上的完整文件。
3.4 会话恢复支持
当用户 resume 一个中断的会话时,需要从 transcript 恢复替换状态:
// src/utils/toolResultStorage.ts
export function reconstructContentReplacementState(
messages: Message[],
): ContentReplacementState
遍历所有消息,找到已被替换的 tool_result(通过 <persisted-output> 标签识别),重建 seenIds 和 replacements Map。这确保了 resume 后的会话不会重复处理已替换的结果。
四、第2层:Snip Compact(剪断式压缩)
4.1 设计动机
在长会话中,早期的交互步骤(如"先 grep 找到文件,再 read 读取内容")在任务完成后就不再有用。Snip 的目标是识别并移除这些"已完成的历史步骤"。
4.2 在主管线中的位置
// src/query.ts (约 L396-L409)
// Apply snip before microcompact (both may run — they are not mutually exclusive).
// snipTokensFreed is plumbed to autocompact so its threshold check reflects
// what snip removed; tokenCountWithEstimation alone can't see it (reads usage
// from the protected-tail assistant, which survives snip unchanged).
let snipTokensFreed = 0
if (feature('HISTORY_SNIP')) {
queryCheckpoint('query_snip_start')
const snipResult = snipModule!.snipCompactIfNeeded(messagesForQuery)
messagesForQuery = snipResult.messages
snipTokensFreed = snipResult.tokensFreed
if (snipResult.boundaryMessage) {
yield snipResult.boundaryMessage
}
queryCheckpoint('query_snip_end')
}
关键设计点:
snipTokensFreed被传递给后续的 autocompact,因为tokenCountWithEstimation读取的是最后一条 API 响应中的 usage 信息(该 assistant 消息在 snip 后仍然保留),无法感知 snip 释放了多少 token- Snip 是一个 Feature Flag 控制的实验性功能(
HISTORY_SNIP),通过条件 require 引入
4.3 与其他层的关系
Snip 和 Microcompact 不是互斥的——它们可以同时运行。Snip 移除的是完整的 API-round(一轮完整的 user→assistant→tool_result 交互),而 Microcompact 清理的是单个工具结果的内容。
五、第3层:Microcompact(微压缩)
5.1 设计动机
即使 Tool Result Budget 裁剪了过大的结果,历史中仍积累了大量已经"不再需要"的工具输出。比如你在 10 轮前 Read 了一个文件,后来又 Read 了其他 5 个文件——前一个文件的内容大概率已经不需要了。
5.2 可压缩的工具集合
// src/services/compact/microCompact.ts
const COMPACTABLE_TOOLS = new Set([
'Read', // 文件读取
'Bash', // Shell 命令
'Grep', // 内容搜索
'Glob', // 文件搜索
'WebSearch', // 网页搜索
'WebFetch', // 网页获取
'Edit', // 文件编辑
'Write', // 文件写入
])
这些工具的共同特点是:输出是"事实性"的(文件内容、搜索结果),随着时间推移其价值递减。
5.3 两条压缩路径
路径 A:Time-based Microcompact(基于时间的微压缩)
// src/services/compact/microCompact.ts
function evaluateTimeBasedTrigger(
messages: Message[],
config: TimeBasedMCConfig,
): boolean
核心思想:检测距离上一条 assistant 消息的时间间隔。如果间隔超过阈值(默认 60 分钟),说明服务端的 prompt cache 已经过期(1 小时 TTL),此时发送请求无论如何都会全量重写前缀——那么先清理旧的工具结果就能减小重写的数据量。
// src/services/compact/timeBasedMCConfig.ts
export type TimeBasedMCConfig = {
enabled: boolean // 主开关
gapThresholdMinutes: number // 时间间隔阈值(默认 60 分钟)
keepRecent: number // 保留最近 N 个工具结果(默认 5)
}
清理策略:保留最近的 keepRecent 个 compactable 工具结果,其余全部替换为占位文本:
'[Old tool result content cleared]'
路径 B:Cached Microcompact(基于 API Cache Editing 的微压缩)
这是一条更精巧的路径——利用 API 的 cache_edits 能力,直接在服务端删除旧的 tool_result 缓存条目,而不需要修改本地消息内容。
// src/services/compact/apiMicrocompact.ts
export type ContextEditStrategy =
| {
type: 'clear_tool_uses_20250919'
trigger?: { type: 'input_tokens'; value: number }
keep?: { type: 'tool_uses'; value: number }
clear_tool_inputs?: boolean | string[]
exclude_tools?: string[]
clear_at_least?: { type: 'input_tokens'; value: number }
}
| {
type: 'clear_thinking_20251015'
keep: { type: 'thinking_turns'; value: number } | 'all'
}
配置参数:
trigger:当 input_tokens 超过此阈值时触发清理clear_at_least:至少清理多少 token(trigger - keepTarget)clear_tool_inputs:指定哪些工具的输入可以被清理exclude_tools:排除哪些工具不被清理
优势:本地消息不变(用户可以在 UI 中看到完整历史),只是发给 API 时旧内容被服务端裁剪。
5.4 Thinking Block 清理
除了工具结果,microcompact 还支持清理 thinking block:
// src/services/compact/apiMicrocompact.ts
if (hasThinking && !isRedactThinkingActive) {
strategies.push({
type: 'clear_thinking_20251015',
keep: clearAllThinking
? { type: 'thinking_turns', value: 1 } // 只保留最后1轮思考
: 'all', // 保留全部思考
})
}
当时间间隔超过 1 小时(cache miss),只保留最后 1 轮 thinking turn——旧的思考过程对当前决策的价值已经很低。
六、第4层:Context Collapse(上下文折叠)
6.1 设计动机
Context Collapse 是一个比 Microcompact 更"智能"的压缩层。它不是简单地清除内容,而是将已完成的逻辑步骤"折叠"为结构化摘要。
6.2 核心特性
从 query.ts 中的注释可以了解其设计:
// src/query.ts (约 L428-L447)
// Project the collapsed context view and maybe commit more collapses.
// Runs BEFORE autocompact so that if collapse gets us under the
// autocompact threshold, autocompact is a no-op and we keep granular
// context instead of a single summary.
//
// Nothing is yielded — the collapsed view is a read-time projection
// over the REPL's full history. Summary messages live in the collapse
// store, not the REPL array. This is what makes collapses persist
// across turns: projectView() replays the commit log on every entry.
关键设计点:
- 读时投影(Read-time Projection):折叠不是修改消息数组,而是维护一个独立的"折叠存储"。每次发送给 API 时,通过
projectView()函数将折叠应用到完整历史上 - 可持久化:折叠操作记录在 commit log 中,跨 turn 持久存在
- 优先于 AutoCompact:如果折叠就能将 token 数降到阈值以下,就不需要执行代价更高的 AutoCompact
- 保留粒度:折叠保留的是结构化摘要(比 AutoCompact 的单段摘要更细),让模型能回顾已完成步骤的关键细节
6.3 溢出恢复
当 API 返回 413(Prompt Too Long)时,Context Collapse 可以作为第一道防线:
// src/query.ts (约 L1085-L1116)
if (isWithheld413) {
// First: drain all staged context-collapses
if (
feature('CONTEXT_COLLAPSE') &&
contextCollapse &&
state.transition?.reason !== 'collapse_drain_retry'
) {
const drained = contextCollapse.recoverFromOverflow(
messagesForQuery,
querySource,
)
if (drained.committed > 0) {
const next: State = {
messages: drained.messages,
// ...
transition: {
reason: 'collapse_drain_retry',
committed: drained.committed,
},
}
state = next
continue // 用折叠后的消息重试
}
}
}
recoverFromOverflow 将所有"已暂存但未提交"的折叠操作立即提交,释放空间后用新消息重试。通过 transition.reason !== 'collapse_drain_retry' 防止无限循环——如果上次已经 drain 过且仍然 413,就不再尝试,fall through 到 reactive compact。
6.4 Feature Flag 控制
Context Collapse 是实验性功能,通过 CONTEXT_COLLAPSE feature flag 控制,在外部构建中会被编译移除。
七、第5层:AutoCompact(自动摘要压缩)
7.1 设计动机
当上述四层压缩仍无法将 token 数控制在阈值以下时,需要"核武器"——调用模型对整个会话历史生成一段结构化摘要,用摘要替换全部旧消息。
7.2 触发阈值计算
// src/services/compact/autoCompact.ts
const AUTOCOMPACT_BUFFER_TOKENS = 13_000
function calculateAutoCompactThreshold(model: string): number {
const effectiveContextWindow = getEffectiveContextWindow(model)
return effectiveContextWindow - AUTOCOMPACT_BUFFER_TOKENS
}
阈值 = 有效上下文窗口 - 13K buffer。Buffer 是为当前轮次的模型输出和工具结果预留的空间。
7.3 触发条件判断
// src/services/compact/autoCompact.ts
export function shouldAutoCompact(
messages: Message[],
model: string,
snipTokensFreed: number,
querySource: QuerySource,
): boolean
判断逻辑:
- Token 数检查:当前 token 使用量是否超过阈值
- 递归保护:排除
querySource为session_memory、compact、marble_origami的情况——这些是压缩过程本身的子 agent 调用,不应该递归触发压缩 - Snip 补偿:token 计数需要减去
snipTokensFreed(因为tokenCountWithEstimation使用的是最后一条 API 响应中的 stale usage)
7.4 熔断机制
// src/services/compact/autoCompact.ts
const MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3
如果连续 3 次 AutoCompact 失败(如摘要生成模型本身返回 prompt-too-long),就停止尝试。这防止了无限重试浪费 API 调用。
// src/services/compact/autoCompact.ts (简化逻辑)
export async function autoCompactIfNeeded(
messages: Message[],
toolUseContext: ToolUseContext,
cacheSafeParams: CacheSafeParams,
querySource: QuerySource,
tracking: AutoCompactTrackingState,
snipTokensFreed: number,
): Promise<{ compactionResult: CompactionResult | null; consecutiveFailures: number }>
执行流程:
1. 检查 shouldAutoCompact() → 不需要则直接返回 null
2. 检查 consecutiveFailures >= 3 → 是则直接返回 null(熔断)
3. 优先尝试 Session Memory Compaction(更快、不消耗 API 调用)
↓ 如果不满足条件或失败
4. 尝试 Full Compact(调用模型生成摘要)
↓ 如果成功
5. consecutiveFailures = 0
↓ 如果失败
6. consecutiveFailures++
7.5 Token 使用量预警
// src/services/compact/autoCompact.ts
export function calculateTokenWarningState(
tokenCount: number,
model: string,
): {
percentLeft: number
isWarning: boolean // 黄色警告(接近阈值)
isError: boolean // 红色警告(非常接近阈值)
isAutoCompact: boolean // 已触发自动压缩
isAtBlockingLimit: boolean // 已达硬上限(阻止新请求)
}
四级预警体系让用户在 UI 上能看到当前上下文使用情况。
八、AutoCompact 核心实现:compactConversation
8.1 完整流程
// src/services/compact/compact.ts (~1706行)
export async function compactConversation(
messages: Message[],
options: CompactOptions,
): Promise<CompactionResult>
完整执行流程:
1. 执行 PreCompact hooks
2. 剥离图片/文档(stripImagesFromMessages)
3. 调用模型生成摘要(通过 runForkedAgent 或流式调用)
4. PTL 重试(最多3次,每次截断最旧的 API-round groups)
5. 生成 post-compact 附件
6. 创建 compact boundary marker
7. 执行 PostCompact hooks + SessionStart hooks
8.2 摘要生成:共享 Prompt Cache
// src/services/compact/compact.ts
// 通过 runForkedAgent 复用主会话的 prompt cache 前缀
const forkResult = await runForkedAgent({
messages: messagesForFork,
systemPrompt,
// ...
})
这是一个重要的优化——压缩本身也需要调用模型,如果压缩调用的 prompt 和主会话共享相同的前缀(system prompt + 消息历史),就能复用 API 的 prompt cache,大幅降低压缩的 token 成本。
8.3 PTL 重试策略
当压缩调用本身也返回 prompt-too-long 时(被压缩的历史太长了!):
// src/services/compact/compact.ts (简化逻辑)
// 最多重试3次,每次截断最旧的 API-round group
for (let attempt = 0; attempt < MAX_PTL_RETRIES; attempt++) {
try {
result = await generateSummary(truncatedMessages)
break
} catch (e) {
if (isPromptTooLong(e)) {
// 使用 groupMessagesByApiRound 分组,移除最旧的一组
const groups = groupMessagesByApiRound(truncatedMessages)
groups.shift() // 移除最旧的 API-round
truncatedMessages = groups.flat()
}
}
}
8.4 API-Round 分组
// src/services/compact/grouping.ts
export function groupMessagesByApiRound(messages: Message[]): Message[][]
分组边界:每当出现一个新的 assistant.message.id(与上一条 assistant 消息的 id 不同)时,就开启一个新的 API-round。
消息序列:
user[1] → assistant[A] → tool_result → assistant[A] → user(tool_result) → assistant[B] → ...
分组结果:
Group 1: [user[1], assistant[A], tool_result, assistant[A], user(tool_result)]
Group 2: [assistant[B], ...]
这保证了截断操作不会打破 tool_use / tool_result 的配对关系。
8.5 压缩提示词模板
// src/services/compact/prompt.ts (~375行)
const BASE_COMPACT_PROMO = `
Create a detailed summary of the conversation so far...
The summary should include the following sections:
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** (CRITICAL - include EVERY user message verbatim)
7. **Pending Tasks**
8. **Current Work**
9. **Optional Next Step**
`
9 段式摘要结构确保关键信息不会在压缩中丢失。特别地,所有用户消息都被要求逐字保留——因为用户的原始意图是后续决策的锚点。
输出格式要求:
<analysis>
[内部思考过程,会被 formatCompactSummary 剥离]
</analysis>
<summary>
[最终摘要内容,保留在消息中]
</summary>
8.6 Post-Compact 附件恢复
压缩后不能只留一段摘要——某些上下文必须恢复:
// src/services/compact/compact.ts
// 1. 文件恢复:从 readFileState 恢复最近访问的文件内容
const fileAttachments = createPostCompactFileAttachments(
readFileState,
preservedMessages,
maxFiles: 5, // 最多恢复5个文件
maxTotalTokens: 50_000 // 总 token 预算 50K
)
// 2. Plan 恢复:如果存在活跃的计划文件
const planAttachment = createPlanAttachmentIfNeeded(agentId)
// 3. Skills 恢复:重新注入已加载的技能描述
// 每个 skill 最多 5K token,总 budget 25K
// 4. Deferred Tools Delta:重新注入延迟加载的工具列表
// 5. Agent Listing:重新注入可用的子 agent 列表
// 6. MCP Instructions:重新注入 MCP 服务器指令
文件恢复的智能跳过逻辑:
// src/services/compact/compact.ts
function createPostCompactFileAttachments(
readFileState: FileStateCache,
preservedMessages: Message[],
): AttachmentMessage[] {
// 跳过已在 preserved messages 中 Read 过的文件
// 避免重复注入(模型已经看到了这些文件的内容)
for (const msg of preservedMessages) {
if (isReadToolResult(msg)) {
alreadyReadFiles.add(getFilePath(msg))
}
}
// ...
}
8.7 Compact Boundary Marker
// src/services/compact/compact.ts
const boundaryMarker = createCompactBoundaryMessage(
'auto', // 压缩类型
preCompactTokenCount, // 压缩前的 token 数
lastMessageUuid, // 最后一条消息的 UUID
)
Boundary marker 是一条特殊的系统消息,标记压缩发生的位置。它的作用:
- 后续恢复时可以找到压缩点
- 记录压缩前的 token 数(用于调试和分析)
- 携带
preCompactDiscoveredTools(压缩前发现的工具列表)
九、Session Memory Compact(会话记忆压缩)
9.1 设计动机
传统的 AutoCompact 需要调用模型生成摘要——这本身消耗 API token 和时间。Session Memory Compact 是一条更快的路径:如果系统已经在后台持续提取了"会话记忆"(Session Memory),就可以直接用记忆内容作为摘要,跳过模型调用。
9.2 在 AutoCompact 中的优先级
// src/services/compact/autoCompact.ts (简化逻辑)
export async function autoCompactIfNeeded(...) {
// 优先尝试 Session Memory Compaction
const smResult = await trySessionMemoryCompaction(messages, agentId, threshold)
if (smResult) {
return { compactionResult: smResult, consecutiveFailures: 0 }
}
// SM-compact 不可用时,才执行 Full Compact
return await compactConversation(messages, ...)
}
9.3 核心算法:保留消息的计算
// src/services/compact/sessionMemoryCompact.ts
export function calculateMessagesToKeepIndex(
messages: Message[],
lastSummarizedIndex: number,
): number
从 lastSummarizedMessageId(会话记忆已总结到的位置)开始,向前扩展,直到满足:
- 至少保留
minTokens(默认 10K)token - 至少保留
minTextBlockMessages(默认 5)条含文本的消息 - 不超过
maxTokens(默认 40K)token
// src/services/compact/sessionMemoryCompact.ts
export const DEFAULT_SM_COMPACT_CONFIG: SessionMemoryCompactConfig = {
minTokens: 10_000, // 最少保留 10K token
minTextBlockMessages: 5, // 最少保留 5 条文本消息
maxTokens: 40_000, // 最多保留 40K token
}
9.4 API 不变量保护
保留消息的起始索引必须调整,以确保不破坏 API 的 tool_use / tool_result 配对:
// src/services/compact/sessionMemoryCompact.ts
export function adjustIndexToPreserveAPIInvariants(
messages: Message[],
startIndex: number,
): number
两种场景需要向前扩展 startIndex:
场景 1:tool_use / tool_result 配对
如果保留的消息中包含 tool_result,但对应的 tool_use 在被截断的部分,就必须把 startIndex 向前移到包含该 tool_use 的 assistant 消息。
场景 2:thinking block 合并
如果保留的 assistant 消息与前面的 assistant 消息共享相同的 message.id(流式输出时,同一轮响应被拆成多条消息),就必须把那些消息也包含进来,否则 normalizeMessagesForAPI 无法正确合并 thinking block。
9.5 Feature Flag 控制
// src/services/compact/sessionMemoryCompact.ts
export function shouldUseSessionMemoryCompaction(): boolean {
// 环境变量覆盖(用于测试)
if (isEnvTruthy(process.env.ENABLE_CLAUDE_CODE_SM_COMPACT)) return true
if (isEnvTruthy(process.env.DISABLE_CLAUDE_CODE_SM_COMPACT)) return false
// 双 feature flag:需要同时开启 session_memory 和 sm_compact
const sessionMemoryFlag = getFeatureValue_CACHED_MAY_BE_STALE('tengu_session_memory', false)
const smCompactFlag = getFeatureValue_CACHED_MAY_BE_STALE('tengu_sm_compact', false)
return sessionMemoryFlag && smCompactFlag
}
十、Reactive Compact(响应式压缩)
10.1 设计动机
前面五层都是"主动压缩"——在发送请求之前就压缩。但有时候主动压缩不够及时或判断不准确,模型调用后才发现 prompt 太长(API 返回 413 错误)。Reactive Compact 是"被动触发"的紧急压缩。
10.2 触发条件
// src/query.ts (约 L1070-L1119)
const isWithheld413 =
lastMessage?.type === 'assistant' &&
lastMessage.isApiErrorMessage &&
isPromptTooLongMessage(lastMessage)
const isWithheldMedia =
mediaRecoveryEnabled &&
reactiveCompact?.isWithheldMediaSizeError(lastMessage)
if ((isWithheld413 || isWithheldMedia) && reactiveCompact) {
const compacted = await reactiveCompact.tryReactiveCompact({
hasAttempted: hasAttemptedReactiveCompact,
querySource,
aborted: toolUseContext.abortController.signal.aborted,
messages: messagesForQuery,
cacheSafeParams: { systemPrompt, userContext, systemContext, toolUseContext, forkContextMessages: messagesForQuery },
})
// ...
}
两种触发场景:
- Prompt Too Long (413):消息总 token 超过模型上下文窗口
- Media Size Error:图片/文档太大或数量太多
10.3 多级恢复策略
当 API 返回 413 时,恢复策略是分级的:
第一步:Context Collapse drain(如果有 CONTEXT_COLLAPSE)
↓ 仍然 413
第二步:Reactive Compact(紧急摘要压缩)
↓ 仍然失败
第三步:向用户显示错误,终止循环
// src/query.ts (约 L1085-L1183)
if (isWithheld413) {
// 第一步:drain staged collapses
if (feature('CONTEXT_COLLAPSE') && contextCollapse
&& state.transition?.reason !== 'collapse_drain_retry') {
const drained = contextCollapse.recoverFromOverflow(messagesForQuery, querySource)
if (drained.committed > 0) {
state = { ...state, messages: drained.messages,
transition: { reason: 'collapse_drain_retry' } }
continue // 重试
}
}
}
// 第二步:reactive compact
if ((isWithheld413 || isWithheldMedia) && reactiveCompact) {
const compacted = await reactiveCompact.tryReactiveCompact({ ... })
if (compacted) {
state = { ...state, messages: buildPostCompactMessages(compacted),
hasAttemptedReactiveCompact: true,
transition: { reason: 'reactive_compact_retry' } }
continue // 重试
}
// 第三步:无法恢复
yield lastMessage
return { reason: isWithheldMedia ? 'image_error' : 'prompt_too_long' }
}
10.4 防螺旋保护
// State 中的 hasAttemptedReactiveCompact 字段
type State = {
hasAttemptedReactiveCompact: boolean // 是否已经尝试过响应式压缩
// ...
}
如果 Reactive Compact 后仍然 413(比如压缩后的摘要加上新的工具结果仍然太长),hasAttemptedReactiveCompact = true 阻止再次尝试,避免无限循环。
十一、压缩后清理(Post-Compact Cleanup)
11.1 需要重置的模块状态
压缩替换了消息序列,但各个子系统可能持有基于旧消息序列的内部状态。必须统一重置:
// src/services/compact/postCompactCleanup.ts
export function runPostCompactCleanup(
toolUseContext: ToolUseContext,
querySource: QuerySource,
): void {
// 1. 重置 microcompact 状态(清除旧的工具结果追踪)
// 2. 重置 context collapse 状态(清除折叠存储)
// 3. 清除 memory file 缓存(强制重新读取)
// 4. 清除 system prompt sections(强制重新生成)
// 5. 清除 classifier approvals(强制重新分类)
// 6. 清除 speculative checks(清除投机性检查结果)
// 7. 清除 beta tracing 状态
// 8. 清除 session messages cache
}
11.2 主线程 vs 子 Agent 区分
// src/services/compact/postCompactCleanup.ts
// 区分主线程压缩 vs 子 agent 压缩
// 子 agent 的清理不能影响主线程的模块级状态
if (querySource === 'compact' || querySource === 'session_memory') {
// 子 agent 压缩:只清理局部状态
} else {
// 主线程压缩:清理所有模块级状态
}
十二、完整数据流图
queryLoop() 每轮开始
│
▼
┌──────────────────────┐
│ applyToolResultBudget │ 裁剪超大 tool_result
│ • 分区:mustReapply │ 持久化到磁盘
│ / frozen / fresh │ 替换为预览+路径
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ snipCompactIfNeeded │ 剪断旧的 API-round
│ (feature-gated) │ 释放 tokens 传递给 autocompact
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ microcompact │
│ • Time-based: │ 间隔>60min → 清理旧工具结果
│ 保留最近N个 │
│ • Cached (API): │ 通过 cache_edits 服务端清理
│ clear_tool_uses │
│ • Thinking: │ 清除旧的 thinking block
│ clear_thinking │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ applyCollapsesIfNeeded│ 折叠已完成步骤为结构化摘要
│ (feature-gated) │ 读时投影,不修改原始消息
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ autoCompactIfNeeded │
│ ① 优先 SM-compact │ 用已有的 session memory 作为摘要
│ ② 否则 Full compact │ 调用模型生成9段式摘要
│ ③ PTL 重试 │ 最多3次,每次截掉最旧的 API-round
│ ④ Post-compact 恢复 │ 文件/plan/skills/tools 附件
│ ⑤ 熔断保护 │ 连续3次失败则停止
└──────────┬───────────┘
│
▼
发送给模型 API
│
▼
┌──────────────────────┐
│ 模型返回 413? │
│ YES: │
│ ① Collapse drain │ 提交暂存的折叠操作
│ ② Reactive compact │ 紧急调用模型压缩
│ ③ 显示错误 │ 无法恢复
│ NO: │
│ 正常处理模型响应 │
└──────────────────────┘
十三、关键设计原则总结
| 原则 | 实现方式 |
|---|---|
| 渐进式压缩 | 5层从轻到重,尽量保留信息粒度 |
| Prompt Cache 稳定性 | ContentReplacementState 保证替换内容不变,避免 cache 失效 |
| API 不变量保护 | adjustIndexToPreserveAPIInvariants 保证 tool_use/result 配对 |
| 递归保护 | 排除压缩过程自身的子 agent 调用 |
| 熔断机制 | 连续3次失败停止重试,避免浪费 API 调用 |
| Feature Flag 控制 | Snip / Context Collapse / Reactive Compact 均可独立开关 |
| 共享 Cache | 压缩调用复用主会话的 prompt cache 前缀 |
| 读时投影 | Context Collapse 不修改原始消息,通过投影函数动态应用 |
| 防螺旋保护 | hasAttemptedReactiveCompact 阻止 Reactive Compact 无限循环 |
| 模块状态同步 | Post-compact cleanup 重置所有子系统的旧状态 |
十四、源码文件索引
| 文件路径 | 职责 | 关键函数 |
|---|---|---|
src/utils/toolResultStorage.ts | Tool Result Budget 实现 | applyToolResultBudget, persistToolResult, partitionByPriorDecision, reconstructContentReplacementState |
src/services/compact/microCompact.ts | 微压缩(time-based + cached) | microcompact, evaluateTimeBasedTrigger, COMPACTABLE_TOOLS |
src/services/compact/apiMicrocompact.ts | API 原生上下文管理 | getAPIContextManagement, ContextEditStrategy |
src/services/compact/timeBasedMCConfig.ts | Time-based 配置 | getTimeBasedMCConfig |
src/services/compact/autoCompact.ts | 自动压缩触发 + 阈值 + 熔断 | autoCompactIfNeeded, shouldAutoCompact, calculateTokenWarningState |
src/services/compact/compact.ts | 核心压缩 + post-compact 恢复 | compactConversation, buildPostCompactMessages, createPostCompactFileAttachments |
src/services/compact/prompt.ts | 压缩提示词模板 | BASE_COMPACT_PROMPT, formatCompactSummary |
src/services/compact/grouping.ts | API-round 分组 | groupMessagesByApiRound |
src/services/compact/sessionMemoryCompact.ts | 会话记忆压缩 | trySessionMemoryCompaction, calculateMessagesToKeepIndex, adjustIndexToPreserveAPIInvariants |
src/services/compact/postCompactCleanup.ts | 压缩后清理 | runPostCompactCleanup |
src/query.ts | 主管线集成 | queryLoop (L380-L468 压缩管线, L1070-L1183 错误恢复) |
更多推荐



所有评论(0)