五个月,把 bug 当病人、把编码 agent 当医疗团队
Cockroach Labs 如何把 MOLT Sinai 建成一座 AI 编码“教学医院”,不到两天就交付了 IBM Db2 迁移支持。
中文
复制

四月的一个周三傍晚到周五下午之间,MOLT——Cockroach Labs 用来把数据库迁移到 CockroachDB 的工具——获得了从 IBM Db2 源迁移的能力。
Db2 是最早商用化的关系数据库之一,SQL 方言丰富,类型系统复杂,还有自己的原生 wire protocol。要在 MOLT 里支持它,意味着要写一个新的 schema converter、一条新的 Fetch 路径(把行取出来并写入 CockroachDB)、一条新的 Verify 路径(在数据加载后做比对)、一份 vendored 的 ANTLR grammar、一个用于 CI 的 Docker 镜像,以及一万多行测试 fixture。2024 年我们给 MOLT 工具加 Oracle 支持时,同等规模的工作花了 9 个月,工程时间成本约 16 万美元。
Db2 用了不到两天,没有一行代码是人写的。token 账单是 4172 美元:快 164 倍,便宜 38 倍。
一切始于一个 GitHub issue,需求写得很简单:

随后一个 planning agent 读了它,判断整体太大、无法一次处理,于是把它拆成十五个子 issue,并给出明确的依赖图:先是基础设施,然后是类型系统,再是行迭代器,接着是 Fetch、Verify、Convert、CI,最后是测试数据。其中两个子 issue 在各自的工作分解中被判定仍然过大,又被拆了一次。过程中,agents 还针对自己的工作另外开了一打 issue:fixture 缺口、一个类型映射 bug、一个 isolation level 的修复。等到父 issue 关闭时,它下面已经开了三十二个子 issue,合并了二十七个 pull request,reviewer 打回工作五十五次,九个 issue 被升级处理,其中两个升级到了人。以 PostgreSQL(我们测试最充分的方言)为基准衡量,测试覆盖率与之相当。
但这一切是怎么发生的?驱动它的是什么,更重要的是,是什么在保证最终产出质量足够高?在开 Db2 支持的 issue 之前,我们先建了一所编码医院,叫 MOLT Sinai。MOLT 是 CockroachDB 迁移工具集的名字,而我们一旦决定把整条流水线按医院来建模,MOLT Sinai 这个混成词就实在让人无法拒绝。Mount Sinai 是 Toronto 和 New York 都有的著名教学医院——也正是我们居住的城市。名字就这么定下来了,词汇体系也是。Issue 是病人。合并是出院。负责的人叫 Chiefs of Medicine。
智能体快速编写大量代码并非这个故事独有,软件工厂正在整个行业遍地开花(比如 OpenAI 的这个)。我们这里的独特之处在于:这些智能体与 main 分支之间隔着什么,我们为什么把它建成一家医院的样子,以及我们用它做了五个月真实工作后发生了什么。
为什么要建一家教学医院
编码智能体的问题在于,它们虽然完全有能力写代码,却很难让人放心地把生产级代码交给它们——那种经过充分测试、可维护、且不超出预期范围的代码。智能体的运行成本相对低廉,但它们的代码终归要落到客户手里,这一点安慰不了谁。我们宁愿智能体跑得慢一些(哪怕慢 10 倍),也不愿在客户向 CockroachDB 迁移时写入错误的数据。速度重要,质量重要得多得多。
过去一年里出现过若干次自动化智能体编码的尝试,其中不少是以吞吐量为优化目标的。比如 Gas Town,它针对同一份代码库并行运行大量智能体,再用一个合并处理层来收拾冲突。当瓶颈是代码写得多快时,这是个好设计。还有 Cursor 的这个实验,他们重建 SQLite 的速度快到不得不自己写一套版本控制系统来处理变更量。这是个有趣的实验,但显然不是为了让高质量数据库代码交付到用户手中而设计的。在绝大多数早期的智能体自动化实验中,速度才是主要动机。
我们的动机不同。我们做的是一个分布式 SQL 数据库,以及一套把工作负载迁移到该数据库的工具。迁移工具里一个隐蔽的 bug,可能在数据进入数据库的路上就把它损坏了——而这个数据库本应以正确性为第一要务。因此,我们关心的不是「每小时能出多少个 PR」,而是「这些 PR 里有多少,如果是我们自己合并的,会让我们觉得丢人」。换个问法就很清楚:一个以吞吐量为优化目标的智能体集群,并不是合适的模式。
于是我们去找一种模式:长期处理高风险工作,干活的人水平参差不齐,交接必须做,二次意见必须出,而且还有现成的文献讲交接草率会出什么乱子。找来找去,我们落在了教学医院上。
这个比喻给了我们这些角色:
| 角色 | 做什么 |
|---|---|
| 分诊护士 | 读进来的 issue,判断严重程度和复杂度,决定收治还是退回。 |
| 住院医师 | 做检查:复现 bug、翻代码、写出治疗方案。之后写代码把它实现出来。 |
| 审核主治医师 | 另一个 agent 实例,提示词偏向审查。先批方案,再批代码。 |
| 出院护士 | 在合并之前,确认医院的所有流程都走完了。 |
| 医务主任 | 人。只有 agent 团队解决不了问题时才找他。 |
围绕这个核心,还有你在医院里(或者看《The Pitt》时)会见到的那几个角色:查房护士每三十分钟巡一遍,找卡住的病人;感染控制 agent 在 main 挂掉时可以锁死楼里所有工作流;安全部门每周写流程复盘,开事故后复盘会;研究部门提新活。
我们不说这套比 swarm 便宜,也不说它比单个 agent 简单。但它产出的东西质量比两者都高。做数据库这行,你愿意为这个质量付钱。下面你会看到,它也带来了教学医院那些让人无奈的东西(官僚、等待、成本),但我们认为这些代价值得。
怎么搭起来的
医院里的一切都跑在 GitHub Actions 上。每个病人的状态存在它 issue 的 label 里,也存在医院为跟踪每个病人进度而建的一些临时文件里。

每个阶段都是一个 GitHub workflow,当它的“label in”落到 issue 上时启动。agent 完成后会写上一个“label out”,这个标签就是下一阶段的“label in”。虚线箭头表示工作被打回:计划被拒回到 workup,CI 跑红或要求修改回到 treatment,阶段卡住则上报给 Chief。
图中未画出:有四个部门按自己的排期在这个流程之外运行。Charge Nurse 重跑停滞的阶段,Infection Control 在 main 挂掉时封锁医院,Research 和 Safety 部门提交新的 issue 和政策 PR,这些和任何其他病人一样,经由 Chief 进入流程。
打上标签会触发一个 GitHub workflow,加载特定角色的 prompt,让 agent 扮演医院里的某位员工。完成工作后,agent 会把一条结构化记录追加到存放该 issue 病历的文件中,并打上流程中的下一个标签。这条记录带有一个类似 <!-- SINAI:TREATMENT_PLAN --> 的哨兵标记,方便后续 agent 找到并解析它。
几条基本的医院规则写进了 skill 里,提供了大部分的安全保障:
没有计划不写代码,没有评审不算计划。 Fellow 在动手修任何东西之前,必须先复现问题,做一次鉴别诊断(用医院的术语说),跑一遍能缩小范围的针对性调查,然后发布治疗方案:诊断、要改的文件、要加的测试、风险、待解决的问题。Review Attending——另一个 agent 实例,跑的是以挑错而非修复为核心的 prompt——会读这份计划,批准或驳回。只有到这时才开始 treatment。我们加这道关卡,是因为一个自信地实现错误修复的 agent,浪费的时间和 token 远多于一个停下来检查计划的 agent。
不要即兴发挥。 如果 treatment 过程中范围发生变化,Fellow 要停下来,发布修订后的计划,回到 plan review。skill 文件里的指令只有两个词:“Don't improvise.”
不走捷径,尤其是在测试上。 agent 不得为了让测试通过而禁用它、跳过它、削弱它或修改它。测试失败时,除非能证明相反,否则测试是对的。如果 agent 确实认为测试有错,那么对测试的修改必须单独成一个 commit,单独说明理由,并与修复分开评审。同样,一个通过了但实际没有覆盖到改动的测试,比没有测试更糟。所有代码评审都从禁用该改动、确认所有新测试都会失败开始。如果它们不失败,要么是超出范围,要么就是错的。
绝不乱来。 Fellow 卡住时,它不会硬着头皮蛮干,而是写一份 I-PASS 交接单(Illness severity、Patient summary、Action list、Situation awareness、Synthesis),这套结构化格式直接借自教学医院,然后升级给 Attending。接手的 agent 必须先写下自己对交接内容的理解,才能开始干活。针对医院 I-PASS 的研究显示,如果医生和护士以标准化方式记录患者信息,交接班期间的不良事件能减少 50%。我们的 agent 遵循同样的规程。
Discharge Nurse 审计审查者。 合并前的最后一道关卡并不重新审查代码,而是核实审查本身是否合规:审批存在、审查模板填写完整、没有未解决的讨论串、CI 全绿、提交历史干净。它会贴出一份清单,逐项列出自己核实了什么;只要有哪一项打不上勾,discharge 就以失败告终,那一项保持未勾选并附上说明。这条规则的存在是因为,当你把控制权交给 agent 时,你希望确保它们照吩咐办事,而有时候它们并没有。
先查先例,再去打扰 Chief。 人类 Chief 做出的每一个可推广的决策,都会记入一份只追加的先例日志。想要升级的 agent 必须先读日志;如果有适用的先例,就照先例办并引用它,而不是升级。只有人类才能创建先例。五个月下来,这减少了 agent 升级到 Chief 的次数。
实际交付成果
开始做 MOLT Sinai 时,我们为构建 MOLT 工具的仓库建了一个镜像。这个镜像随后成了试验场,用来检验医院模式是否有效。以下是 4 月 21 日到 9 月 11 日期间该仓库里发生的事:
| 合并的 Treatment PR | 1,238 |
| 结案的 issue | 1,322 |
| 由 agent 提交、供其他 agent 处理的 issue | 1,299 |
| 合并后又被回滚的 PR | 7 |
| 到达人类 Chief 的 issue | 92 |
| PR 大小中位数 | 新增 527 行 |
不到 5 个月,医院流水线就产出了远超一百万行代码,而且每一处改动都拆得足够小,小到 agent 可以放心审查。
你或许已经注意到上表里有个奇怪的数字:1299。仓库里提交的 issue 有近一半是医院为自己创建的:分解产生的子任务、Fellow 在计划之外圈出的后续事项、研究部提出的方案、安全部的纠正措施。流水线会自己生成待办队列。哪些能进入由人决定,但到第二个月,人已经不再是工作的主要来源。
Db2 支持是我们送进医院的第一项大型功能,但那只是一次实验。自主运行第一周之后,我们开启了“人工审批模式”,要求每一次合并都必须有人过目。只有在这种模式下,我们才能确信代码质量足以交付给客户。有了这层额外的安全保障,我们开始做医院真正为之而建的东西:Migration Assistant——一个基于 AI 的工具,引导用户完成从 Postgres 到 CockroachDB 的完整迁移,包括 schema 转换、数据加载与校验,以及例行转换。Migration Assistant 目前处于 Postgres 迁移的预览阶段,几乎完全由医院的工人写成。
成本
MOLT Sinai 从 4 月开始运行以来,平均每位病人(也就是每个 GitHub issue)的花费略高于 84。issue 通常在医院里待一到两天,时间大多耗在等待人工审查上。就 LLM 模型而言,医院随着模型的进步而演进。

起初,流水线的每个阶段都跑 Opus 模型;引入 Fable 之后,现在由 Fable 做规划,再由 Fable 生成的计划选择实现阶段用哪个模型。如果实现阶段在 Sonnet 或 Opus 上遇到困难,可以升级到 Fable 模型。这样既压住了成本,又能在必要时动用更贵(也更强)的模型。
如上图所示,每次切换模型后,都需要做一些工作来调整 agent 技能以优化成本。我们每隔几周就会进行一轮优化,这也是上图起伏不定的原因之一。还有其他原因,比如某个时间段内的工作构成(UI 任务成本更高,因为 agent 必须处理截图和其他视觉数据),以及我们 CI 环境中的一些怪癖,但这些细节太琐碎,不适合在一篇入门博客里展开。
医院自己建起了自己
我们创建 MOLT Sinai 时,把最初的一套工作流、技能和提示词直接写进了镜像仓库。很快,我们意识到自己构建的东西很有价值,并猜测公司里其他人也会想要利用它。为了实现这一点,我们把 “Sinai” 代码(真正的医院流水线)从 MOLT Sinai 仓库(医院流水线的第一个消费者)中拆分出来,放进一个单独的仓库,其他人如果想利用医院模型,就可以把它嵌入自己的仓库。如今,Sinai 仓库大约有 1,570 次提交。其中大约 1,340 次——85%——是由流水线自己完成的。
这不是我们计划好的。框架有 bug,我们就像处理其他任何问题一样,把它们送进医院。那个每三十分钟查房一次的 Charge Nurse 工作流之所以存在,是因为一个 agent 在 GitHub Actions 的 label 事件中撞上了一个竞态条件,提交了一个 issue,然后另一个 agent 设计并发布了修复。这也是快速演进医院模型最省事的办法,而且随着几乎所有病人都出院,医院模型已经变得更健壮、更高效、也更容易使用。
如今,Sinai 模型已在 Cockroach Labs 的四个仓库中落地,还有几个仓库计划在未来几个月内采用它。
指令变成了遗留代码
每个 agent 角色都由一个技能文件定义:一份 Markdown 文档,包含流程、规则、模板和禁止行为。它们的写法就像知识库通常的写法一样——每次出问题就加一条规则。
七月,我们审计了这些文件。25 个 skill 文件,略多于 10 万字。其中约 2.3 万字——23%——被标记为可删除,且不会改动任何 gate、命令、模板或 sentinel。最主要的模式是同一条规则在多处重复表述:70 处发现,合计 6000 字。有一个文件 47% 的内容是冗余的。
这些冗余实实在在地消耗着我们的金钱和时间。医院里的 worker 会同时加载多个 skill。Discharge Nurse 每次运行都要加载约 21,800 字的指令,大约 29,000 个 token,而这时它还没读到它要检查的那个 PR 的任何一行。hospital-protocol 这个基础 skill 会被全部 15 个 agent 加载,所以这里的冗余代价最大。
我们怀疑这不是 MOLT Sinai 独有的问题。我们的 prompt 像代码一样腐烂了:规则只增不减,直到文件长到没人能从头读到尾。我们只是因为 token 账单才注意到这件事。目前我们还没有针对这个的 linter,但很可能很快就会做一个。
经验教训
与 MOLT Sinai(以及 Sinai 模型)共事五个月,我们学到了很多。下面是一些比较有意思的部分。
行之有效的做法
模型能做到的比预期更多。 每个角色的 skill 文件开头都有几句话来框定这个角色,我们发现这几句话是最有价值的。Fellow 的开头是:“你是一名 Fellow,正在对一位已入院患者(GitHub issue)做诊断检查。你的职责是彻底调查,并产出一份另一位 Fellow 可以执行的书面治疗方案。”Review Attending 的开头是:“你是与治疗此病例的 Fellow 不同的另一个 agent 实例。你的职责是找出 bug、安全问题、回归风险,以及偏离已批准方案之处。”这打下了底子,因为底层 LLM 非常清楚教学医院如何运转、鉴别诊断是什么、交接为什么要有固定结构、第二意见意味着什么。当你告诉 LLM 去扮演一个角色,它会扮演得非常非常好。举个例子,一位 Review Attending 在一个已经经过好几轮 review 的 PR 上发现了一行 docstring 缺陷,但它拒绝凭自己的权限把 PR 打回去重写,它写道这是在“把它提交给你裁决,而不是在患者已经精疲力竭的时候,仅因一处 docstring 修复就单方面把病例退回给 Fellow”。另一个案例中,一位 Attending 要决定是否重新拆分一个 Chief 已经裁定过的 issue,它写道:“一个由 Chief 拥有的决定,如果其结果证伪了它的前提,就应当交还给它的所有者,而不是绕开它自行发挥。”这些句子没有一句直接出现在 skill 文件里。它们是 agent 自己写出来的,因为它们在扮演一个角色。
**先审计划,再写代码。**经过评审的计划被驳回的比例不到 10%,但一旦被驳回,这就是整套系统里价值最高的一次评审,因为它把返工挪到了尽可能最早的节点。举个例子:有个遥测修复要从采集的日志里去掉客户的 hostname 和数据库名,计划评审时发现还有一处小疏漏,信息仍可能泄露。如果没在计划评审时抓到,代码评审几乎不可能发现,这个问题要么直接上线,要么得再开一个 issue 才能解决。在规划阶段抓 bug,能省下后面堆积如山的活。
**拆分,并且设硬上限。**如果 workup 估算改动会超过 1,000 行代码(含测试),Fellow 必须把它拆成带依赖图的子 issue。任何自身会超过 1,000 行的子 issue,再拆一次。设这个上限,是为了让每个改动都足够小,小到评审者——不管是 agent 还是人——能把它整个装进脑子里。Db2 那个 sprint 从一次 admission 拆出了 32 个 issue。几万行代码,单个 agent(或人)不可能有说服力地评审完,所以医院的做法是把活拆开,让它变得容易。
**记录。**每个医院 issue 都记录它产出的一切:workup 计划;Fellow 在动代码之前写的复现测试,以及它失败的完整输出;治疗方案及每一次修订;每一次 I-PASS 交接;每一个评审结论及其理由;每个阶段的临床记录,包含模型、token 数、耗时和成本。记录放在一个配套的 archive 分支上,与代码一起合并,这样 PR diff 里只有代码,评审起来更容易。五个月后,仅凭 issue 线程,我们就能轻松还原 1,238 个 PR 中任何一个为什么被合并;安全部门的每周评审之所以能开展,也全靠这条线索存在。事实证明,一丝不苟地留记录,是医院这个比喻带给我们最有力的东西之一。
我们改了什么,还有什么要改
模型有时会做过头。 让角色分工奏效的那套提示词,也让它们变得官僚。一个 Fellow 对某个 issue 的计划就是「close as duplicate;PR #691 已经合入了修复」。计划审查确认这个判断没错,却仍然升级处理,理由是「review-attending-plan-protocol 记录的三种结果(APPROVE / REJECT / ESCALATE)不包含『close as duplicate』」。一个已确认的重复 issue 竟然需要人来处理(这个问题后来已经解决)。一处一行 UI 文案改动——把副标题里的「Validating」改成「Verifying」——走了五轮返工,还惊动了 Chief,争论的是一份低风险 PR 能否在一次不稳定的 CI 跑绿之前合入(agent 经常在 CI 红灯时试图合入,理由是导致失败的不是它们的问题)。一次代码审查拦下了一份 PR,没有任何代码缺陷,只因为 PR 描述里有个小拼写错误。而当审查者留下非阻塞性的小意见时,流水线倾向于开新 issue 去跟踪它们(后来也已修复),把一条小评论变成一个新病人。其中一些问题我们已经处理,但有些是一个把质量放在第一位、严格遵循自身规则的系统的代价。
审查需要熔断机制。 Issue 1777 被标为紧急,修复只有一行:在一条代码路径里调用一个已有函数,和另外三条同类路径的做法一样。它花了两天、十一轮返工,先是评论规范问题,然后是 PR 描述里的一个不实说法,接着是偏离原计划的一处小改动,再是测试质量,两次 rebase,两次工作流失败,还有一道人工审批关卡。任何超过三轮返工的都会触发 Safety review,而在某一个 Safety review 窗口里,三个复杂 issue 分别走了九轮、九轮和七轮,吃掉了那段时间医院花费的约 1,046。同一份 safety review 也记录到,代码审查流程没有「可计数的非收敛熔断机制」,因此不收敛的循环「要么很晚才升级,要么永远不升级」。它提出的修复最近已合入——现在,一份 PR 连续四轮返工、且每轮都暴露出新的阻塞性问题,或者累计六轮实质性返工之后,审查者必须移交给 Attending。这次合入时间还不长,我们也没有足够数据说明它的效果,但早期迹象看起来是正面的。
**这家医院很高产,但未必总是好事。**它很擅长给自己找活干。如上所述,近一半的 issue 都是 agent 提交的。Research Department 负责调研代码库并提出新功能,最初每三小时运行一次,它产出的 issue 数量多到我们根本来不及逐一审核是否接纳。我们先是把它限流到每天一次,随后又把处于待审核状态的提案数量上限设为十个。问题在于,它找到的 issue 大多确实有价值。我们经常在用交互式 Claude 会话处理代码库里的某个问题,结果它指向的正是这家医院几周(甚至几个月)前提交的同一个 issue。话虽如此,把它们全部推进下去既昂贵又耗时,价值也存疑。从河里淘出金子始终是最难的部分,而我们至今还没解决。
**我们建了一家并不真正教学的 teaching hospital。**MOLT Sinai 完全符合两位资深工程师(合计超过 40 年行业经验)会做出来的东西。模型极其强大,能在我们睡觉时处理完一批 issue,第二天早上很快就能审完。然后我们可以迅速给医院塞进更多 issue,让它们在我们白天开会时被解决,下班前再审核并放行。现在,开一整天会也能换来 10 个高质量、已合并的 PR。但那些日程表没排满、不具备数十年经验带来的软件工程判断力、又真心想深入学习如何成为更强工程师的应届生呢?这套模式对他们管用吗?简而言之,并不能完全满足他们的需求。我们目前正在改进这家医院,让人可以接手已分诊的 issue,自己制定方案,再交由医院的 agent 审核。审核过程中,他们需要为自己的决定辩护,并被追问是否还有其他可能更全面的方案。同样,当拿到一份经过审核的方案时,人可以选择自己写代码(小修小补就手写,或者借助交互式 LLM 编码会话),从而学会如何在代码库中导航、实时做出实现上的取舍,并在随后的代码审查中接受评估。我们需要培养未来的软件工程师,而无论十年后 agentic coding 的世界是什么样子,我们依然需要具备良好判断力和批判性思维的人。
下一步计划
我们一直在努力提升医院里各个 agent 的效率、速度和准确率,同时也在想办法让医院对资历较浅的工程师更有吸引力、更有教育意义,更能锻炼技能。此外,我们还在确认 IBM Db2 支持是否正确——最初的测试中,我们并没有审查医院写出的任何代码。最后,我们正在做一系列实验,看看要让医院重新回到完全自主模式需要什么条件,至少对某些类别的问题是如此。如果你有兴趣帮我们解决上面一个或多个问题,欢迎联系我们。