Codex 不照你的项目结构来:AGENTS.md 修复法

新文件落到 /src/ 但你的 repo 用 /app/;monorepo 里依赖被加到根 package.json;测试用了你没装的框架。用根目录 AGENTS.md、嵌套 override、canonical example 指针修好——按 2026 年 6 月的 Codex 实际行为核实。

你让 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.mdOpenAI 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 只取第一个非空文件
2git 根 → 当前目录的每一级每级先查 AGENTS.override.md,再 AGENTS.md,再 project_doc_fallback_filenames 里的名;每个目录最多取一份
合并最近的那份生效Codex 从根往下拼接、各文件之间空一行;离 working directory 越近的文件排得越后,所以会覆盖前面的指引
上限合并后 32 KiBproject_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.mdpackages/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 之前先核三件事:

  1. Codex 确实加载了这份文件。 在 Codex CLI 里跑 /status(或 /context),它拉进来的指令文件会列在那里。要是根目录 AGENTS.md 没出现,说明你不在 git 根、文件是空的,或者超了 32 KiB 上限。用 git rev-parse --show-toplevel 确认根路径,并保证 AGENTS.md 就在那一级。
  2. 下一个 patch 落在正确位置。 让它加一个小模块,应用前先看 diff——git diff --stat 应该显示文件落在你地图声明的路径下(apps/web/src/features/<name>/),而不是新建一个 src/ 文件夹。
  3. 新测试跑得起来。 如果 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/ vs components/),统一掉。

常见问题

我得手动把 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.5gpt-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 #排查 #排查 #项目结构