opencode 系统提示词注入方案:静态前缀、幂等与缓存
本文基于 opencode-goal 插件(开源,SDK 1.18)真实实现编写。主题:如何将强制规则集注入每条消息的系统提示词,并满足幂等与缓存要求。
1. 问题域
目标插件需要把约 500 行的中文法规式规则(MANDATORY_PREAMBLE)附着到每条 AI 消息上。约束有三:附着必须无条件(每条消息);重复组装不得重复注入(幂等);注入不得破坏提示词缓存(静态前缀)。
2. 注入点:experimental.chat.system.transform
SDK 提供experimental.chat.system.transform钩子,回调签名:
async"experimental.chat.system.transform"(_input:{sessionID?:string},output:{system:string[]},):Promise<void>output.system为该会话系统提示词块数组。实现为追加一块:
if(output.system.some((block:string)=>block.includes("[绝对禁止] 本规则为最高级硬性约束")))returnoutput.system.push(SYSTEM_PRINCIPLES)3. 幂等:去重守卫
该钩子在每次组装提示词时调用,若无条件追加,同一规则块将随对话推进重复出现。守卫以规则块字面量开头为锚判定已注入:
- 命中即返回,不追加;
- 锚字符串必须与
MANDATORY_PREAMBLE的开头严格同步:文本开头变更而不更新守卫,将导致逐条消息重复注入; - 该同步关系由单元测试不变式锁定(测试断言共 164 项)。
4. 缓存:静态前缀
LLM 服务端提示词缓存(KV cache)按前缀匹配:前缀字节一致可复用已计算的 KV。因此规则块必须满足:
- 完全静态:无时间戳、无会话变量、无易变内容;
- 开头定字:锚点在第一行,后续全部为固定条款。
本实现中规则块的第一行为固定标语,其后条款均为静态文本——前缀不携带任何动态内容(动态数据经工具返回与压缩钩子进入上下文,见 5 节)。
5. 数据与规则的通道分离
| 数据类型 | 通道 | 位置 |
|---|---|---|
| 静态规则 | system.transform | 系统提示词数组末尾 |
| 目标状态、token、子代理信息 | 工具返回值(JSON) | 对话消息(位置可变,缓存影响小) |
| 压缩时的目标快照 | experimental.session.compacting | 压缩上下文 |
该分离开销最小化:规则高频且静态,进前缀;状态低频且动态,走工具;压缩不丢状态。
6. 行为约束的表述方式
规则块以法条式书写(必须/禁止/不得),不使用散文式劝说。取舍依据:规范性用语歧义最小、遵从度可测、文本稳定(缓存与测试皆可锁定)。
7. 验证
tests/test-prompts.mjs(164 断言):注入块结构、锚点一致性、转义;tests/test-server-helpers.mjs:system.transform 幂等(重复组装不重复注入);- 集成状态下提示词以实际组装路径进入每条消息。
8. 结论
系统提示词注入的三条约束不可拆分:注入点负责"放在哪",守卫负责"只放一次",静态前缀负责"放得便宜"。任一缺失,要么规则缺失、要么重复、要么缓存失效。此为强制规则注入的通用方案,不限于 opencode。