本文是「AI 效能」系列的第六篇。前五篇讨论自动开发的边界、流程编排、初始化加速、工作区设计与 Loop 架构;这一篇把视角收回到 AI 编程代理本身:它如何从文件系统里加载项目记忆,为什么同一条规则有时生效、有时不生效。

版本说明:本文以 OpenCode V2 为准,关键边界在本机 v2.2.6 上核对。V1 的加载规则不同,迁移时不要直接套用。

很多人第一次配置 AGENTS.md,会把它当成一份“项目说明”。写完却发现:

  • 子目录里的规则有时生效,有时像没写;
  • 改完文件,当前会话没有任何反应;
  • 在 home 某个目录放了一份 AGENTS.md,结果所有 home 内项目都被它影响;
  • 一次普通的 read 之后,上下文突然多出几百行。

这篇文章解决的是:规则写了却不生效,或者不知道什么时候生效。它面向已经把 OpenCode 用作日常编码代理的开发者,也面向需要维护团队项目规则的人;不适合还没用过 OpenCode 的完全新手,也不打算给出一套跨工具的通用记忆方案。

这些都不是偶发问题,而是同一套机制的不同侧面:OpenCode 读取 AGENTS.md 的方式,不是“打开一个文件”,而是“按路径分层加载”。

本文从零开始,把这套机制拆成四件事:启动时加载什么、执行中补什么、什么时候更新、以及怎么设计一套不会互相打架的层级。

先说结论

问题 结论
启动时读哪些 全局文件 + 从 workspace 向上冒泡
冒泡到哪停 workspace 在 home 内 → 到 home;在 home 外 → 到项目根
workspace 不在项目内 只加载全局文件
子目录规则何时生效 用 read 读到该区域时按需加载
多个文件冲突怎么办 不解决冲突,只按顺序拼接
改了文件何时生效 启动时发现的文件热更新;子目录文件要新会话

先统一几个概念

词 含义
会话(session) 一次对话及其上下文
workspace 当前会话的工作目录,也是向上冒泡的起点
项目根(project root) 最近的 Git 或 Mercurial 仓库根;没有仓库时回退为启动目录
默认全局文件 ~/.config/opencode/AGENTS.md,所有项目共享

“项目根”不是 .git 目录本身,而是仓库的工作区根:

  • 普通 Git 仓库:.git 所在目录;
  • linked worktree:当前 worktree 自己的根;
  • 子模块:子模块自己的根;
  • Mercurial 仓库:包含 .hg 的仓库根目录;
  • 没有检测到仓库:项目根等于启动目录。

Git 下可以理解为 git rev-parse --show-toplevel 的输出。这个定义很重要,因为“冒泡到项目根”只在 workspace 位于 home 之外时才会用到。

一、心智模型:它是分层记忆,不是一份项目说明

AGENTS.md 是文件式的持久指令:可读、可审查、可提交到 Git、可回滚。它不是模型自动积累的记忆,也不是 RAG 检索;它是一套确定性的文件注入规则。

可以把它分成五层:

层级 典型位置 何时进入上下文 适合放什么
全局 ~/.config/opencode/AGENTS.md 每次会话启动 跨项目的个人偏好
仓库 <repo>/AGENTS.md 会话启动 团队共享的通用规则
workspace 下方的子目录 <repo>/packages/web/AGENTS.md read 到该区域 局部约定、模块架构
外挂指令 instructions 配置 会话启动 复用已有文档、远端规范
外部资料 references 配置 按需读取 大文档、另一个仓库

这张表里最关键的区分是:前三个是“记忆”,第四个是“把别的文件接进记忆”,第五个是“给模型一个可以查阅的书架”。

子目录文件如果正好是 workspace 或其祖先,会在启动时加载;只有 workspace 下方的文件才按需加载。

记忆不是越多越好。进入上下文的每一行,都会在后续请求里重复付费。所以真正要设计的不是“写多少”,而是“放在哪一层、什么时候加载”。

如果你还没有根文件,可以在会话里运行 /init,它会扫描仓库并生成或改进 AGENTS.md;生成结果仍要人工审阅。项目级文件应该提交到 Git,团队共享;全局文件属于个人配置,不提交到项目仓库。

二、启动时:全局文件加向上冒泡

一次会话启动时,OpenCode 按固定顺序收集指令。

1. 全局文件

默认读取(不存在则跳过):

~/.config/opencode/AGENTS.md

这个文件对所有项目生效,适合放个人习惯:回答语言、代码风格偏好、常用命令、禁止事项。

V2 只自动识别 AGENTS.md 这个文件名;其他规则文件不会被自动加载,需要时通过 instructions 显式挂载。

2. 从 workspace 向上冒泡

然后从当前 workspace 目录开始,逐级向上,收集每一层已存在的 AGENTS.md:

start = workspace
stop  = workspace 在 $HOME 内 ? $HOME : 项目根
for dir from workspace up to stop:
    if dir/AGENTS.md exists:
        load

两个细节:

  • 只收集已存在的文件,不存在的层级直接跳过;
  • 冒泡包含 stop 目录本身。

3. 停止边界

这是最容易记错的地方:

workspace 位置 向上收集到
在 $HOME 内 $HOME,包含 ~/AGENTS.md
在 $HOME 外 项目根,包含项目根的 AGENTS.md
不属于当前项目 项目侧文件不加载,只剩全局文件

也就是说,home 内和 home 外的策略确实不一样。home 内不会停在项目根,而是一路冒到 home;home 外才停在项目根。

4. 加载顺序

顺序不是从根到叶,而是:

1. 全局文件
2. workspace 的 AGENTS.md
3. 上一级的 AGENTS.md
4. ……
5. 项目根或 home 的 AGENTS.md

全局文件排在最前,然后由近到远。

5. 一个 home 内的例子

假设 workspace 是 ~/code/my-project/packages/web,项目根是 ~/code/my-project:

~/
└── code/
    └── my-project/            ← 项目根
        ├── AGENTS.md
        └── packages/
            ├── AGENTS.md
            └── web/           ← workspace
                └── AGENTS.md

实际加载顺序:

1. ~/.config/opencode/AGENTS.md
2. ~/code/my-project/packages/web/AGENTS.md
3. ~/code/my-project/packages/AGENTS.md
4. ~/code/my-project/AGENTS.md
5. ~/code/AGENTS.md        (存在才加载)
6. ~/AGENTS.md             (存在才加载)

第 5、6 项就是“home 污染”的来源:~/code/AGENTS.md 会影响 ~/code 下的所有项目,~/AGENTS.md 会影响整个 home。它们都在最后加载。

注意 ~/AGENTS.md 不是全局文件:默认全局文件是 ~/.config/opencode/AGENTS.md;~/AGENTS.md 只是冒泡链的最后一层,只在 workspace 位于 home 内时才可能被读到。

如果不想让某个仓库受 home 层文件影响,最直接的办法是不要在 $HOME 到该仓库之间放 AGENTS.md。全局规则放 ~/.config/opencode/AGENTS.md,它不参与这条冒泡链。

6. 一个 home 外的例子

假设 workspace 是 /work/my-project/packages/web:

1. ~/.config/opencode/AGENTS.md
2. /work/my-project/packages/web/AGENTS.md
3. /work/my-project/packages/AGENTS.md
4. /work/my-project/AGENTS.md

到项目根为止,不会再往 /work 或 / 冒。

7. 关闭项目侧发现

设置环境变量:

export OPENCODE_DISABLE_PROJECT_CONFIG=1

效果是跳过项目侧配置和 AGENTS.md 发现,但全局文件仍然加载。它适合临时判断“问题是不是来自项目规则”,不是日常细粒度控制开关。

三、执行中:读到哪,补到哪

启动时只加载 workspace 及其上方。workspace 下方的子目录规则,是执行过程中按需加载的。

1. 触发条件

触发者是 read 工具:

  • 读文件会触发;
  • 列目录也会触发;
  • glob、grep、shell ls 不会触发。

这是一个很实际的差异:用 grep 扫过某个目录,不等于该目录的 AGENTS.md 已经进入上下文。

2. 收集范围

起点取决于读的是什么:

  • 读目录:从目录本身开始;
  • 读文件:从文件所在目录开始。

然后逐级向上,收集到 workspace 为止,但不包含 workspace 自己,因为它在启动时已经加载过。

start = 读目录 ? 目标目录 : 目标文件的父目录
stop  = workspace
for dir from start up to workspace(不含 workspace):
    if dir/AGENTS.md exists:
        load

3. 一个例子

读 packages/web/src/app.ts:

my-project/
├── AGENTS.md                 ← 启动时已加载,不重复
└── packages/
    ├── AGENTS.md             ← 加载
    └── web/
        ├── AGENTS.md         ← 加载
        └── src/
            ├── AGENTS.md     ← 加载
            └── app.ts        ← 读取目标

加载顺序是最近优先:src → web → packages。

4. 只向上,不向下

读 packages/ 不会预加载 packages/web/AGENTS.md;它只会收集从该目录向上到 workspace 之间的文件。

所以第一次读某个深层文件时,可能一次注入好几层 AGENTS.md。这不是 bug,而是用上下文换精度:不相关区域的规则不会提前占位置。

5. 去重与生效时机

  • 加载后以 Instructions from: <path> 的独立条目加入会话;
  • 在下一次模型请求前生效;
  • 同一会话内去重,已经进入可见历史的文件不会重复注入;
  • 只有压缩或回退把那条记录移除后,后续读取才可能重新加载。

6. 工作区外

文档只明确保证 workspace 下方的行为。读工作区外的目录时,实现上也会收集那个外部目录链上的 AGENTS.md,但边界没有文档承诺,不建议依赖。

四、更新:哪些会变,哪些不会

AGENTS.md 不是只在启动时读一次。OpenCode 会监听启动时发现的文件,但子目录文件是另一套逻辑。

事件 行为
修改全局或向上发现的 AGENTS.md 下一次模型请求前作为指令更新送达
删除全部启动时发现的 AGENTS.md 告知会话旧的环境指令不再适用
读取暂时失败 保留上一次已知内容,不当作删除
移动会话 保留指令状态,目标位置的指令作为更新引入
回退会话 清空指令状态,下一次请求前重新加载
修改已加载的子目录文件 不自动检测,需要新会话
压缩或回退移除指令条目 后续 read 可能重新加载该文件

还有一个隐私细节:客户端可以看到“哪些来源变了”,但看不到指令内容。指令值本身是受保护的。

实践上记住一条就够了:根文件和全局文件可以热改;子目录文件改完,新开会话最稳。

五、顺序与冲突:它只拼接,不裁决

完整顺序可以理解为:

1. Agent 或 Provider 的系统提示
2. Code Mode 工具说明(启用时)
3. MCP、references、skills 的说明
4. 全局和项目 AGENTS.md,然后是 instructions 配置
5. 内置日期和环境信息
6. 会话级指令条目

AGENTS.md 位于第 4 层:在基础能力和工具说明之后,在环境信息之前。它不能覆盖前面的系统提示和工具说明。

多个 AGENTS.md 之间是拼接关系,不是覆盖关系。文档明确说明:OpenCode 会合并这些文件,不解决它们之间的冲突。instructions 也一样:它排在 AGENTS.md 之后,但仍然是拼接,不是覆盖。

这意味着:

  • 不要指望后面的文件覆盖前面的文件;
  • 不要在全局写死一条规则,又在仓库里写相反的规则;
  • 出现冲突时,模型看到的是两条矛盾指令,行为不确定。

正确做法是分层表达,而不是互相覆盖。例如全局写“默认使用 pnpm,仓库可以声明自己的包管理器”,仓库再写“本仓库使用 npm”。

六、instructions:显式挂载更多指令

AGENTS.md 是自动发现,instructions 是手动挂载。它是 opencode.json(c) 里的一个数组:

{
  "$schema": "https://opencode.ai/config.json",
  "instructions": [
    "CONTRIBUTING.md",
    "docs/standards/*.md",
    "packages/*/AGENTS.md",
    "~/shared/team-rules.md",
    "https://raw.githubusercontent.com/acme/rules/main/opencode.md",
  ],
}

1. 能写什么

写法 解析方式
CONTRIBUTING.md、docs/*.md 从 workspace 逐级向上到项目根,每一层都扫描
~/rules/opencode.md 相对 home 解析
/abs/path/rules.md 绝对路径,文件名可带 glob
https://.../rules.md 配置加载时拉取,5 秒超时

2. 加载规则

  • 排在所有 AGENTS.md 之后,按数组顺序拼接;
  • 全局配置和项目配置里的 instructions 会合并,不是二选一;
  • 本地文件会热更新;glob 新增的文件要等下一次配置变更才进来;
  • 文件不存在直接跳过;
  • URL 拉取失败时跳过,或保留上一次成功的内容。

3. 一个容易踩的路径问题

instructions 里的相对路径不是相对配置文件,而是从 workspace 到项目根逐层匹配。

即使写在全局配置里,默认也是按项目树解析;只有设置 OPENCODE_DISABLE_PROJECT_CONFIG=1,或 workspace 不在项目内时,才改为相对全局配置目录解析。

4. 适合用它做什么

  • 复用仓库里已有的 CONTRIBUTING.md、.cursor/rules/*.mdc;
  • 从远端拉团队统一规范;
  • monorepo 里按包挂载 packages/*/AGENTS.md;
  • 把“规则”和“资料”分开:规则进 instructions,大块资料进 references。

不要做的一件事:把 AGENTS.md 的内容再复制一份到 instructions。那只会让同一段话进入上下文两次。

七、references:按需查阅的外部书架

references 解决的是另一类问题:内容不在项目内,或者体积太大,不适合每次请求都注入。

它把项目外的目录或 Git 仓库注册成一个别名:

{
  "$schema": "https://opencode.ai/config.json",
  "references": {
    "docs": {
      "path": "../product-docs",
      "description": "产品行为、术语和接口文档",
    },
    "sdk": {
      "repository": "acme/sdk",
      "branch": "main",
      "description": "内部 SDK 源码,排查调用链时使用",
    },
  },
}

几个要点:

  • 本地路径支持相对、绝对和 ~/;相对路径以配置文件所在目录为基准,这点和 instructions 不同;
  • 字符串简写只有以 .、/、~ 开头才当作本地路径,裸 docs 会被当成 Git 仓库;
  • Git 仓库支持可选的 branch,不写则跟随默认分支;通常会缓存到 ~/.local/share/opencode/repos/<host>/<repo>;
  • Git 仓库在后台刷新,距离上次尝试超过 24 小时才会再次检查,失败不影响其他 reference;
  • 带 description 的 reference 会出现在 agent 指令里,告诉模型什么时候该用;
  • 客户端 attach 别名后,会给出一层根目录清单,不会递归展开;
  • hidden: true 只影响交互选择器,不影响 API 和 agent;
  • references 不授予额外权限,读项目外路径仍受 external_directory 和普通 read 规则约束。

三者的分工可以总结为:

机制 本质 适合
AGENTS.md 按路径分层的规则 每次都必须看到的约定
instructions 显式挂载的文件或 URL 复用已有文档、远端规范
references 命名的外部书架 大文档、另一个仓库、共享库

八、设计一套可维护的记忆层级

理解了机制,接下来是取舍。可以用四个问题决定一条内容放哪:

  1. 这条规则每次请求都必须看到吗?
  2. 它只适用于某个目录吗?
  3. 它是规则,还是资料?
  4. 它能不能用命令、路径或验证步骤表达?

对应的策略:

  • 每次都必须看到 → 仓库根 AGENTS.md;
  • 只适用于某个模块 → 子目录 AGENTS.md;
  • 只是资料 → instructions 或 references;
  • 能写成命令 → 直接写命令,不要写“注意代码质量”这种无法执行的话。

1. 推荐的目录布局

~/.config/opencode/AGENTS.md          # 个人跨项目规则,保持短
repo/
├── AGENTS.md                         # 团队通用规则
├── packages/
│   ├── AGENTS.md                     # 包级通用约定
│   ├── web/
│   │   └── AGENTS.md                 # 前端局部约定
│   └── api/
│       └── AGENTS.md                 # 后端局部约定
└── docs/
    └── architecture.md               # 大块资料,放 instructions 或 references

原则:

  • 全局文件只放跨项目个人偏好,不写具体仓库的命令;
  • 仓库根放构建、测试、架构、验证步骤;
  • 子目录放该模块的局部约定,让它按需加载;
  • 大块文档不要塞进 AGENTS.md,用 instructions 或 references;
  • 不要在多个层级重复同一条规则。

2. 根文件模板

# 项目约定

## 命令

- 安装:`pnpm install`
- 开发:`pnpm dev`
- 测试:`pnpm test`
- 类型检查:`pnpm typecheck`

## 架构

- `apps/web`:前端应用
- `packages/core`:共享领域逻辑
- `packages/api`:HTTP 接口

## 规范

- 共享代码放 `packages/core`,通过包名导入
- 数据库查询集中在 `src/database`
- 不要直接修改生成文件

## 验证

- 改完 TypeScript 必须跑 `pnpm typecheck`
- 提交前跑 `pnpm test`

这个模板的共同点是:每条都能执行、能定位、能验证。它没有复述 README,也没有写“请写出高质量代码”。

3. 好规则与坏规则

好 坏
pnpm typecheck 必须在提交前通过 注意代码质量
数据库查询集中在 src/database 合理分层
不要编辑 dist/ 和生成文件 保持仓库整洁
packages/web 的组件用函数式写法 前端要写得现代一点

坏规则的共同问题是:模型无法判断自己有没有做到。

4. 一个完整的加载例子

假设项目位于 ~/code/my-project,目录是:

my-project/
├── AGENTS.md
├── packages/
│   ├── AGENTS.md
│   ├── web/
│   │   ├── AGENTS.md
│   │   └── src/app.ts
│   └── api/
│       ├── AGENTS.md
│       └── src/server.ts

会话从 packages/web 启动:

1. ~/.config/opencode/AGENTS.md
2. packages/web/AGENTS.md
3. packages/AGENTS.md
4. my-project/AGENTS.md
5. ~/code/AGENTS.md        (存在才加载)
6. ~/AGENTS.md             (存在才加载)

然后读 packages/api/src/server.ts:

加载 packages/api/AGENTS.md
不会加载 packages/web/AGENTS.md

这就是“按区域加载”的完整效果:启动时只加载当前区域和它的祖先,其他区域等真正访问时再补。

5. 最小落地步骤

  1. 在仓库根创建 AGENTS.md,先只写构建、测试、架构与验证四类内容。
  2. 把模块专属规则下沉到对应子目录,例如 packages/web/AGENTS.md。
  3. 全局个人偏好放 ~/.config/opencode/AGENTS.md,不要在 $HOME 到仓库之间放其他 AGENTS.md。
  4. 大段资料用 instructions 或 references 挂载,不塞进根文件。
  5. 新开一个会话,读一个子目录文件,确认出现 Instructions from: ...;再用日志核对加载路径。

6. 验收标准

  • 新会话能复述根文件里的关键约束;
  • 读子目录文件后,下一次请求能看到对应的 Instructions from: 条目;
  • 修改根文件后,下一次请求生效;
  • 修改子目录文件后,新会话生效;
  • 日志中没有意外的 home 层文件。

7. 落地检查清单

  • 根文件只放每次请求都必须看到的内容;
  • 模块专属规则放在对应子目录,而不是全部堆到根文件;
  • $HOME 到仓库之间没有意外的 AGENTS.md;
  • 全局、仓库、子目录之间没有互相矛盾的指令;
  • 大段资料已经移到 instructions 或 references;
  • 子目录文件修改后,用新会话验证。

九、常见误区与排查

现象 原因 处理
子目录 AGENTS.md 没生效 还没 read 到该区域,或只用 grep 扫过 读一次该目录或其中的文件
改了子目录文件,当前会话没反应 子目录文件加载后不监听修改 新开会话
改了根文件,当前会话生效 启动时发现的文件会热更新 正常现象
~/AGENTS.md 影响了所有 home 内项目 home 内冒泡到 home 删除或移走该文件
以为后加载会覆盖先加载 只拼接,不解决冲突 消除矛盾,分层表达
opencode.jsonc 一路找到文件系统根 配置发现是另一套规则 不要和 AGENTS.md 边界混淆
读一个文件后上下文暴涨 一次加载了多层 AGENTS.md 检查路径上是否有过多规则文件
压缩后子目录规则又出现 指令条目被移除后可重新加载 正常现象

其中 opencode.jsonc 的发现范围是从当前目录到文件系统根,和 AGENTS.md 的 home/项目根边界不同;不要把两者的冒泡规则混为一谈。

排查时先看日志:

grep "instructions rescanned" ~/.local/share/opencode/log/opencode.log

日志会列出本次解析到的文件路径。要判断问题是否来自项目侧,可以临时运行:

OPENCODE_DISABLE_PROJECT_CONFIG=1 opencode

此时项目侧 AGENTS.md 不再加载,只剩全局文件;项目配置也会被关闭,适合临时排查。如果问题消失,说明是某个项目侧 AGENTS.md 造成的。

十、最后五条原则

  1. 全局短,仓库稳,子目录细。 越靠近根的文件,越应该只放每次都必须遵守的内容。
  2. 只放必须每次都看到的内容。 大段资料用 instructions 或 references,不要塞进记忆。
  3. 不制造冲突。 OpenCode 不会替你裁决两条矛盾的指令。
  4. 子目录规则按需加载。 不要假设全仓库的 AGENTS.md 都在启动时进入了上下文。
  5. 改子目录文件,新开会话。 根文件可以热改,子目录文件不会自动感知。

AGENTS.md 的价值不在于“写得多”,而在于“放得对”。把它当成一套分层、就近、按需加载的记忆系统,而不是一份越来越长的项目说明,很多看似诡异的行为都会变得可解释。

如果只记一件事:workspace 在 home 内,冒到 home;在 home 外,冒到项目根。

一句话记忆:启动看边界,执行看 read,冲突看层级,更新看来源。

相关阅读

参考资料