OpenCode 自研 SDD 流程注入方案
生成时间:2026-05-07
目标:在不改 sandbox 镜像、不动 某企业级 Agent 框架 存量 system prompt 的前提下,把自研 SDD 流程(自研 SDD 流程 / 自研 SDD 流程)稳定塞进 OpenCode,让它在每个项目里都能可复现地盖过默认 openspec-* 流程。
适用读者:希望在某 sandbox 平台 内做 agent 行为定制的开发者
综合来源:3 份前置探索文档(配置体系探索之旅 / Sandbox 配置全解 v4 / 需求澄清流程控制实验)
0. TL;DR
自研 SDD 被默认 openspec-* 盖掉,根因不是权限不够,是 LLM API 协议层的字段归属之争。要稳定不被盖掉,得把自研流程的优先级声明放到 API 顶层 system 字段里,再加上多层兜底。
最小可行方案,按优先级从上到下:
| 优先级 | 改动 | 协议层位置 | 投入 | 跨项目生效 | 抗 sandbox 重建 |
|---|---|---|---|---|---|
| P0 | 编辑 ~/.config/opencode/AGENTS.md 顶部加「流程优先级声明」 |
system 顶层字段 |
5 分钟 | ✅ | ❌(需 P3 兜底) |
| P1 | 自研 SDD 所有子 skill 的 description 加防干扰锚 |
tools 顶层字段 |
30 分钟 | ✅ | ❌(需 P3 兜底) |
| P2 | 注册 /自研 SDD 流程-* slash command |
tools + 项目 command 目录 |
30 分钟 | ✅ | ❌(需 P3 兜底) |
| P3 | 通过 sandbox dotfiles / init script 持久化 P0-P2 | 同上 + 启动脚本 | 需协调 sandbox 团队 | ✅ | ✅ |
为什么必须动全局 AGENTS.md 而不是项目级:全局 AGENTS.md 进到 API 顶层 system 字段,永远在请求最前面,不会被对话推走;项目级 AGENTS.md 是 opencode 在你 read 项目目录时塞到 tool_result 里的一段文本,模型把它当成 user 说的话看,位置会随后续 turn 沉到中间。
1. 系统原理:从 Transformer 到 OpenCode 的四层归因
要解释清楚"为什么自研流程会被盖掉、为什么必须改全局 AGENTS.md",得先把"对话结构"拆到协议层看清楚。
1.1 四层模型
1 | |
层 1 的模型本身完全不知道对话结构。层 2 通过训练让模型学会了「看到 system 字段要重视」。层 3 把 role 区分提升到接口级别,system 和 tools 是顶层独立字段,不在 messages 数组里,根本不是 turn。层 4 的 <system-reminder> 标签其实不是协议特殊概念,只是 opencode 在 tool_result 字符串里加的普通文本,模型靠训练数据里出现过类似模式才学会重视它。
1.2 一次真实 API 请求长什么样
1 | |
1.3 顶层 tools 字段 vs messages 里的 tool_result:到底是什么关系?
这两个字段名都带「tool」,但完全不是一回事。一个是工具说明书,一个是工具执行结果。
| 维度 | 顶层 tools 字段 |
messages[].content 里的 tool_result block |
|---|---|---|
| 在 API 协议中的位置 | 顶层独立字段,和 system / messages 平级 |
messages 数组里某个 user message 的 content block |
| 内容是什么 | 工具的「说明书」:name、description、input_schema | 工具实际执行后的「输出」:read 读到的文件内容、bash 跑完的 stdout |
| 谁写的 | agent 框架(opencode)拼装好后传给模型 | agent 框架执行完工具,把返回结果包成 user role 的 message 发回去 |
| 在请求中出现几次 | 每次请求都完整发,永远在请求最前 | 每次工具调用后产生一条,跟着 turn 累积 |
| 模型怎么用 | 「这次对话能用哪些工具、参数怎么传」 | 「上一步 tool_use 的实际输出是什么,下一步该干什么」 |
| 注意力位置 | 顶部 ★★★★★,永不被推走 | 跟随 turn 沉到中间塌陷区 |
| 例子 | {"name": "read", "input_schema": {...}} |
{"type": "tool_result", "content": "<文件内容>..."} |
举一次完整循环看就清楚了:
1 | |
注意 opencode 干了一件协议没规定的事:在 tool_result 字符串末尾追加了 <system-reminder> 加 AGENTS.md 全文。这段东西在协议层就是普通字符串,模型把它读成「user 说的话」。
回到本方案的核心矛盾:
- 默认 openspec-* 流程的描述放在 顶层
tools字段的 description 里(永不衰减) - 项目级 AGENTS.md 的「禁用 openspec」声明放在
messages里某个 tool_result block 里(沉降)
字段不同,胜负就定了。
1.4 关键事实清单
| 事实 | 协议层证据 |
|---|---|
system 是顶层独立字段 |
不在 messages 数组里,根本不是 turn |
messages 只有 user / assistant 两种 role |
Anthropic 协议规定,没有 role: system |
tool_result 是 user role 的 content block |
协议规定 |
<system-reminder> 在协议层不是特殊概念 |
只是 tool_result 字符串里的普通文本 |
tools schema 在哪 |
顶层独立 tools 字段,不在 messages 里 |
2. Context Window 的物理布局与注意力分布
2.1 物理布局图
1 | |
2.2 U 型注意力曲线
LLM 对开头(Primacy)和结尾(Recency)记忆最强,中间塌陷,详见 Lost in the Middle (Liu et al. 2023)。
1 | |
2.3 两类 system-reminder:触发式 vs 事件式,到底差在哪
这是本文档最容易混淆的两个概念。它们都叫 system-reminder,注入位置都在某个 turn 的末尾,但触发机制和频率完全不同。
核心区别一句话:触发式由「agent 干了某个动作」激活,事件式由「环境状态满足某个条件」激活。
| 维度 | 触发式注入 | 事件式注入 |
|---|---|---|
| 触发条件 | agent 调用了某个会触及目录文件的工具(read / grep / bash 等) | hook 监控的某个状态满足阈值(token 数、todo 状态、调用频率等) |
| 谁判断要不要注入 | opencode 框架内置规则:「检测到访问 X 目录就附加 X 目录的 AGENTS.md」 | 注册的 hook 函数:「if condition then inject」 |
| 频率 | 同一目录只在「首次访问时」注入一次,之后不再注入 | 每次条件满足都会注入(同一 session 可能注入很多次) |
| 注入位置 | 附加在那次 tool_result 的末尾 | 附加在当前 turn 的某个末尾位置 |
| 跟着谁沉降 | 跟着首次访问那个 turn 沉到对话中部 | 永远在最新位置(每次新触发都是新 turn 的末尾) |
| 典型例子 | 项目级 AGENTS.md(只在第一次 read 示例项目/* 时注入) | [CONTEXT WINDOW MONITOR](token 用量超过 80% 触发)[TODO CONTINUATION](todo 列表非空时触发)[Category+Skill Reminder](task 调用次数到阈值触发) |
| 在请求里能看到几条 | 一份目录的 AGENTS.md 全 session 只有 1 条 | 同一类 reminder 可能有 N 条,每次条件满足就多一条 |
举例对照:
1 | |
为什么这个区别对本方案重要:
- 触发式注入 = 项目级 AGENTS.md 的命运。它只有一次进入注意力顶峰的机会(首次访问那个 turn 的末尾),之后随对话沉降。这就是为什么靠项目级 AGENTS.md 想覆盖全局 SDD 流程会越来越无力。
- 事件式注入 = 系统给 agent 的「实时硬指令」通道。它持续在最新位置,注意力始终是 ★★★★★。本方案不直接利用事件式(因为是 opencode 框架内置的,用户写不了),但理解它有助于知道:项目级 AGENTS.md 拿不到这种持续 recency。
2.4 项目级 AGENTS.md 的真实命运
把上面两个概念套在项目级 AGENTS.md 身上:
1 | |
2.5 为什么「中文 + 某企业级 Agent 框架」覆盖能成功,「禁用 openspec-*」会失败?
不是因为项目级 AGENTS.md 一直靠后,而是综合四个机制:
| 机制 | 「中文/身份」覆盖 | 「禁用 openspec-*」覆盖 |
|---|---|---|
| 首次注入位置最强 | ✅ | ✅ |
| 简短条款高凸显度 | ✅ 10 行中文+3 行身份 | ✅ |
| 全局对应条款的共振强度 | ★★ 隐含约束,弱 | ★★★★★ 30KB + 9 个 openspec-* skill name |
| agent 自我强化回声 | ✅ 每次回答都用中文,约定被反复强化 | ❌ SDD 偶发触发,没有持续回声 |
回声效应决定了什么样的项目级条款能稳定盖过全局:每 turn 都触发的简短约定(语言、身份、风格)项目级能盖;偶发的复杂流程(SDD、部署、测试模式)项目级盖不了,得改全局。
3. 失败案例与对照实验:信号竞争的实证
为了验证「协议层位置劣势」不是空想,复盘一组真实 A/B 实验。
3.1 失败侧(被覆盖)
用户首条消息:
1 | |
用户在 question 工具回答里说:「我不按 agents.md ,就走普通的 自研 SDD 流程 流程」
Sisyphus 实际执行:
1 | |
LLM 在歧义消解上走偏了:
1 | |
3.2 成功侧(未被覆盖)
用户首条消息:
1 | |
Sisyphus 实际执行:
1 | |
3.3 对照分析
| 因子 | 失败侧 | 成功侧 |
|---|---|---|
| 用户提及 自研 SDD 流程 的位置 | question 回答里(第 2 轮才出现) | user message #1 首句首词 |
| 用户措辞 | 「走普通的 自研 SDD 流程 流程」 | 「基于 自研 SDD 流程-start」 |
| 关键词形态 | 自研 SDD 流程(被「普通的」修饰) |
自研 SDD 流程-start(带连字符 = skill 名级精确) |
| 与 skill 列表的字面匹配度 | 弱(要语义跳跃) | 强(直接命中 自研 SDD 流程-start) |
| AGENTS.md 信号 | 同样的 ~250 行 system-reminder | 完全相同 |
| system prompt | 同样 | 完全相同 |
相同的容器、相同的 AGENTS.md anchor、相同的 system prompt,仅用户关键词形态与位置变了,routing 就从偏移变成了忠实。这反过来说明当前的对齐完全依赖用户每次精准措辞,是脆弱的。要让自研 SDD 不被覆盖能稳定复现,不能把宝压在用户层。
4. 三层防御方案:与 某企业级 Agent 框架 存量设计和谐共处
要稳定不被盖掉,必须同时在三层建防御,单点都不足以扛住。这套方案的核心特征是不删除任何 某企业级 Agent 框架 存量约束、只增量插入自研流程的优先级声明,可回滚。
4.1 防御机制全景
1 | |
三条原则:宝不能压在用户层,用户措辞会变;宝不能压在个人层,agent 自我约束会被强 anchor 压过;必须在系统层下重手,让自研流程的优先级声明出现在协议顶层字段。
4.2 P0:在全局 AGENTS.md 顶部加「流程让位声明」
改动位置:~/.config/opencode/AGENTS.md
改动内容(在文件最顶部、所有现有内容之前插入):
1 | |
为什么必须改全局而不是项目级:全局 AGENTS.md 进协议层 system 顶层字段,永不被推走 ★★★★★;项目 AGENTS.md 进协议层 messages[N].tool_result,跟着 turn 沉降 ★★。
和谐共处的关键:不删除任何现有条款;不修改「两阶段流程」等核心约束;只在顶部加一段 if-else 让位声明;用户不提自研关键词时,某企业级 Agent 框架 完全按原流程跑。
4.3 P1:自研 SDD 所有子 skill description 加防干扰锚
改动位置:每一个 自研 SDD 流程-* 子 skill 的 SKILL.md frontmatter
改动内容(在 description 末尾追加):
1 | |
为什么每个子 skill 都要加,不能只加总路由:skill description 进协议顶层 tools 字段,永不被推走;但只有总路由被激活时总路由内容才进 context,靠「先调总路由再传递规则」会有鸡生蛋问题——用户消息直接命中子 skill 时总路由没机会发声。每个子 skill 都带这条规则,任何入口被命中都能立即生效。
4.4 P2:注册 /自研 SDD 流程-* slash command
改动位置:~/.config/opencode/command/
改动内容:创建以下文件
1 | |
每个文件内容示例(自研 SDD 流程-start.md):
1 | |
slash command 是最可靠的激活方式:执行路径绕过 LLM 的歧义消解;用户打 /自研 SDD 流程-start 是硬开关,不存在被解读为 openspec 的可能;即使 P0、P1 失效,slash command 仍能强制走自研流程。
4.5 P3:通过 sandbox dotfiles / init script 持久化
问题:~/.config/opencode/ 在 sandbox 重建时被还原为镜像默认值,P0、P1、P2 的改动会丢。
三种持久化路径,按可行性排序:
| 方案 | 做法 | 前提 |
|---|---|---|
| 3a. dotfiles 仓库 | 把 AGENTS.md + command/自研 SDD 流程-*.md + skills/自研 SDD 流程-*/ 放到一个 git 仓库,sandbox 启动脚本 git clone + cp -r 到 ~/.config/opencode/ |
某 sandbox 平台 支持启动脚本注入 |
| 3b. 环境变量驱动 init | sandbox 接受环境变量指定 dotfiles 仓库 URL,启动时自动应用 | sandbox 平台提供该机制 |
| 3c. 自定义镜像层 | 基于 某 sandbox 镜像 出一个 baked-in 自定义配置的镜像 |
有镜像构建权限 |
短期先做 P0+P1+P2 立即生效;中期用 3a 落地,团队可共用;长期推动 某企业级 Agent 框架 团队官方支持「用户自定义 SDD 流程」的扩展点。
4.6 个人层(agent 行为硬规则)
把以下规则写进自研 SDD 的总路由 skill 内部(作为 agent 自检清单):
| Rule | 说明 |
|---|---|
| 关键词触发即加载总路由 | 用户消息出现自研框架关键词时,第一动作必须是 skill("自研 SDD 流程"),禁止跳过总路由直接调子 skill |
| 当前 turn 用户明示 > 项目静态规则 | AGENTS.md 是静态规则,user 当前 turn 是动态最高优先级。冲突时显式向用户复述:「你刚才说 X,与 AGENTS.md 的 Y 冲突,按 X 执行」 |
| 歧义消解必须列举 | 遇到「普通的 X 流程」这类有多解的措辞,列出所有可能解析让用户拍板,不要让 attention anchor 替我做选择 |
| 禁止字面相近的语义跳跃 | 自研 SDD 流程 与 openspec 仅相似不等价,必须当作两个独立框架处理 |
4.7 用户层(措辞习惯建议)
虽然不能依赖用户每次精准,但好习惯能显著降低风险:
| 不推荐 | 推荐 |
|---|---|
| 「走普通的 自研 SDD 流程 流程」 | 「基于 自研 SDD 流程-start 开始」 |
| 「用 自研 SDD 流程」 | 「用 自研 SDD 流程 这个 skill 家族」 |
| 在 question 回答里第二轮才提框架名 | 在 user message #1 首句首位就锚定框架 |
| 自然语言描述 | 直接打 /自研 SDD 流程-start slash command |
5. 协议层胜负盘点
把方案叠到 §1 的协议结构图上,可视化「武器在哪个字段」:
1 | |
改造前后对比:
| 维度 | 改造前 | 改造后 |
|---|---|---|
| 自研 SDD 在协议顶层的存在感 | 仅 skill description 里平级一行 | system 让位声明 + 每个子 skill description 强调 + slash command |
| 用户措辞精准度依赖 | 高(必须用 自研 SDD 流程-start) |
低(说 自研 SDD 流程 / 自研 SDD 流程 都能触发) |
| 与 AGENTS.md 强 anchor 的对抗 | 项目级单点对抗,胜算 30% | 系统级让位 + 多 skill 共振,胜算 95% |
| sandbox 重建后状态 | — | P0-P2 丢,P3 持久 |
6. 实操清单
6.1 立即可做(30 分钟)
1 | |
6.2 中期持久化(1-2 周)
- [ ] 询问某 sandbox 平台 团队:是否支持 dotfiles / 启动脚本注入
- [ ] 把 P0-P2 的所有改动放到一个独立 git 仓库(如
opencode-自研 SDD 流程-dotfiles) - [ ] 配置 sandbox 在启动时拉取并 sync 到
~/.config/opencode/
6.3 长期规范化
- [ ] 团队内传播「用
自研 SDD 流程-start这种带连字符的精确名」措辞约定 - [ ] 推动 某企业级 Agent 框架 官方提供「自定义 SDD 流程」的扩展点(让 P0 不需要直接编辑 AGENTS.md)
7. 风险与回滚
| 风险 | 缓解措施 | 回滚 |
|---|---|---|
| P0 改动影响其他人 / 其他流程 | 改动只新增「if 自研关键词命中」的 if 分支,不删除任何现有条款 | 删除新增段落即可 |
| sandbox 重建丢失 | P3 dotfiles 持久化 | 重新跑 dotfiles sync 脚本 |
| 自研 SDD 内部 bug 影响业务 | P0 让位声明里写明「仅当用户显式提及关键词时生效」 | 改动仅影响显式触发场景 |
| 与未来 某企业级 Agent 框架 升级冲突 | 用 dotfiles 仓库管理改动,每次 sandbox 重建后比对 | git diff 后选择性合并 |
附录 A:本方案与 3 份前置文档的映射
| 前置文档 | 贡献的核心结论 | 在本方案中的位置 |
|---|---|---|
OpenCode 配置体系探索之旅.md |
协议层四层模型 + system/tools/messages 字段归属 | §1 系统原理 |
OpenCode Sandbox 配置体系全解 v4.md |
context window 物理布局 + U 型注意力 + 触发式 vs 事件式注入 + 自我强化回声 | §2 context window |
需求澄清的流程控制.md |
信号竞争失败/成功 A/B 实验 + 三层防御机制 | §3 失败案例 + §4 三层方案 |
附录 B:关键术语速查
| 术语 | 含义 |
|---|---|
| Primacy | 注意力曲线开头的高权重区——system prompt / 全局 AGENTS.md |
| Recency | 注意力曲线末尾的高权重区——当前 user message / 动态 reminder |
| Lost in the Middle | 中间塌陷区——历史消息累积后的弱注意力区 |
| 触发式注入 | agent 干了某个动作(如首次 read 一个目录)才被注入;同一目录全 session 只注入一次 |
| 事件式注入 | hook 监控的状态满足条件就注入;同一类条件可能反复触发 |
顶层 tools 字段 |
API 请求里和 system / messages 平级的独立字段,装的是工具说明书(name/description/schema),永不被推走 |
tool_result block |
messages 数组里某个 user message 的 content block,装的是工具实际执行后的输出,会随对话沉降 |
| 自我强化回声 | agent 输出本身在历史中持续重复约定,相当于约定被反复「再注入」 |
| 协议顶层字段 | Anthropic API 的 system / tools 字段,独立于 messages 数组,永不被推走 |





