源码:
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
触发条件(全部满足):
- 功能启用 :
config.enabled === true - 主线程 source :
querySource已定义且以repl_main_thread开头(或为 undefined------但有定义时才触发) - 有 assistant 消息 :messages 中存在
type === 'assistant'的消息 - 超过时间阈值 :距最后一条 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
步骤:
- 收集所有 COMPACTABLE_TOOLS 的 tool_use ID(按出现顺序)
- 保留最近
keepRecent条(最少 1 条,第 461 行Math.max(1, config.keepRecent)) - 剩余的全部标记为待清理
- 遍历 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 配置的 triggerThreshold 和 keepRecent,决定哪些 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 小时回来继续对话
- 第二天
--resume继续之前的会话后发第一条消息 - 长时间断连后重连
为什么要在缓存过期后清理 tool results?
css
正常情况下:
[消息1][消息2]...[消息N] → 缓存命中,只发送增量
不需要清理旧 tool results,因为不需要重发完整的 prompt
缓存过期后:
[消息1][消息2]...[消息N] → 缓存 MISS,需重写整个前缀
既然无论如何都要重写,不如把旧的 tool result 清掉
减少传输量,减少 API 的处理时间
一个比喻:太久没用冰箱,里面的冰都化了,趁重新制冷的时候顺便清掉一些旧冰块。
6.2 Cached MC 使用场景
对话中 tool results 持续累积,数量超过阈值。
典型场景:
- 连续多次
Read文件------每轮对话都可能读几个文件 - 多次执行 Shell 命令查看输出
- 多次
Grep/Glob搜索文件 - 多次
WebSearch/WebFetch - 多次
FileEdit/FileWrite操作
为什么可以用 cache_edits 而不是直接清内容?
arduino
服务器缓存仍然有效(对话在持续进行,没有长时间中断)
→ 不需要重写整个前缀
→ 只需要告诉服务器:"把最早的 N 条 tool result 忽略掉"
→ 后续请求继续命中缓存(cache_reference + cache_edits)
一个比喻:冰箱还在正常运行,把一些不太可能再用的旧食材标记为可丢弃,但不影响整体制冷效果。