opencode 为什么备了 14 份系统提示词:开源终端编码 Agent 的提示词分家现实

2026-08-04

本文基于 opencode 仓库 commit 7fe9938(2026-08-04)梳理,该项目仍在高频迭代,具体行为以仓库 https://github.com/anomalyco/opencode 最新代码与文档为准。

同一个 Agent 备 14 份系统提示词,说明的不是”提示词写得不够好”,而是提示词根本不是一份可以收敛的文档——它是一份要按运行环境分发的配置。 opencode 这个跑在终端里的开源编码 Agent,把这件事直接做成了代码:packages/opencode/src/session/prompt/ 下躺着 14 个 .txt,具体用哪一份,由 packages/opencode/src/session/system.ts 里一个几行长的函数当场判定。你换个模型,Agent 的”人格”就换了一套,代码一行没动。

这篇只讲分发这件事:它按什么维度分、每一份差在哪、你在什么时候会被这套机制咬到。站内另外三篇讲的是别的层面——pi 的系统提示词怎么组织 讲的是单一提示词内部的分层结构,browser-use 的系统提示词 讲的是浏览器动作型 Agent 怎么描述可用动作,AI 编程提示词怎么写 讲的是你自己给工具写指令时的写法;本篇不碰”怎么写好一份”,只碰”一份为什么要拆成十几份”。

一、14 份文件先分成三堆

先把目录数清楚,不然后面的讨论会飘。packages/opencode/src/session/prompt/ 下 14 个 .txt,按谁引用它们分成三堆:

第一堆是模型家族提示词,9 份,全部由 system.ts 顶部 import 进来:default.txtanthropic.txtbeast.txtgemini.txtgpt.txtkimi.txtmeta.txtcodex.txttrinity.txt。这堆是完整的系统提示词,一份能顶整个 Agent 的开场白。

第二堆是运行模式提示词,3 份,由 packages/opencode/src/session/reminders.ts import:plan.txtplan-mode.txtbuild-switch.txt。这三份都不长,build-switch.txt 只有五行,而且它们不是系统提示词——后面会讲它们走的是完全不同的注入通道。

第三堆是 copilot-gpt-5.txtplan-reminder-anthropic.txt。在这个 commit 上,我在整个仓库(排除 node_modules 与 .git)里搜不到任何代码引用它们。文件在,接线不在。这本身就是这类设计的一个副产品:提示词一旦变成”可以随手多加一份”的资源,就会出现只加文件没接线、或者接线撤了文件还留着的状态。你 fork 下来改的时候,别看见文件名像是生效的就当它生效。

二、选哪一份,是一条顺序敏感的 if 链

system.ts 里的 provider 函数就是全部的分发逻辑,它拿到一个 Provider.Model,返回一个只含一份提示词的数组:

export function provider(model: Provider.Model) {
  if (model.api.id.includes("muse-spark")) return [PROMPT_META]
  if (model.api.id.includes("gpt-4") || model.api.id.includes("o1") || model.api.id.includes("o3"))
    return [PROMPT_BEAST]
  if (model.api.id.includes("gpt")) {
    if (model.api.id.includes("codex")) {
      return [PROMPT_CODEX]
    }
    return [PROMPT_GPT]
  }
  if (model.api.id.includes("gemini-")) return [PROMPT_GEMINI]
  if (model.api.id.includes("claude")) return [PROMPT_ANTHROPIC]
  if (model.api.id.toLowerCase().includes("trinity")) return [PROMPT_TRINITY]
  if (model.api.id.toLowerCase().includes("kimi")) return [PROMPT_KIMI]
  return [PROMPT_DEFAULT]
}

有三个点值得你盯一眼。

一是它匹配的是 model.api.id 的子串,不是枚举,不是精确相等。这意味着模型 ID 的命名方式直接决定了你落到哪一份提示词上。前两条 gpt 相关的判断有先后关系:含 gpt-4/o1/o3 的先被 beast.txt 截走,剩下含 gpt 的再看是不是 codex。顺序换一下,行为就变了。

二是大小写处理不一致。前面几条直接 includestrinitykimi 两条先 toLowerCase()。也就是说 ID 里大写的 Claude 不会命中 claude 那条,会一路掉到兜底的 default.txt。这不是 bug,是子串匹配这种做法天然带的脆弱性——它换来的是加一个新家族只要加一行。

三是没有任何配置项参与这条链。你不能在配置文件里指定”这个模型用那份提示词”,路由是硬编码的。要换只能改代码,或者走下一节说的 agent 自定义提示词那条路。

三、家族之间到底差多远

如果 9 份文件只是措辞不同,这套分发就没必要存在。实际差别是行为层面的。

default.txt 是最克制的一份。它反复压缩输出:要求少于 4 行、不要开场白和总结、能一个词回答就一个词,还配了一串 <example> 演示”用户问 2+2,助手答 4”。它明确写了 /help 和把问题报到 issues 的地址。

anthropic.txt 开头就是另一种气质——“You are OpenCode, the best coding agent on the planet.”,反馈路径从 /help 换成了 ctrl+p。更实质的区别是它加了两大块 default.txt 没有的内容:一整节 Professional objectivity(要求优先技术准确而不是迎合用户的判断,必要时直接反驳),以及一整节 Task Management,反复强制使用 TodoWrite 工具,还配了两个长示例演示怎么把”跑构建修类型错误”拆成待办项。它还额外要求探索代码库时优先用 Task 工具而不是直接搜索,理由是省上下文。

beast.txt 是三份里最极端的。它的核心指令是”不解决完不许交回控制权”,反复用大写强调 MUST iterate、NEVER end your turn。它断言”这个问题没有大量互联网检索解决不了”,要求每次用到第三方包都去 webfetch 搜一遍再读页面;它给了一套十步工作流,要求用 markdown 待办清单展示进度;它甚至规定了一个记忆文件路径 .github/instructions/memory.instruction.md 和该文件的 front matter 格式;结尾一句是”你永远不被允许自动 stage 和 commit”。

剩下几份各有各的适配点:gemini.txt 专门写了一条 Path Construction,要求任何文件工具调用前先把相对路径拼成绝对路径;gpt.txt 强调用 multi_tool_use.parallel 并行、并注明 Glob 和 Grep 底层是 rgcodex.txt 要求编辑文件默认 ASCII、单文件改动优先走 apply_patchkimi.txt 反复强调默认动手改而不是描述方案;meta.txt 里直接写明模型身份是 Muse Spark;trinity.txt 的开头段落与 default.txt 几乎一致,是从兜底那份分出来的变体。

把这些差异排在一起看,规律很清楚:分家分的不是文风,是那些模型各自不稳的行为——有的不肯用待办工具,有的不肯坚持到底,有的会用相对路径,有的爱写解释不爱动手。提示词在这里的角色更接近补丁,而不是说明书。这也是多模型混搭在工程上真正麻烦的地方:换模型不只是换一个 API 端点。

四、运行模式那三份走的是另一条路

plan 和 build 的切换,opencode 没有做成”换一份系统提示词”。看 reminders.ts 就明白:它把 plan.txtplan-mode.txtbuild-switch.txt 的内容当成一个 synthetic: true 的文本 part,追加到最后一条用户消息的 parts 里。

内容形态也配套:这三份的正文都用 <system-reminder> 标签包住。比如 build-switch.txt 全文就是:

<system-reminder>
Your operational mode has changed from plan to build.
You are no longer in read-only mode.
You are permitted to make file changes, run shell commands, and utilize your arsenal of tools as needed.
</system-reminder>

为什么不塞进系统提示词?因为模式切换是会话中途发生的事。系统提示词在一次请求里是靠前的固定块,改它等于让前面所有缓存过的前缀失效;而追加到最新一条用户消息,位置靠后、离当前决策近,还不动前面的内容。default.txt 里那句”工具结果和用户消息里可能出现 <system-reminder> 标签,它们不是用户输入的一部分”,就是给模型解释这个通道的。

分发本身还分两条路径,由运行时标志 experimentalPlanMode 控制。关掉时逻辑很朴素:当前 agent 是 plan 就追加 plan.txt,历史里出现过 plan、当前切到 build 就追加 build-switch.txt。开启后走的是 plan-mode.txt,它里面有一个 ${planInfo} 占位符,代码会按计划文件是否已存在替换成”读它并增量编辑”或”用 write 工具在此路径创建”两种不同的话术。

要强调的是,模式的实际约束力不靠提示词。packages/opencode/src/agent/agent.tsplan 这个内置 agent 的 permission 明确写了 edit"*": "deny",只给 .opencode/plans/*.md 之类的路径开口子。提示词负责让模型”知道自己在只读阶段”,权限层负责让它”做不到”。这两层缺一不可——只有提示词没有权限,就是权限摊太大的经典翻车姿势。

五、一次请求里,系统提示词是怎么拼起来的

家族提示词只是第一块。真正发出去的系统消息由两处代码合成。

packages/opencode/src/session/prompt.ts 那边并发拿到四类内容并按固定顺序排好:环境信息、项目指令、MCP server 的说明、技能清单。环境信息由 system.tsenvironment 生成,是一段 <env> 块,里面有工作目录、workspace 根目录、是不是 git 仓库、平台、当天日期,前面还有一句告诉模型自己被哪个模型 ID 驱动。技能清单和 MCP 说明都会先过权限过滤——被禁掉的技能和工具不会出现在提示词里。

packages/opencode/src/session/llm/request.ts 那边做最后拼接。关键是这一行:

...(input.agent.prompt ? [input.agent.prompt] : SystemPrompt.provider(input.model)),

三元表达式,不是拼接。agent 自己带了 prompt 就整份顶掉家族提示词,家族那 9 份一个字都不会出现。内置 agent 里 explorecompactiontitlesummary 都带 prompt 字段,走的是各自的专用提示词;而 buildplan 没带,所以它俩才会吃到家族路由。

组成部分它负责什么仓库位置你什么时候会碰到它
家族路由函数按模型 ID 子串选一份提示词packages/opencode/src/session/system.ts换模型后 Agent 行为整体变样时
9 份家族提示词每个模型家族的行为补丁packages/opencode/src/session/prompt/想改某个模型下的默认风格时
环境块生成注入工作目录、平台、日期等system.tsenvironmentAgent 路径判断出错时
技能与 MCP 说明按权限过滤后注入可用能力system.tsskills / mcp装了 MCP 却发现模型不知道时
模式提醒注入把模式变更挂到最新用户消息packages/opencode/src/session/reminders.tsplan 与 build 来回切时
最终拼接agent 提示词覆盖家族提示词packages/opencode/src/session/llm/request.ts自定义 agent 后行为反而变差时
内置 agent 权限真正拦住写操作的那一层packages/opencode/src/agent/agent.ts排查”它怎么还是改了文件”时

顺带一提,这份提示词还有对外的改写口:拼接后会触发一个插件钩子把 system 数组交出去让插件改。这意味着装第三方插件等于给系统提示词开了写权限,选插件时该有的警惕不能省。

六、边界与代价:这套分法放弃了什么

放弃了单一真相。 9 份家族提示词之间存在成段的重复——比如那节叫 Code References 的规范,default.txttrinity.txtanthropic.txt 三份各带一版,措辞高度接近;trinity.txt 的开头几段更是几乎照抄 default.txt。改一条通用规则,你得挨份改,或者接受它们逐渐漂移。上面提到的那两份没有被引用的文件,就是漂移的第一个可见症状。

放弃了配置驱动。 路由写死在函数里。你想让某个自建端点上的模型用某份提示词,正规路径只有两条:让模型 ID 里带上能命中的子串,或者写一个带 prompt 的自定义 agent 把整份换掉。没有中间档。

放弃了行为的可预期性。 子串匹配意味着模型 ID 一改名,提示词可能就换了一份。同一个底层模型在不同服务商那里 ID 写法不同,落到的提示词也可能不同。你做评测时如果不把”命中了哪份提示词”记进变量,两次结果对不上会很难查。

它明确不管的事。 提示词不是安全边界。beast 那份写着”永远不许自动 commit”,anthropic 那份写着”非必要不创建文件”——这些都是请求,不是拦截。真正拦得住的是权限规则。这类工具会在你机器上跑 shell 命令、直接改你的工作区文件、把读到的代码内容发给模型服务商,这三件事没有一件是靠提示词兜底的。误删误改要靠版本控制和权限规则,密钥外泄要靠不让它读到——内置权限默认里对 *.env 之类是 ask 而不是 allow,这个默认值别随手改成放行。至于哪些内容会离开你的机器、被留存多久,各家服务商规则不同且会调整,以官方最新说明为准。

七、上手与避坑清单

别拿”换个模型试试”当无成本操作。 会踩是因为换模型这件事在界面上只是选一项,感知不到提示词也跟着换了。避法:换模型后先跑一个你熟悉的基准任务,看它是否还主动列待办、是否还坚持跑完测试——这些正是各家族提示词差异最大的地方。行为变了先怀疑路由,别急着怀疑模型能力。

给自定义 agent 写 prompt 前,想清楚你在覆盖什么。 会踩是因为那行是三元表达式,写了就整份顶掉,家族适配全没了。避法:如果你只是想追加几条项目规则,优先用项目指令那条通道(它是独立拼进去的,不会顶掉家族提示词),把 prompt 留给”我确实要一个完全不同人格的子 agent”的场景。

自建端点和中转服务要看模型 ID 长什么样。 会踩是因为路由认的是 ID 子串,而中转服务经常会把 ID 改名或加前缀。避法:先确认你这条链路上 model.api.id 的真实取值,再判断它会命中哪一条 if。掉到兜底的 default.txt 不是坏事,但你得知道自己掉下去了。

plan 模式别只靠提示词理解它的边界。 会踩是因为 plan-mode.txt 里写了一大段”绝对禁止任何修改”,读起来像铁律,实际铁律在 agent 的 permission 配置里。避法:要判断 plan 模式下什么能做什么不能做,去看 agent.ts 里那份权限规则,而不是读提示词。两者若不一致,权限说了算。这套思路和最小权限设计是一回事。

改提示词后别忘了它进的是缓存前缀。 会踩是因为家族提示词位于系统消息最前面,动它等于让整段前缀重算。避法:把频繁变动的内容(比如你自己的临时约束)放到靠后的通道去,别去改前面那份。这也是模式提醒被设计成挂在最新用户消息上、而不是塞进系统提示词的原因。

别把没接线的文件当成生效的配置。 会踩是因为目录里文件名看起来都很正经。避法:改任何一份提示词之前,先在仓库里搜一遍它的文件名,确认真有代码 import 它。

收个尾

这套设计传达的现实是:提示词工程到了产品阶段,重点会从”这句话怎么措辞”转到”这段话该发给谁、什么时候发、发在哪个位置”。opencode 把这三个问题分别落在了三处代码——system.ts 决定发哪份,reminders.ts 决定什么时候补一段,request.ts 决定拼接顺序与谁能覆盖谁。你自己搭 Agent 时,哪怕只用一份提示词,也值得先把这三个问题分开想。

想继续往下读,建议按这个顺序开文件:先 packages/opencode/src/session/system.ts(文件不长,开头那个 provider 函数十几行就能看完路由全貌,后半截是环境块、技能、MCP 三个生成器),再 packages/opencode/src/session/prompt/anthropic.txtdefault.txt 对照着看差异,最后 packages/opencode/src/session/reminders.ts 看模式提醒的注入时机。这三个文件读完,你对这类终端 Agent 的提示词层就有了完整的地图。它是 MIT 许可证的项目(LICENSE,Copyright 2025 opencode),仓库在 https://github.com/anomalyco/opencode ,直接 clone 下来对着读比看任何转述都准。

本篇属于一个把开源AI 编程 Agent 项目 opencode逐层拆开讲的系列,整体地图见 opencode 是什么:终端里的开源 AI 编程 Agent 全景地图;沿着这条线往下,还可以看 opencode 上下文压缩拆解:压缩与溢出是两条线,压完之后你会丢什么opencode 工具层拆解:实现与描述分家,注册表决定模型看见什么

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。