Codex 生成的代码能跑,但”感觉”不像你的项目。命名用 get* 但你们用 find*;错误处理 throw string 但你们 throw 类型化的 AppError;测试用 expect(x).toBe(y) 但你们的风格是 assert.strictEqual(actual, expected)。Review 起来很别扭——不是错了,而是形状错了。
最快的修法:把每条出问题的约定写进 AGENTS.md,一行规则,再配一个已经遵守该约定的真实文件指针,然后跑 /status 确认文件真的加载了。Codex 抄的是 example 文件,规则只是个标签。指向 src/services/user/getUserById.ts 比任何文字描述都管用。
约定从几个文件 sample 里看不出来,必须在 session 一开始就显式化——以「规则 + 具体例子」的形式让 Codex 能读到。截至 2026 年 6 月,Codex(OpenAI 的编码 agent)会自动读 AGENTS.md:它把全局的 ~/.codex/AGENTS.md,加上从 git 根目录一路向下到你正在编辑的目录里的每一份 AGENTS.md,按”根在前”拼接,总量上限 32 KiB(project_doc_max_bytes)。AGENTS.md 是合同,指向 canonical example 文件就是落实。
你属于哪一类
动手改之前,先花 20 秒做个分诊:
| 你看到的现象 | 最可能的原因 | 跳到 |
|---|---|---|
| 没人说得出规则写在哪个文件 | 约定根本没写下来 | 原因 1 |
规则在 STYLE.md / CONTRIBUTING.md(不是 AGENTS.md) | Codex 没读到那份文档 | 原因 2 |
| 输出更像某个流行博客/Storybook 模式,不像你的 | 输给了训练数据的默认 | 原因 3 |
AGENTS.md 有规则但下面没代码/例子 | 光规则没例子 | 原因 4 |
grep 显示 src/ 里两种拼法都有 | repo 自相矛盾 | 原因 5 |
Codex 的文件 lint 不过,但规则在 .eslintrc 里 | Codex 跳过了 lint | 原因 6 |
一个能覆盖大部分情况的快速检查:在 session 里跑 /status 看当前用的模型和配置,再让 Codex “按顺序列出你加载的所有 instruction 文件”。如果你的 style 文档不在这个列表里,你就在原因 1 或 2——先把文件路径修对,再去动 prompt。
常见原因
按命中率从高到低:
1. 约定根本没写下来
团队上季度在群里说定 find* 表示可能返回 null、get* 表示必须存在或 throw。Codex 看不到群——也没人把这条写进文件。它就按训练里先看到的那个挑。
如何判断:问团队”这条约定写在哪儿”——没人指得出来文件,Codex 也找不到。
2. 约定写了但 Codex 没读
STYLE.md 存在,但 Codex 只自动加载 AGENTS.md 文件(全局的 ~/.codex/AGENTS.md,加上从 git 根目录向下到你正在编辑文件的每个目录里各一份)。叫 STYLE.md、CONTRIBUTING.md、docs/conventions.md 的文档,除非有东西明确指向它,否则不会进入 context。截至 2026 年 6 月,Codex 仍把 README.md 当成普通源文件、而不是 instruction——只有 AGENTS.md 链(以及它的 AGENTS.override.md 同级文件)会被自动注入。
如何判断:跑 /status,再让 Codex “列出你加载的 instruction 文件”。如果 STYLE.md 不在列表里,就是从没被读到。
怎么修:要么把内容并进 AGENTS.md(或用 /init slash 命令生成一份脚手架再粘进去),要么保留 STYLE.md,在 prompt 里用 /mention STYLE.md 把它挂到当前这一轮。
3. Codex 用了常见模式而不是你的
团队用 kebab-case 的 testid(data-testid="user-card-name"),Codex 生成 camelCase——因为它训练用的公开样例里 camelCase 远比 kebab-case 多。你的约定输给了统计学上的默认值。
如何判断:新代码更像某个流行博客或框架文档的模式,而不像你现有代码——testid / lint 规则 / 测试断言 / 命名这几类最常见。
4. AGENTS.md 写了规则但没给例子
“用团队的错误模式”——什么模式?Codex 需要一个具体例子来抄——一行规则不够。
如何判断:AGENTS.md 某节有标题但下面没代码块也没文件指针。光规则没例子的章节经常被忽略。
5. 项目里约定本身就不一致
一半代码用 findUserById,一半用 getUserById(迁移没做完)。Codex 抽到哪个就用哪个,没明确 canonical 就是随机输出。
如何判断:grep -c "findUserById" src/ && grep -c "getUserById" src/ 都非 0——Codex 没办法挑对,因为根本没”胜方”。
6. 约定靠 linter 强制但 Codex 没跑 lint
约定确实被编码了——.eslintrc 的 naming-convention: camelCase——但 Codex 写完代码没跑 lint。问题到 CI 才暴露。
如何判断:Codex patch 后跑 pnpm lint path/to/new-file.ts——有报错就是它绕过了 lint 编码的约定。
最短修复路径
按收益从高到低,前 3 步覆盖最常见情况。
Step 1:每条约定 = 规则 + canonical example
别只写规则,配一个 Codex 能读的真实文件:
## 命名约定
- `find*` 表示可空查找(返回 `T | null`)。
例子:`src/services/user/findUserById.ts`
- `get*` 表示必有查找(缺则 throw)。
例子:`src/services/user/getUserById.ts`
## 错误处理
所有 throw 必须 extend `src/lib/errors.ts` 的 `AppError`。
例子:`src/api/billing/handlers.ts:42`
## 测试风格
用 vitest + `assert`(**不**用 `expect`)。
canonical:`src/services/user/user.test.ts`
Codex 实际抄的是 example 文件,规则只是导航。
Step 2:task prompt 里指向 example
加 `getOrgById` 查找。
完全照 `src/services/user/getUserById.ts` 的样子:
- 函数形状一样
- 错误 throw 风格一样
- 测试布局一样(见 `getUserById.test.ts`)
文件 + 测试一起生成。
指向一个真实文件比任何文字描述都有效。
Step 3:形状错了拒收 + 重 prompt 时点 canonical
Codex 形状错了:
你的输出用了 `expect(x).toBe(y)`——那是 jest 风格。
本 repo 用 `assert.strictEqual(actual, expected)`。
canonical 见 `src/services/user/user.test.ts`。
按 canonical 风格重生成测试。
每次拒收都是给当前 session 训练,1-2 次纠正后剩下的就稳了。
Step 4:可视化标记列成必需产出
data-testid、ARIA、自定义 decorator 这种,列成必需输出:
新组件必须包含:
- 所有可交互元素上 `data-testid="<kebab-name>"`
- icon-only 按钮加 `aria-label`
- `<Component>.stories.tsx` Storybook story 文件
缺任何一项都拒收。
Step 5:把 lint 加进 verifier
Lint 编码了很多 Codex 绕过的约定,把它写进”完成”:
写完后跑:
pnpm eslint <new-files> --max-warnings 0
任意 warn/error 都修,修完再说完成。
ESLint config 严格点(naming-convention、import/order、自定义规则),很多约定就自动强制了。你也可以把 lint 步骤直接写进 AGENTS.md,让每个 session 都知道这是”完成”的一部分:
## 完成的定义
- `pnpm eslint <changed-files> --max-warnings 0` 通过
- `pnpm vitest run <changed-files>` 通过
Step 6:Monorepo 每包一份 AGENTS.md
不同包可以有不同约定,不要强用一份根规则。每包加一份:
AGENTS.md → 全 repo 通用规则(根)
apps/web/AGENTS.md → Next.js App Router 约定
packages/ui/AGENTS.md → 组件库约定
packages/db/AGENTS.md → Prisma + repository 模式约定
Codex 把从 git 根目录向下到被编辑文件的整条链按”根在前”拼接。离被编辑文件越近的文件在拼接后的 prompt 里越靠后,所以遇到冲突的规则时最近的那份胜出。截至 2026 年 6 月有两个坑:
- 拼接后的总量上限是 32 KiB(
project_doc_max_bytes)。臃肿的根文件会把你的分包规则挤出去——根文件要保持精简。 - 某一级如果存在
~/.codex/AGENTS.override.md(或目录里的AGENTS.override.md),它会取代该级正常的AGENTS.md。某个包突然不守自己的规则时,先查是不是有个游离的 override 文件。
怎么确认修好了
- 确认文件加载了。在 session 里跑
/status,再让 Codex “按顺序列出你加载的所有 instruction 文件”。你的AGENTS.md(以及任何嵌套的)应当出现在里面。想要一份留底记录,就用codex -c log_dir=./.codex-log启动,然后在./.codex-log/codex-tui.log里 grep 这些文件路径。 - 重跑那个出错的任务。让 Codex 再生成一次之前写错的那类文件(比如一个新的
get*查找加它的测试)。 - 和 canonical 对比。新文件应当在函数形状、错误风格、测试布局上都和你指向的 example 文件一致。
- 跑 verifier。
pnpm eslint <new-file> --max-warnings 0和你的测试命令都应当通过,没有约定相关的 warning。
如果第 1 步显示文件加载了、但输出形状还是错的,那问题就出在规则本身——它几乎肯定缺一个具体的 example 文件(原因 4)。补上指针再重跑。
常见问答
Codex 会读 CLAUDE.md 或 .cursorrules 吗? Codex 自动加载的是 AGENTS.md 链。AGENTS.md 现在是一个开放标准(由 Linux 基金会下的 Agentic AI Foundation 维护),Codex、Cursor、Gemini CLI、Windsurf、Copilot 都支持,所以单用一份 AGENTS.md 是最通用的选择。如果你的 repo 里还留着 CLAUDE.md,最干净的做法是写一行 AGENTS.md 说”遵守 CLAUDE.md 里的约定”,并在任务里 /mention CLAUDE.md,而不是指望 Codex 自己去找。
为什么 Codex 第一个文件守规则、下一个就漂了? 通常两个原因:规则没配例子(模型没东西可抄——原因 4),或者 32 KiB 溢出,大的根 AGENTS.md 把分包文件挤掉了。用 /status 查加载顺序,把根文件瘦下来。
规则加了 Codex 还是不理,怎么办? 先确认它真的加载了(上面第 1 步)。最常见的疏漏是规则写在 STYLE.md/CONTRIBUTING.md 而不是 AGENTS.md,所以从没被注入。并进 AGENTS.md,或用 /mention 挂上去。
AGENTS.md 要提交进 repo 吗? 要——把每个项目的 AGENTS.md 提交进去,整个团队(以及 CI 上的 agent)才能共享。机器相关或个人偏好放进你的全局 ~/.codex/AGENTS.md,那份不进版本库。
Codex 在不同包之间自相矛盾。 这是 override 或优先级规则在起作用。离被编辑文件最近的那份胜出;游离的 AGENTS.override.md 会取代该级正常的文件。在目录树里搜一下 override 文件和冲突规则。
预防建议
AGENTS.md每条约定都配一个具体 example 文件——光规则的章节会被忽略- 项目内不一致先解决(钦定一个胜方),再指望 Codex 跟约定
- 能用 ESLint/Biome/Prettier 编码的尽量编码——让 linter 强制
AGENTS.md描述的内容 - Monorepo 每个 package 一份
AGENTS.md,最近的那份生效;根文件保持小,别撑爆 32 KiB 上限 - 形状错了上线,当成文档缺口而不是 Codex 失败——补规则 + example
- 每季度让 Codex 按项目风格写一段,看输出有没有漂移——漂了就刷新 doc
相关阅读
标签: #Codex #Coding Agent #排查 #排查 #约定