Codex 生成的代码能跑,但”感觉”不像你的项目。命名用 get* 但你们用 find*;错误处理 throw string 但你们 throw 类型化的 AppError;测试用 expect(x).toBe(y) 但你们的风格是 assert.strictEqual(actual, expected)。Review 起来很别扭——不是错了,而是形状错了。
约定从几个文件 sample 里看不出来,必须显式化——以「规则 + 具体例子」的形式让 Codex 能读到。AGENTS.md 是合同,指向 canonical example 文件就是落实。
常见原因
按命中率从高到低:
1. 约定根本没写下来
团队上季度在群里说定 find* 表示可能返回 null、get* 表示必须存在或 throw。Codex 看不到群——也没人把这条写进文件。它就按训练里先看到的那个挑。
如何判断:问团队”这条约定写在哪儿”——没人指得出来文件,Codex 也找不到。
2. 约定写了但 Codex 没读
STYLE.md 存在,但 Codex 的 read 范围只扫 src/。除了 AGENTS.md/CLAUDE.md/README.md 之外的名字经常被跳过。
如何判断:把 STYLE.md 改名 AGENTS.md(或合并进去),Codex 现在遵守了——就是文件名问题。
3. Codex 套了”网红”模式不是你的
团队用 kebab-case 的 testid(data-testid="user-card-name"),Codex 生成 camelCase——Storybook 文档里 camelCase 更常见。约定输给 meme。
如何判断:新代码更像某个流行博客的模式而不像你现有代码——lint / test / 命名这类最常见。
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、自定义规则),很多约定就自动强制了。
Step 6:Monorepo 每包一份 AGENTS.md
不同包可以有不同约定,不要强用一份根规则。每包加:
apps/web/AGENTS.md → Next.js App Router 约定
packages/ui/AGENTS.md → 组件库约定
packages/db/AGENTS.md → Prisma + repository 模式约定
Codex 按”从被编辑文件向上找最近一份”取规则。
预防建议
- AGENTS.md 每条约定都配一个具体 example 文件——光规则的章节会被忽略
- 项目内不一致先解决(钦定一个胜方),再指望 Codex 跟约定
- 能用 ESLint/Biome/Prettier 编码的尽量编码——linter 强制 AGENTS.md 描述
- Monorepo 每个 package 一份 AGENTS.md,最近的那份生效
- 形状错了上线,当成文档缺口而不是 Codex 失败——补规则 + example
- 每季度让 Codex 按项目风格写一段,看输出有没有漂移——漂了就刷新 doc
相关阅读
标签: #Codex #Coding Agent #排查 #排查 #约定