Codex 生成的代码不合现有 style:6 个漂移来源 + canonical + lint 双层锁

代码能跑,但读起来像陌生人写的——async/await 混 .then()、import 顺序错、注释风格不对。指向 canonical 文件 + lint 强制。

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 #排查 #排查 #风格不符