上下文压缩
修剪旧工具输出、由 compaction agent 生成锚定摘要、按预算原样保留最近几轮,以及压缩请求如何作为一条消息在主循环里流转。
长会话迟早撑满上下文窗口。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(溢出判定的预留量)。 - 想换压缩用的模型,给
compactionagent 配model;插件可通过experimental.session.compacting注入上下文或替换整段提示词,通过experimental.compaction.autocontinue关掉自动续跑。
压缩请求是一条消息
SessionCompaction.create(compaction.ts:559)并不立即压缩,只写一条 user 消息,里面放一个 compaction Part(带 auto、overflow 标记)。主循环下一步从历史里取到这个任务,再调用 process 执行。写入请求的时机有四个:
- 处理器步末发现用量超限,返回
compact。 - 提供方直接报上下文溢出,同样返回
compact;这一步没有finish,请求带上overflow: true。 - 每步开始前复查上一条已完成 assistant 的用量。
- 用户手动
/compact,此时auto默认为假,压缩完不会自动续跑。
把请求落成消息,它就和其他消息一样可见、可回看,还能充当后面历史视图的锚点。
process:生成摘要
compaction.ts:319 的流程:
- 溢出重放:
overflow请求会找到触发溢出的那条真实 user 消息,把它及之后的内容排除在摘要之外,压缩完再重放一遍,其中的媒体附件换成[Attached 类型: 文件名]式的占位文本。 - 选模型:使用隐藏的
compactionagent(提示词为packages/opencode/src/agent/prompt/compaction.txt,工具权限全部 deny),模型取 agent 配置,否则沿用 user 消息的模型。 - 增量摘要:已完成的旧压缩对(请求与摘要)从待压缩历史中隐去,最近一次的摘要作为
previousSummary传入,新摘要在它的基础上合并。 - 选保留尾部:
select(compaction.ts:223)从最新一轮往前累计估算 token,整轮放得下就整轮保留,放不下就用splitTurn保留该轮的后半截。预算默认是可用量的 25%,夹在 2000 到 15000 之间;tail_turns可再限制轮数。尾部之前的部分才是要被摘要的“头”。 - 序列化:头部被拍平成纯文本对话记录,每行带角色标签(
[User]、[Assistant]、[Assistant tool call]、[Tool result]等),工具结果截断到 2000 字符。 - 调用模型:新建一条
summary: true、agent 为compaction的 assistant 消息,交给处理器执行,工具表为空;连压缩请求本身都溢出时,记一个ContextOverflowError并停止。 - 回写锚点:把保留尾部的起点写回 compaction Part 的
tail_start_id。 - 续跑:自动压缩时,有待重放的消息就重放;否则在插件允许时追加一条 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 背后的请求组装与两条运行时。