Codex 代码不合代码库 style:canonical + lint 双层锁

代码能跑,但读起来像陌生人写的:async/await 混 .then()、import 顺序错、注释风格不对。在 AGENTS.md 里指向一个 canonical 文件,剩下的交给 lint 强制。

PR 能跑、测试过、类型对,但每个 reviewer 都留同类评论:「用 async/await 不要 .then()」「early return 不要嵌套 if」「参数 2 个以上走 object」「缺 JSDoc」。代码看着像不熟代码库的人写的,确实是。Codex 的中性默认风格在 greenfield 上没问题,但在 10 万行代码、风格成型的项目里就是局外人嗓音。

最快的修法(截至 2026 年 6 月):AGENTS.md 里加一张「Canonical 风格样本」表,把每个风格维度指向 repo 里一个真实文件(async/awaitsrc/services/auth.ts,等等),然后在 prompt 里点名这个文件:「Match the style of src/services/auth.ts」。Codex 在动任何代码之前先读 AGENTS.md,所以一个具体文件指针胜过任何抽象的「match existing style」规则。凡是能 lint 的(then 还是 await、import 顺序、引号、命名)都交给 ESLint + Prettier,Codex 的格式就漂不进 PR。这两步能去掉约 70% 的风格挑刺。

根因:风格不合就是 Codex 退回到通用风格,因为你代码库的特定风格从没写进它会读的任何地方。两个杠杆:指向一个 canonical 文件让它照抄,再用 lint 把琐碎风格强制住,Codex 的默认就到不了 PR。

AGENTS.md 是怎么加载的(Codex CLI,2026 年 6 月):Codex 先读 ~/.codex/AGENTS.md(全局),再从 Git 根目录往当前目录走,每层目录拼一个 AGENTS.md。越靠近工作目录的文件排在越后面、覆盖前面的指导,所以单个 package 的 AGENTS.md 可以设比 repo 根更严的风格。合并后的指令默认上限 32 KiB(~/.codex/config.toml 里的 project_doc_max_bytes),超了 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" 规则。用 AST selector 禁掉 .then():
    "no-restricted-syntax": [
      "error",
      {
        selector: "MemberExpression[property.name='then']",
        message: "Use async/await, not .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"] }],
  },
};

注意:"no-then" 不是 ESLint 内置规则(只在第三方插件里有),所以上面用 no-restricted-syntax 的 selector 写法才是不装额外依赖就能禁 .then() 的办法。如果 early-return 也想要规则强制,unicorn/no-negated-conditionno-else-return 基本能覆盖。

加 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 不再延续它。

怎么确认修好了

对 Codex 的 diff 跑一遍 reviewer 会跑的同一道闸:

# 1. 对 Codex 改动的文件做 format + lint(先 stage)
pnpm prettier --write $(git diff --name-only --cached)
pnpm eslint $(git diff --name-only --cached) --max-warnings 0

# 2. 跟 canonical 对照抽查
git diff --cached src/services/newFeature.ts   # 应该读起来像 auth.ts

修好的标志:eslint --max-warnings 0 退出码 0prettier --check 报无改动,并排 git diff 跟 canonical 文件对比,异步写法、import 顺序、注释密度都一致。如果人类 reviewer 还在挑某条风格,那条规则要么不在你的 lint 配置里,要么不在 AGENTS.md 的表里——把它加进那里,别留一次性评论。

预防建议

  • 每个风格维度一个 canonical 文件,在 AGENTS.md 用表格列出来
  • 能 lint 的规则全 lint,人和 Codex 都不要在格式上争
  • Prettier 走 pre-commit(husky + lint-staged),漂移到不了 PR
  • 每个代码 prompt 点名 canonical 文件让 Codex 照抄
  • 项目内风格不一致的先钦定一个胜方,Codex 延续它看到的第一个
  • 生成代码(Prisma、codegen)写进 .eslintignore,lint 只盯人和 Codex 的输出
  • AGENTS.md 的风格表写紧凑:它跟 Codex 加载的所有东西共用 32 KiB 指令预算

常见问题

Codex 过了 lint 但还是被挑风格,为啥? 被挑的是一条没 lint 化的约定,它只活在 reviewer 脑子里。两个修法:把这条规则写进 AGENTS.md 的 canonical 表让 Codex 照一个真实例子,如果这条规则有 AST 形态(then 还是 await、named 还是 default export),再加一条 no-restricted-syntax selector 让它可强制。凡是 lint 不了的,就用文件锚定。

Codex 到底从哪读风格规则? Codex CLI 先加载 ~/.codex/AGENTS.md(全局),再从 Git 根目录往工作目录走、每层一个 AGENTS.md,越近的覆盖越远的。没有单独的「风格配置」文件。canonical 表放进 repo 根的 AGENTS.md,或某个 package 需要更严就放进该 package 的 AGENTS.md。某目录下放一个 AGENTS.override.md 会替换该目录的正常文件。

规则该放 AGENTS.md 还是放每次任务的 prompt? 两边都放。AGENTS.md 是 Codex 每个 session 都读的持久指导;prompt 是你针对这次改动点名具体 canonical 文件的地方(「match src/services/auth.ts」)。持久规则在关键 prompt 里被重复一遍,漂得更少。

我的 repo 真有两套对立风格,怎么跟 Codex 说? 明确钦定一个胜方,因为 Codex 延续它先 retrieve 到的那套。用 grep -rc 数一下两边,把少数派放进迁移清单,并把那些文件排除在 canonical 样本之外,Codex 就不会照抄输的那套。

这套对 Codex 云端 agent 和 IDE 插件也管用,还是只管 CLI? AGENTS.md 格式在 Codex 各形态(CLI、云端、IDE)之间通用,所以 canonical 表的办法到处都管用。本文写的精确发现顺序和 32 KiB 上限是 Codex CLI 的默认值;云端和 IDE 读同一份 repo AGENTS.md,但各自管自己的上下文预算。

相关阅读

标签: #Codex #Coding Agent #排查 #排查 #风格不符