Codex PR 太大没法合:6 个 scope 失控来源 + 拆 PR 策略

50 个文件 1500 行,没人 review 也没人敢合——在 prompt + AGENTS.md + CI 三层封顶 diff 大小。

你让 Codex「重构 billing 模块」,它返回一个动了 50 个文件、1500 行的 PR。Diff 看着合理——每一处都说得过去——但团队没人愿意 review 1500 行 agent 写的代码。PR 挂一周、和 main 冲突、最后被关掉,留言「拆一下」。

Codex 出大 PR 不是模型问题,是 scope 问题:任务描述本身就授权了它动这么多。修法是从三层封顶 diff 大小:prompt(一个 task 一个关切)、AGENTS.md(每 PR 预算)、CI gate(超大 PR 直接 fail)。

常见原因

按命中率从高到低:

1. Task 是开放式的(“重构 X”)

「重构 billing 模块」「优化性能」「清理 auth 层」——这种动词没有自然停止点。Codex 会一直改到没”显然可以改”的为止,不是到一个有意义的 chunk 就停。

如何判断:回看 prompt——出现「refactor」「improve」「clean up」「modernize」之类没限边界的开放动词,就是你授权了无限。

2. Codex 做了”顺手”的相邻清理

原 task:修 billing.ts 里一个 typo。Codex 4 行改完,环顾一周发现同目录 import 不一致,“顺便”在 30 个文件里 normalized 了。

如何判断:diff 一个小核心 + 一圈”反正路过就改”的 halo。Halo 文件不动 bug,是为一致性改的。

3. AGENTS.md 没限 diff 大小

没明确「每 PR 最大 diff」规则,Codex 把每个看似合理的修改都当 in-scope。多数团队有隐式默契,Codex 不读隐式。

如何判断grep -i "diff\|pr size\|scope" AGENTS.md——空就是没封顶。

4. 重构混了多个独立关切

PR 改名一个函数、补了新测试、顺手修了 3 个发现的不相关 bug、还更新了 doc。每块都小,加一起就是 1500 行的 review 负担。

如何判断:PR description(或 commit list)有 4+ 个独立 bullet,每个本应是独立 PR。

5. Codex 没先提 plan

直接让它干大事,它就直接出代码——没 plan 就没机会让你说”先做 1-3、4-7 先停”。

如何判断:session 历史里代码之前没有 plan 或拆分。只有「task → 1500 行代码」。

6. 修一个 bug 意外级联

formatDate 的”修”合理地需要更新 14 个 caller。但 Codex 全打进一个 PR——而本该 PR1 = 新签名、PR2..N = call site 逐个迁。

如何判断:diff 一个小”真改” + 大量 call site 更新——本应拆成 PR 链。

最短修复路径

按收益从高到低,前 2 步预防大 PR 在它发生之前。

Step 1:先要 plan 再写代码

非琐碎 task 都加:

写代码前先提 plan:
1. 编号列出离散步骤,每步都能独立成 PR。
2. 每步:目标文件(数量 + 路径)、估算 diff 大小、对其他步的依赖。
3. 指出能最快带来可见价值的最小那步——从那步开始。

等我批准再生成代码。

50 文件的 plan 会在它发生前给你看到,你能说”只做 step 1”。

Step 2:prompt + AGENTS.md 封顶 diff

Task 里:

约束:
- 最多动 5 个文件。
- diff 控制在 200 行内(不含测试和生成代码)。
- 不要为"一致性"动 task scope 外的文件。
- 装不下就停下来提拆分方案。

AGENTS.md 持久规则:

## PR 大小

- 默认上限:每 PR 增删 200 行(不含测试 / 生成)。
- 硬上限:500 行;超过要显式批准。
- 相邻"顺手清理"独立成 PR,不要打包。
- 重构必须先 plan、拆成 PR 链。

Step 3:禁掉机会主义改动

Prompt 里:

不要做任务之外的任何改动:
- 不要"反正路过就 normalize import"。
- 除非是你正在改的那几行,否则不要 format。
- 不要为一致性改名字。
- 看到其他问题在文末列成 TODO,不要顺手修。

Step 4:已经出了大 PR,用 Codex 自己帮忙拆

PR 已经存在但你不想丢掉工作:

这个 PR 有 50 个文件,拆成 4 个独立 PR:
1. 读 diff:`gh pr diff <number>`。
2. 按独立关切提一份 4-PR 拆分方案。
3. 每个 PR 列:文件、行数、commit message。

我之后 cherry-pick 到独立分支。

git:

# 每 PR 一个分支
git checkout main
git checkout -b pr1-rename-helpers
git cherry-pick <commits-for-pr1>

git checkout main
git checkout -b pr2-add-tests
git cherry-pick <commits-for-pr2>
# ... 以此类推

Step 5:加一条 CI check 拦住大 PR

# .github/workflows/pr-size.yml
name: PR size
on: pull_request
jobs:
  size:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }
      - name: Check PR size
        run: |
          ADDED=$(git diff --shortstat origin/${{ github.base_ref }}...HEAD -- \
            ':!**/*.snap' ':!**/__generated__/**' ':!pnpm-lock.yaml' \
            | grep -oP '\d+(?= insertions)')
          if [ "${ADDED:-0}" -gt 500 ]; then
            echo "::error::PR 增加了 ${ADDED} 行(上限 500),拆一下。"
            exit 1
          fi

排除 lockfile、snapshot、生成代码——拦的是真正可 review 的行数。

Step 6:真要大重构,走 feature flag 链

变更确实必须原子时(如迁 API)就阶段发布:

Step 1:新路径和老路径共存(小 PR,flag 关)。
Step 2:caller 逐个迁(小 PR,flag 还关)。
Step 3:翻 flag(一行 PR)。
Step 4:删老代码(小 PR)。

一个 1500 行的 PR 变 4 个 300 行的,各自独立 review。

预防建议

  • AGENTS.md 写「plan-first」规则:非琐碎 task 先编号 plan,你批准再出代码
  • AGENTS.md 写硬 PR-size 上限(默认 200,硬顶 500)——Codex 按写的来
  • 每个 prompt 关掉机会主义相邻修改——“只动指定问题”
  • CI 加 PR-size gate,超大 PR 不能进 review
  • 重构走 feature flag + 小 PR 链,不要一份原子大 PR
  • Codex 提 50 文件 plan 时推回去(“只做 step 1”),不要接受整个 plan

相关阅读

标签: #Codex #排查 #PR 体积 #重构 #审查