跳到主要内容

microCompact.ts:轻量级局部压缩策略

🔴 深度

全量压缩(compact)会替换整个对话历史,是一次"重启"。但在很多情况下,真正占用大量 token 的只是少数几个工具调用的结果——比如读取了一个 1000 行的文件,或者执行了一个输出了几十 KB 的 bash 命令。针对这类场景,microCompact.ts 提供了一种外科手术式的局部压缩方案。

microCompact vs 全量 compact 的根本区别

维度microCompact全量 compact
压缩范围仅针对特定工具的 tool_result整个对话历史
是否调用 Claude不调用(直接截断/清除)调用 Claude 生成摘要
信息损失丢弃工具结果原文,保留标记转为结构化摘要
速度极快(毫秒级)慢(需要一次 API 调用)
适用场景大量工具输出,但不影响任务进行对话历史过长,需要整体清理

microCompact 的核心思路是:工具结果(tool_result)通常在被使用后就没有保留全文的必要了。Claude 看过文件内容后已经"知道"了,没有必要在每次 API 调用时都把整个文件内容重复传递。

可压缩工具集合

microCompact.ts 只对特定工具的结果进行压缩:

// source/src/services/compact/microCompact.ts
const COMPACTABLE_TOOLS = new Set<string>([
FILE_READ_TOOL_NAME, // 文件读取结果(最常见的大块内容)
...SHELL_TOOL_NAMES, // bash/shell 执行结果
GREP_TOOL_NAME, // grep 搜索结果
GLOB_TOOL_NAME, // glob 文件列表
WEB_SEARCH_TOOL_NAME, // 网页搜索结果
WEB_FETCH_TOOL_NAME, // 网页内容获取结果
FILE_EDIT_TOOL_NAME, // 文件编辑结果(含 diff)
FILE_WRITE_TOOL_NAME, // 文件写入结果
])

有意排除的工具:

  • AgentTool(子代理结果):包含复杂的协作信息,不宜轻易丢弃
  • ToolSearchTool:搜索结果需要保留供后续使用
  • 所有写入类工具的输入:仍然需要知道改了什么

token 估算器:estimateMessageTokens

microCompact 的决策依赖快速的 token 估算,而不是精确计数:

// source/src/services/compact/microCompact.ts
export function estimateMessageTokens(messages: Message[]): number {
let totalTokens = 0

for (const message of messages) {
if (message.type !== 'user' && message.type !== 'assistant') continue
if (!Array.isArray(message.message.content)) continue

for (const block of message.message.content) {
if (block.type === 'text') {
totalTokens += roughTokenCountEstimation(block.text)
} else if (block.type === 'tool_result') {
totalTokens += calculateToolResultTokens(block)
} else if (block.type === 'image' || block.type === 'document') {
totalTokens += IMAGE_MAX_TOKEN_SIZE // 固定 2000 tokens
} else if (block.type === 'thinking') {
// 只计算 thinking 文本本身,不含 JSON wrapper
totalTokens += roughTokenCountEstimation(block.thinking)
} else if (block.type === 'tool_use') {
// 计算工具名 + 输入参数
totalTokens += roughTokenCountEstimation(
block.name + jsonStringify(block.input ?? {})
)
} else {
totalTokens += roughTokenCountEstimation(jsonStringify(block))
}
}
}

// 保守估计:乘以 4/3 系数
return Math.ceil(totalTokens * (4 / 3))
}

注意最后的 * (4 / 3) 系数。roughTokenCountEstimation 是基于字符数的粗略估算,实际 token 数通常比字符数少(因为 BPE 分词会合并常见词组),但为了安全起见,乘以 4/3 确保估算偏高而非偏低。

可压缩工具 ID 的收集

microCompact 通过遍历消息列表,收集所有可压缩工具的调用 ID:

function collectCompactableToolIds(messages: Message[]): string[] {
const ids: string[] = []
for (const message of messages) {
if (
message.type === 'assistant' &&
Array.isArray(message.message.content)
) {
for (const block of message.message.content) {
if (block.type === 'tool_use' && COMPACTABLE_TOOLS.has(block.name)) {
ids.push(block.id)
}
}
}
}
return ids
}

收集到 ID 后,对应的 tool_result 消息中的内容会被替换为一个占位标记:

export const TIME_BASED_MC_CLEARED_MESSAGE = '[Old tool result content cleared]'

用户或模型如果看到这条消息,就知道这个工具结果已经被清除了,但工具调用本身确实发生过。

Cached microCompact:ant-only 的高级模式

除了基础的内容清除,microCompact.ts 还有一个更复杂的模式——cached microcompact,通过 feature flag CACHED_MICROCOMPACT 控制:

// 懒加载 cached MC 模块(外部构建中通过死代码消除去除)
let cachedMCModule: typeof import('./cachedMicrocompact.js') | null = null
let cachedMCState: import('./cachedMicrocompact.js').CachedMCState | null = null
let pendingCacheEdits: import('./cachedMicrocompact.js').CacheEditsBlock | null = null

async function getCachedMCModule() {
if (!cachedMCModule) {
cachedMCModule = await import('./cachedMicrocompact.js')
}
return cachedMCModule
}

这个模式利用 Anthropic API 的 prompt caching 机制。大致原理是:

  1. 对经常重复出现的大块工具结果(如反复读取的文件),将其"缓存"到 API 端
  2. 后续调用时不再重复传输内容,而是通过 cache token 引用
  3. 通过 cache_edits 块来通知 API 哪些内容已被清除/修改

相关的操作函数:

// 获取待发送的 cache edits(清除后必须通知 API)
export function consumePendingCacheEdits(): CacheEditsBlock | null {
const edits = pendingCacheEdits
pendingCacheEdits = null
return edits
}

// 固定 cache edits 到特定用户消息位置
export function pinCacheEdits(
userMessageIndex: number,
block: CacheEditsBlock,
): void {
if (cachedMCState) {
cachedMCState.pinnedEdits.push({ userMessageIndex, block })
}
}

// 获取所有已固定的 cache edits(每次 API 调用都需要重发)
export function getPinnedCacheEdits(): PinnedCacheEdits[] {
if (!cachedMCState) return []
return cachedMCState.pinnedEdits
}

Time-based microcompact:时间驱动的清理策略

timeBasedMCConfig.ts 定义了另一种 microCompact 触发策略——基于时间的清理:

// source/src/services/compact/timeBasedMCConfig.ts
export type TimeBasedMCConfig = {
// 工具结果在被保留多少毫秒后可以被清除
retentionMs: number
// 清除时最少保留多少条工具结果(保留最近的)
minResultsToKeep: number
}

基于时间的清理逻辑:如果一个工具结果已经在上下文中保留超过 retentionMs 毫秒,且它不是最近 minResultsToKeep 条结果之一,就可以被清除。

这比基于 token 数量的策略更加稳健——即使 token 使用量还没到警戒线,长时间前的大型工具结果也可以被提前清理,为后续操作预留空间。

压缩质量 vs 速度的权衡

microCompact 有意选择了速度优先的设计:

速度优势:

  • 不需要调用 Claude API(节省时间和费用)
  • 纯内存操作,毫秒级完成
  • 可以在每轮对话后自动运行,用户无感知

质量代价:

  • 丢弃的工具结果无法恢复(不像全量 compact 会生成文字摘要)
  • 如果被清除的工具结果后来又需要,Claude 需要重新执行工具调用
  • 对于"跨多轮引用相同文件"的场景不友好

这就是为什么 microCompact 只清除"可压缩工具"的结果,而不是所有工具结果——对于复杂的推理工具(如 Agent 工具的结果),不能简单丢弃。

在实际使用中,microCompact 和全量 compact 形成互补:

  • 日常使用:microCompact 不断清理积累的工具结果
  • 当 microCompact 的清理不足以维持在阈值以下时:全量 compact 介入
📄source/src/services/compact/microCompact.tsL1-250查看源码 →