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 机制。大致原理是:
- 对经常重复出现的大块工具结果(如反复读取的文件),将其"缓存"到 API 端
- 后续调用时不再重复传输内容,而是通过 cache token 引用
- 通过
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 介入