上下文压缩

修剪旧工具输出、由 compaction agent 生成锚定摘要、按预算原样保留最近几轮,以及压缩请求如何作为一条消息在主循环里流转。

作者 David更新于 第 10 篇(共 38 篇)

长会话迟早撑满上下文窗口。OpenCode V1 的对策集中在 packages/opencode/src/session/compaction.ts 与 message-v2.ts,分三层:

机制做什么默认
修剪(prune)把较早的工具输出替换成占位符关闭
压缩(compaction)让 compaction agent 把旧历史写成结构化摘要开启
历史视图(filterCompacted)读历史时只取摘要及其后的部分始终生效

怎么用

  • 自动:默认开启,上下文快满时触发,无需操作。
  • 手动:TUI 执行 /compact(别名 /summarize),对应服务端 POST /session/:id/summarize。
  • 配置 compaction 段:auto(默认开)、prune(默认关)、tail_turns(最多原样保留几轮)、preserve_recent_tokens(原样保留的 token 预算)、reserved(溢出判定的预留量)。
  • 想换压缩用的模型,给 compaction agent 配 model;插件可通过 experimental.session.compacting 注入上下文或替换整段提示词,通过 experimental.compaction.autocontinue 关掉自动续跑。

压缩请求是一条消息

SessionCompaction.create(compaction.ts:559)并不立即压缩,只写一条 user 消息,里面放一个 compaction Part(带 auto、overflow 标记)。主循环下一步从历史里取到这个任务,再调用 process 执行。写入请求的时机有四个:

  1. 处理器步末发现用量超限,返回 compact。
  2. 提供方直接报上下文溢出,同样返回 compact;这一步没有 finish,请求带上 overflow: true。
  3. 每步开始前复查上一条已完成 assistant 的用量。
  4. 用户手动 /compact,此时 auto 默认为假,压缩完不会自动续跑。

把请求落成消息,它就和其他消息一样可见、可回看,还能充当后面历史视图的锚点。

process:生成摘要

compaction.ts:319 的流程:

  1. 溢出重放:overflow 请求会找到触发溢出的那条真实 user 消息,把它及之后的内容排除在摘要之外,压缩完再重放一遍,其中的媒体附件换成 [Attached 类型: 文件名] 式的占位文本。
  2. 选模型:使用隐藏的 compaction agent(提示词为 packages/opencode/src/agent/prompt/compaction.txt,工具权限全部 deny),模型取 agent 配置,否则沿用 user 消息的模型。
  3. 增量摘要:已完成的旧压缩对(请求与摘要)从待压缩历史中隐去,最近一次的摘要作为 previousSummary 传入,新摘要在它的基础上合并。
  4. 选保留尾部:select(compaction.ts:223)从最新一轮往前累计估算 token,整轮放得下就整轮保留,放不下就用 splitTurn 保留该轮的后半截。预算默认是可用量的 25%,夹在 2000 到 15000 之间;tail_turns 可再限制轮数。尾部之前的部分才是要被摘要的“头”。
  5. 序列化:头部被拍平成纯文本对话记录,每行带角色标签([User]、[Assistant]、[Assistant tool call]、[Tool result] 等),工具结果截断到 2000 字符。
  6. 调用模型:新建一条 summary: true、agent 为 compaction 的 assistant 消息,交给处理器执行,工具表为空;连压缩请求本身都溢出时,记一个 ContextOverflowError 并停止。
  7. 回写锚点:把保留尾部的起点写回 compaction Part 的 tail_start_id。
  8. 续跑:自动压缩时,有待重放的消息就重放;否则在插件允许时追加一条 synthetic user 消息,内容是“Continue if you have next steps, or stop and ask for clarification if you are unsure how to proceed.”,并用 metadata.compaction_continue 标记它是自动续跑。溢出场景下这段话前面还会加一段说明:请求因大体积媒体附件超限,附件已从上下文中移除。

摘要模板

buildPrompt 来自 V2 内核的 packages/core/src/session/compaction.ts,V1 直接复用。模板要求输出固定的 Markdown 结构(packages/core/src/session/compaction.ts:16,删减后,只保留各节标题):

## Objective
## Important Details
## Work State
### Completed
### Active
### Blocked
## Next Move
## Relevant Files

规则要求:每节都保留(空就写 (none))、用简短条目、保留准确的路径、符号、命令与错误串,且不得提及“摘要”或“压缩”本身。有旧摘要时再附上 <prior-summary> 与合并规则:旧摘要里的目标、约束和并行工作流都要带过来,冲突时以新对话为准,完成的事项从 Active 移到 Completed。compaction agent 的系统提示词还有一句对中文用户很实用:

Respond in the same language as the conversation.

历史视图:模型看到什么

filterCompacted(message-v2.ts:521)从新到旧扫描历史,遇到已有成功摘要的 compaction 请求就停下;若记录了 tail_start_id,就继续往前取到尾部起点为止。更早的消息不再发给模型。随后重排(message-v2.ts:564):

  if (tailIndex >= 0 && tailIndex < compactionIndex && summaryIndex > compactionIndex) {
    return [
      ...result.slice(compactionIndex, summaryIndex + 1),
      ...result.slice(tailIndex, compactionIndex),
      ...result.slice(summaryIndex + 1),
    ]
  }

模型看到的顺序是:压缩请求、摘要、原样保留的尾部、之后的新消息。compaction Part 在转成模型消息时渲染为一句 user 文本“What did we do so far?”(message-v2.ts:228),摘要恰好成了 assistant 对它的回答。数据库里的旧消息一条没删,界面仍能完整回看,只是模型看不到。

修剪

prune(compaction.ts:271)只在 compaction.prune 为真时工作,每次循环结束后在后台运行。源码注释:

goes backwards through parts until there are PRUNE_PROTECT tokens worth of tool calls, then erases output of older tool calls to free context space

具体做法:跳过最近两轮;遇到摘要消息或已修剪过的输出就停;skill 工具的输出受保护;最近 4 万 token(PRUNE_PROTECT)的工具输出原样保留,更早的列入候选;候选累计超过 2 万 token(PRUNE_MINIMUM)才动手,给它们打上 time.compacted,此后发给模型时输出变成“[Old tool result content cleared]”。这里的 token 数一律按“字符数 ÷ 4”估算(packages/core/src/util/token.ts)。

三个 summary

V1 里 summary 一词有好几种意思:assistant 消息上的 summary: true 指压缩摘要;SessionSummary(summary.ts)根本不调用模型,只根据快照计算文件差异,写进 user 消息的 summary.diffs;agent/prompt/summary.txt 注册成了隐藏的 summary agent(要求像 PR 描述那样写两三句),但在 packages/opencode/src 里找不到取用它的代码。标题生成则是另一个 title agent,见主循环。

V2 的做法不同:compactIfNeeded 在请求发出前估算整个请求的大小,超过“窗口减去输出上限与 buffer 中的较大者”才压缩,默认原样保留 8000 token,见V2 会话内核。

下一篇:模型调用层:AI SDK 与原生双轨——llm.stream 背后的请求组装与两条运行时。

本页目录