Microcompact(微压缩)机制分析

源码:src/services/compact/microCompact.ts(531 行)


1. 概述

Microcompact 是一种轻量级的 tool result 清理机制,在每次 API 调用前运行。它只处理特定工具的返回结果,不做消息级别的语义压缩。

执行位置

query.ts 的 query loop 中,三个压缩机制串行执行:

scss 复制代码
microcompact  (line 414)  →  只清理 tool result 内容
context collapse (line 441) → 消息区间折叠为 LLM 摘要
autocompact   (line 454)  →  批量消息压缩

2. 可清理的工具范围(第 41-50 行)

typescript 复制代码
const COMPACTABLE_TOOLS = new Set<string>([
  FILE_READ_TOOL_NAME,    // Read
  ...SHELL_TOOL_NAMES,    // Shell 命令
  GREP_TOOL_NAME,         // Grep
  GLOB_TOOL_NAME,         // Glob
  WEB_SEARCH_TOOL_NAME,   // WebSearch
  WEB_FETCH_TOOL_NAME,    // WebFetch
  FILE_EDIT_TOOL_NAME,    // FileEdit
  FILE_WRITE_TOOL_NAME,   // FileWrite
])

这些工具的特点是输出内容体积大且通常是中间结果,model 在后续推理中不太需要精确的原始输出,只要知道"这个操作已经执行过了"即可。

不包括在内的重要工具:Agent tool、Think tool 等------这些要么是控制流工具,要么输出很小。


3. 整体执行流程(第 253-293 行)

typescript 复制代码
export async function microcompactMessages(
  messages: Message[],
  toolUseContext?: ToolUseContext,
  querySource?: QuerySource,
): Promise<MicrocompactResult>

执行顺序:

js 复制代码
microcompactMessages()
  │
  ├─ 1. `Time-based` 检查 maybeTimeBasedMicrocompact()(第 267 行)
  │    如果触发 → 直接返回(short-circuit)
  │
  ├─ 2. `Cached MC` 检查(第 276 行,feature('CACHED_MICROCOMPACT'))
  │    条件:功能启用 + 模型支持 + 主线程
  │    是 → cachedMicrocompactPath()
  │
  └─ 3. 兜底(第 292 行)
       返回 { messages } 不变
       

4. Time-based Microcompact(第 422-530 行)

maybeTimeBasedMicrocompact()函数的内部的实现细节:

4.1 触发判断(第 422-444 行)

typescript 复制代码
export function evaluateTimeBasedTrigger(
  messages: Message[],
  querySource: QuerySource | undefined,
): { gapMinutes: number; config: TimeBasedMCConfig } | null

触发条件(全部满足):

  1. 功能启用config.enabled === true
  2. 主线程 sourcequerySource 已定义且以 repl_main_thread 开头(或为 undefined------但有定义时才触发)
  3. 有 assistant 消息 :messages 中存在 type === 'assistant' 的消息
  4. 超过时间阈值 :距最后一条 assistant 消息的间隔 ≥ config.gapThresholdMinutes
js 复制代码
// config
config 来自 GrowthBook(远程配置平台)的 A/B 测试/功能开关,key 是
  'tengu_slate_heron':

  所以 config.enabled === true 意味着 GrowthBook
  远程配置将该功能标记为启用。只有当 Anthropic 通过 GrowthBook 控制台将这个
  flag 设为 true 时,time-based microcompact 才会生效 ------ 否则默认不启用。
  
// querySource
querySource
  是一个标识当前查询来源/调用者的分类标签,本质上是一个 string 类型(从
  src/constants/querySource.js 导入,但该文件在此 fork 中不存在,是闭源的)

第 438 行的 gap 计算:

typescript 复制代码
const gapMinutes =
  (Date.now() - new Date(lastAssistant.timestamp).getTime()) / 60_000

TimeBasedMCConfig 来自 GrowthBook 远程配置(timeBasedMCConfig.ts),支持动态调整而不需要发版。

4.2 执行逻辑(第 446-530 行)

typescript 复制代码
function maybeTimeBasedMicrocompact(...): MicrocompactResult | null

步骤

  1. 收集所有 COMPACTABLE_TOOLS 的 tool_use ID(按出现顺序)
  2. 保留最近 keepRecent 条(最少 1 条,第 461 行 Math.max(1, config.keepRecent)
  3. 剩余的全部标记为待清理
  4. 遍历 messages,将匹配的 tool result 的 content 替换为常量字符串

替换操作(第 470-492 行)

typescript 复制代码
const result: Message[] = messages.map(message => {
  if (message.type !== 'user' || !Array.isArray(message.message.content)) {
    return message
  }
  let touched = false
  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  // 跳过已清理的
    ) {
      tokensSaved += calculateToolResultTokens(block)
      touched = true
      return { ...block, content: TIME_BASED_MC_CLEARED_MESSAGE }
    }
    return block
  })
  if (!touched) return message
  return { ...message, message: { ...message.message, content: newContent } }
})

关键设计细节

  • 幂等性保护 (第 479 行):检查 block.content !== TIME_BASED_MC_CLEARED_MESSAGE,避免重复计算已清理的 tool result
  • 最少保留 1 条 (第 461 行):Math.max(1, config.keepRecent),防止 slice(-0) 返回全部或 keepRecent=0 导致清空所有结果让 model 失去上下文
  • 只处理 user 消息(第 471 行):tool_result 只出现在 user message 中
  • **floor at 1 的注释(第 458-460 行)**解释了为什么 keepRecent 最少为 1:slice(-0) 返回整个数组(与直觉相反,保留了所有内容),而清除所有结果会让 model 失去工作上下文

4.3 替换后的字符串常量(第 36 行)

typescript 复制代码
export const TIME_BASED_MC_CLEARED_MESSAGE = '[Old tool result content cleared]'

注意这个常量是从 microCompact.ts 导出的(而非从 toolResultStorage.ts 导入),注释(第 33-35 行)解释了原因:

arduino 复制代码
// Inline from utils/toolResultStorage.ts --- importing that file pulls in
// sessionStorage → utils/messages → services/api/errors, completing a
// circular-deps loop back through this file via promptCacheBreakDetection.

这是为了避免循环依赖,通过内联常量和维护一个测试来保证与源头的值一致。

4.4 Time-based 的后处理(第 511-529 行)

typescript 复制代码
// 1. 抑制压缩警告
suppressCompactWarning()

// 2. 重置 cached MC state(因为修改了 prompt 内容,缓存已失效)
resetMicrocompactState()

// 3. 通知缓存断裂检测器(避免误报)
notifyCacheDeletion(querySource)

return { messages: result }

4.5 Token 节省估算(第 137-157 行)

calculateToolResultTokens 函数用于估算被清理的 tool result 的 token 数:

content 类型 token 估算
string roughTokenCountEstimation(text)
TextBlockParam(text 类型) roughTokenCountEstimation(text)
ImageBlockParam / DocumentBlockParam 固定 IMAGE_MAX_TOKEN_SIZE = 2000

5. Cached Microcompact

5.1 设计目标(第 296-303 行注释)

sql 复制代码
Key differences from regular microcompact:
- Does NOT modify local message content
  (cache_reference and cache_edits are added at API layer)
- Uses count-based trigger/keep thresholds from GrowthBook config
- Takes precedence over regular microcompact (no disk persistence)
- Tracks tool results and queues cache edits for the API layer

核心思想:利用 Anthropic API 的 cache_edits 机制,在不使缓存失效的前提下删除旧 tool results。

5.2 可用性检查(第 272-286 行)

typescript 复制代码
if (feature('CACHED_MICROCOMPACT')) {
  const mod = await getCachedMCModule()
  const model = toolUseContext?.options.mainLoopModel ?? getMainLoopModel()
  if (
    mod.isCachedMicrocompactEnabled() &&    // GrowthBook 功能开关
    mod.isModelSupportedForCacheEditing(model) &&  // 模型支持
    isMainThreadSource(querySource)          // 仅主线程
  ) {
    return await cachedMicrocompactPath(messages, querySource)
  }
}

三个条件缺一不可:

  • 功能开关:GrowthBook 远程配置
  • 模型支持:只有特定模型支持 cache editing(如 Claude 4.x Sonnet/Opus)
  • 主线程限制 :排除子 agent(session_memory、prompt_suggestion 等),防止它们把 tool results 注册到全局 cachedMCState,导致主线程试图删除不属于自己的工具

5.3 核心逻辑:cachedMicrocompactPath(第 305-399 行)

Phase 1:注册 tool results(第 313-329 行)

typescript 复制代码
const compactableToolIds = new Set(collectCompactableToolIds(messages))
// 按 user message 分组注册 tool results
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 results(!state.registeredTools.has(block.tool_use_id)
  • 按 user message 分组注册(registerToolMessage),方便 count-based 策略按"轮次"计数

Phase 2:计算要删除的 tools(第 332 行)

typescript 复制代码
const toolsToDelete = mod.getToolResultsToDelete(state)

基于 GrowthBook 配置的 triggerThresholdkeepRecent,决定哪些 tool results 需要删除。这由 cachedMicrocompact.ts 中的算法决定(通常是当某个组的 tool 数量超过阈值时,删除最早的那批)。

Phase 3:生成 cache_edits block(第 334-394 行)

typescript 复制代码
if (toolsToDelete.length > 0) {
  const cacheEdits = mod.createCacheEditsBlock(state, toolsToDelete)
  if (cacheEdits) {
    pendingCacheEdits = cacheEdits  // 存到模块级变量,API 层使用
  }
  // ...
  // 计算 baseline cache_deleted_input_tokens,用于后续计算本次操作的 delta
  const lastAsst = messages.findLast(m => m.type === 'assistant')
  const baseline = lastAsst?.type === 'assistant'
    ? (lastAsst.message.usage?.cache_deleted_input_tokens ?? 0)
    : 0

  return {
    messages,  // 不变
    compactionInfo: {
      pendingCacheEdits: {
        trigger: 'auto',
        deletedToolIds: toolsToDelete,
        baselineCacheDeletedTokens: baseline,
      },
    },
  }
}

关键设计

  • messages 数组不变:cache_edits 在 API 传输层处理,不修改本地数据
  • pendingCacheEdits:模块级变量(第 57-60 行),API 层在构造请求时消费
  • baselineCacheDeletedTokens :API 返回的 cache_deleted_input_tokens 是累积值,需要 baseline 才能计算本次操作实际释放了多少

5.4 Cached MC 状态管理

模块级的 cached MC 状态(第 57-60 行):

typescript 复制代码
let cachedMCModule: typeof import('./cachedMicrocompact.js') | null = null
let cachedMCState: CachedMCState | null = null
let pendingCacheEdits: CacheEditsBlock | null = null

相关函数:

函数 行号 用途
getCachedMCModule() 62-69 懒加载 cachedMicrocompact 模块
ensureCachedMCState() 71-81 确保状态已初始化
consumePendingCacheEdits() 88-94 API 层消费 pending edits
getPinnedCacheEdits() 100-105 获取已固定的 edits(需重现发送)
pinCacheEdits() 111-118 固定 edits 到特定 user message 位置
markToolsSentToAPIState() 124-128 标记工具已发送
resetMicrocompactState() 130-135 重置状态(compact 时调用)

resetMicrocompactState() 的调用点:

  • postCompactCleanup.ts:autocompact/reactive compact 后
  • microCompact.ts:517:time-based MC 触发后
  • /clear 新会话时

5.5 为什么需要 pinnedEdits(第 96-104 行注释)

Get all previously-pinned cache edits that must be re-sent at their original positions for cache hits.

cache_edits 是位置敏感的------每次 API 请求都需要带上之前所有已发送的 edits(固定在原始位置),新 API 才能正确命中缓存并继续应用之前的编辑。


6. 总结:两种路径对比

维度 Time-based MC Cached MC
触发条件 时间间隔超过阈值(分钟级) tool result 数量超过阈值
配置来源 TimeBasedMCConfig(GrowthBook) CachedMCConfig(GrowthBook)
修改方式 直接修改 message content 生成 cache_edits block,API 层处理
消息数组 修改后返回新数组 返回原始数组不变
缓存影响 缓存失效(prompt 内容变了) 缓存保留(通过 cache_edits 编辑)
典型场景 用户离开很久后回来 对话中 tool results 持续累积
适用对象 所有支持 cached MC 的模型 仅部分模型 + ant-only + 主线程
后处理 重置 cached MC state 更新 cached MC state(注册新 tool results)

两者的共同目标都是在不丢失语义信息的前提下,减少发送给 API 的 tool result 体积。Time-based MC 是粗暴的"清内容保留结构",Cached MC 是精细的"在缓存层做减法"。当 Time-based 触发时,Cached MC 被跳过(short-circuit),因为缓存已经失效了。

6.1 Time-based MC 使用场景

唯一触发原因:长时间不活动,服务器的 prompt cache 已经过期(TTL 1 小时)。

典型场景:

  1. 用户离开电脑超过 1 小时回来继续对话
  2. 第二天 --resume 继续之前的会话后发第一条消息
  3. 长时间断连后重连

为什么要在缓存过期后清理 tool results?

css 复制代码
正常情况下:
  [消息1][消息2]...[消息N]  →  缓存命中,只发送增量
  不需要清理旧 tool results,因为不需要重发完整的 prompt

缓存过期后:
  [消息1][消息2]...[消息N]  →  缓存 MISS,需重写整个前缀
  既然无论如何都要重写,不如把旧的 tool result 清掉
  减少传输量,减少 API 的处理时间

一个比喻:太久没用冰箱,里面的冰都化了,趁重新制冷的时候顺便清掉一些旧冰块。


6.2 Cached MC 使用场景

对话中 tool results 持续累积,数量超过阈值。

典型场景:

  1. 连续多次 Read 文件------每轮对话都可能读几个文件
  2. 多次执行 Shell 命令查看输出
  3. 多次 Grep / Glob 搜索文件
  4. 多次 WebSearch / WebFetch
  5. 多次 FileEdit / FileWrite 操作

为什么可以用 cache_edits 而不是直接清内容?

arduino 复制代码
服务器缓存仍然有效(对话在持续进行,没有长时间中断)
→ 不需要重写整个前缀
→ 只需要告诉服务器:"把最早的 N 条 tool result 忽略掉"
→ 后续请求继续命中缓存(cache_reference + cache_edits)

一个比喻:冰箱还在正常运行,把一些不太可能再用的旧食材标记为可丢弃,但不影响整体制冷效果。


相关推荐
沉默王二3 小时前
刚上线就斩获 2.3K 星标!AnySearch 搜索能力拉满!
agent·ai编程·claude
Better Bench3 小时前
Ubuntu 22.04系统中解决运行CC-Switch-v3.16.1-Linux-x86_64.AppImage中文乱码
linux·ubuntu·claude·claude code·cc-switch
黏刚4 小时前
2025 最新 Claude Code 教程:从安装部署到 SpringBoot 项目实战(附完整 Java 示例)
java·ai编程·claude
jiayong2315 小时前
Claude Code 快速参考卡片
大数据·elasticsearch·搜索引擎·ai·claude·claude code
ZzT1 天前
Harness 怎么拿捏 agent:权限与 effort
openai·ai编程·claude
jiayong231 天前
Claude Code 常见操作实战指南
linux·服务器·网络·ai·claude·claude code
初旭save1 天前
Agent Skill 不是写 Prompt,是给 LLM 做存储分层
llm·agent·claude