Cockroach Labs 用「医院」式多智能体流水线五个月跑通 Db2 迁移,两天完成原本九个月的工作
Five months treating bugs like patients and coding agents like a medical team
Cockroach Labs 把编码智能体流水线建模为「教学医院」MOLT Sinai,用 GitHub Actions 标签驱动角色分工,五个月内合并 1,238 个 PR、落地超百万行代码。
在四月的某个周三晚上到周五下午之间,MOLT——Cockroach Labs 为将数据库迁移到 CockroachDB 而提供的工具——获得了从 IBM Db2 源迁移的能力。
Db2 是最早商用化的关系型数据库之一,拥有丰富的 SQL 方言、复杂的类型系统和原生线协议。在 MOLT 中支持它意味着需要一个新的模式转换器、一条用于拉取行并将其导入 CockroachDB 的新 Fetch 路径、一条用于在加载后进行比较的新 Verify 路径、一份内置的 ANTLR 语法、一个用于 CI 的 Docker 镜像,以及超过一万行的测试夹具。当我们在 2024 年为 MOLT 工具添加 Oracle 支持时,同等的工作耗时 9 个月,工程时间成本约为 16 万美元。
Db2 用了不到两天,而且没有任何人写过一行代码。Token 账单为 4172 美元:快了 164 倍,便宜了 38 倍。
这一切都始于一个描述该需求的 GitHub issue:
随后一个规划 agent 读取了它,判定其规模太大无法一次性处理,于是将其分解为十五个子 issue,并附有明确的依赖图:先是基础部分,然后是类型系统,接着是行迭代器,然后是 Fetch,接着是 Verify,然后是 Convert,接着是 CI,最后是测试数据。
其中两个子 issue 在各自的工作分解中被判定为规模过大,于是又被进一步分解。在此过程中,agent 们还针对自身工作另外提交了十几个 issue:夹具缺口、一个类型映射 bug,以及一个隔离级别修复。
当父 issue 关闭时,其下已开启了 32 个子 issue,合并了 27 个 pull request,审查者将工作打回了 55 次,并有 9 个 issue 被升级处理,其中两个升级给了人类。
以 PostgreSQL(我们测试最充分的方言)为基准衡量,测试覆盖率相当。
但这一切是如何发生的?是什么在驱动它,更重要的是,是什么在确保最终产出的质量足够高?在为 Db2 支持开启 issue 之前,我们首先建造了一家编码医院,我们称之为 MOLT Sinai。MOLT 是 CockroachDB 迁移工具集 的名称,而一旦我们决定将流水线建模为一家医院,MOLT Sinai 这个混成词就太妙了,让人无法抗拒。这是因为西奈山(Mount Sinai)是我们所居住的两座城市——多伦多和纽约——中一家著名的教学医院。这个名字被保留了下来,随之保留的还有这套词汇。Issue 是病人。合并是出院。负责的人类是医学主任。
Agent 快速编写大量代码并非这个故事独有,因为软件工厂正在整个行业遍地开花(比如 OpenAI 的这个)。我们案例的独特之处在于,这些 agent 与主干之间隔着什么,我们为什么把它建成一家医院的样子,以及当我们用它在真实工作上运行五个月时发生了什么。
为什么是教学医院
编码 agent 的问题在于,尽管它们完全有能力编写代码,但很难信任它们能产出经过良好测试、可维护且符合预期范围的生产级代码。运行 agent 的成本相对低廉,但当它们的代码要落到客户手中时,这一点并不能让人安心。我们宁愿让 agent 运行更长时间(哪怕长 10 倍),也不愿在客户向 CockroachDB 的迁移中落入错误的数据。 速度很重要,但质量重要得多得多。
在过去一年里,出现了若干尝试将智能体编码自动化的方案,其中许多都针对吞吐量进行了优化。例如,Gas Town 会针对一个代码库并行运行许多智能体,并带有一个合并处理层来解决冲突。当瓶颈在于代码编写速度时,这是一种很好的设计。还有 Cursor 的那个重建 SQLite 的实验,速度快到他们需要编写自己的版本控制系统来处理变更量。这是一个有趣的实验,但显然它并非旨在向用户交付高质量的数据库代码。在绝大多数早期的智能体自动化实验中,速度是主要驱动力。
我们的动机不同。我们构建的是一个分布式 SQL 数据库,以及一套将工作负载迁移到该数据库的工具。迁移工具中一个细微的 bug 可能会在数据进入一个本应以正确性为最高优先级的数据库的过程中损坏某人的数据。因此,我们关心的不是“每小时能产出多少个 PR?”,而是“这些 PR 中有多少是我们自己都不好意思合并的?”有了这种重新定义,很明显,一个为吞吐量优化的智能体集群并不是一个好的模型。
于是我们寻找一种模型,它拥有管理由能力参差不齐的员工完成的高风险工作的长期经验,有强制性的交接、强制性的第二意见,以及关于交接草率时会出现什么问题的文献。我们最终选定了教学医院。
这个比喻给了我们以下角色:
围绕这个核心,还有如果你进过医院(或者看过《匹兹堡》)就会预期看到的其他角色:一位每三十分钟巡房一次、寻找卡住患者的护士长,一个在 main 分支出问题时可以锁定大楼内所有工作流的感染控制智能体,一个撰写每周流程审查并召开事后会议的安全部门,以及一个提出新工作的研究部门。
我们并不声称这比集群更便宜,或比单个智能体更简单。然而,它确实能产出比两者质量更高的产品。当你身处数据库行业时,你愿意为这种质量付出代价。正如你将在下面看到的,它也带来教学医院的一些令人遗憾的方面(官僚主义、等待时间和成本),但我们认为这些是值得的权衡。
它是如何构建的
医院中的一切都运行在 GitHub Actions 上。每位患者的状态都存在于其 issue 上的标签中,以及医院为跟踪每位患者进展而创建的一些临时文件中。
每个阶段都是一个 GitHub 工作流,当其“输入标签”落到 issue 上时启动。当智能体完成时,它会写入一个“输出标签”,该标签就是下一阶段的“输入标签”。虚线箭头将工作送回:被拒绝的计划送回检查,红色的 CI 运行或请求的更改送回治疗,卡住的阶段送回给主任。
未显示:四个部门按照自己的时间表在此流程之外运行。护士长重新运行停滞的阶段,感染控制在 main 分支出问题时锁定医院,而研究和安全部门提交新的 issue 和政策 PR,它们像任何其他患者一样通过主任进入。
应用标签会触发一个 GitHub workflow,它会加载特定角色的 prompt,使 agent 扮演医院中的一名员工。完成工作后,agent 会向包含该 issue 病历的文件中添加一条结构化备注,并应用流程中的下一个标签。该备注带有一个类似 <!-- SINAI:TREATMENT_PLAN --> 的哨兵标记,以便后续 agent 能够找到并解析它。
一些编码在 skills 中的基本医院规则提供了大部分安全保障:
没有计划不写代码,没有审查不算计划。 在 Fellow 修复任何东西之前,它必须复现问题、进行鉴别诊断(用医院术语来说)、运行任何能够缩小范围的针对性调查,并发布一份治疗方案:诊断、要更改的文件、要添加的测试、风险、未决问题。一位 Review Attending——一个不同的 agent 实例,运行着以挑错而非修复为核心的 prompt——会阅读该计划并批准或拒绝它。只有到那时才开始治疗。我们添加这道关卡,是因为一个自信地实施错误修复的 agent,比一个停下来检查自己计划的 agent 浪费的时间和 token 要多得多。
不要即兴发挥。 如果治疗过程中范围发生变化,Fellow 会停下来,发布修订后的计划,并回到计划审查环节。skill 文件中的指令只有两个词:“Don't improvise.”
不要走捷径,尤其是在测试方面。 agent 不得为了让测试通过而禁用、跳过、削弱或修改测试。如果测试失败,在证明并非如此之前,测试就是正确的。如果 agent 确实认为测试有误,对测试的更改必须作为单独的 commit,基于其自身的合理性进行论证,并与修复分开审查。同样,一个实际上并未覆盖该变更的通过测试,比完全没有测试更糟。所有代码审查都从禁用该变更并确保所有新测试失败开始。如果它们没有失败,那么它们要么超出范围,要么不正确。
绝不胡乱挣扎。 当 Fellow 卡住时,它不会更加努力。它会写一份 I-PASS(病情严重程度、患者摘要、行动清单、态势感知、综合)交接,这是一种直接借鉴自教学医院的结构化格式,然后上报给 Attending。接收的 agent 必须先写下它对交接内容的理解,然后才能开始工作。关于 I-PASS 的研究在医院中报告称,如果医生和护士以标准化方式写下患者信息,交接班期间的不良事件可减少 50%。我们的 agent 遵循同样的协议。
Discharge Nurse 审查审查者。 合并前的最后一道关卡不会重新审查代码。它验证审查是否恰当进行:存在批准、审查模板已填写、没有未解决的讨论串、CI 为绿色、commit 历史干净。它会发布一份清单,准确显示它验证了什么;如果任何一项无法勾选,discharge 就会失败,并留下该项未勾选并加以注释。这条规则之所以存在,是因为当你把控制权交给 agent 时,你想确保它们在按吩咐行事,而有时它们并没有。
在打扰 Chief 之前先检查先例。 人类 Chief 做出的每一个可推广的决定,都会记录在一个仅追加的先例日志中。想要上报的 agent 必须先阅读该日志;如果有适用的先例,它就遵循该先例并引用它,而不是上报。只有人类才能创建先例。在五个月里,这减少了 agent 上报给 Chief 的频率。
它交付了什么
当我们开始开发 MOLT Sinai 时,我们创建了一个用于构建 MOLT 工具的仓库镜像。这个镜像随后成为了一个试验场,我们可以在其中测试医院模型的有效性。以下是 4 月 21 日至 9 月 11 日期间该仓库中发生的事情:
在不到 5 个月的时间里,医院流水线提交了远超一百万行的代码,这些代码被拆分成足够小的变更,以便智能体能够自信地进行审查。
你可能会注意到上表中一个奇怪的数字:1299。仓库中提交的近一半 issue 是由医院为医院创建的:分解子任务、Fellow 从计划中划出的后续工作、研究部门提出的提案、安全部门的纠正措施。流水线会生成自己的待办事项。由人类决定哪些被接纳,但到了第二个月,人类已不再是工作的主要来源。
虽然 Db2 支持是我们送进医院的第一项大型功能,但那只是一次实验。在自主运行的第一周之后,我们开启了“人工审批模式”,该模式要求每次合并都必须经过人工审查。只有在这种模式下,我们才能确信代码质量足够高,可以交付给客户。有了医院这层额外的安全保障,我们开始着手我们创建医院的真正目的:构建 Migration Assistant,这是一个基于 AI 的工具,引导用户完成从 Postgres 到 CockroachDB 的完整迁移,包括模式转换、数据加载与验证以及常规转换。Migration Assistant 目前处于 Postgres 迁移的预览阶段,几乎完全由医院的工作人员编写。
它花费了多少
自 4 月份 MOLT Sinai 开始运行以来,它消耗了略超 135,000 美元的 Claude token,平均每位患者(即 GitHub issue)约 84 美元。Issue 通常在医院中停留一到两天,其中大部分时间往往花在等待人工审查者上。在 LLM 模型方面,医院随着模型的进步而不断演进。
起初,流水线的每个阶段都运行 Opus 模型,但随着 Fable 的引入,现在它使用 Fable 进行规划,而 Fable 生成的计划会选择用于实现的模型。在 Sonnet 或 Opus 模型实现遇到困难的情况下,它可以升级使用 Fable 模型。这有助于降低成本,同时在必要时仍能利用更昂贵(且更强大)的模型。
如上图所示,每次模型切换都需要做一些工作来调整智能体技能以优化成本。我们每几周进行一次连续的优化轮次,这也是上图波动的原因之一。还有其他原因,例如特定时期的工作组合(UI 任务成本更高,因为智能体必须处理截图和其他视觉数据)以及我们 CI 环境中的一些特殊情况,但这些细节太过琐碎,不适合在入门博客文章中深入讨论。
医院构建了自身
当我们创建 MOLT Sinai 时,我们直接将初始的工作流、技能和提示构建到了镜像仓库中。不久之后,我们意识到了我们所构建之物的价值,并猜测公司中的其他人也会想要利用它。
为了实现这一点,我们将“Sinai”代码(实际的医院流水线)从 MOLT Sinai 仓库(医院流水线的第一个消费者)中拆分出来,放入一个单独的仓库,其他人如果希望利用医院模型,可以将其嵌入自己的仓库。
如今,Sinai 仓库大约有 1,570 次提交。其中大约 1,340 次——85%——是由流水线本身创作的。
我们并没有计划这样做。框架有 bug,我们像处理其他任何问题一样把它们送进医院。每三十分钟查房一次的 Charge Nurse 工作流之所以存在,是因为一个 agent 在 GitHub Actions 标签事件中遇到了竞态,为此提交了一个 issue,另一个 agent 则设计并交付了修复。这也是快速演进医院模型最便捷的方式,而且随着几乎所有患者出院,医院模型已经变得更加健壮、高效且易于使用。
如今,Sinai 模型已在 Cockroach Labs 的四个仓库中落地,另有几个仓库计划在未来几个月内采用它。
指令变成了遗留代码
每个 agent 角色由一个技能文件定义:一份包含流程、规则、模板和禁止行为的 Markdown 文档。它们的编写方式与知识库常见的做法一样,每当出问题时就添加一条规则。
7 月我们对它们进行了审计。二十五个技能文件,略多于 100,000 词。其中约 23,000 词,23%,被标记为可移除,而不会改变任何一道门、命令、模板或哨兵。主要模式是同一条规则在多个地方重复表述:在七十项发现中共有六千词。有一个文件有百分之四十七的冗余。
这种冗余让我们付出了真金白银和时间。医院工作人员会同时加载多个技能。Discharge Nurse 在每次运行中都会加载约 21,800 词的指令,大约 29,000 个 token,而这时它还没有读到它本该检查的 PR 的一行内容。基础 hospital-protocol 技能被全部十五个 agent 加载,因此那里的冗余代价最大。
我们怀疑这并非 MOLT Sinai 所特有。我们的提示词像代码一样腐化了:我们不断添加规则,却从不移除它们,直到文件长到没人能从头到尾读完。我们只是因为 token 账单才注意到这一点。我们目前还没有针对此的 linter,但我们很可能很快就会创建一个。
经验教训
与 MOLT Sinai(以及 Sinai 模型)合作五个月让我们学到了很多。以下是一些比较有趣的要点。
哪些做法有效
模型的表现超出预期。每个角色的技能文件都以几句话开头来界定该角色,我们发现这几句话最有价值。Fellow 的开头是:“你是一名 Fellow,正在对一名入院患者(GitHub issue)进行诊断性检查。你的工作是彻底调查并撰写一份另一位 Fellow 可以执行的治疗计划。”Review Attending 的开头是:“你是与治疗此病例的 Fellow 不同的另一个 agent 实例。你的工作是发现 bug、安全问题、回归风险以及与已批准计划的偏差。”这奠定了基础,因为底层 LLM 非常清楚教学医院如何运作、什么是鉴别诊断、为什么交接要有结构,以及第二意见意味着什么。当你告诉 LLM 要扮演一个人设时,它会做得非常非常好。举个例子,一位 Review Attending 在一个已经经过好几轮审查的 PR 上发现了一行 docstring 缺陷,并拒绝凭自己的权限将其退回重写,写道这是“将其提交给你裁决,而不是在患者已经精疲力竭的情况下,仅因一个 docstring 修复就单方面把病例退回给 Fellow。”在另一个案例中,一位 Attending 在决定是否重新分解一个 Chief 已经裁定过的 issue 时写道:“一个由 Chief 拥有的决策,如果其结果推翻了其前提,应归还给其所有者,而不是在其周围即兴发挥。”这些句子都没有直接出现在技能文件中。相反,agent 们写出它们是因为它们在扮演一个角色。
代码之前先审查计划。经过审查的计划被拒绝的比例不到 10%,但当它们被拒绝时,这是系统中价值最高的审查,因为它将返工转移到尽可能最早的时点。在一个例子中,对一项遥测修复的计划审查——该修复旨在从我们捕获的日志中移除客户的 hostname 和数据库名——发现了一个小漏洞,信息仍可能被暴露。如果这没有在计划审查中被发现,它极不可能在代码审查中被发现,而该问题要么会被发布出去,要么需要第二个 issue 来解决。在规划时发现 bug 能节省未来大量的工作。
分解,并设有硬性上限。如果一次检查估计该变更将超过 1,000 行代码(包括测试),Fellow 必须将其分解为带有依赖图的子 issue。任何本身会超过 1,000 行的子 issue 都要再次分解。设置上限的目的是让每个变更都足够小,以便审查者——无论是 agent 还是人类——能够将其全部装进脑子里。Db2 sprint 从一次入院中产生了 32 个 issue。数万行代码无法由单个 agent(或人类)令人信服地审查,因此医院通过拆分工作来让事情变得更容易。
记录。每个医院问题都记录它产生的一切:检查计划;Fellow 在接触代码之前编写的复现测试及其失败记录;治疗方案及其每一次修订;每一次 I-PASS 交接;每一个带有推理的审查结论;每个阶段的临床记录,包括模型、token 数、时长和成本。记录存放在一个伴随的归档分支中,与代码一起合并,这样 PR diff 只保留代码,因此更易于审查。五个月后,我们可以仅凭问题线程轻松重建 1,238 个 PR 中任何一个被合并的原因,而安全部门的每周审查之所以可能,正是因为这条线索存在。细致的记录保存被证明是医院隐喻带给我们最强大的东西之一。
我们改变了什么,或者仍然需要
模型有时做得太多。让角色发挥作用的同一套提示词也让它们变得官僚化。Fellow 对某个问题的计划只是“作为重复关闭;PR #691 已经发布了修复。”计划审查确认这是正确的,但还是升级了,因为“审查-主治-计划-协议的三个记录结果(APPROVE / REJECT / ESCALATE)不包括‘作为重复关闭’”。一个已确认的重复需要人工处理(我们后来已经解决了)。一行 UI 文案编辑,将副标题中的“Validating”改为“Verifying”,经历了五轮返工,并到达 Chief 那里,争论一个低风险 PR 是否可以在不稳定的 CI 运行变绿之前合并(代理经常尝试在 CI 红灯时合并,辩称这不是它们的问题导致的)。一次代码审查阻止了一个完全没有代码缺陷的 PR,理由是 PR 描述有一个小拼写错误。而当审查者留下非阻塞性的小问题时,流水线倾向于打开新问题来跟踪它们(后来也已修复),将一条小评论变成一个新病人。其中一些我们已经解决,但有些是遵循自身规则并将质量作为第一优先级的系统的代价。
审查需要一个断路器。问题 1777 被标记为紧急,修复只有一行:在一个代码路径中调用一个现有函数,就像三个同级路径已经做的那样。它花了两天时间进行了十一轮返工,首先是因为评论卫生,然后是 PR 描述中的虚假声明,然后是与原始计划的微小偏差,然后是测试质量,两次 rebase,两次失败的工作流,以及一个人工批准门。任何超过三轮返工的事情都会得到安全审查,在一个安全审查窗口中,三个复杂问题分别进行了九轮、九轮和七轮,消耗了该窗口医院花费的 $1,046 中的约 $631。同一次安全审查记录到,代码审查过程没有“可计数的非收敛断路器”,因此不收敛的循环“升级得太晚或从不升级”。它提出的修复最近已合并——现在,一旦一个 PR 连续四轮返工且每轮都出现新的阻塞性发现,或者六轮任何类型的实质性返工,审查者必须移交给 Attending。虽然这最近才合并,我们也没有好的数据来证明其有效性,但早期迹象似乎是积极的。
这家医院产出很多,但并不总是好事。这家医院很擅长为自己找活干。如上所述,近一半的 issue 都是由 agent 提交的。研究部门负责调研代码库并提出新功能,最初每三小时运行一次,它创建的 issue 数量多到我们无法及时审核接纳。我们先是将其限制为每天一次,然后又将其可等待未接纳的提案数量上限设为十个。问题在于,它发现的 issue 大多是好的。我们经常在用交互式 Claude 会话处理代码库中的某个问题时,它把我们指向医院几周(甚至几个月)前提交的同一个 issue。话虽如此,把它们全部推进下去既昂贵又耗时,而且价值存疑。从河流中淘出金块仍然是困难的部分,也是我们尚未解决的问题。
我们建了一所教学医院,但它并不真正教学。MOLT Sinai 正是你预期由两位资深工程师(合计拥有 40 多年行业经验)所构建的东西。这个模型极其强大,能在我们睡觉时处理 issue,早上再快速审核。然后,我们可以迅速给医院加载更多 issue 让其解决,而我们白天开会、审核并在下班前结清它们。一整天的会议现在可以带来 10 个高质量合并的 PR。但对于那些日程不满、没有数十年经验带来的软件工程判断力、并且渴望深入学习如何成为更强工程师的应届毕业生呢?这个模型对他们来说如何运作?简而言之,它并不能完全满足他们的需求。我们目前正在对医院进行增强,让人类可以接手已分诊的 issue,制定自己的计划,并交由医院 agent 审核。作为审核的一部分,他们将被要求为自己的决定辩护,并被挑战去思考可能产生更全面解决方案的其他选项。同样,当面对一份已审核的计划时,人类可以选择编写代码(小修复可手写,或借助交互式 LLM 驱动的编码会话),以便学习如何浏览代码库、实时做出实现权衡,并在最终的代码审查中根据表现接受评估。我们需要培养未来的软件工程师,无论十年后 agent 编码世界是什么样子,我们仍然需要具备良好判断力和批判性思维能力的人。
下一步
我们一直在努力提高医院中 agent 的效率、速度和准确性。此外,我们正在努力让医院对更初级的工程师来说更有参与感、更具教育意义并能培养技能。哦,我们还在确认 IBM Db2 支持是否正确,因为在最初测试期间我们没有审查医院编写的任何代码。最后,我们正在进行一系列实验,看看需要什么才能让医院重新回到完全自主模式,至少对某些类别的 issue 如此。如果你有兴趣帮助我们解决上述一个或多个问题,我们很乐意收到你的来信。
来源:Hacker News · cockroachlabs.com


