PR 能跑、测试过、类型对——但每个 reviewer 留同类评论:「用 async/await 不要 .then()」「early return 不要嵌套 if」「参数 2 个以上走 object」「缺 JSDoc」。代码看着像不熟代码库的人写的——确实是。Codex 的”中性”风格在 greenfield 上没问题,但在 10 万行代码、风格成型的项目里就是局外人嗓音。
风格不合是 Codex 在你项目有特定风格时退回到通用风格。两个杠杆:指向 canonical 文件让它必须照抄,加 lint 把琐碎风格强制住,Codex 默认就漂不到不该漂的地方。
常见原因
按命中率从高到低:
1. AGENTS.md 里没风格 example
AGENTS.md 写「match existing style」——哪种 style?Codex 自己挑了一个——没指 canonical 例子,规则太虚无法跟。
如何判断:grep -i "style\|canonical\|example" AGENTS.md 出来的是抽象规则没文件指针。
2. 风格 lint 规则缺失或被禁了
你信 early return,ESLint 没规则强制;你信 object 参数,没规则强制。“风格”只活在脑子里,Codex 输出就漂。
如何判断:对 Codex 输出跑 pnpm eslint——过了但代码还是别扭——就是约定没 lint 化。
3. Codex 把多份 retrieved 文件的风格混了
它读了 3 个文件:一个 async/await、一个 .then()、一个 raw callback——没 canonical 指导就挑一个或更糟产出混合体。
如何判断:新代码内部模式都不一致——混合体就是信号。
4. 没 prettier / formatter 兜底琐碎差异
缩进 / 引号 / 尾逗号——没 Prettier-on-CI,每次提交都积累微漂移,Codex 出的还往上堆。
如何判断:pnpm prettier --check . 出一长串未格式化文件——风格是自愿的不是强制的。
5. Codex 套了网红写法不是你的
代码库用 const Component = () => {}(arrow),Codex 出 function Component() {}(declaration)。都能跑,但团队特定写法没指名。
如何判断:grep 两种形式数一下——主流那种是 canonical,Codex 挑的是少数。
6. JSDoc / 注释 tone 或密度不一致
代码库 @param 写得详细带例子,Codex 写得简短。或反之——你简洁但 Codex 给琐碎函数写大段块。
如何判断:挑 tech lead 最近合的 PR 对比 Codex 的——密度 / tone 不一致一眼看出。
最短修复路径
按收益从高到低,前 2 步加起来去 70% 的漂移。
Step 1:每个风格维度挑一个 canonical 文件
挑团队里最标杆的样本,写进 AGENTS.md:
## Canonical 风格样本
| 维度 | Canonical 文件 |
|---|---|
| 异步 / Promise | src/services/auth.ts(只 async/await) |
| 错误处理 | src/lib/errors.ts(typed AppError) |
| React 组件 | src/components/UserCard.tsx(arrow,props 解构) |
| API route | src/app/api/users/route.ts(NextResponse + zod) |
| Repository 模式 | src/db/repositories/user.repository.ts |
| 测试布局 | src/services/auth.test.ts |
在对应区域写新代码时照这些文件。
Codex 读到这个就有了具体目标,不是抽象规则。
Step 2:每个代码 prompt 都指 canonical
加 `findOrgById` 查找。
风格照 `src/services/auth.ts`:
- async/await(不要 .then())
- early return,不要嵌套 if/else
- 参数 ≥ 2 用 object
- 缺值 / 非法 throw typed `AppError`
文件 + 测试一起生(test 照 `src/services/auth.test.ts`)。
Step 3:琐碎风格走 lint + format
能 lint 的都 lint:
// .eslintrc.cjs(节选)
module.exports = {
rules: {
"prefer-arrow-callback": "error",
"no-then": "error", // 不允许 .then()
"@typescript-eslint/consistent-type-imports": "error",
"@typescript-eslint/naming-convention": [
"error",
{ selector: "variable", format: ["camelCase", "UPPER_CASE"] },
{ selector: "function", format: ["camelCase"] },
{ selector: "typeLike", format: ["PascalCase"] },
],
"import/order": ["error", { groups: ["builtin", "external", "internal"] }],
},
};
加 Prettier .prettierrc.json:
{
"semi": true,
"singleQuote": false,
"trailingComma": "all",
"printWidth": 100,
"tabWidth": 2
}
pnpm lint && pnpm format 把 Codex 微漂的全清掉。
Step 4:lint 进”完成”定义
prompt 里:
写完跑:
1. `pnpm prettier --write <files>`
2. `pnpm eslint <files> --max-warnings 0`
两条都 pass 才算完成。贴 exit code。
Step 5:漂了就拒收、重 prompt 指 canonical
Codex 还在漂:
你的输出用了 `.then()` 和 `function Foo() { }`。
本代码库用 async/await + arrow 组件——见 `src/services/auth.ts` 和 `src/components/UserCard.tsx`。
重写匹配这些文件。完成前跑 `pnpm eslint`。
session 内的纠正后续会保持。
Step 6:审老文件、统一对立风格
repo 里真有两种对立风格——钦定一个胜方迁移,不然 Codex 还是会随机挑:
# 数每种模式
grep -rc "\.then(" src/ | awk -F: '{s+=$2} END {print "then:", s}'
grep -rc "await " src/ | awk -F: '{s+=$2} END {print "await:", s}'
少数派进”迁移清单”,Codex 不再延续它。
预防建议
- 每个风格维度一个 canonical 文件,在 AGENTS.md 用表格列出来
- 能 lint 的规则全 lint——人 / Codex 都不要在格式上争
- Prettier 走 pre-commit(husky + lint-staged),漂移到不了 PR
- 每个代码 prompt 点名 canonical 文件让 Codex 照抄
- 项目内风格不一致的先钦定一个胜方——Codex 延续它看到的第一个
- 生成代码(Prisma、codegen)写进
.eslintignore,lint 只盯人 / Codex 的输出
相关阅读
标签: #Codex #Coding Agent #排查 #排查 #风格不符