PR 能跑、测试过、类型对,但每个 reviewer 都留同类评论:「用 async/await 不要 .then()」「early return 不要嵌套 if」「参数 2 个以上走 object」「缺 JSDoc」。代码看着像不熟代码库的人写的,确实是。Codex 的中性默认风格在 greenfield 上没问题,但在 10 万行代码、风格成型的项目里就是局外人嗓音。
最快的修法(截至 2026 年 6 月): 在 AGENTS.md 里加一张「Canonical 风格样本」表,把每个风格维度指向 repo 里一个真实文件(async/await 指 src/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-condition 和 no-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 退出码 0,prettier --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 漏项目特定约定
- Codex 不照你的项目结构来
- Codex 产出不合项目布局
- Codex 新手入门
- Codex 代码审查工作流
- Codex 对比 Claude Code
- Custom instructions with AGENTS.md(OpenAI Codex 官方文档)
no-restricted-syntax(ESLint 官方文档)
标签: #Codex #Coding Agent #排查 #排查 #风格不符