[AI_TOOLS_LOG]// 2026.04.17 // 10 分钟阅读

我的 pre-commit 里跑着两个 AI 在互相吵架

#AI#PRE-COMMIT#CODEX#CLAUDE#CODE-REVIEW#AUTOMATION

提交之前,我会先让 Codex 把这次 staged diff 骂一遍。如果 Claude 觉得它骂错了,就放行;如果两个都觉得有问题,提交就会被拦下来。

这套东西跑在 pre-commit 里,下面说说它怎么来的,以及三层里每一层在解决什么问题。

我为什么想把第一轮 review 往前移

不是说提交后的 review 不重要。相反,我还专门接了 argue bot 去处理飞书那一轮,那篇单独写过。

但有一类问题,我觉得不值得拖到提交后才第一次被看到:

  • 从设计稿复制过来的图,还是原始未压缩的
  • 我动了别人写的文件,其实只有纯格式化,结果混进了功能 commit
  • staged diff 里有个比较明显的逻辑漏洞,但当时在想别的事没注意到

这类问题的共同点是:在本地最容易处理。真等代码提上去、报告出来,再切回来改,成本就高很多了。

所以我把这条线划得很清楚:

  • 提交前:把机械问题和能在本地判断的明显风险先处理掉
  • 提交后:再过一遍这次改动对别人的影响,CI 怎么说,PR 整体看起来合不合理

现在这个 pre-commit 主要就三层,顺序按确定性从高到低:图片资源、格式化拦截、adversarial review。

第一层:图片资源,不想手动做的事让脚本做

背景:以前是这么干的

我在做 Android 项目。图片放在 res/drawable,代码里用 R.drawable.icon_name,XML 里用 @drawable/icon_name。

设计稿在蓝湖上,切图直接下载是 png 或 jpg,未压缩的。以前要把一张图放进项目,流程是这样:

  1. 蓝湖切图,下载到本地
  2. 打开 squoosh.app,上传图片
  3. 有损和无损压缩各跑一遍,对比体积,手动选小的那个
  4. 看压完以后值不值得换成 webp
  5. 再把最后选中的那个文件放进 res/drawable

每次都得做,繁琐但不容易出错。问题是每次加图都要走一遍这个流程,而且压缩这一步基本都得自己手动试:有损还是无损、压完到底小多少、值不值得换成 webp,甚至有些 case 压完以后 png 反而更小,最后还是得保留 png。

现在的做法

有了脚本以后,我做的只是一步:把蓝湖的图直接复制到 drawable 目录。剩下的让 pre-commit 来:

脚本只处理 staged 里的图片,.9.png、gif、动画 webp 这些会跳过。对可处理的图,同时跑有损(cwebp)和无损压缩,选更小的,改名为 _opt.webp,然后扫全部 kt、java、xml 文件,把资源引用从旧名字同步更新。

有一个特殊情况:如果我已经自己压缩过,放进来的是 webp 且文件名里带 _opt.,脚本就会直接跳过这张图。

命名冲突的情况——两张不同的图压完都变成同一个名字——脚本会直接阻断提交,不允许互相覆盖。

第二层:纯格式化,不想混进功能提交里

这一层完全没有 AI,反而是最朴素也最有必要的一层。

我用的 IDE 会在保存时自动格式化。有时候只是打开了一个别人写的文件改了几行,IDE 就顺手把整个文件的缩进、import 顺序全重排了一遍。这种改动混进功能 commit 以后,会把 git blame、review、问题追溯全搞脏。

所以 hook 会对 staged 里的 kt/java 文件做一个判断:

  • 这个文件上一次提交不是我改的
  • staged 内容去掉空白以后和 HEAD 基本一样

就把它标记为"纯格式化",然后给我三个选择:继续提交(包含格式化)、把格式化文件从 staging 撤掉只提功能改动、或者取消这次 commit。

我后来发现这一步比想象中更值。很多时候根本没意识到 IDE 帮我动了别人的文件,直到这里才被提醒。

第三层:两个 AI 在 staged diff 上互相吵架

这一层才是这篇文章真正想说的。

先说 Codex 是什么

Codex 是 OpenAI 的代码 agent。有一个官方插件 openai/codex-plugin-cc,可以直接在 Claude Code 里安装:

/plugin install codex@openai-codex

装好以后,它提供了 /codex:adversarial-review——专门做"挑刺"式 review,质疑实现方向、压测假设、找潜在的崩溃路径和数据风险,不是找命名或格式问题。

我把它接进了 scripts/pre_commit_review.py,每次 git commit 时自动触发。

流程是这样的

  1. 取出 staged diff 里有实际改动的代码文件(kt、java、py、js 等)
  2. Codex 做 adversarial review
  3. Codex 觉得没问题 → 直接放行,Claude 不介入
  4. Codex 提了问题 → 把问题交给 Claude,让它站到辩护方的位置看一遍
  5. Claude 认为 Codex 说的不成立 → 允许提交
  6. Claude 也确认有问题 → 阻断提交,输出两方的判断

review 的重点只有这几类:逻辑 bug、crash 风险、数据损坏、明显性能问题、安全漏洞。命名、注释、风格不在这一轮——那是提交后的事。

为什么是两个模型

两个原因。

第一,单模型容易误杀。Codex 有时候会对一些有意为之的取舍提出质疑,但那些取舍在当时的上下文里是合理的。Claude 作为辩护方,能用本地完整背景来反驳,比只让 Codex 一票否决更准确。

第二,互相兜底。Codex 顶不住的时候 Claude 还能接住;Claude 额度吃紧的时候 Codex 可以独立跑完整流程。两个模型都能各自单独完成这一轮,只是组合起来误报率更低。

我还专门给这一层做了缓存

没有缓存,这套东西很快就会让人想用 --no-verify 绕过去。

我现在是对每个文件的 diff 内容做 SHA256,取前 24 位作为 key,把 review 结果缓存到 .git/review-cache/ 里。只要代码没变,下次 commit 时直接命中缓存,不会重新调用模型;改了代码,hash 自然不同,缓存自动失效。

# 清除全部缓存(强制重新 review)
rm -rf .git/review-cache/

# 查看当前缓存了多少条
ls .git/review-cache/ | wc -l

缓存命中时,整个 pre-commit 额外加的时间接近零。没命中时,要等模型跑完,从几秒到几分钟,取决于 diff 大小。


所以我现在看 pre-commit 这一层,重点其实很明确:

  • 图片资源这类机械活,尽量交给脚本处理
  • 纯格式化这种会把 commit 搞脏的东西,尽量别混进去
  • staged diff 里最容易漏掉的硬伤,先让模型在本地过一遍

这样等代码真的提上去以后,我面对的就不是一堆本来可以更早处理的小问题,而是提交后的那份 review 报告里真正值得看的东西。

SX
Written by sunxu

Android 工程师,写代码也写字。

© 2026 sunxu · CC BY-NC 4.0转载请注明原文链接,不可商用