# 上下文压缩

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

- 作者：David（道雾轩）
- 专栏：OpenCode 源码解读（https://daiw.org/manual/opencode.md）
- 最后更新：2026-09-15
- 原文：https://daiw.org/manual/opencode/compaction
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

长会话迟早撑满上下文窗口。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` 标记）。[主循环](https://daiw.org/manual/opencode/prompt-loop)下一步从历史里取到这个任务，再调用 `process` 执行。写入请求的时机有四个：

1. [处理器](https://daiw.org/manual/opencode/processor)步末发现用量超限，返回 `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`，删减后，只保留各节标题）：

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

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

```text
Respond in the same language as the conversation.
```

## 历史视图：模型看到什么

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

```ts
  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，见[主循环](https://daiw.org/manual/opencode/prompt-loop)。

V2 的做法不同：`compactIfNeeded` 在请求发出前估算整个请求的大小，超过“窗口减去输出上限与 buffer 中的较大者”才压缩，默认原样保留 8000 token，见[V2 会话内核](https://daiw.org/manual/opencode/session-v2)。

下一篇：[模型调用层：AI SDK 与原生双轨](https://daiw.org/manual/opencode/llm-bridge)——`llm.stream` 背后的请求组装与两条运行时。
