Cockroach Labs 用教学医院模式跑了五个月编码智能体
Five months treating bugs like patients and coding agents like a medical team
Cockroach Labs 把编码智能体流水线设计成教学医院 MOLT Sinai,用角色分工、计划评审和强制交接来保证代码质量。五个月里该流水线在镜像仓库合并了 1238 个 PR、落地超过一百万行代码,消耗约 13.5 万美元 Claude token,平均每个 issue 约 84 美元。
在四月的某个周三晚上到周五下午之间,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:
随后一个规划智能体读取了它,判断其规模太大无法一次性处理,于是将其分解为十五个子 issue,并附有明确的依赖图:先是基础部分,然后是类型系统,接着是行迭代器,然后是 Fetch,再是 Verify,之后是 Convert,再之后是 CI,最后是测试数据。其中两个子 issue 在各自的工作评估中被判定为过大,于是又被进一步分解。在此过程中,智能体们还针对自身工作提交了另外十几个 issue:夹具缺口、一个类型映射 bug,以及一个隔离级别修复。当父 issue 关闭时,其下已开启了 32 个子 issue,合并了 27 个拉取请求,审查者退回工作 55 次,升级了 9 个 issue,其中两个升级给了人类。以 PostgreSQL(我们测试最充分的方言)为基准衡量的测试覆盖率是相当的。
但这一切是如何发生的?是什么在驱动这一切,更重要的是,是什么在确保最终产出的质量很高?在我们为 Db2 支持开启 issue 之前,我们首先构建了一个编码医院,我们称之为 MOLT Sinai。MOLT 是 CockroachDB 迁移工具集的名称,而一旦我们决定将流水线建模为医院,MOLT Sinai 这个混成词就太妙了,让人无法抗拒。这是因为西奈山(Mount Sinai)是我们所居住的城市多伦多和纽约的著名教学医院。这个名字保留了下来,相关词汇也是如此。Issue 是病人。合并是出院。负责的人类是医学主管。
智能体快速编写大量代码并非这个故事独有,因为软件工厂正在整个行业涌现(比如 OpenAI 的这个)。我们案例的独特之处在于,那些智能体与主干之间隔着什么,我们为什么把它构建得像一家医院,以及当我们用它在真实工作上运行了五个月后发生了什么。
为什么是教学医院
编码智能体的问题在于,尽管它们完全有能力编写代码,但很难信任它们能产出经过良好测试、可维护且符合预期范围的生产级代码。智能体的运行成本相对较低,但当它们的代码要交到客户手中时,这一点并不能让人安心。我们宁愿智能体运行更长时间(甚至 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 能够找到并解析它。
技能中编码的几条基本医院规则提供了大部分安全保障:
没有计划不写代码,没有审查不算计划。在 Fellow 修复任何东西之前,它必须复现问题、进行鉴别诊断(借用医院术语)、运行任何能缩小范围的针对性调查,并发布治疗方案:诊断、要更改的文件、要添加的测试、风险、未决问题。一位 Review Attending——一个不同的 agent 实例,运行着以挑错而非修复为核心的 prompt——会阅读该计划并批准或拒绝它。只有到这时才开始治疗。我们添加这道关卡,是因为一个自信地实施错误修复的 agent,比一个停下来检查自己计划的 agent 浪费的时间和 token 要多得多。
不要即兴发挥。如果治疗过程中范围发生变化,Fellow 会停下来,发布修订后的计划,并回到计划审查环节。技能文件中的指令只有两个词:“不要即兴发挥。”
不要走捷径,尤其是在测试方面。agent 不得为了让测试通过而禁用、跳过、削弱或修改测试。如果测试失败,在证明并非如此之前,测试就是正确的。如果 agent 确实认为测试有误,对测试的更改必须作为独立的 commit,基于其自身的合理性进行论证,并与修复分开审查。同样,一个实际上并未覆盖该变更的通过测试,比没有测试更糟。所有代码审查都从禁用该变更并确保所有新测试失败开始。如果它们没有失败,那么它们要么超出范围,要么就是错误的。
绝不胡乱挣扎。当 Fellow 卡住时,它不会更用力地尝试。它会写一份 I-PASS(病情严重程度、患者摘要、行动清单、态势感知、综合)交接,这是一种直接借鉴自教学医院的结构化格式,然后上报给 Attending。接收方 agent 在开始工作前,必须写下它对交接内容的理解。医院中关于 I-PASS 的研究报告称,如果医生和护士以标准化方式写下患者信息,交接班期间的不良事件可减少 50%。我们的 agent 遵循同样的协议。
出院护士审计审查者。合并前的最后一道关卡不会重新审查代码。它验证审查是否恰当进行:存在批准、审查模板已填写、没有未解决的讨论串、CI 为绿色、commit 历史干净。它会发布一份清单,准确显示它验证了什么;如果任何一项无法勾选,出院就会失败,该项保持未勾选并附有注释。这条规则之所以存在,是因为当你把控制权交给 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 就只包含代码,因此更容易审查。五个月后,我们可以仅凭 issue 线程轻松重建 1,238 个 PR 中任何一个被合并的原因,而安全部门的每周审查之所以可能,正是因为这条轨迹存在。细致的记录保存最终成为医院隐喻带给我们最强大的东西之一。
我们改变了什么,或者仍然需要
模型有时做得太多。让角色发挥作用的同一套提示词也让它们变得官僚化。Fellow 对某个问题的计划只是“作为重复项关闭;PR #691 已经发布了修复。”计划审查确认这是正确的,但仍然升级了,因为“审查-主治-计划-协议的三个记录结果(APPROVE / REJECT / ESCALATE)不包括‘作为重复项关闭’”。一个已确认的重复项需要人工处理(我们后来已经解决了这个问题)。一行 UI 文案编辑,将副标题中的“Validating”改为“Verifying”,经历了五轮返工,并到达了 Chief 那里,争论一个低风险 PR 是否可以在不稳定的 CI 运行变绿之前合并(代理经常尝试在 CI 红灯时合并,理由是这不是它们的问题导致的)。一次代码审查以 PR 描述有一个小拼写错误为由,阻止了一个完全没有代码缺陷的 PR。当审查者留下非阻塞性的小问题时,流水线倾向于打开新问题来跟踪它们(后来也已修复),将一条小评论变成了一个新病人。其中一些问题我们已经解决,但有些是系统遵循自己的规则并将质量作为第一优先级的代价。
审查需要一个断路器。Issue 1777 被标记为紧急,修复只有一行:在一个代码路径中调用一个现有函数,就像三个同级路径已经做的那样。它花了两天时间进行了十一轮返工,首先是因为评论卫生,然后是 PR 描述中的虚假声明,然后是与原始计划的微小偏差,然后是测试质量,两次 rebase,两次失败的工作流,以及一个人工批准门。任何超过三轮返工的事情都会得到安全审查,在一个安全审查窗口中,三个复杂问题分别进行了九轮、九轮和七轮,消耗了该窗口医院花费的 1,046 美元中的约 631 美元。同一份安全审查记录指出,代码审查过程没有“可计数的非收敛断路器”,因此不收敛的循环“要么延迟升级,要么从不升级”。它提出的修复最近已合并——一旦 PR 连续经历四轮返工且每轮都出现新的阻塞性发现,或六轮任何类型的实质性返工,审查者现在必须移交给主治医生。虽然这最近才合并,我们也没有好的数据来证明其有效性,但早期迹象似乎是积极的。
这家医院产出很多,但并不总是好事。这家医院很擅长为自己找活干。如上所述,近一半的 issue 都是由 agent 提交的。研究部门负责调研代码库并提出新功能,最初每三小时运行一次,它创建的 issue 数量之多,以至于我们无法及时审核接纳。我们先是将其限制为每天一次,然后又将其可等待未接纳的提案数量上限设为十个。问题在于,它发现的 issue 大多质量不错。我们经常在用交互式 Claude 会话处理代码库中的某个问题时,它会把我们引向这家医院几周(甚至几个月)前提交的同一个 issue。话虽如此,把它们全部推进下去既昂贵又耗时,而且价值存疑。从河里淘出金块仍然是难点,也是我们尚未解决的问题。
我们建了一家并不真正教学的 teaching hospital。MOLT Sinai 正是你预期由两位资深工程师(合计拥有超过 40 年行业经验)构建出来的东西。这个模型极其强大,能在我们睡觉时处理 issue,早上再快速审核。然后,我们可以迅速给医院加载更多 issue 去解决,而我们白天开会、审核、下班前结清。一整天开会现在能带来 10 个高质量合并的 PR。但那些日程不满、没有数十年经验带来的软件工程判断力、又渴望深入学习如何成为更强工程师的应届生呢?这个模型对他们来说如何?简而言之,它并不能完全满足他们的需求。我们目前正在对医院进行增强,让人类可以接手已分诊的 issue,制定自己的计划,并交由医院 agent 审核。作为审核的一部分,他们会被要求为自己的决定辩护,并被挑战去思考可能产生更全面解决方案的其他选项。同样,当面对一份已审核的计划时,人类可以选择自己写代码(小修复手写,或借助交互式 LLM 驱动的编码会话),以便学习如何浏览代码库、实时做出实现权衡,并在由此产生的代码审核中接受衡量。我们需要培养未来的软件工程师,无论十年后 agentic 编码世界是什么样子,我们仍然需要具备良好判断力和批判性思维能力的人。
下一步
我们一直在努力提高医院中 agent 的效率、速度和准确性。此外,我们正在努力让医院对更初级的工程师来说更有参与感、更具教育性并能培养技能。哦,我们还在确认 IBM Db2 支持是否正确,因为在最初测试期间,我们没有审核医院编写的任何代码。最后,我们正在进行一系列实验,看看需要什么才能让医院重新进入完全自主模式,至少对某些类别的 issue 如此。如果你有兴趣帮助我们解决上述一个或多个问题,我们很乐意收到你的来信。
来源:Hacker News · cockroachlabs.com


