你让 Codex 加一个 feature 模块。PR 把它放到了 /src/features/billing/,但你的 repo 用的是 /app/(dashboard)/billing/(Next.js App Router)。Diff 看着合理——只是和你的代码库完全不在一个频道。或者:pnpm workspace 项目里,Codex 把 vitest 依赖加到了根 package.json,而每个包本来都有自己的 package.json。再或者:repo 全用 vitest,它写出来一份 jest 风格的测试。
Codex 不是故意瞎猜。是当你的 repo 惯例在它构建的对话里不可见时,它就退回到通用模式。真正长效的修法:在 repo 根放一份 AGENTS.md,里面给目录地图和指向 canonical example 的指针,再在每个 task 里显式指名目标路径。截至 2026 年 6 月,ChatGPT 登录态下的 Codex CLI 默认跑 OpenAI 的 gpt-5.5,并且经过专门训练会认真读、认真遵守 AGENTS.md(OpenAI Codex 文档)。
一句话总结
- 为什么会发生:没有
AGENTS.md,Codex 就从最先读到的几个文件里泛化出结构。它落地的第一个目录,就成了它心里的”项目布局”。 - 能修 80% 的那一招:在 git 根放一份
AGENTS.md,写上目录地图、一节指向真实目录的「Canonical examples」、再加一个「禁止」清单。Codex 每个 task 都会自动加载它。 - 剩下 20%:在 prompt 里写死目标路径(
apps/web/src/features/billing/),指一个真实样本让它照抄,monorepo 里钉死 working package。结构错了立刻拒收重 prompt,别手动改。 - 规模化的杠杆:每个包加一份
AGENTS.md。Codex 会把从 git 根一路到当前目录的所有文件合并,冲突时离得最近的那份生效。
Codex 到底怎么加载指令(2026 年 6 月)
下面的修法都建立在这套机制上,所以值得说清楚。Codex 会按以下顺序拼出一条指令链(OpenAI 文档):
| 步骤 | 来源 | 说明 |
|---|---|---|
| 1 | ~/.codex/AGENTS.override.md,然后 ~/.codex/AGENTS.md | 你的全局、跨项目规则;这一级 Codex 只取第一个非空文件 |
| 2 | 从 git 根 → 当前目录的每一级 | 每级先查 AGENTS.override.md,再 AGENTS.md,再 project_doc_fallback_filenames 里的名;每个目录最多取一份 |
| 合并 | 最近的那份生效 | Codex 从根往下拼接、各文件之间空一行;离 working directory 越近的文件排得越后,所以会覆盖前面的指引 |
| 上限 | 合并后 32 KiB | project_doc_max_bytes(默认 32 KiB,在 ~/.codex/config.toml 里设);Codex 跳过空文件,一到上限就停止追加 |
AGENTS.md 就是纯 Markdown——没有必填字段,没有强制 schema。它是一个开放标准(agents.md),Codex 和大多数别的编码 agent 都会解析。OpenAI 自己的 Codex 仓库就带了几十份嵌套的 AGENTS.md,一个包一份——你依赖的正是这套模型行为。
读它的是哪个模型,影响其实没你想的大:gpt-5.5 是 ChatGPT 登录态 Codex(CLI、app、IDE 扩展)的默认模型,gpt-5.4 作为 fallback,gpt-5.2-codex 走 API key 工作流(Codex 模型页)。它们都遵守 AGENTS.md;但它们都没法遵守你从没写下来的惯例。
常见原因
按命中率从高到低:
1. 没 AGENTS.md(或者过期了)
repo 根没 AGENTS.md,Codex 只能抽几个文件读、从中泛化——但几个文件读不出结构惯例。它落地的第一个目录就成了”项目布局”。这是命中率最高的一条。
如何判断:在 repo 根跑 ls AGENTS.md——没有就是 agent 在从文件样本里猜。有的话跑 git log -1 --format=%cr AGENTS.md 看上次更新——超过 6 个月、期间又上了很多 feature,通常就是目录地图已经和现实对不上了,而 Codex 会照着过期的地图走。
2. Task 没指定目标路径
「加一个 billing feature」由 Codex 自己挑。「在 apps/web/src/features/ 下加 billing feature,参照 apps/web/src/features/auth/ 的布局」就确定了。第一种是抛硬币,第二种是确定的。
如何判断:回看 prompt——没指明目录就是 Codex 自己选的。
3. Codex 把错的样本当 canonical
它先读到 legacy/v1/UserService.ts(按字母序遍历更靠前),判定”项目风格就是这样”,忽略了你现代的 src/services/UserService.ts。两个都存在,Codex 挑了错的。
如何判断:新代码的 style / import / 结构匹配某个 deprecated 区——Codex 读的是错的子树。
4. Monorepo 被当成单包
Codex 没意识到这是 pnpm/yarn workspace。依赖加到根 package.json、跨包 import 没用 workspace 别名、pnpm install 在错的层级跑。
如何判断:新 import 直接跨包边界(相对路径或顶级包名)。看 pnpm-workspace.yaml / lerna.json / nx.json——任何一个存在,Codex 都要尊重 workspace。
5. 测试框架 / lint 工具不匹配
Repo 用 vitest,Codex 写出 jest 风格——因为 jest 在它训练数据里更常见。测试看着对,但跑不起来,因为框架根本没装。
如何判断:新测试 import 一个 package.json 里没有的框架。跑 pnpm test path/to/new-file.test.ts——报 Cannot find module 'jest'(反过来用错时则是 vi is not defined)就是。
6. 同名不同大小写的文件夹
你的 repo 有 src/Components/(大写 C),Codex 建了 src/components/(小写)。case-insensitive 文件系统(macOS 默认)上看着重复;case-sensitive(Linux CI)上 import 莫名其妙就断了。
如何判断:两个只差大小写的目录。ls -la src/ | sort 一目了然。
最短修复路径
按收益从高到低。实战里光 Step 1 一步就能去掉绝大多数结构不匹配,因为它彻底消掉了”猜”这件事。
Step 1:写 AGENTS.md,里面带目录地图
repo 根加:
# AGENTS.md
## 目录地图
- `apps/web/` — Next.js App Router(**不是** pages router)
- `apps/web/src/features/` — feature 模块,一个 feature 一个目录
- `apps/web/src/components/` — 共享 UI 原语(小写!)
- `apps/web/app/` — 路由,分组目录用 `(name)` 语法
- `packages/ui/` — 共享组件库,tsup build
- `packages/db/` — Prisma client + migration
- `scripts/` — Node CLI 工具
## Canonical examples
- Feature 模块布局:见 `apps/web/src/features/auth/`
- 共享组件:见 `packages/ui/src/button/`
- API route 约定:见 `apps/web/app/api/users/route.ts`
## 约定
- 包管理器:**pnpm** workspace。不要用 npm 或 yarn。
- 测试框架:**vitest** + `@testing-library/react`。不要 jest。
- Lint:ESLint config 在 `packages/eslint-config-acme/`。
- Import:用 workspace 别名(`@acme/ui`),不要相对路径 `../../`。
## 禁止
- 在 `src/` 下直接建文件夹——feature 进 `apps/web/src/features/`。
- 依赖加到根 `package.json`——加到用它的那个包里。
- 同名不同大小写文件夹(`Components/` vs `components/`)——统一小写。
Codex 每个 task 都自动加载这个——不用加任何 flag。注意把整条指令链控制在 32 KiB 上限(project_doc_max_bytes)之内;要是根文件涨过这个数,就把规则拆进每个包各自的文件里(见 Step 6),别堆成一份巨大的文档,否则 Codex 一到上限就会悄悄不再往下读。
Step 2:在 task 里指一个 canonical example
新 feature:
加 billing feature。结构照 `apps/web/src/features/auth/`:
- index.ts (公开 API)
- types.ts
- components/
- hooks/
- billing.test.tsx (vitest,不是 jest)
新文件放 `apps/web/src/features/billing/`。
指一个真实样本比任何文字描述都管用。
Step 3:monorepo 里指定 working package
Working directory: apps/web
- 本次 task 所有路径都相对 apps/web。
- 加依赖用 `pnpm --filter=@acme/web add <pkg>`。
- import 共享组件用 workspace 别名 `@acme/ui`。
- 不要动根 `package.json` 或其他 package。
Step 4:结构错了立刻拒收 + 重 prompt
Codex 把目录建错了,不要手动改——删错的,重 prompt:
你上一个 patch 把文件建在 `src/billing/`,错了——见 AGENTS.md。
正确路径:`apps/web/src/features/billing/`。
删 `src/billing/`。在正确路径下重写,照 `apps/web/src/features/auth/` 的样子。
这是给本次 session 训练,不只是修一次错。
Step 5:大 repo 里钉住 Codex 的”第一视野”
顶层目录多的 repo,Codex 最先读到的几个文件决定了它的心智模型。提前钉死:
写任何代码前先读这三个文件:
1. apps/web/src/features/auth/index.ts
2. apps/web/src/features/auth/billing.test.tsx
3. apps/web/src/features/auth/components/LoginForm.tsx
这些是本代码库的 canonical pattern。新 feature 照这个来。
Step 6:大 monorepo 每个包一份 AGENTS.md(含 override)
30 个包的 repo,一份根 AGENTS.md 既装不下所有人的约定,又容易撞上 32 KiB 上限。加 apps/web/AGENTS.md、packages/ui/AGENTS.md,每份写本包专属规则。Codex 会从 git 根一路走到正在编辑的文件,合并沿途每一份 AGENTS.md;冲突时离得最近的那份生效,因为它在 prompt 里排最后。
apps/web/AGENTS.md
└── "本包用 App Router,默认 Server Component。"
packages/ui/AGENTS.md
└── "所有组件都是 client component,文件首行写 'use client'。"
当某个包是要覆盖继承来的规则、而不是补充时,就在该目录放一份 AGENTS.override.md。在某一级,只要 AGENTS.override.md 存在,Codex 就加载它、忽略同一级的普通 AGENTS.md——但它仍会合并上层父目录的文件,所以 override 换掉的是这一级的文件,而不是整条链(OpenAI 文档)。同一套查找逻辑也适用于你的全局 ~/.codex/AGENTS.md,那里适合放个人的、跨项目的偏好,而不是某个 repo 的约定。
如何确认修好了
加完 AGENTS.md 后,跑一个真实 task,信任本次 session 之前先核三件事:
- Codex 确实加载了这份文件。 在 Codex CLI 里跑
/status(或/context),它拉进来的指令文件会列在那里。要是根目录AGENTS.md没出现,说明你不在 git 根、文件是空的,或者超了 32 KiB 上限。用git rev-parse --show-toplevel确认根路径,并保证AGENTS.md就在那一级。 - 下一个 patch 落在正确位置。 让它加一个小模块,应用前先看 diff——
git diff --stat应该显示文件落在你地图声明的路径下(apps/web/src/features/<name>/),而不是新建一个src/文件夹。 - 新测试跑得起来。 如果 task 加了测试,跑一下(
pnpm test path/to/new-file.test.ts)。干净通过说明 Codex 用了你装好的框架;报Cannot find module 'jest'就说明它没理会约定,得把「约定」/「禁止」两节写得更死。
一个随手丢掉的 task 上这三条都过,就说明长效修复到位了,可以不用再手动改路径。
预防建议
- 把
AGENTS.md当活文档维护——架构变了立刻更新,别只在项目初期写一次。 - 永远带「Canonical examples」一节,指向 Codex 能读的真实目录。
- Task prompt 里显式指名目标路径;“放对位置”从来不管用。
- Monorepo 每个 prompt 都指定 working directory 或
--filter。 - 大 repo 每个 package 一份
AGENTS.md;只有当子目录必须和根目录唱反调时才动用AGENTS.override.md。 - 把合并后的指令链控制在 32 KiB 以内,免得有内容被悄悄丢掉。
- 季度审一次同名不同大小写的文件夹(
Components/vscomponents/),统一掉。
常见问题
我得手动把 AGENTS.md 喂给 Codex 吗,还是它自己会找?
它自己会找。Codex 会自动枚举 ~/.codex 加上从 git 根一路到 working directory 每一级目录里的 AGENTS.md,注入到对话里——不用加 flag,也不用 @ 提及。你只在想在 task 里指一个 canonical example 时才需要点名文件。
Codex 去哪些地方找?两份冲突时哪份生效?
查找顺序是 ~/.codex/AGENTS.override.md → ~/.codex/AGENTS.md(全局这一级只取第一个非空文件),然后是从 git 根到当前目录的每一级(先 AGENTS.override.md,再 AGENTS.md,再 project_doc_fallback_filenames 里的名),每个目录取一份。Codex 从根开始往下拼接,所以离 working directory 越近的文件排得越后、冲突时它赢。AGENTS.override.md 只顶掉它自己那一级的普通 AGENTS.md,不会丢掉上层父目录贡献的内容。
AGENTS.md 有大小限制吗?
有。合并后的体积一到 project_doc_max_bytes(默认 32 KiB),Codex 就停止追加文件,空文件会被跳过。超过这个上限后,多出来的 AGENTS.md 会被悄悄忽略——这也是为什么要把规则拆进每个包,而不是堆成一份巨大的根文档。
是哪个模型在读 AGENTS.md?版本不同会有影响吗?
截至 2026 年 6 月,ChatGPT 登录态的 Codex(CLI、app、IDE 扩展)默认用 gpt-5.5,gpt-5.4 作 fallback,gpt-5.2-codex 走 API key 工作流。它们都被训练成会遵守 AGENTS.md。模型版本影响的是代码质量,而不是你的惯例会不会被照做——后者完全取决于你有没有写下来。
Codex 还是把文件放错文件夹了,我直接手动挪走行吗? 不行。删掉放错的位置,带上正确路径和一个 canonical example 重 prompt。手动挪只修好这一个 PR,session 的心智模型还是错的,下一个 task 又会犯同样的错。
AGENTS.md 在 Codex 之外也能用吗?
能。AGENTS.md 是一个开放标准,大多数编码 agent 都会读它,所以同一份文件一般也能指导别的工具。本文说的查找顺序和大小上限是 Codex 专属的,但目录地图加 canonical example 这套做法是通用的。
相关阅读
标签: #Codex #Coding Agent #排查 #排查 #项目结构