Codex Agent 输出与 Prettier 冲突:6 个原因 + format-in-loop 模板

Codex 写完文件、Prettier 保存时重排,diff 变 5 倍。修法是在 agent 循环里跑项目锁定的 Prettier、AGENTS.md 风格对齐 .prettierrc。

Codex 写出一份看着干净的 patch。你在编辑器里打开文件,Prettier-on-save 把每一行重排:引号翻转、加上 trailing comma、加分号或去分号、断行重布。git diff 变成实际改动的两倍长。更糟的是 agent 后续想继续接它自己写的代码时,对不上自己原本的行。Pre-commit hook fail,CI lint 报错。“改了 10 行”变成 300 行。

最快的修法:AGENTS.md 里加一条规则,让 Codex 在它报告 “done” 之前,先用项目锁定的 Prettier 格式化:

Run `pnpm exec prettier --write` on every file you touch.
Then `pnpm exec prettier --check` must return 0 before you finish.

这一步握手就把编辑器从”隐藏的第二个格式化器”里拿掉了。本文剩下的部分讲为什么你的第一稿还会漂(版本不一致、AGENTS.md 和 .prettierrc 打架、ESLint 跟 Prettier 抢),以及怎么让 agent 第一稿就和 Prettier 产出一致。

这不是 Codex 的 bug,是握手没做好。Agent 不知道有 .prettierrc,或者知道但 prompt 没加载它,或者跑了 format 但用的是默认设置不是项目设置。修法是让 Prettier 成为 agent”完成”定义的一部分,并把相关规则喂进 agent 的风格提示,让它第一稿就已经符合 Prettier 会产出的样子。

你属于哪一类

症状最可能的原因跳到
Diff 比 agent 报告的大 5 倍;prettier --check 失败Agent 没格式化,编辑器保存时格式化了原因 1、Step 1
清一色的改动(全部 '"、到处加分号)Agent 跑的 Prettier 版本不一样原因 2、Step 3
还没保存,agent 的稿子就一直”不对”AGENTS.md 风格和 .prettierrc 不一致原因 3、Step 2
取决于最后跑哪个工具,文件来回翻ESLint 和 Prettier 都在格式化原因 4、Step 4
只在 agent 写的行里出现非 ASCII(智能引号、nbsp)Agent 输出了不可见字符原因 5、Step 5
一个手对齐的块总被挪动改动落在 // prettier-ignore 区里原因 6、Step 6

常见原因

按命中率排序。

1. Agent 没跑 Prettier,编辑器跑了

Agent 改完保存、宣布”done”。在你机器上编辑器 on-save Prettier 又重排了一遍。看着干净的输出最后一秒被改了形。

如何判断git diff 比 agent 报告的大很多。跑 prettier --check <file>——会有不少行 fail。

2. Agent 跑的 Prettier 和项目的不是同一个

pnpm prettier --write vs npx prettier@3.6 --write vs 全局 prettier,不同版本产出不同。Prettier 在小版本之间会带来格式上的微调(3.5 加了 objectWrapexperimentalOperatorPosition 选项,3.6 上线了实验性的 fast CLI,截至 2026 年 6 月这条线在 3.8.x),所以一个解析到 3.8 的全局 prettier,不会和项目锁定在 3.3 的产出一致。

如何判断:项目里跑 pnpm exec prettier --version(用项目本地解析,不是全局)和 agent 跑的不一样。Diff 是清一色的格式噪声(比如每个 ' 都变成 ",或者每个多行调用都加上 trailing comma)。

3. AGENTS.md 风格提示和 .prettierrc 不一致

AGENTS.md 说”用单引号”,但 .prettierrc"singleQuote": false。Agent 听 AGENTS.md、保存时 Prettier 赢——两边在打架。

如何判断:手动对比 AGENTS.md 的风格主张和 .prettierrc / prettier.config.js。任何不一致 = 永远的吵闹 diff。

4. Prettier 和 ESLint 两个格式化器混用

ESLint 配 eslint-plugin-prettier + 独立 on-save Prettier,对 arrow paren、JSX trailing comma 排序略有不同。Agent 挑一个,工具链挑另一个。

如何判断:取决于最后跑哪个工具文件会不一样。跑 pnpm prettier --check . && pnpm eslint .——如果 prettier --write 后两个都过,但 eslint --fix 又把行改回去,就是有冲突。

5. Agent 输出了 Prettier 会改写的不可见字符

智能引号、不间断空格、en-dash——Codex 有时在 comment 或 string 里塞这些。Prettier(或某些插件)会标准化。

如何判断cat -A <file> | grep -E '\xc2\xa0|\xe2\x80' 能找到非 ASCII。对比 agent 输出文本。

6. 文件有 // prettier-ignore 指令

现有代码用 // prettier-ignore 保留手对齐的块。Agent 在块内部改——Prettier 尊重 ignore,但 agent 的对齐和原对齐对不上。

如何判断grep -rn "prettier-ignore" <files> 列出保护区,对照 agent 编辑的区域。

动手前先确认

  • 存快照:git stashgit diff > before.patch,方便清晰对比。
  • 记 Prettier 版本:cat package.json | grep '"prettier"'pnpm exec prettier --version(用项目本地解析,不是全局)。
  • 记编辑器的 Prettier 集成:VS Code 里的 editor.formatOnSaveeditor.defaultFormatter(应为 esbenp.prettier-vscode)和 prettier.requireConfig。注意:自 Prettier VS Code 扩展 v12.1.0(2026 年 1 月)起,扩展不再自动检查 editor.defaultFormatter 是否设成 Prettier、editor.formatOnSave 是否为 true,所以这两项要手动确认,别想当然以为已经接好了。

需要收集的信息

  • .prettierrc / prettier.config.js / package.json#prettier 内容。
  • AGENTS.md 里关于风格的章节。
  • 保存时被重排的具体文件(对比 git diff 行数和 agent 实际写的)。
  • 用的编辑器 + 扩展(VSCode + Prettier 扩展等)。
  • 是否装了 prettier-eslinteslint-config-prettier

最短修复路径

按收益从高到低。

Step 1:把 Prettier 写进 agent 的”完成”循环

放进 AGENTS.md(Codex 会在 session 启动时自动读它),或放进任务 prompt:

每次改完文件,跑:
  pnpm exec prettier --write <被改文件>

确认:pnpm exec prettier --check <被改文件>  返回 0。
非 0 就修完再说完成。

按 OpenAI 的 Codex 指南,AGENTS.md 是这条规则的长期归宿——它每次运行都会加载进上下文,你不用在每个 prompt 里重复。这样 agent 最后一动就和编辑器会产出的一样:保存时零重排。大仓库上,prettier --cache(Prettier 3.6+ 默认开启缓存)能让每次改完的检查保持快。

Step 2:AGENTS.md 风格主张对齐 .prettierrc

打开 .prettierrc(或 prettier.config.js),每个选项翻译到 AGENTS.md:

## 格式化(与 .prettierrc 一致 —— 不要偏离)

- printWidth: 100
- semi: false       → 行尾不加分号
- singleQuote: true → 用单引号
- trailingComma: "all"
- arrowParens: "always" → (x) => x 不是 x => x
- bracketSpacing: true → { foo } 不是 {foo}

Codex 的第一稿就和 Prettier 输出一致——消除 diff 膨胀。

Step 3:钉死 agent 调用的 Prettier 版本

强制走项目锁的版本:

每次改完跑:
  pnpm exec prettier --write <files>
  # 不要:npx prettier --write   (可能下到更新的 Prettier,比如 3.8 vs 你锁的 3.3)
  # 不要:prettier --write        (可能解析到全局装的)

pnpm exec(或 yarn exec / npm exec)走 lockfile 里的版本。在 package.json 里钉死确切版本(比如 "prettier": "3.8.4",不是 "^3.0.0"),这样 agent、你的编辑器、CI、队友跑的是逐字节相同的格式化器。

Step 4:彻底解决 ESLint + Prettier 冲突

修法是 eslint-config-prettier,它关掉所有和 Prettier 重叠的 ESLint 规则,让两个工具不再抢:

pnpm add -D eslint-config-prettier

如果你用的是 ESLint 9 flat config(eslint.config.js,ESLint 9 起的默认),导入 /flat 入口,并放在数组最后让它生效:

// eslint.config.js
import js from '@eslint/js'
import tseslint from 'typescript-eslint'
import eslintConfigPrettier from 'eslint-config-prettier/flat'

export default [
  js.configs.recommended,
  ...tseslint.configs.recommended,
  eslintConfigPrettier, // 必须放最后
]

如果还在用旧的 .eslintrc 格式,在 extends 里把 'prettier' 放最后:

extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier']

不要eslint-plugin-prettier 把 Prettier 当成一条 ESLint 规则来跑。截至 2026 年,Prettier 维护者不推荐这种做法:它会让 ESLint 变慢、把格式差异变成红波浪线”错误”、还让 agent 要同时满足两个格式化器。保持分工干净——ESLint 管正确性,Prettier 管格式。这样之后,eslint --fixprettier --write 输出一致,不再互相改回去。

Step 5:清理 agent 输出里的不可见字符

任务 prompt 加一段:

输出规则:
- 代码里只用 ASCII(不要智能引号、不要 en-dash、不要 nbsp)
- 注释:标准 ASCII " ' - --
- 字符串字面量:仅按用户字面要求

偏执一点的话改完跑一遍 sanitizer:

sed -i 's/\xc2\xa0/ /g; s/\xe2\x80\x9c/"/g; s/\xe2\x80\x9d/"/g' <files>

Step 6:显式处理 prettier-ignore 区域

任务 prompt 里列出受保护区:

以下块是 `// prettier-ignore` 保护区——按原对齐保留,非必要不要改:

- src/data/timezones.ts:42-78
- src/config/menus.tsx:120-180

必须改的话,缩进 / 对齐按字节对齐到现有的。

Step 7:可选——agent 跑期间关编辑器的 format-on-save

如果实在让 agent 的 Prettier 对不齐编辑器的,暂时:

"editor.formatOnSave": false

会话结束时自己跑 Prettier。把编辑器从隐藏 actor 拿掉。

怎么确认已经修好

  • Agent 输出的 git diff:行数应该和 agent 报告一致,不是 5 倍。
  • Agent 完成后立刻 pnpm prettier --check <files> 返回 0。
  • 在编辑器打开文件、什么都不动、保存:零变更。
  • Pre-commit hook(lint-staged + prettier --check)第一次就过。

长期预防

  • AGENTS.md 格式化章节从 .prettierrc 自动生成,它们就不会漂。
  • 每个 agent 任务模板都包含”改完文件先 prettier 再完成”。
  • Agent tool 调用里用 pnpm exec(或同类),不用全局 prettier
  • eslint-config-prettier,ESLint 不会和 Prettier 吵。
  • 加 CI job 跑 prettier --check PR 检查——merge 之前抓 drift。
  • 想要 ASCII 纯度,加 pre-commit hook 拒收 .ts / .js 里的非 ASCII。

常见坑

  • Agent 改完之后整个项目跑 prettier --write——掩盖 agent 真正改了哪些行、diff 膨胀。
  • 改了 AGENTS.md 风格章节但忘了改 .prettierrc(或反过来)——下个月又开始打架。
  • 信任 npx prettier 不走 pnpm exec——版本锁定被静默忽略。
  • editor.formatOnSave 开着的时候去”只是看看”agent diff——你一点进文件就被重排了。
  • 把 trailing comma 不一致当美观问题忽略——它打断 git blame、之后每个 diff 都膨胀。

常见 FAQ

Q:我把 Prettier 加进 agent 循环了,编辑器保存还在重排,为什么?

两个常见嫌疑。第一,prettier.requireConfig:为 false 时编辑器用 Prettier 默认设置、忽略你的 .prettierrc,设成 true 就只在找到 config 时才跑 Prettier。第二,自 Prettier VS Code 扩展 v12.1.0(2026 年 1 月)起,扩展不再自动确认自己是当前格式化器,所以要显式设 "editor.defaultFormatter": "esbenp.prettier-vscode""editor.formatOnSave": true(如果有别的格式化器在抢,用 "[typescript]" 这样的按语言块来限定)。

Q:我们迁到 ESLint 9 flat config,eslint-config-prettier 不灵了,变了什么?

flat config 用的是另一种导入。用 import eslintConfigPrettier from 'eslint-config-prettier/flat',并放在 export default [ ... ] 数组最后。不带 /flat 的裸 eslint-config-prettier、以及 extends: ['prettier'] 字符串,都是旧 .eslintrc 格式的写法,在 flat config 里会静默地什么都不做。

Q:我们团队用 Biome / dprint,不是 Prettier,修法一样吗?

一样——把 Prettier 命令换成 biome format --writedprint fmt。结构性修法(formatter 进 agent 循环 + 风格提示对齐 config)完全一样。

Q:能让 Codex 直接别格式化吗?

可以,但你得有一套清晰的”agent 之外统一格式化”流程。Commit 时 diff 还是会吵。不如让 agent 第一稿就对齐 Prettier 产出。

Q:.prettierignore 呢?

同样的——确保 AGENTS.md 列出 agent 不该碰的 pattern,和 .prettierignore 一致。否则 agent 改了一个生成文件、Prettier 拒绝格式化——又是不一致。

相关阅读

外部参考

标签: #Codex #agent #排查 #prettier #formatting