本文是「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 |
命名的外部书架 | 大文档、另一个仓库、共享库 |
八、设计一套可维护的记忆层级
理解了机制,接下来是取舍。可以用四个问题决定一条内容放哪:
- 这条规则每次请求都必须看到吗?
- 它只适用于某个目录吗?
- 它是规则,还是资料?
- 它能不能用命令、路径或验证步骤表达?
对应的策略:
- 每次都必须看到 → 仓库根
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. 最小落地步骤
- 在仓库根创建
AGENTS.md,先只写构建、测试、架构与验证四类内容。 - 把模块专属规则下沉到对应子目录,例如
packages/web/AGENTS.md。 - 全局个人偏好放
~/.config/opencode/AGENTS.md,不要在$HOME到仓库之间放其他AGENTS.md。 - 大段资料用
instructions或references挂载,不塞进根文件。 - 新开一个会话,读一个子目录文件,确认出现
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 造成的。
十、最后五条原则
- 全局短,仓库稳,子目录细。 越靠近根的文件,越应该只放每次都必须遵守的内容。
- 只放必须每次都看到的内容。 大段资料用
instructions或references,不要塞进记忆。 - 不制造冲突。 OpenCode 不会替你裁决两条矛盾的指令。
- 子目录规则按需加载。 不要假设全仓库的
AGENTS.md都在启动时进入了上下文。 - 改子目录文件,新开会话。 根文件可以热改,子目录文件不会自动感知。
AGENTS.md 的价值不在于“写得多”,而在于“放得对”。把它当成一套分层、就近、按需加载的记忆系统,而不是一份越来越长的项目说明,很多看似诡异的行为都会变得可解释。
如果只记一件事:workspace 在 home 内,冒到 home;在 home 外,冒到项目根。
一句话记忆:启动看边界,执行看 read,冲突看层级,更新看来源。