Topic | 上下文压缩(Context Compaction)系统全拆¶
重要性:⭐⭐⭐⭐(LLM 应用的核心工程问题) 出现位置:
src/services/compact/(10 文件) 关联:phase-06-agent-loop.md 的压缩策略对比、glossary 的 Compact 词条
1. 为什么需要压缩¶
根本问题:LLM 有 token 上限(Claude Sonnet 4 是 200K,1M context 是 beta)。
对话会无限增长: - 用户问 10 个问题 → ~5K tokens - 用户问 100 个问题 → ~50K tokens - LLM 跑 30 分钟 → 可能 100K+ tokens(每条 tool result 都很长)
如果不压缩: 1. 超过上限 → 报错 2. 接近上限 → 单次响应变贵、变慢 3. 更糟糕:LLM 容易"迷失"在长上下文里
2. 10 个文件全景¶
src/services/compact/
├── autoCompact.ts 自动压缩(token 接近上限时)
├── microCompact.ts 轻量压缩(只压工具结果)
├── apiMicrocompact.ts API 级别的 microCompact
├── reactiveCompact.ts (DCE) 反应式压缩
├── contextCollapse.ts (DCE) 上下文坍缩
├── compact.ts 手动 /compact 命令
├── sessionMemoryCompact.ts 压缩到 session memory(跨会话)
├── compactWarningHook.ts 压缩前警告 hook
├── compactWarningState.ts 警告状态
├── grouping.ts 消息分组(决定压哪几段)
├── timeBasedMCConfig.ts 时间维度的 microCompact 配置
└── postCompactCleanup.ts 压缩后清理
总计 12 个文件(包含 autoCompact 的额外文件如 compactWarningHook 等)。
3. 五种压缩策略对比¶
| 策略 | 触发条件 | 压缩内容 | 频率 | 实现位置 |
|---|---|---|---|---|
| microCompact | 工具结果超大(单条 > N tokens) | 仅工具结果 | 每次工具调用 | microCompact.ts |
| apiMicrocompact | API 层(Claude API 服务端) | 服务端做 | API 内部 | apiMicrocompact.ts |
| autoCompact | 累计 token 接近上限 | 整段对话 | 中频 | autoCompact.ts |
| reactiveCompact | (DCE) 动态判断 | 智能选段 | 中频 | reactiveCompact.ts |
| contextCollapse | (DCE) 上下文坍缩 | 高级算法 | 低频 | contextCollapse.ts |
手动 /compact:用户主动触发 compact.ts,等价于 autoCompact 但立即执行。
4. autoCompact 详细¶
// src/services/compact/autoCompact.ts
export function isAutoCompactEnabled(): boolean {
return !process.env.DISABLE_AUTO_COMPACT
}
export function calculateTokenWarningState(
totalTokens: number,
maxTokens: number
): 'ok' | 'warning' | 'critical' | 'auto_compact' {
const ratio = totalTokens / maxTokens
if (ratio < 0.7) return 'ok'
if (ratio < 0.85) return 'warning'
if (ratio < 0.95) return 'critical'
return 'auto_compact' // 触发压缩
}
// 在 query.ts 里
const warningState = calculateTokenWarningState(
currentTokens,
maxContextTokens
)
if (warningState === 'auto_compact') {
await runAutoCompact(messages, ctx)
}
关键设计: - 分级警告(ok / warning / critical / auto_compact)—— UI 提前提示 - 阈值化触发 —— 不是"满了才压",是"到 95% 就压" - 不阻塞 —— 压缩在 query.ts 里串行执行(不能并发压两次)
5. microCompact 详细¶
// src/services/compact/microCompact.ts
export function shouldMicroCompact(toolResult: ToolResult): boolean {
const size = estimateTokens(toolResult.content)
return size > MICRO_COMPACT_THRESHOLD // 假设 2000 tokens
}
export function microCompactToolResult(result: ToolResult): ToolResult {
// 保留关键信息(exit code、错误),摘要正文
return {
...result,
content: [
...result.content.filter(isMetadata), // 保留元数据
{
type: 'text',
text: `[Output truncated: ${originalSize} tokens compressed to ${summarySize} tokens]\n\n${summary}`,
},
],
}
}
特点: - 轻量、单条粒度、每次工具调用都跑 - 不调 LLM 摘要(成本太高)—— 用启发式(保留头尾 + 元数据) - 高频、低开销
6. reactiveCompact 详细(Ant-only)¶
// src/services/compact/reactiveCompact.ts (DCE: feature('REACTIVE_COMPACT'))
if (feature('REACTIVE_COMPACT')) {
// 1. 监控 LLM 响应延迟
// 2. 延迟变高 → 触发压缩
// 3. 智能选段(保留"重要"消息,丢"次要"消息)
// 4. 用小模型做摘要(避免主模型开销)
}
为什么是 DCE: - 实验性,外部用户不需要 - 调用额外小模型 = 额外成本 - 主流程不需要 reactive 也能跑(autoCompact 兜底)
7. sessionMemoryCompact 跨会话¶
// src/services/compact/sessionMemoryCompact.ts
export async function saveToSessionMemory(messages: Message[]): Promise<void> {
// 1. 提取"重要"信息(用户偏好、项目约定、决策)
// 2. 写入 ~/.claude/memories/...
// 3. 下次启动时自动注入到 system prompt
}
特点: - 不是压缩(不丢信息) - 是迁移(把信息搬到长期记忆) - 下次启动时 Claude 还能用
8. grouping.ts 决定"压哪几段"¶
// src/services/compact/grouping.ts
export function groupMessagesForCompaction(
messages: Message[]
): CompactionGroup[] {
// 1. 把消息分成"组"(每组 ~10 条)
// 2. 给每组打分(最近 + 用户相关 = 高分)
// 3. 优先压低分组(旧的 + 工具结果多的)
return groups.map(g => ({
messages: g,
priority: g.userRelated ? 0 : 10, // 用户相关不压
tokens: g.reduce((sum, m) => sum + tokenEstimate(m), 0),
}))
}
关键启发式: - 用户相关消息永远不压 - 最近的 N 轮永远不压(保留上下文) - 工具结果优先压(占空间大、信息密度低) - 系统消息不压
9. buildPostCompactMessages¶
// src/services/compact/compact.ts
export function buildPostCompactMessages(
originalMessages: Message[],
summary: string
): Message[] {
return [
// 1. 保留 system prompt(如果有)
...originalMessages.filter(m => m.role === 'system'),
// 2. 加一个 "compact_boundary" 消息标记"压缩发生在这里"
createCompactBoundaryMessage(),
// 3. 摘要
createAssistantMessage(summary),
// 4. 压缩后的用户消息
...originalMessages.filter(m => m.shouldKeepAfterCompact()),
]
}
为什么需要"compact_boundary": - LLM 需要知道"上下文被压缩过" - 边界消息是 system-level 提示:"之前的对话被摘要了,下面是摘要"
10. postCompactCleanup¶
// src/services/compact/postCompactCleanup.ts
export async function postCompactCleanup(ctx: AppState): Promise<void> {
// 1. 清理已压缩的 file state cache
ctx.fileStateCache.clear()
// 2. 清理过时的 tool result 缓存
ctx.toolResultCache.purgeExpired()
// 3. 清理未引用的 attachments
await ctx.attachments.cleanupOrphans()
}
关键:压缩后 LLM 看不到旧文件,本地缓存也要清(避免内存膨胀)。
11. 实战:触发一次压缩的完整流程¶
1. LLM 响应完毕,累计 token = 195K / 200K
↓
2. calculateTokenWarningState → 'auto_compact'
↓
3. query.ts 调 runAutoCompact(messages, ctx)
↓
4. grouping.ts 把消息分 10 组
↓
5. 选低分组的 5 组 → 调小模型摘要(4K 输出)
↓
6. buildPostCompactMessages(messages, summary) → 新 messages
↓
7. 通知 LLM "上下文变了"(buildPostCompactMessages 包含 compact_boundary)
↓
8. 继续 LLM 调用(在新上下文里)
↓
9. postCompactCleanup 清理旧缓存
12. 关键洞察¶
12.1 压缩是"分层"策略¶
- microCompact(每条工具结果)= 微任务(每次 IO 触发)
- autoCompact(累计)= 长任务(到阈值触发)
- sessionMemoryCompact(跨会话)= 长期任务(异步)
前端类比:和前端性能优化分层的思路一样 —— debounce / virtual scroll / lazy load。
12.2 压缩不调主 LLM¶
- microCompact 不用 LLM(启发式)
- autoCompact 用小模型(Haiku 级别)摘要
- 省成本 + 省时间
12.3 grouping 是核心算法¶
- 不是"压最早的消息"那么简单
- 要识别"用户相关"、"工具结果密集"、"决定性消息"
- 启发式 + 评分,不是 ML
12.4 reactiveCompact 是"动态"思路¶
- 不靠固定阈值
- 靠响应延迟、用户行为推断
- 未来方向
13. 阅读清单¶
- ✅
src/services/compact/autoCompact.ts(通读) - ✅
src/services/compact/microCompact.ts(通读) - ✅
src/services/compact/grouping.ts(通读) - ✅
src/services/compact/compact.ts(手动命令) - ✅
src/services/compact/buildPostCompactMessages(在 query.ts 引用) - 📌
src/services/compact/reactiveCompact.ts(DCE,看注释) - 📌
src/services/compact/sessionMemoryCompact.ts(跨会话) - 📌
src/services/compact/postCompactCleanup.ts(清理)
14. 练习任务¶
- 实现一个简单的 microCompact —— 把超过 1000 字符的工具结果截断到头尾各 200 字符
- 手写一个 token 估算器 —— 简单版(4 字符 ≈ 1 token)
- 设计一个 grouping 算法 —— 用户消息 + 最近 5 轮不压,其他按时间倒序压
- 思考:如果你做一个 Web ChatGPT 类应用,压缩策略应该怎么选?为什么 Claude Code 选了 5 种而非 1 种?