本文基于 Claude Code 生产级源码,深度剖析 Agent 主循环中"上下文预算管理"这一核心机制的完整实现。从工具结果裁剪到五层压缩管线,再到错误恢复,逐层拆解每个环节的设计动机、核心算法和关键代码。


一、为什么需要上下文预算管理

LLM 的上下文窗口是有限的(如 200K tokens)。在 Agent 持续决策循环中,每一轮工具调用都会产生大量输出(一个 grep 可能返回上万行、一个 Read 可能读取数千行代码),如果不加控制,几轮交互后上下文就会被撑满。

上下文预算管理要解决三个问题:

  1. 空间问题:在有限窗口内尽可能保留有价值的信息
  2. 成本问题:token 就是钱,无效 token 浪费 API 调用费用
  3. 质量问题:过多的历史噪音会干扰模型决策("迷失在中间"效应)

二、五层压缩管线总览

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级别的,在整个会话生命周期中维护。它有两个核心作用:

  1. 去重:通过 seenIds 避免对同一个 tool_result 重复处理
  2. 缓存稳定性:通过 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> 标签识别),重建 seenIdsreplacements 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.

关键设计点:

  1. 读时投影(Read-time Projection):折叠不是修改消息数组,而是维护一个独立的"折叠存储"。每次发送给 API 时,通过 projectView() 函数将折叠应用到完整历史上
  2. 可持久化:折叠操作记录在 commit log 中,跨 turn 持久存在
  3. 优先于 AutoCompact:如果折叠就能将 token 数降到阈值以下,就不需要执行代价更高的 AutoCompact
  4. 保留粒度:折叠保留的是结构化摘要(比 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

判断逻辑:

  1. Token 数检查:当前 token 使用量是否超过阈值
  2. 递归保护:排除 querySourcesession_memorycompactmarble_origami 的情况——这些是压缩过程本身的子 agent 调用,不应该递归触发压缩
  3. 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 是一条特殊的系统消息,标记压缩发生的位置。它的作用:

  1. 后续恢复时可以找到压缩点
  2. 记录压缩前的 token 数(用于调试和分析)
  3. 携带 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 },
  })
  // ...
}

两种触发场景:

  1. Prompt Too Long (413):消息总 token 超过模型上下文窗口
  2. 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.tsTool Result Budget 实现applyToolResultBudget, persistToolResult, partitionByPriorDecision, reconstructContentReplacementState
src/services/compact/microCompact.ts微压缩(time-based + cached)microcompact, evaluateTimeBasedTrigger, COMPACTABLE_TOOLS
src/services/compact/apiMicrocompact.tsAPI 原生上下文管理getAPIContextManagement, ContextEditStrategy
src/services/compact/timeBasedMCConfig.tsTime-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.tsAPI-round 分组groupMessagesByApiRound
src/services/compact/sessionMemoryCompact.ts会话记忆压缩trySessionMemoryCompaction, calculateMessagesToKeepIndex, adjustIndexToPreserveAPIInvariants
src/services/compact/postCompactCleanup.ts压缩后清理runPostCompactCleanup
src/query.ts主管线集成queryLoop (L380-L468 压缩管线, L1070-L1183 错误恢复)

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐