系列: 与 AI 一起学编程

Gemini 评审了 Claude 的 PR。三十六条评论之后,代码变好了。

多厂商评审在两个线程安全 bug 上线前将其捕获。

Different Model, Different Blind Spots Cross-vendor review across eight subsystems Claude writes Gemini reviews 36 comments 2 real bugs caught thread-safety, would have shipped Human decides — not Gemini, not Claude
Claude 编写,Gemini 评审,人类合并。bug 输了。

一个总是同意作者的评审者算不上评审者。让你的 AI 写的代码经过一个对手 AI 的检验,盲点就会暴露出来——包括那些原本会上线到生产环境的线程安全 bug。

我反复回到的那个模式是:Claude 写代码,Gemini 评审它。不同的训练,不同的盲点,对于什么是站得住脚的模式、什么是代码异味有不同的看法。这个周六是我目前为止对「这个模式为什么是对的」这一判断最具体的一次印证。

前一周,一批 Phase-2 端点扩展的 PR 已经落地在整个 API 表面上。八个独立的子系统各自扩展了它们的公开表面。大部分实现由 Claude 编写。在合并任何一个之前,我把它们都过了一遍 Gemini 的评审。Gemini 针对这一批 PR 一共回复了三十六条具体评论。

我逐条处理了这些评论。这篇文章讲的是 Gemini 抓到了什么,以及这些「抓到」本身属于哪一类,又意味着什么。

这八个子系统是什么

这批 PR 覆盖了需要 Phase-2 扩展的八个 API 路由模块:动画(animation)、网络(network)、音频(audio)、AI 感知(AI perception)、场景构造实体几何(scene constructive-solid-geometry)、输入动作与手柄绑定(input action and gamepad bindings)、XR 锚点姿态类型(XR anchor pose types),以及脚本 Lua 虚拟机生命周期(scripting Lua VM lifecycle)。每个 PR 新增了十几到四十个不等的端点,附带完整的请求/响应模式(schema)、辅助函数和测试。

这些 PR 都不是什么小打小闹。每一个都是公开表面的重大扩展。加在一起,它们代表了几周的设计工作,由智能体在大约两个周末里实现完成。

Gemini 抓到了什么

我想说得具体一点,因为分类很重要。

动画与网络:blend tree 引用和辅助函数。 Gemini 注意到,动画 API 中的 blend-tree 辅助函数和网络 API 中的拓扑辅助函数,在处理缺失引用时用了微妙不同的约定。动画返回一个等价于 None 的值,把决定权留给调用方;网络则抛出异常。两种都是合理的模式,但它们在一天之内先后落地的两个 PR 之间并不一致。修复方式是把二者对齐;我们选择了显式抛异常这条路径,因为它能在 API 边界就把缺失引用暴露出来,而不是让它作为一个沉默的空值继续往下传播。

音频:响应模型的默认值和辅助函数。 Gemini 发现若干音频响应模型对可选字段的默认值不一致。有些默认为空字符串,有些默认为 None,有些是显式的 null。这种不一致会在客户端绑定层(不同语言对每种选项的序列化方式各不相同)产生令人困惑的行为。修复方式是选定一个统一约定(Python 类型里用 None,线上传输用 null)并一以贯之地应用它。

AI 感知:句柄映射(handle map)和绑定。 Gemini 在感知子系统的句柄映射里指出了一个线程安全隐患:这个映射在没有加锁的情况下,一边被后台线程修改,一边被 API 请求线程读取。在负载压力下,这会产生间歇性的映射损坏 bug,而且非常难以诊断。修复方式是引入一个读写锁,针对常见情形(查找次数远多于插入次数)优化读路径。

场景:句柄映射与 CSG 响应。 和 AI 感知那个发现属于同一类 bug。Gemini 在场景子系统的 CSG 句柄映射里抓到了同样的线程安全隐患。修复方式也是同样的形状:一个读写锁。这正是那种「训练有素的眼睛一旦见过这个模式,就会在任何地方都标记出来」的 bug——Claude 在两个地方写了同样的模式,却都没有注意到。

输入:动作和手柄绑定。 Gemini 发现动作绑定 API 对无效动作 ID 使用了魔法数字约定(-1),而手柄绑定 API 用的是一个哨兵结构体值。这种不一致会在开发者同时使用这两个 API 时,因为不小心用错无效标记而产生微妙的 bug。修复方式是在两个 API 中都引入一个带类型的 ActionId::Invalid 常量,并把所有魔法数字迁移到它上面。

XR:锚点姿态类型与处理器复用。 Gemini 发现 XR API 在不同端点里暴露了两种微妙不同的姿态类型:一种是世界坐标系,一种是锚点局部坐标系。这个差异是真实存在的,对使用方也很重要,但这些端点没有清楚地说明这个差异。Gemini 建议拆分这些类型,让类型系统来强制这一区分。修复方式是引入 WorldPoseAnchorPose 作为两个独立类型,二者之间没有隐式转换。

脚本:Lua 虚拟机生命周期与泄漏。 Gemini 发现 Lua 虚拟机是按请求分配的,却没有清晰的销毁路径。在持续负载下,这会把虚拟机状态泄漏进地址空间,直到 API 服务器崩溃。修复方式是引入一个按服务器线程划分的虚拟机池,带有显式的获取/释放语义,并添加一条无论成功还是失败都会在请求结束时执行的销毁路径。

我从这些「抓到」中,作为一个类别观察到的三点

三个观察。

大多数「抓到」都是一致性问题。 Gemini 三分之二的评论都是「这个约定和你刚落地的另一个子系统里用的约定不一样」。这恰恰是单一模型很不擅长抓的一类问题,因为每个 PR 都是孤立落地的,编写它的模型脑子里没有其他 PR 的上下文。一个跨批次工作的评审者,能看到作者脑子里没有同时装着的那些不一致之处。

少数「抓到」是真正的 bug。 句柄映射上的线程安全发现是真实的 bug,它们本来会上线,而且会是间歇性、难以诊断的那种。Gemini 在这批代码里抓到了两个实例(一个在 AI 感知,一个在场景 CSG),因为它对「共享可变映射且没有加锁 = 线程安全隐患」有相应的模式识别能力。不同的模型,不同的训练,被校准去标记的东西也不同。

少数是风格上的,引发了争论。 并非每一条 Gemini 的评论都是对的。有一小部分是风格偏好,我要么直接反驳了,要么留给人类判断。有些评论被驳回这件事本身并不削弱这个模式,反而强化了它。一个总是同意作者的评审者,算不上评审者。

这个模式能做到什么,又做不到什么

它能做到的:抓住一类单模型评审会漏掉的 bug。具体说,就是跨 PR 的一致性 bug,以及那些「一个在不同数据上训练的评审者标记出作者的训练没能看到的东西」的模式匹配式发现。

它做不到的:取代人类评审。Gemini 的评论只是第一道关卡,我读了每一条,驳回了一些,采纳了大部分。最终的合并决定是我做的。这个模式是「Claude 写,Gemini 评审,人类决定」,不是「Gemini 决定」。

这对 AI 实验室意味着什么:值得优化的指标不是「这个模型自己写的代码能不能通过它自己的评审」,而是「这个模型写的代码能不能通过另一家厂商模型的评审」。在跨厂商这项指标上表现好的智能体,才是我在正经工作中信得过的那些。

合作伙伴和构建者应该从中得到什么

如果你正在运行一个由智能体驱动的工作流程,而你还没有让 PR 评审经过另一家厂商的模型,不妨在下一批代码上试试。搭建成本很小,而抓 bug 的比率相当可观。今天这一批就抓到了两个原本会上线的真实线程安全 bug。

如果你是一家 AI 实验室,还没有把「针对另一家厂商模型的 PR 评审」作为编码智能体的一个优化目标,值得考虑一下。这个指标是诚实的,信号是真实的。在这项指标上得分高的智能体,正是认真的团队会采用的那些。

如果你正在评估一个引擎是否值得合作,多厂商评审模式是我会问的纪律信号之一。一个对每个有意义的 PR 都做跨厂商评审的团队,和一个不这么做的团队,是两种不同的团队。代码库会反映出这种差异。

三十六条评论,八个子系统,一个周六。这批代码比今天早上时更好了。这个模式再一次证明了自己的价值。

回去继续构建。

运行时 AI——各实验室既拿它做评审,也在它之上构建

RakuAI 是 LLM 厂商用来对照构建的空间运行时。跨厂商评审、诚实的指标、合作伙伴级的纪律。看看你的模型在真实世界中处于什么位置。

← 所有文章