跳转至

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. 阅读清单

  1. src/services/compact/autoCompact.ts(通读)
  2. src/services/compact/microCompact.ts(通读)
  3. src/services/compact/grouping.ts(通读)
  4. src/services/compact/compact.ts(手动命令)
  5. src/services/compact/buildPostCompactMessages(在 query.ts 引用)
  6. 📌 src/services/compact/reactiveCompact.ts(DCE,看注释)
  7. 📌 src/services/compact/sessionMemoryCompact.ts(跨会话)
  8. 📌 src/services/compact/postCompactCleanup.ts(清理)

14. 练习任务

  1. 实现一个简单的 microCompact —— 把超过 1000 字符的工具结果截断到头尾各 200 字符
  2. 手写一个 token 估算器 —— 简单版(4 字符 ≈ 1 token)
  3. 设计一个 grouping 算法 —— 用户消息 + 最近 5 轮不压,其他按时间倒序压
  4. 思考:如果你做一个 Web ChatGPT 类应用,压缩策略应该怎么选?为什么 Claude Code 选了 5 种而非 1 种?