第 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 的处理分两层:
- 将 bounded projection 写入 Session history,保证下一次 provider turn 可用;
- 完整输出放入 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,又准确地记录动态事实和长对话压缩。