05 · 上下文压缩:长会话不爆 token
本章讲:当一次会话越来越长、快超过模型上下文窗口时,opencode 怎么自动"瘦身"还不丢关键信息。
1. 它要解决的小问题
每轮都把全部历史喂给模型,token 只增不减,迟早撑爆上下文窗口(也越来越贵)。需要一个机制:在快溢出时,把早期历史压成一段摘要,只保留最近的细节,让对话能继续。
2. 思路 / 直觉:总结 + 裁剪
opencode 的压缩(compaction)分两件事:
- 总结(summarize):让模型读早期历史,写一段"到目前为止发生了什么"的摘要,作为一条特殊的 compaction part 插入。
- 裁剪(prune):把摘要之前的旧消息从"喂给模型的历史"里剔掉(原始记录仍在 SQLite,只是不再发给模型),但保护最近若干轮和某些关键工具输出。
完整历史(很长): [旧...旧][中][最近2轮]
│ 检测到接近溢出
▼
① 总结旧+中段 ──► [摘要]
② 裁剪 ──► 喂给模型的 = [摘要] + [最近2轮]
│
▼ 之后的轮次基于瘦身后的历史继续
怎么读:左边是膨胀的完整历史;压缩后只把"摘要 + 最近几轮"发给模型,token 立刻降下来。
3. 触发:溢出检测
压缩的触发点在两处,都靠"用量 vs 模型上下文上限"的判断(session/overflow.ts 的 isOverflow):
- 流处理中:每个
step-finish后,processor 检查本步用量是否溢出,是则置ctx.needsCompaction = true,提前停掉当前流(processor.ts:475-480)。 - 循环顶部:
runLoop在下一轮看到上一步溢出,就compaction.create({ auto: true })排一个压缩任务(prompt.ts:1161-1168)。
压缩也可被 ContextOverflowError 触发:provider 直接报上下文超限时,halt(processor.ts:605)把它转成"需要压缩"(除非用户把 compaction.auto 关了)。
4. 总结怎么生成
总结复用同一套 LLM 调用,但用专门的 prompt。压缩 prompt 由 buildPrompt 构造——它从 core 包导入(compaction.ts:23 的 import { buildPrompt } from "@opencode-ai/core/session/compaction"),在压缩流程里被调用(compaction.ts:348)。压缩产物是一条带 summary 标记的 assistant 消息;completedCompactions(compaction.ts:62)负责在历史里找出"已完成的压缩点"——即"有 compaction part 的 user 消息" + "对应的、已 finish 且无 error 的 summary assistant 消息"配对。
5. 裁剪的预算规则
裁剪不是"砍到摘要为止"那么粗暴,它有一套预算常量(compaction.ts:28-34):
| 常量 | 值 | 含义 |
|---|---|---|
PRUNE_MINIMUM | 20_000 | 低于这个 token 量不值得裁 |
PRUNE_PROTECT | 40_000 | 这一段最近历史受保护、不裁 |
DEFAULT_TAIL_TURNS | 2 | 至少保留最近 2 轮完整对话 |
MIN/MAX_PRESERVE_RECENT_TOKENS | 2_000 / 8_000 | 保留的"最近内容"token 区间 |
TOOL_OUTPUT_MAX_CHARS | 2_000 | 裁剪时工具输出压到这个字符上限 |
PRUNE_PROTECTED_TOOLS | ["skill"] | skill 工具的输出不被裁(它含长效指令) |
直觉:越近的越完整保留,越远的越敢压,而 skill 这类"加载后要长期生效的指令"被单独护住,免得压缩把 agent 的能力说明丢了。
6. 关键细节 / 坑
- 可手动可自动。 用户可主动触发压缩;
compaction.auto配置项可关掉自动压缩(processor.ts:606),关掉后溢出会直接报错而非静默压缩。 - 压缩本身也是一次 LLM 调用,所以也走 processor;
processor.ts:314-316、330-332明确禁止"在生成 summary 时再调工具"(会抛错)。 - prompt 构造在 core 包:
buildPrompt(从@opencode-ai/core/session/compaction导入,compaction.ts:23),压缩逻辑跨packages/opencode与packages/core两层。 - 裁剪只影响"发给模型的投影",原始消息/part 仍持久化在 SQLite——这呼应 CONTEXT.md 里"Session History 是投影"的术语设计。
7. 横向对比(同 shelf 兄弟)
上下文管理是所有编码 agent 的共同关切,各家取舍不同:
- 总结式压缩(opencode 本章):溢出时让模型自己写摘要 + 按预算裁旧历史。优点是无限续航;代价是摘要有损,早期细节会丢。
- 靠工具结果截断(见 02 章
truncate):opencode 同时用"单条工具输出截断 + 写临时文件"防止单次输出撑爆——这是"入口处削峰",和 compaction 的"全局瘦身"互补。 - 与纯"滑动窗口丢最旧"的朴素做法相比,opencode 保留摘要 + 保护最近轮 + 保护 skill 输出,更不容易丢关键状态。
8. 代码地图
| 主题 | 文件路径 | 符号名 |
|---|---|---|
| 压缩服务 | packages/opencode/src/session/compaction.ts | Service(process, create, prune, isOverflow) |
| 已完成压缩点识别 | packages/opencode/src/session/compaction.ts | completedCompactions, summaryText |
| 预算常量 | packages/opencode/src/session/compaction.ts | PRUNE_PROTECT, DEFAULT_TAIL_TURNS, PRUNE_PROTECTED_TOOLS |
| 溢出检测 | packages/opencode/src/session/overflow.ts | isOverflow, usable |
| 流中触发压缩 | packages/opencode/src/session/processor.ts | handleEvent(step-finish), halt |
| 循环中排压缩任务 | packages/opencode/src/session/prompt.ts | runLoop(compaction.create) |
| 压缩 prompt 构造 | packages/opencode/src/session/compaction.ts | buildPrompt(来自 core) |