opencode 读哪些规则文件:终端编码 Agent 的规则加载顺序

2026-08-04

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

opencode 挑选规则文件的方式是「按类别取第一个命中的文件名」,不是「把能找到的规则全部叠起来按优先级打分」。这意味着规则写在错位置时,它不会被降权,而是整份被跳过——你读不到任何提示,模型也不会告诉你。

这个判断能直接从代码里核对。规则文件的发现、去重、拼装全部集中在 packages/opencode/src/session/instruction.ts 这一个文件里,它对外只暴露五个方法:clearsystemPathssystemfindresolve。下面按「这块解决什么问题 → 它怎么做的 → 对你意味着什么」的顺序拆开讲。

站内已有三篇相邻话题:CLAUDE.md 怎么写 讲的是规则文件的内容该写什么,Cursor 的 .mdc 规则写法 讲的是另一套工具的规则格式,Agent 上下文预算 讲的是通用的额度分配方法;本篇只管 opencode 这个终端 Agent 的加载机制——哪些路径会被扫、谁覆盖谁、什么时候注入,是可以在仓库里逐行验证的那部分。

一、它到底去哪些位置找规则

systemPaths() 里有两组写死的候选清单,一组是全局的,一组是项目内的:

const globalFiles = [
  path.join(global.config, "AGENTS.md"),
  ...(!flags.disableClaudeCodePrompt ? [path.join(global.home, ".claude", "CLAUDE.md")] : []),
]
const instructionFiles = [
  "AGENTS.md",
  ...(!flags.disableClaudeCodePrompt ? ["CLAUDE.md"] : []),
  "CONTEXT.md", // deprecated
]

全局那组按顺序探测,命中一个就 break。项目那组不是简单探测,而是对每个文件名调 findUp,从当前工作目录一路向上找到 worktree 边界,把这一路上所有同名文件都收集起来;某个文件名只要有命中,就把它的全部结果加进集合并 break,后面的文件名不再看。

除此之外还有配置项 instructions。它是一个字符串数组,写在 opencode.json 或全局配置里,元素可以是相对路径、~/ 开头的路径、绝对路径、glob,也可以是 http:// / https:// 开头的远程地址。文档 packages/web/src/content/docs/rules.mdx 给的例子是这样的:

{
  "$schema": "https://opencode.ai/config.json",
  "instructions": ["CONTRIBUTING.md", "docs/guidelines.md", ".cursor/rules/*.md"]
}

相对路径和 glob 走的是向上遍历匹配(同样受 worktree 边界约束),绝对路径则退化成对该目录做一次文件级 glob。远程地址不进路径集合,留给 system() 单独抓,超时按代码里的 5000 毫秒算,抓失败返回空串然后被丢掉——不报错,不重试到底,你不会看到任何提示。

对你意味着什么:规则文件的位置是有限枚举的,不是模糊搜索。放在 docs/agent-rules.md 里的东西,除非你在 instructions 里显式声明,否则 opencode 一个字都不会读。

二、多份规则叠加时谁盖谁

「谁盖谁」这件事要分两层看,很多人踩坑是因为把两层混成一层。

类别内是先到先得,后面的直接出局。 全局那组里,配置目录下的 AGENTS.md 存在,~/.claude/CLAUDE.md 就完全不读;项目那组里,只要向上遍历路径上有任何一个 AGENTS.md,同一路径上的 CLAUDE.md 和已标记废弃的 CONTEXT.md 就一并出局。注意这里的粒度是文件名而不是目录——不是「离得近的赢」,而是「排在清单前面的名字赢」。

类别之间是全都要,而且顺序固定。 全局、项目、instructions 三部分的结果依次塞进同一个 Setsystem() 再按集合顺序读取内容,每份包装成 Instructions from: <绝对路径> 加正文,最后把远程抓来的内容追加在最尾部。仓库自带的测试 packages/opencode/test/session/instruction.test.ts 里有一条直接断言了这个顺序:同时存在全局和项目 AGENTS.md 时,返回数组长度为 2,第 0 项是全局的,第 1 项是项目的。

再往上一层,这些字符串在 packages/opencode/src/session/prompt.ts 里被拼进发给模型的 system 数组,和环境信息、MCP 说明、技能说明排在一起。也就是说——规则之间没有权重、没有冲突裁决、没有「后者覆盖前者」的语义。两份规则说了相反的话,最终是模型自己在一堆并列文本里选一个听。所谓”覆盖”完全靠你写的措辞,而不是引擎保证。

多份 opencode.json 的合并规则又是另一回事:packages/opencode/src/config/config.ts 里对 instructions 做的是数组求并集去重,不是整体替换。全局配了三份、项目又配了两份,最终生效的是五份。

三、子目录规则:读到哪儿才生效

大仓库里常见的做法是每个子包放一份自己的 AGENTS.md。opencode 对这类文件的处理和根目录那份完全不同,走的是 resolve() 这条路:它不进常驻的 system 提示词,而是在 read 工具真正读到那个子目录下的文件时,临时挂在工具返回结果后面。

packages/opencode/src/tool/read.ts 里能看到这段拼接:

if (loaded.length > 0) {
  output += `\n\n<system-reminder>\n${loaded.map((item) => item.content).join("\n\n")}\n</system-reminder>`
}

触发条件比想象中严格。resolve() 从被读文件所在目录逐级向上走,走到会话的工作目录就停——注意代码里这个停止点取的是实例上下文中的 directory(你启动会话的那个目录),而不是 worktree(git 工作树根),两者在仓库子目录里启动时并不相等;工作目录那一层的规则本来就已经进了 system,不需要再挂一次。沿途任何一份规则文件,只要满足下列任一条就跳过:它本身就是你正在读的那个文件、它已经在 systemPaths() 的集合里、历史消息里已经有 read 工具报告过加载它、或者当前这条 assistant 消息的 claims 集合里已经记过它。

claims 是一个以消息 ID 为键的 Map,配套的 clear 方法按消息 ID 清除。历史消息那一路的判断来自 read 工具写回的 loaded 元数据,并且跳过了被标记为已压缩的工具结果——上下文被压缩掉之后,同一份子目录规则允许再挂一次。

对你意味着什么:子目录规则是按需、一次性的。你以为它一直在上下文里管着,实际上它只在某次读文件时闪现了一次;后续几十轮对话里模型早把它挤出注意力范围了。真正必须全程遵守的约束,写子目录 AGENTS.md 是不牢靠的,得写进根目录那份。

四、各部分职责一览

组成部分它负责什么对应仓库位置你什么时候会碰到它
规则发现与拼装枚举候选路径、去重、读内容、抓远程packages/opencode/src/session/instruction.ts每次发起对话
系统提示词组装把规则和环境、MCP、技能说明排进 system 数组packages/opencode/src/session/prompt.ts每一轮请求
就近规则注入把子目录规则挂进 read 工具的返回结果packages/opencode/src/tool/read.ts读子包里的文件时
配置项合并多份配置里的 instructions 求并集去重packages/opencode/src/config/config.ts全局与项目都配了规则时
向上遍历与 globfindUp / globUp 的边界与返回集合packages/core/src/fs-util.ts规则放在祖先目录时
另一条环境上下文通路core/instructions 为键注册的上下文来源packages/core/src/instruction-context.ts走 V2 会话链路时
生成规范/initAGENTS.md 时的取舍标准packages/core/src/plugin/command/initialize.txt让它自动生成规则时
用户文档位置、优先级、外部文件引用的官方说法packages/web/src/content/docs/rules.mdx对不上代码时回查
行为测试顺序、去重、禁用开关的断言packages/opencode/test/session/instruction.test.ts想确认某个行为时

顺带说一句这个仓库的体量:packages/ 下有 32 个包,英文文档 36 份 mdx,会话提示词 14 份,工具目录 25 个 .ts 配 15 个 .txt 说明,根目录还挂着 21 份 README 翻译,全仓受版本控制的文件 6358 个。规则加载这件事只占其中很小一块,但它决定了另外那些东西以什么姿势被使用,所以值得单独读一遍。

五、写太长为什么反而没效果

规则文件的内容会原样、全量、每一轮进入 system 提示词。这里没有摘要、没有相关性筛选、没有分段命中——system() 只做了一件事:读文件、拼字符串、返回。

由此得到三个直接后果。

第一,长度直接换成钱和延迟。规则文本每轮重发一次,多轮长会话里这是一笔持续支出,各家服务商的计费与缓存规则不同且会调整,以官方最新说明为准,但”每轮重发”这个事实是引擎侧确定的。

第二,长度会稀释指令强度。三千字的规则里塞进二十条”必须”,模型能稳定遵守的通常只有其中最扎眼的几条;把真正的红线和”代码风格建议”平铺在一起,等于主动把红线降权。仓库自己的 AGENTS.md 是个可参照的样本:它通篇是命令、边界、约定,例如”Tests cannot run from repo root”、“Always run bun typecheck from package directories”、“Avoid try/catch where possible”,几乎每条都是”不写就会做错”的信息,没有一句在解释 TypeScript 是什么。

第三,/init 的取舍标准写得比大多数团队的内部规范更狠。initialize.txt 里给的判据是每一行都要回答「Would an agent likely miss this without help?」,不满足就删掉;明确列进排除清单的有:通用软件工程建议、长篇教程与完整目录树、语言本身的显然约定、无法验证的推测性说法,以及”更适合放在别的文件里再用 instructions 引用的内容”。最后一句是”When in doubt, omit”。

所以「写太长没效果」不是玄学,而是三层机制叠加的结果:全量注入 + 无优先级 + 无相关性筛选。想省额度的通用方法可以看 Agent 上下文预算,这里只强调一点——在 opencode 里,删掉一句废话的收益是乘以对话轮数的。

六、边界与代价:这套设计明确不管什么

它简单,代价也清清楚楚。

不解析文件引用。 文档里写得很直白:opencode 不会自动解析 AGENTS.md 里的文件引用。想拆分规则只有两条路——用 instructions 显式列出(支持 glob),或者在规则正文里用自然语言教模型”看到这种引用就用 read 工具去读”。后者能不能生效取决于模型听不听话,不是引擎保证。

不做冲突裁决。 前面说过,所有规则平铺进 system 数组。全局规则和项目规则打架时,没有任何一方在机制上更强。

不做内容校验。 规则里写错的命令,Agent 会照着跑。这类工具本来就在你机器上执行 shell 命令、直接改你的代码文件,一句”测试失败就删掉重写”写进 AGENTS.md,它真会那么干。

不越过 worktree 与工作目录边界。 两处向上遍历都有明确的 stop 点,而且这两个 stop 点还不是同一个:system 那条路以 worktree 为终点,子目录就近注入那条路以工作目录为终点。把团队共用规则放在 git 工作树之外的上级目录里,两条路都扫不到。

远程规则是尽力而为。 抓取超时或失败就静默丢弃,那一轮对话相当于没有这份规则。把强约束放在远程 URL 上,等于给自己留了一个不确定开关。

两条链路的行为并不完全一致。 packages/core/src/instruction-context.ts 是另一条通路,以 core/instructions 为键注册成一个上下文来源,它只认 AGENTS.md(没有 CLAUDE.md 回退),并且在内容变化时会显式发出替换语义的说明文本,规则被移除时也会发出对应说明。走哪条链路取决于会话实现,排查行为差异时别拿一条链路的结论去套另一条。

最需要当回事的是外泄面。 规则文件的全文每轮都发给模型服务商;子目录规则还会随着 read 工具的返回一起发出去,也就是”你读到哪儿,哪儿的规则就外发”。密钥、内网地址、客户名称、未公开的架构细节写进 AGENTS.md,就等于主动交出去了。这块的通用讨论见 AI 编程的数据安全风险

七、上手与避坑清单

  • 规则没生效,先确认文件名而不是内容。 会踩是因为项目里同时存在 AGENTS.mdCLAUDE.md,你改的是后者。怎么避:类别内先到先得,AGENTS.md 一旦存在,CLAUDE.md 整份出局;要么统一到 AGENTS.md,要么把内容合并过去。
  • 从 Claude Code 迁过来,别指望两套文件同时生效。 会踩是因为兼容性是”回退”而不是”合并”。怎么避:把 CLAUDE.md 当成没有 AGENTS.md 时的备胎;不想要这层兼容就用文档里那几个 OPENCODE_DISABLE_CLAUDE_CODE 系列环境变量关掉,关掉之后连全局的 ~/.claude/CLAUDE.md 也不再读。
  • 别把硬约束写进子目录 AGENTS.md。 会踩是因为它只在 read 工具读到该目录下文件时挂一次,之后不再常驻。怎么避:红线写根目录那份,子目录那份只放”改这个包时才需要知道”的局部信息。
  • 在子目录里启动会话,扫描起点会跟着变。 会踩是因为向上遍历以当前目录为起点、worktree 为终点。怎么避:验证规则是否加载时,在你平时真正启动的那个目录下验,别在仓库根验一次就当过了。
  • 多份配置里的 instructions 是求并集不是替换。 会踩是因为你以为项目配置会盖掉全局配置,结果全局那几份还在悄悄注入。怎么避:先确认最终生效的清单,再决定加不加。
  • 远程规则别放强约束。 会踩是因为抓取失败静默丢弃,你无法从对话里看出来。怎么避:远程 URL 只放变动频繁的参考资料,强约束落到本地文件。
  • 规则里的每一条”必须”都要能通过 /init 那把尺子。 会踩是因为顺手把团队 wiki 整段贴进去。怎么避:逐行问”不写这句,Agent 会不会做错”,答案是否就删;把细节挪到独立文件,用 instructions 按需引用。
  • 规则文件按机密文件对待。 会踩是因为它看起来像文档,实际上是每轮外发的提示词。怎么避:提交前当成公开内容审一遍,密钥和敏感信息一律不进。
  • 给规则加”自动执行”类措辞前想清楚后果。 会踩是因为规则会放大工具本来就有的破坏力——它能跑命令、能改文件。怎么避:涉及删除、重置、推送、部署的动作,规则里写成需要确认,而不是写成默认放行。

收尾:三步自检

改完规则文件,按这个顺序核一遍就够了:一,AGENTS.md 是不是在你真正启动会话的那个目录向上能找到的第一份?二,里面每一行是不是”不写就会做错”的信息?三,里面有没有任何你不愿意发给模型服务商的东西?

想继续往下挖,按依赖顺序读三个文件:先 packages/web/src/content/docs/rules.mdx 拿官方说法,再 packages/opencode/src/session/instruction.ts 核实际行为,最后 packages/opencode/test/session/instruction.test.ts 看边界断言——测试文件里那几条关于顺序和去重的断言,比任何文档都直接。选型层面的横向比较,可以参考 开源终端 Agent 怎么选

本篇属于一个把开源AI 编程 Agent 项目 opencode逐层拆开讲的系列,整体地图见 opencode 是什么:终端里的开源 AI 编程 Agent 全景地图;沿着这条线往下,还可以看 终端 AI 编程 Agent opencode 的权限闸门怎么设给 opencode 写自定义命令:把重复指令固化成一条斜杠命令

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