在 4 月的某个周三傍晚到周五下午之间,MOLT,Cockroach Labs 为将数据库迁移到 CockroachDB 而提供的工具,获得了从 IBM Db2 源迁移的能力。

Db2 是最早商用的关系型数据库之一,拥有丰富的 SQL 方言、复杂的类型系统以及原生线协议。要在 MOLT 中支持它,意味着需要一个新的 schema 转换器、一条用于拉取行并将其导入 CockroachDB 的新 Fetch 路径、一条用于在加载后进行比较的新 Verify 路径、一份 vendored 的 ANTLR 语法、一个用于 CI 的 Docker 镜像,以及超过一万行的测试夹具。当我们在 2024 年为 MOLT 工具添加 Oracle 支持时,同等的工作耗时 9 个月,工程时间成本约为 16 万美元。

Db2 花了不到两天,而且没有任何人编写任何代码。token 账单为 4172 美元:快了 164 倍,便宜了 38 倍。

这一切都始于一个描述该需求的 GitHub issue:

Support Db2 LUW for convert/fetch/verify #526

随后一个规划智能体读取了它,判断其规模太大无法一次性处理,并将其分解为十五个子 issue,附有明确的依赖图:先是基础,然后是类型系统,然后是行迭代器,然后是 Fetch,然后是 Verify,然后是 Convert,然后是 CI,然后是测试数据。其中两个子 issue 在各自的工作评估中被判定为规模过大,于是又被进一步分解。在此过程中,智能体们还针对自身工作提交了另外十几个 issue:夹具缺口、一个类型映射 bug,以及一个隔离级别修复。当父 issue 关闭时,其下已开启了三十二个子 issue,合并了二十七个 pull request,审查者退回工作五十五次,九个 issue 被升级,其中两个升级给了人类。以 PostgreSQL(我们测试最充分的方言)为基准衡量的测试覆盖率是相当的。

但这一切是如何发生的?是什么在驱动它,更重要的是,是什么在确保最终产出的东西是高质量的?在我们为 Db2 支持开启那个 issue 之前,我们首先构建了一个编码医院,我们称之为 MOLT Sinai。MOLT 是CockroachDB 的迁移工具集的名称,而一旦我们决定把这条流水线建模成一家医院,MOLT Sinai 这个混成词就太妙了,让人无法抗拒。这是因为 Mount Sinai 是我们在多伦多和纽约——我们居住的两座城市——都很有名的教学医院。这个名字就这么定下来了,配套的词汇也一样。问题叫病人。合并叫出院。负责的人叫内科主任。

智能体快速写出大量代码并不是这个故事独有的,因为软件工厂正在整个行业里遍地开花(比如这个OpenAI 的项目)。我们这个案例的独特之处在于,那些智能体和 main 之间隔着什么、我们为什么把它建得像一家医院,以及当我们把它投入真实工作运行五个月后发生了什么。

为什么是教学医院Copy Icon

编码智能体的问题在于,尽管它们完全有能力写代码,但很难信任它们能产出生产级代码——经过良好测试、可维护,并且处在期望的范围之内。智能体的运行成本相对便宜,但当它们的代码要落到客户手里时,这一点并不能让人安心。我们宁愿让智能体跑得更久(哪怕久 10 倍),也不愿在客户的 CockroachDB 迁移中落入错误的数据。速度很重要,但质量重要得多得多。

过去一年里出现了若干次自动化智能体编码的尝试,其中许多都是为吞吐量而优化的。Gas Town就是一个例子,它针对一个代码库并行运行许多智能体,并配有一个合并处理层来梳理冲突。当瓶颈是代码写得多快时,这是个好设计。还有这个来自 Cursor 的实验,他们在其中重建了 SQLite,速度快到需要自己写一套版本控制系统来处理变更量。这是个有趣的实验,但显然它并不是为了向用户交付高质量的数据库代码而设计的。在绝大多数早期的智能体自动化实验中,速度是主要动机。

我们的动机不同。我们构建了一个分布式 SQL 数据库,以及一套用于将工作负载迁移到该数据库的工具。迁移工具中一个微妙的 bug 可能会在数据进入一个本应以正确性为最高优先级的数据库的过程中损坏某人的数据。因此,我们关心的不是“每小时有多少个 PR?”,而是“这些 PR 中有多少是我们自己会不好意思合并的?”有了这种重新定义,就很清楚:一个以吞吐量优化的智能体集群并不是一个好的模型。

于是我们寻找一种模式,它拥有长期管理由能力参差不齐的员工完成的高风险工作的经验,具备强制交接、强制第二意见,以及关于交接草率时会出现什么问题的文献。我们最终落在了教学医院上。

这个隐喻给了我们以下角色:

围绕这个核心,还坐着其他一些角色——如果你进过医院(或者看过《The Pitt》),就会觉得这些角色理所当然:一位每三十分钟巡房一次、寻找卡住患者的护士长,一位在主流程中断时可以封锁整栋楼里所有工作流的感染控制智能体,一个每周撰写流程审查并召开事后复盘会议的安全部门,以及一个提出新工作的研究部门。

我们并不声称这比集群更便宜,或比单个智能体更简单。但它确实产生了比两者质量更高的产品。当你身处数据库行业时,你愿意为这种质量付出代价。如下文所示,它也带来了一些教学医院令人遗憾的方面(官僚作风、等待时间和成本),但我们相信这些是值得的权衡。

它是如何构建的Copy Icon

医院里的一切都运行在 GitHub Actions 上。每位患者的状态存在于其 issue 上的标签中,也存在于医院为跟踪每位患者进展而创建的一些临时文件中。

Each stage is a GitHub workflow that starts when its “label in” lands on the issue. When the agent finishes, it writes a “label out”, and that label is the next stage's “label in”. Dashed arrows send work back: a rejected plan to workup, a red CI run or requested changes to treatment, and a stuck stage up to the Chief.

每个阶段都是一个 GitHub workflow,当它的“输入标签”落到 issue 上时启动。当智能体完成时,它会写入一个“输出标签”,而那个标签就是下一阶段的“输入标签”。虚线箭头将工作送回:被拒绝的计划送回检查阶段,红色的 CI 运行或请求的更改送回治疗阶段,卡住的阶段上报给主任。

未展示:四个部门按照自己的时间表在这条流程之外运行。Charge Nurse 会重新运行停滞的阶段,Infection Control 在 main 分支出问题时将医院封锁,Research 和 Safety 部门提交新的 issue 和政策 PR,这些会像其他任何患者一样经由 Chief 进入。

应用标签会触发一个 GitHub workflow,它会加载特定角色的 prompt,使智能体以医院中某位员工的身份行事。完成工作后,智能体会向包含该 issue 病历的文件添加一条结构化备注,并应用流程中的下一个标签。该备注带有一个哨兵标记,例如<!-- SINAI:TREATMENT_PLAN -->以便后续的智能体能够找到并解析它。

一些基本的医院规则被编码在 skills 中,提供了大部分的安全保障:

没有计划就不写代码,没有审查就不算计划。在 Fellow 修复任何东西之前,它必须先复现问题,进行鉴别诊断(用医院术语来说),运行任何能缩小范围的针对性调查,并发布一份治疗方案:诊断、要更改的文件、要添加的测试、风险、未决问题。一位 Review Attending——一个不同的智能体实例,运行着以挑错而非修复为核心的 prompt——会阅读该计划并批准或拒绝它。只有到那时才开始治疗。我们添加这道关卡,是因为一个自信地实施错误修复的智能体,比一个停下来检查自己计划的智能体浪费的时间和 token 要多得多。

不要即兴发挥。如果治疗过程中范围发生变化,Fellow 会停下来,发布修订后的计划,并回到计划审查环节。skill 文件中的指令只有两个词:“Don't improvise.”

不要走捷径,尤其是在测试方面。智能体不得为了让测试通过而禁用、跳过、削弱或修改测试。如果测试失败,在证明并非如此之前,测试就是正确的。如果智能体确实认为测试有误,对测试的更改必须作为独立的 commit,基于其自身的合理性进行论证,并与修复分开审查。同样,一个并未真正检验该更改的通过测试,比没有测试更糟。所有代码审查都从禁用该更改并确保所有新测试失败开始。如果它们没有失败,那么它们要么超出范围,要么就是错误的。

永远不要瞎折腾。当 Fellow 卡住时,它不会更努力地尝试。它会写一份 I-PASS(病情严重程度、患者摘要、行动清单、情境意识、综合)交接,这是一种直接借鉴自教学医院的结构化格式,然后升级给 Attending。接收的智能体必须先写下它对交接内容的理解,然后才能开始工作。关于 I-PASS 的研究在医院中的研究报告称,如果医生和护士以标准化方式记录患者信息,交接班期间的不良事件会减少 50%。我们的智能体遵循同样的协议。

Discharge Nurse 审核审查者。合并前的最后一道关卡不会重新审查代码。它验证审查是否被正确执行:存在批准、审查模板已填写、没有未解决的讨论串、CI 通过、提交历史干净。它会发布一份清单,准确显示它验证了什么,如果任何一项无法勾选,discharge 就会失败,并留下该项未勾选并加以注释。这条规则之所以存在,是因为当你把控制权交给智能体时,你想确保它们在按吩咐行事,而有时它们并没有。

在打扰 Chief 之前先查看先例。人类 Chief 做出的每一个可推广的决策,都会记录在一个仅追加的先例日志中。想要升级的智能体必须先阅读该日志,如果有适用的先例,它就遵循该先例并引用它,而不是升级。只有人类才能创建先例。五个月来,这减少了智能体升级到 Chief 的频率。

它交付了什么Copy Icon

当我们开始开发 MOLT Sinai 时,我们创建了一个构建 MOLT 工具的仓库的镜像。这个镜像随后成为一个试验场,我们可以在其中测试医院模型的有效性。以下是 4 月 21 日至 9 月 11 日期间该仓库中发生的情况:

在不到 5 个月的时间里,医院流水线落地了远超一百万行的代码,并被拆分成足够小的变更,以便智能体能够自信地审查。

你可能已经注意到上表中一个奇怪的数字:1299。仓库中提交的 issue 有近一半是由医院为医院创建的:分解子任务、Fellow 从计划中划出的后续事项、研究部门的提案、安全部门的纠正措施。这条流水线会生成自己的待办事项。由人类决定哪些被接纳,但到了第二个月,人类就不再是工作的主要来源了。

虽然 Db2 支持是我们送进医院的第一项大型功能,但那只是一次实验。在自主运行的第一周之后,我们开启了“人工审批模式”,要求每一次合并都必须经过一组人工审查。只有在这种模式下,我们才能确定代码质量足够高,可以交付给客户。有了医院这层额外的安全保障,我们便开始着手做我们真正创建医院的目的:构建Migration Assistant,这是一个基于 AI 的工具,引导用户完成从 Postgres 到 CockroachDB 的完整迁移,包括 schema 转换、数据加载与验证,以及常规转换。Migration Assistant 目前处于 Postgres 迁移的预览阶段,几乎完全由医院的工人编写。

成本是多少Copy Icon

自 4 月 MOLT Sinai 开始运行以来,它消耗了略高于 135,000 美元的 Claude token,平均每位患者(即 GitHub issue)约合 84 美元。issue 通常在医院里待一到两天,其中大部分时间往往花在等待人工审查者上。就 LLM 模型而言,医院随着模型的进步而不断演进。

Cost over time by workflow type

起初,流水线的每个阶段都运行 Opus 模型,但随着 Fable 的引入,现在它用 Fable 做规划,而 Fable 生成的计划会选择用于实现的模型。当实现在 Sonnet 或 Opus 模型上遇到困难时,它可以升级到使用 Fable 模型。这有助于降低成本,同时在必要时仍能使用更昂贵(也更强大)的模型。

如上图所示,每次切换模型都需要做一些工作来调整智能体技能以优化成本。我们每隔几周就会进行一轮接一轮的优化,这也是上图波动较大的原因之一。还有其他原因,比如某个时间段内的工作组合(UI 任务成本更高,因为智能体必须处理截图和其他视觉数据)以及我们 CI 环境中的一些怪癖,但这些细节太琐碎,不适合在一篇入门博客文章中展开。

医院自己建成了自己Copy Icon

当我们创建 MOLT Sinai 时,我们把最初的一套工作流、技能和提示词直接构建在镜像仓库中。不久之后,我们意识到自己所构建的东西的价值,并猜测公司里的其他人也会想要利用它。为了实现这一点,我们把“Sinai”代码(实际的医院流水线)从 MOLT Sinai 仓库(医院流水线的第一个消费者)中拆分出来,放入一个单独的仓库,其他人如果愿意利用医院模型,就可以把它嵌入自己的仓库。如今,Sinai 仓库大约有 1,570 次提交。其中大约 1,340 次——85%——是由流水线本身撰写的。

我们并没有计划这样做。这个框架有 bug,我们像处理其他任何问题一样把它们送进医院。那个每三十分钟查房一次的 Charge Nurse 工作流之所以存在,是因为一个智能体在 GitHub Actions 标签事件中遇到了竞态,提交了一个 issue,然后另一个智能体设计并发布了修复。这也是快速演进医院模型最便捷的方式,而且随着几乎所有病人都出院,医院模型变得更加健壮、高效,也更容易使用。

如今,Sinai 模型已在 Cockroach Labs 的四个仓库中落地,还有几个其他仓库计划在未来几个月内采用它。

这些指令变成了遗留代码Copy Icon

每个智能体角色都由一个技能文件定义:一份包含流程、规则、模板和禁止行为的 Markdown 文档。它们的编写方式就像知识库通常的编写方式一样——每次出了问题就加一条规则。

七月份我们对它们进行了审计。二十五个技能文件,略多于 100,000 词。其中大约 23,000 词,23%,被标记为可移除,而无需更改任何单个 gate、command、template 或 sentinel。主要模式是同一条规则在多个地方重复表述:七十项发现中共有六千词。有一个文件百分之四十七是冗余的。

这些冗余让我们付出了真金白银和时间。医院工作人员会同时加载多项技能。Discharge Nurse 在每次运行中都会加载约 21,800 词的指令,大约 29,000 个 token,而这还发生在它读取本应检查的 PR 的任何一行之前。基础的 hospital-protocol 技能被全部十五个智能体加载,因此那里的冗余代价最大。

我们怀疑这并非 MOLT Sinai 独有。我们的提示词像代码一样腐化了:我们不断添加规则,却从不删除它们,直到文件长到没人能从头读到尾。我们只是因为 token 账单才注意到这一点。我们目前还没有针对此问题的 linter,但很可能很快就会做一个。

经验教训Copy Icon

与 MOLT Sinai(以及 Sinai 模型)合作五个月让我们学到了很多。以下是一些比较有趣的要点。

哪些做法有效Copy Icon

模型的表现超出预期。每个角色的技能文件都以几句话开头来框定该角色,而我们发现这几句话是最有价值的。Fellow 的开头是:“你是一名 Fellow,正在对一名入院患者(GitHub issue)进行诊断性检查。你的工作是彻底调查并产出一份书面治疗方案,让另一位 Fellow 能够执行。”Review Attending 的开头是:“你是与治疗此病例的 Fellow 不同的另一个智能体实例。你的工作是找出 bug、安全问题、回归风险,以及偏离已批准方案之处。”这奠定了基础,因为底层 LLM 非常清楚教学医院如何运作、什么是鉴别诊断、为什么交接要有结构,以及成为第二意见意味着什么。当你告诉 LLM 要扮演某个人设时,它会做得非常非常好。举个例子,一位 Review Attending 在一个已经经过好几轮审查的 PR 上发现了一处单行 docstring 缺陷,却拒绝凭自己的权限将其打回重写,写道这是“将其提交给你裁决,而不是在患者已经精疲力竭时,仅因一处 docstring 修复就单方面把病例打回给 Fellow。”在另一个案例中,一位 Attending 在决定是否要重新拆分一个 Chief 已经裁定的 issue 时写道:“一个由 Chief 拥有的决定,若其结果证伪了其前提,应归还给其所有者,而不是绕过它即兴处理。”这些句子没有一句直接出现在技能文件中。相反,智能体们写出它们,是因为它们在扮演一个角色。

先做计划审查,再做代码。经过审查的计划被驳回的比例不到 10%,但当它们被驳回时,这是整个系统中价值最高的审查,因为它把返工转移到了尽可能最早的节点。在一个例子中,对一项遥测修复的计划审查——该修复旨在从我们捕获的日志中移除某客户的 hostname 和数据库名——发现了一个小缺口,信息仍可能在那里被暴露。如果这没有在计划审查中被发现,它极不可能在代码审查中被发现,而这个问题要么会被发布出去,要么需要第二个 issue 来解决。在规划阶段发现 bug 能省下未来大量的工作。

分解,并设定硬性上限。如果工作评估(workup)估计该变更将超过 1,000 行代码(包括测试),Fellow 必须将其分解为带有依赖图的子问题。任何本身会超过 1,000 行的子问题都要再次分解。设定上限的目的是让每个变更都足够小,使审查者——无论是智能体还是人类——都能将其完整地装进脑子里。Db2 冲刺从一次准入产生了 32 个问题。数万行代码无法由一个智能体(或人类)令人信服地审查,因此医院通过拆分工作来让审查变得更容易。

记录。每个医院问题都记录它产生的一切:工作评估计划;Fellow 在动代码之前编写的复现测试,以及它失败的记录;治疗方案及其每一次修订;每一次 I-PASS 交接;每一个带有推理过程的审查结论;每个阶段的临床记录,包含模型、token 数、时长和成本。记录存放在一个伴随的归档分支中,与代码一起合并,这样 PR diff 就只保留代码,因而更易于审查。五个月后,我们可以仅凭问题线程就轻松重建 1,238 个 PR 中任何一个被合并的原因,而安全部门的每周审查之所以可能,正是因为这条轨迹存在。一丝不苟的记录保存最终被证明是医院隐喻带给我们最强大的东西之一。

我们改变了什么,或者仍需要改变什么Copy Icon

模型有时做得太多。让角色奏效的同一套提示词也让它们变得官僚化。Fellow 对某个问题的计划只是“作为重复项关闭;PR#691已经发布了修复。" 计划审查确认这是正确的,但仍然升级了,因为"审查出席计划协议的三个记录结果(APPROVE / REJECT / ESCALATE)不包括'作为重复关闭'"。一个确认的重复需要人工处理(我们后来已经解决了)。一个单行的 UI 文案编辑,将副标题中的"Validating"改为"Verifying",经历了五轮返工,并到达了 Chief 那里,讨论一个低风险 PR 是否可以在不稳定的 CI 运行变绿之前合并(智能体经常尝试在 CI 红灯时合并,理由是这不是它们的问题导致的)。一次代码审查阻止了一个 PR,尽管没有任何代码缺陷,理由是 PR 描述中有一个小拼写错误。当审查者留下非阻塞性的小问题时,流水线倾向于打开新问题来跟踪它们(后来也已修复),将一个小评论变成了一个新患者。其中一些我们已经解决,但另一些是遵循自身规则并将质量作为第一优先级的系统的代价。

审查需要一个断路器。问题 1777 被标记为紧急,修复只有一行:在一个代码路径中调用一个现有函数,就像三个同级路径已经做的那样。它花了十一轮返工,历时两天,首先是由于评论卫生,然后是 PR 描述中的虚假声明,然后是与原始计划的微小偏差,然后是测试质量,两次 rebase,两次失败的工作流,以及一个人工批准门。任何超过三轮返工的事情都会得到安全审查,在一个安全审查窗口中,三个复杂问题分别经历了九轮、九轮和七轮,并消耗了该窗口医院花费的 $1,046 中的约 $631。同一安全审查记录到代码审查过程没有"可计数的非收敛断路器",因此非收敛循环"升级得晚或从不升级"。它提出的修复最近已合并——现在,一旦一个 PR 经历了连续四轮返工且每轮都出现新的阻塞性发现,或六轮任何类型的实质性轮次,审查者必须移交给主治医师。虽然这最近才合并,我们也没有好的数据来证明其有效性,但早期迹象似乎是积极的。

这家医院产出很多,而且并不总是好事。这家医院很擅长给自己找活干。如上所述,近一半的 issue 都是由智能体提交的。研究部门(Research Department)负责调研代码库并提出新功能,最初每三小时运行一次,它产生的 issue 数量多到我们根本来不及审核是否接纳。我们先是把它限流到每天一次,然后又把它那些等待接纳的提案数量上限设为十个。问题在于,它发现的问题大多确实不错。我们经常在用交互式 Claude 会话处理代码库中的某个问题时,它会把我们引向这家医院几周(甚至几个月)前提交的同一个 issue。话虽如此,把它们全部推进下去既昂贵又耗时,而且价值存疑。从河里淘出金块仍然是难点,也是我们尚未解决的问题。

我们建了一所教学医院,但它其实并不怎么教学。MOLT Sinai 正是你期望由两位资深工程师(合计拥有超过 40 年的行业经验)打造出来的东西。这个模型极其强大,能在我们睡觉时处理大量 issue,到了早上再快速审查。然后,我们可以迅速给这家医院加载更多 issue 来解决,而我们白天开会时,可以在离开前审查并结清它们。一整天开会现在能带来 10 个高质量合并的 PR。但对于一个日历并不排满、没有数十年经验所换来的软件工程判断力、并且渴望深入学习如何成为更强工程师的应届毕业生来说呢?这个模型对他们来说效果如何?简而言之,它并不能完全满足他们的需求。我们目前正在对这家医院进行增强,让人类可以接手已分诊的 issue,制定自己的计划,并让医院的智能体进行审查。作为审查的一部分,他们会被要求为自己的决定辩护,并被挑战去思考可能产生更全面解决方案的其他选项。同样,当面对一份已审查的计划时,人类可以选择自己写代码(小修复可以手写,或者借助交互式 LLM 驱动的编码会话),以便学习如何浏览代码库、实时做出实现上的权衡,并根据其在最终代码审查中的表现接受衡量。我们需要培养未来的软件工程师,而且无论十年后的智能体编码世界是什么样子,我们仍然需要具备良好判断力和批判性思维能力的人。

下一步Copy Icon

我们一直在努力提升医院内智能体的效率、速度和准确性。此外,我们也在努力让医院对更初级的工程师来说更有参与感、更具教育意义并能锻炼技能。哦,我们还在确认 IBM Db2 支持是否正确,因为在最初那次测试期间,我们并没有审查医院编写的任何代码。最后,我们正在进行一系列实验,看看要让医院重新进入完全自主模式需要什么条件,至少对某些类别的问题而言是这样。如果你有兴趣帮助我们解决上述一个或多个问题,我们很乐意听到你的消息。