多模型审查提出八项异议,测试证实两处缺陷
作者让三个模型分别承担需求梳理、代码核查与发布判断,对一个外部开源项目的 PR 进行审查。此次审查提出八项异议,其中两处缺陷通过原提交失败、修复后通过的测试得到验证。
AI 深度解读
让三个 AI 分角色审查代码,能比一个 AI 找出更多问题吗?Open Council 作者拿别人的真实 PR 做了一次尝试:8 条异议,最后只留下 2 个有测试证明的 Bug。这个结果有用,却还不足以证明多模型讨论更强。
对经常让 AI 看 diff 的程序员,这篇更值得借鉴的是筛选方法:要求它指出代码位置、给出复现办法,再用修改前失败、修改后通过的测试,把可提交的问题从讨论噪声里挑出来。
三个 AI 提了 8 条异议,为什么只剩两个 Bug
作者此前做过 10 次任务实验,单 Agent 全部通过,Council 只通过了 7 次。简单任务无法说明讨论有什么价值,评论者也提出质疑:真正有用的可能是运行代码,而不是角色分工。
于是,这次作者选择一个流行开源 Claude Code 插件中尚未合并的 PR。项目和维护者没有公开。整个 diff 约 2 MB,实际审查的是其中约 108 KB,涉及修改用户配置文件的模块,包括 .env 编辑和保护写入的锁文件。其余改动也已应用,周边代码是完整的。
运行版本是 0.3.2,使用最多允许 2 轮讨论的 quick 配置。三个席位分别是:
- Lead PM 使用 Gemini 3.8 Flash,概括改动声称要完成什么。作者原计划使用更大的 Gemini 模型,但配额用完了。
- Chief Engineer 使用 Claude Opus,对照代码检查这些主张,提出异议时附上文件和行号。
- Chief Architect 使用 Grok,判断是否应该放行。
两轮后状态停在 STALLED。在 review 模式里,没有全员签字就表示当前版本不应放行,但这个状态本身不等于发现了 Bug。
第一轮的 8 条异议全部来自 Chief Engineer。其中 3 条经作者阅读代码后认为成立;1 条针对注释中描述的另一个工具如何解析 .env,无法在该仓库内核实;另外 4 条针对 Lead PM 写的摘要,而非 PR。
也就是一半讨论在纠正自己的理解。最终,3 条值得反馈的问题里,有 2 条做成了复现测试。
Engineer 明确说自己没有运行代码,因为检出环境没安装测试运行器。它的贡献是静态阅读,以及写清楚怎样验证问题;作者随后执行了这些测试。
一个反引号,为什么会让后面的配置消失
第一个 Bug 来自 .env 写入和读取规则不一致。
写入器遇到空格、#、引号、$ 或制表符时,会给值加引号,但这个判断漏掉了反引号。读取器却把值开头的反引号视为引号起点,并一直读到文件里下一个反引号。
两边各自看似合理,组合起来就会吞行。
测试文件依次包含 API_KEY、DATABASE_URL、带有反引号词语的注释,以及 PORT。测试把 API_KEY 改成一个以反引号开头的值,再读取 DATABASE_URL。在维护者的原始提交上,结果是 undefined:数据库配置已被读进 API_KEY 的值里。
修复只需改一行,把反引号加入需要包裹引号的字符集合。该值随后会被单引号包裹,测试通过。
这里需要区分已验证结果和推断。测试在第一个断言失败后就停止了,作者没有实际看到后续删除操作失败。他根据相同解析逻辑推断:之后删除 API_KEY,还可能连带删除 DATABASE_URL 和那条注释。
因此,读取异常已有测试证明,连带删除配置仍是基于代码逻辑的推断。
触发条件确实苛刻:值以反引号开头,文件后面还要出现另一个反引号。但它修改的是用户真实配置,不能只看输入是否常见。
锁文件没变,不代表持锁进程还活着
第二个 Bug 出在缓存。
写入操作先获取锁,锁文件记录持有者的进程 ID。等待者会检查该进程是否存活,如果进程已经退出,就清理失效锁。为了避免每次轮询都调用 ps,实现把检查结论缓存起来,只要锁文件不变就继续复用。
问题在于,进程崩溃不一定会改变锁文件。第一次检查时还活着,下一秒可能已经退出,缓存却仍然回答「活着」。
测试在等待开始 1 秒后杀掉持锁进程。原始提交一直等满 15 秒才失败,错误消息还指向那个早已不存在的进程。
修复后,每次轮询重新检查成本较低的部分,即该进程 ID 是否仍存在,其余检查继续使用缓存。等待者在 1,018 ms 后成功拿到锁。
作者把它归为轻微问题:没有数据丢失,只会卡住 15 秒、报一个误导性错误,下次重试可以成功。
第三条代码问题没有计入结果。错误消息承诺,失去锁的持有者「会拒绝提交」,但多数调用方并未检查。作者阅读后认为成立,却没有提供测试。
我的判断
我认可这次实验的证据筛选,也认可作者主动收窄结论。
两个已证明的 Bug 都来自 Claude 席位。Grok 做了裁决,没有提出新问题;Gemini 使用的模型也比原计划小。更关键的是,这次根本没有让单 Agent 审查同一个 PR。
这次证明了流程能产出可验证的问题,没有证明三个模型比一个模型更有效。
与直接让单 Agent 审查相比,要求引用位置、提出复现测试,是可以单独采用的约束。角色讨论是否带来额外收益,仍然缺少对照数据;前一次单 Agent 的 10/10 和 Council 的 7/10,也不能回答这次真实 PR 上的差异。
作者说「测试失败之前,发现还不算发现」,作为提交给维护者前的筛选标准很实用,但不宜扩展成所有代码审查的绝对门槛。有些问题靠代码路径就能确认,也有些问题难以稳定复现。这里真正有价值的是证据要求,而不是测试数量。
作者计划用更难的任务继续比较,提前公开任务与通过标准,并为每个任务跑约 10 次单 Agent,估计结果本身的波动。任务将包含误导性说明和幂等键案例,也会分析后期签字异议是否改变放行结果。这些都尚未运行,不能提前当成结论。
能拿来做什么
- 用 AI 审查 Node 配置编辑代码时,要求它同时检查写入与读取规则,并给出往返测试。能写进去的特殊字符,是否还能完整读回来,应该一起验证。
- 检查文件锁和进程状态缓存时,补上「首次检查成功后,状态在等待中改变」的场景。这次复现的关键就是持锁进程在 1 秒后退出。
- 给维护者反馈前,把 AI 发现整理成代码位置、触发输入、原提交上的失败测试和最小修复。作者最终只在 PR 评论中提交两个测试及修复,没有向对方仓库推送代码;复现测试也有 AI 协助编写,并在本机临时副本上运行。
- 想尝试工具,可以在 Git 仓库中用下面的命令审查未提交改动。按原文写作时的状态,npm 版本是 0.3.2;改善首次运行设置的 0.4.0 已合并,但尚未发布到 npm。
npx open-council-cli review
注意命令末尾的 review,它进入的是代码审查模式。