跳转至

第 5 章:上下文工程 —— System Context、History 与 Context Epoch

1. OpenCode 的“上下文”不是一段字符串

源码 CONTEXT.md 明确区分:

  • System Context:由若干 typed Context Source 组合出的初始指令和结构化事实;
  • Session History:在当前 compaction / Context Epoch cutoff 下选出的时间顺序消息;
  • Context Snapshot:模型隐藏的 JSON 状态,用来比较某个 Source 上次被接纳的值;
  • Baseline System Context:一个 Context Epoch 开始时完整渲染的 System Context;
  • Mid-Conversation System Message:上下文变化后写入历史的中途指令。

这套术语是在解决一个实际矛盾:系统信息会变化,但 provider cache 又希望初始 system prompt 稳定。

2. Context Source:把动态信息变成可比较的值

一个 Source 不只是 prompt fragment,而是一个有稳定 key 的小状态单元:

Context Source
  = stable key
  + JSON codec
  + loader
  + baseline renderer
  + update renderer
  + optional removal renderer

Loader 观察当前 Location 下的事实,例如项目规则、skills、references 或运行环境;registry 按顺序合并它们;runner 再把合并结果装进 provider request。

如果某次观察暂时失败,Unavailable Context 不应该把模型上下文抹掉。系统可以保留上一次有效状态,等待后续安全边界继续观察。

3. Context Epoch:缓存基线的生命周期

一个 Context Epoch 从完整 baseline 被渲染开始,到以下事件结束:

  • compaction 完成;
  • Session 移动到不同 Location;
  • 发生不兼容的 context transition,需要重新建立 provider baseline。
sequenceDiagram
  participant R as Runner
  participant C as Context Registry
  participant D as DB
  participant M as Model
  R->>C: load typed sources
  C-->>R: combined System Context
  R->>D: persist baseline + snapshot + epoch
  R->>M: system baseline + history
  Note over R,M: provider turns reuse this baseline
  C-->>R: source value changed
  R->>D: persist mid-conversation system message
  R->>D: advance snapshot atomically
  R->>M: history + chronological update
  Note over R,M: epoch ends after compaction / move / incompatible change

4. 为什么不每次都重写 system prompt

如果每一轮都把完整系统上下文重新放到最前面:

  • provider prompt cache 的命中会下降;
  • 长文本重复占用输入 token;
  • 模型看到的“初始化指令”与“中途变化”混在一起;
  • 变化的事实难以在历史中重放。

Context Epoch 让 baseline 具有明确的生命周期,中途变化则以 durable mid-conversation message 表示。这是“稳定基线 + 有序更新”的组合。

5. History 是投影,不是所有数据库行

V2 的 SessionHistory.entriesForRunner(db, sessionID, system.baselineSeq) 会根据 Context Epoch 的 baseline sequence 选择可用于当前 provider turn 的消息。历史至少要处理:

  • assistant text / reasoning;
  • tool call / tool result;
  • user prompt 和 promoted input;
  • system update;
  • compaction 后的新起点;
  • 失败、取消和 provider 原生 continuation metadata。

因此 messages 不等价于“SELECT * FROM session_message”。它是面向模型的 projection,必须保持角色、顺序、工具配对和 provider 约束。

6. 工具输出是上下文治理的一部分

工具会产生比模型窗口更大的结果。OpenCode 的处理分两层:

  1. 将 bounded projection 写入 Session history,保证下一次 provider turn 可用;
  2. 完整输出放入 shared tool-output directory 的 Managed Tool Output File,并把路径 / 截断元数据告诉模型或 UI。

这比简单 slice(0, N) 更好,因为用户仍可访问完整结果,模型也能知道内容被截断且可以按需读取。

7. Compaction:保存语义,不保存每个 token

旧版 packages/opencode/src/session/compaction.ts 和 V2 core compaction 都体现出类似策略:

  • 根据模型可用窗口和配置判断 overflow;
  • 选择合法的 turn / message cut point;
  • 保留最近若干 turn;
  • 让模型生成结构化 summary;
  • 记录 compacted segment、summary、文件状态等 metadata;
  • 重新建立或推进 Context Epoch。

重要取舍是:切点不能随便落在 tool call 与 tool result 中间。 如果切断一个未闭合的 turn,后续 provider request 可能不再是合法对话。

8. “加法 + 减法”双向上下文工程

OpenCode 的上下文工程同时做两件事:

  • 加法:通过 System Context、skills、references、项目规则、工具定义把必要信息按需装入;
  • 减法:通过 tool output truncation、prune、compaction、history selection 控制窗口规模。

这和只做 summary 的 Agent 有本质差别:摘要只是历史压缩的一种手段,不负责解决动态系统信息、工具输出和 provider cache。

本章小结

OpenCode 把上下文拆成 typed source、stable baseline、chronological update、projected history 和 compacted epoch。这样系统可以既稳定地利用 provider cache,又准确地记录动态事实和长对话压缩。

源码锚点