五个月里,把 bug 当病人,把编码 agent 当医疗团队
四月的一个周三傍晚到周五下午之间,Cockroach Labs 的数据库迁移工具 MOLT 多了一项新能力:支持从 IBM Db2 迁移数据。
Db2 是最早商用的关系型数据库之一,SQL 方言丰富、类型系统复杂,还有自己原生的网络协议。要在 MOLT 里支持它,意味着得新写一个 schema 转换器、一条新的 Fetch 路径(把数据行从源库拉出来、灌进 CockroachDB)、一条新的 Verify 路径(导入后做比对),还要 vendor 一份 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 关闭时,它下面已经开了三十二个子 issue,合并了二十七个 pull request,审查者把工作打回去五十五次,还有九个问题上升级处理,其中两个交给了人类。以 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,是我们自己都不好意思合并的?”换了这个角度就很清楚:一个为吞吐量优化的智能体集群,并不是合适的模式。
于是我们去找一个模型:它要长期处理高风险工作,执行者能力参差不齐,有强制交接、强制复核,还有大量文献研究过交接马虎会出什么问题。最后我们选中了教学医院。
这个比喻给了我们这些角色:
在这个核心之外,还有你在医院里(或看过《匹兹堡医护前线》的话)会见到的那类角色:一个每三十分钟巡一遍、寻找卡住病人的值班护士;一个在主流程出问题时能锁掉整栋楼所有工作流的感染控制智能体;一个每周写流程复盘、开事故后会议的安全部门;还有一个提出新工作的研究部门。
我们并不声称这套方案比一群智能体更便宜,或者比单个智能体更简单。但它的产出质量确实高于两者。当你身处数据库这个行当,你是愿意为质量买单的。下面你会看到,它也带来了一些教学医院特有的糟心事(官僚流程、等待时间、成本),但我们认为这些代价是值得的。
它是怎么搭起来的
医院里的一切都跑在 GitHub Actions 上。每个“病人”的状态存在它的 issue 标签里,同时也存在医院为了追踪病人进展而创建的一些临时文件中。

每个阶段都是一个 GitHub workflow,当它的“输入标签”落到 issue 上时触发。智能体干完活后写一个“输出标签”,这个标签就是下一阶段的“输入标签”。虚线箭头表示工作被退回:被否决的方案退回检查阶段,CI 变红或被要求修改则退回治疗阶段,卡住的阶段则上报给主任。
图中未展示的部分:有四个部门按照自己的时间表在这个流程之外运行。护士长负责重跑卡住的阶段,感染控制部门在 main 分支挂掉时封锁整个医院,研究与安全部门则提交新的 issue 和政策 PR,这些和普通病人一样,都经由主任进入流程。
打上一个标签会触发一个 GitHub workflow,加载特定角色的提示词,让智能体扮演医院里的一名员工。干完活之后,智能体会在存有该 issue 病历的文件里追加一条结构化记录,然后打上流程中的下一个标签。这条记录会带一个类似 <!-- SINAI:TREATMENT_PLAN --> 的标记,方便后面的智能体找到并解析它。
技能里编进了几条基本的医院规则,安全保障主要靠它们:
没有方案不动手,方案不审不通过。 Fellow 在动手修任何东西之前,必须先复现问题,做一次鉴别诊断(借用医院里的说法),跑各种能缩小范围的针对性排查,然后贴出一份治疗方案:诊断结论、要改哪些文件、要加哪些测试、风险在哪、还有哪些待解的问题。接着由审查主治医师(Review Attending)来读这份方案——这是另一个 agent 实例,它的提示词专门围绕「挑毛病」而不是「修问题」来设计——负责批准或驳回。只有批了,治疗才开始。我们加这道关卡,是因为一个自信满满却把错东西改了的 agent,比一个肯停下来检查方案的 agent 要浪费更多时间和 token。
别即兴发挥。 治疗过程中如果范围变了,Fellow 要停下来,贴一份修订后的方案,重新走方案审查。技能文件里的指令只有两个词:「别即兴发挥。」
不走近道,尤其是测试。 agent 不得为了让测试通过而禁用、跳过、弱化或篡改测试。测试失败时,在被证明有错之前,测试就是对的。如果 agent 确实认为测试本身有问题,那对测试的修改必须单独提交,单独讲清理由,且和修复分开审查。同样的道理,一个通过了、但根本没覆盖到改动的测试,比没有测试还糟。所有代码审查的第一步,都是先禁用这次改动,确认所有新增测试确实失败。如果它们不失败,那要么是测试不在改动范围内,要么就是测试本身有错。
绝不瞎扑腾。 Fellow 卡住的时候,不会硬扛。它会写一份 I-PASS 交接单——I-PASS 指病情严重程度(Illness severity)、患者摘要(Patient summary)、行动清单(Action list)、情境认知(Situation awareness)、综合(Synthesis),这个结构化格式直接借自教学医院——然后升级给主治医师。接收方 agent 在开始干活之前,必须先把它对交接内容的理解写回来。医院里关于 I-PASS 的研究显示,如果医生和护士用标准化方式记录患者信息,交班期间的不良事件能减少 50%。我们的 agent 遵循同样的协议。
出院护士负责审计审查者。合并前的最后一道关卡不再重新审查代码,而是核实审查本身是否合规:审批记录是否存在、审查模板是否填写完整、有没有未解决的讨论串、CI 是否通过、提交历史是否干净。它会贴出一份清单,逐项说明自己核实了什么。只要有哪一项打不上勾,出院流程就会中止,并标注出未通过的那一项。设立这条规则的道理在于:当你把控制权交给智能体时,你总得确保它们在按吩咐办事——而有时候它们并没有。
打扰主任之前,先查判例。人类主任做出的每一个具有通用性的决策,都会记入一份只增不改的判例日志。智能体想上报问题时,必须先读这份日志;如果已有先例可循,就照先例办并注明引用,而不是上报。只有人类才能创建判例。五个月下来,智能体上报给主任的次数明显减少了。
它交付了什么
我们刚开始做 MOLT Sinai 时,先在构建 MOLT 工具的仓库里建了一个镜像。这个镜像后来成了试验场,用来检验医院模式到底管不管用。以下就是那个仓库从 4 月 21 日到 9 月 11 日之间发生的事:
在不到 5 个月里,这条医院流水线合入了远超一百万行的代码,而且每笔改动的规模都小到智能体可以放心审查。
你可能注意到上表里有个奇怪的数字:1299。仓库里报的 issue 有将近一半是「医院为医院」创建的:拆解出来的子任务、Fellow 从方案中划出的后续事项、研究部提出的建议、安全部给出的整改措施。流水线会自己产出待办清单。人类决定哪些可以进来,但到了第二个月,人类已经不再是工作的主要来源了。
Db2 支持是我们送进医院的第一个大型功能,但那只是一次实验。自主运行一周后,我们开启了“人工审批模式”,要求每一次合并都必须有人工过目。只有在这个模式下,我们才能确定代码质量足够高,可以交付给客户。有了这层额外的安全保障,我们开始着手做医院真正要服务的目标:打造 Migration Assistant(迁移助手)。这是一款基于 AI 的工具,引导用户完成从 Postgres 到 CockroachDB 的完整迁移,包括 schema 转换、数据加载与校验,以及常规转换。Migration Assistant 目前处于 Postgres 迁移的预览阶段,代码几乎全部由医院的“员工”编写。
成本
从 4 月 MOLT Sinai 开始运行到现在,它消耗的 Claude token 刚刚超过 135,000 美元,平均每个“患者”(也就是一个 GitHub issue)约合 84 美元。issue 通常在医院里待一到两天,大头往往是等人工审查的时间。在 LLM 模型方面,医院随着模型的进步不断演进。

最初每个流水线阶段都跑 Opus 模型,但随着 Fable 的推出,现在规划阶段用 Fable,Fable 生成的计划再决定实现阶段用哪个模型。如果用 Sonnet 或 Opus 实现阶段遇到困难,可以升级到 Fable 模型。这样既控制了成本,又能在必要时调用更贵、更强的模型。
正如上图所示,每次切换模型后,都需要花些功夫调整智能体的技能,把成本压到最优。我们每隔几周就做一轮优化,这也是曲线起伏不定的原因之一。其他原因还包括某个时间段内任务类型的混合(UI 任务更贵,因为智能体要处理截图和其他视觉数据),以及 CI 环境里的一些小毛病,但这些细节太琐碎,不适合在这篇入门博客里展开。
医院自己建起来了
搭建 MOLT Sinai 时,我们把最初的一批工作流、技能和提示词直接写进了镜像仓库。很快我们意识到这些东西很有价值,公司里其他人可能也想用。为了让大家都能用上,我们把“Sinai”代码(真正的医院流程)从 MOLT Sinai 仓库(医院流程的第一个使用者)里拆出来,放进一个独立仓库,别人想用医院这套模式,直接嵌进自己的仓库就行。如今 Sinai 仓库大约有 1570 次提交,其中约 1340 次——85%——是流水线自己写的。
这不是我们计划好的。框架本身有 bug,我们就像处理其他问题一样,把它们送进“医院”。负责每三十分钟查房的“主管护士”(Charge Nurse)工作流之所以存在,是因为有个智能体在 GitHub Actions 的标签事件里撞上了竞态条件,提交了 issue,另一个智能体设计并发布了修复。这也是快速迭代医院模式最省事的办法,而且几乎每个“病人”出院后,医院模型都变得更健壮、更高效、更好用。
如今 Sinai 模型已经在 Cockroach Labs 的四个仓库里落地,还有几个仓库计划在未来几个月跟进。
指令变成了遗留代码
每个智能体角色由一个技能文件定义:一份 Markdown 文档,里面写清流程、规则、模板和禁止行为。它们的写法跟知识库差不多——一出问题就加一条规则。
In July we audited them. Twenty-five skill files, a little over 100,000 words. About 23,000 of those words,23%, were flagged as removable without changing a single gate, command, template, or sentinel. The dominant pattern was the same rule restated in several places: six thousand words of it across seventy findings. One file was forty-seven percent redundant.
The redundancy was costing us real money and time. The hospital workers load several skills at once. The Discharge Nurse loads about 21,800 words of instructions, roughly 29,000 tokens, on every single run, before it has read a line of the PR it is supposed to be checking. The base hospital-protocol skill is loaded by all fifteen agents, so redundancy there costs the most.
七月份我们做了一次审计。25 个技能文件,一共一百万多字。其中大约 2.3 万字(占 23%)被标记为可以直接删掉,而不影响任何一道关卡、命令、模板或哨兵。最常见的冗余模式是同一条规则在好几个地方重复出现:总共 6000 字,分布在 70 处。有一个文件的冗余率高达 47%。
这些冗余在实打实地烧钱、拖时间。医院里的工蜂会同时加载好几个技能。光「出院护士」这一个角色,每次运行都要先加载大约 21,800 字的指令(约 29,000 个 token),而这还只是读完待检查 PR 之前的事。基础技能 hospital-protocol 会被全部 15 个 agent 加载,那里面的冗余代价最高。
我们怀疑这个问题不只是 MOLT Sinai 才有。我们的提示词和代码一样会腐烂:规则只增不减,最后文件长到没人能从头读到尾。我们之所以发现,只是因为看到了 token 账单。目前还没有针对这类问题的 linter,不过我们大概很快就会做一个。
经验教训
跟 MOLT Sinai(以及 Sinai 模型)打交道的这五个月,让我们学到了不少东西。下面是其中比较有意思的几点。
有效的做法
模型的表现超出预期。每个角色的技能文件开头都有几句话来框定角色,我们发现这几句话最有价值。Fellow(住院医师)的开头是:“你是一名 Fellow,正在对一位入院患者(GitHub issue)进行诊断检查。你的职责是彻底调查,并产出一份另一位 Fellow 可以执行的书面治疗方案。”Review Attending(审核主治医师)的开头是:“你是与治疗此病例的 Fellow 不同的另一个 agent 实例。你的职责是发现 bug、安全问题、回归风险,以及对已批准方案的偏离。”这为后续打下了基础——底层大语言模型非常清楚教学医院如何运转、什么是鉴别诊断、为什么交接要有固定结构,以及“第二意见”意味着什么。当你告诉大语言模型要扮演某个角色时,它会扮演得非常非常到位。举个例子,一位 Review Attending 在一个已经经过好几轮审核的 PR 上发现了一行 docstring 缺陷,但它拒绝自行决定将其打回重写,而是写道:它“将此提交给你裁决,而不是在患者已经精疲力竭的时候,仅因一个 docstring 修复就单方面把病例退回给 Fellow。”另一个案例中,一位 Attending 在决定是否要重新拆解一个 Chief 已经裁定的 issue 时写道:“一个由 Chief 拥有的决策,如果其结果推翻了前提,应退回给它的所有者,而不是在周围临时发挥。”这些句子都没有直接出现在技能文件中。相反,agent 之所以写出它们,是因为它们正在扮演一个角色。
写代码前先评审方案。经过评审的方案,被驳回的概率不到 10%;但一旦被驳回,这往往是整个体系里价值最高的一次评审,因为它把返工提前到了最早的节点。有一个例子:团队要给一项遥测修复做方案评审,目的是把客户的主机名和数据库名从采集到的日志里去掉,评审时发现还有个小漏洞,信息仍可能泄露。如果这次没在方案评审里揪出来,代码评审几乎不可能发现,问题要么直接上线,要么得再开一个工单来收尾。规划阶段抓住 bug,能为后面省下大量工作。
拆分,而且要设硬上限。如果一项检查预估改动会超过 1000 行代码(含测试),Fellow 就必须把它拆成若干子工单,并画出依赖关系图。任何子工单如果本身还会超过 1000 行,就继续往下拆。这个上限的目的,是让每个改动的体量足够小,小到无论是智能体还是人,都能把全部内容装进脑子里。Db2 那次冲刺,从一张入院单最终拆出了 32 个工单。几万行代码,靠单个智能体(或人)是没法做出令人信服的评审的,所以这家「医院」把工作切碎,让评审变得更容易。
病历记录。 医院里的每张工单都会把它产出的一切记录下来:检查方案;Fellow 在动代码之前写的复现测试,以及它失败时的完整记录;治疗方案及其每一次修订;每一次 I-PASS 交接;每一个评审结论及其理由;每个阶段都有一份临床记录,写明所用模型、token 数、耗时和成本。这些记录存放在一个配套的归档分支里,与代码一起合并,这样 PR 的 diff 就能保持「只有代码」,评审起来更轻松。五个月后,我们仅凭工单讨论串,就能轻松还原出 1238 个 PR 中任意一个为什么会被合并;而安全部门能坚持每周评审,也全靠这条记录链存在。「医院」这个比喻带给我们最有价值的东西之一,就是这种一丝不苟的记录习惯。
我们改了什么,还有什么要做
让角色分工奏效的那套提示词,也会让它们变得官僚。
有一次,一个 Fellow 针对某个 issue 给出的计划就是一句“标记为重复并关闭;PR#691 已经包含修复”。计划审查确认这个判断没错,但还是升级了——理由是“审查-值班-计划协议里规定的三种结果(APPROVE / REJECT / ESCALATE)不包含‘标记为重复’这一项”。一个已确认的重复问题,竟然需要人类介入(这一点后来已经解决)。
还有一次只是改一行 UI 文案,把副标题里的“Validating”改成“Verifying”,结果走了五轮返工,一路闹到 Chief 那里,只为了争论一个低风险 PR 能不能在 CI 还没跑绿时就合并(智能体经常想在 CI 红着的时候合并,理由是“那不是我的问题”)。
另有一次,代码审查拦下一个 PR,代码本身毫无缺陷,唯一理由是 PR 描述里有个小错别字。还有,当审查者留下一些非阻塞的小建议时,流水线倾向于为它们新建 issue 来跟踪(这一点后来也修了),把一条小评论变成了一位“新病人”。
其中一部分我们已经处理掉了,但有些东西,就是一个把规则放在第一位、把质量当作头号优先级的系统所必须付出的代价。
审阅流程需要一个“断路器”。Issue 1777 被标记为紧急,修复只有一行:在某条代码路径中调用一个现有函数,跟另外三条同类路径的做法保持一致。结果这事折腾了两天、返工十一轮——先是注释规范问题,然后是 PR 描述里的一处不实说法,接着是偏离原计划的一个小改动,再然后是测试质量,两次 rebase、两次 workflow 失败,还有一道人工审批关卡。任何返工超过三轮的问题都会触发 Safety review。而在某一次 Safety review 窗口期内,三个复杂 issue 分别走了九轮、九轮和七轮,吃掉约 631 美元——那个窗口医院总共花了 1,046 美元。那次 Safety review 还记录了一个事实:代码审查流程里没有“可计数的非收敛断路器”,所以不收敛的循环“要么升级太晚,要么永远不升级”。它提出的修复方案最近已合并——现在只要一个 PR 连续四轮返工、每轮都冒出新的阻塞性发现,或者总共经历六轮实质性返工,审查者就必须移交给 Attending。这个改动合并不久,我们还没有足够数据证明它的效果,但早期迹象看起来不错。
医院很能产出,但不总是好事。这家医院特别擅长给自己找活干。前面提到过,近一半的 issue 都是 agent 提交的。Research Department 负责调研代码库并提出新功能,最初每三小时跑一次,它产出的 issue 多到我们根本审不过来、来不及“收治”。我们先把它降到每天一次,然后又给它设了上限:最多十个提案处于等待收治状态。问题在于,它找到的 issue 大多确实靠谱。我们经常在用交互式 Claude 会话处理代码库里的某个问题,结果它指向的正是医院几周甚至几个月前提交的同一个 issue。话虽如此,把这些全部推进下去既费钱又费时,价值也存疑。从河里淘出金块仍然是难点,而我们还没解决。
五个月来,我们把 bug 当病人、把编码智能体当医疗团队
我们建了一所并不真正「教学」的教学医院。MOLT Sinai 的样子完全符合你的预期——它出自两位资深工程师之手(两人合计 40 多年行业经验)。这套模型非常强大,能在我们睡觉时不断处理 issue,等早上起来快速审一遍就行。然后我们再往医院里塞进更多待解决的 issue,白天开会的时候让它继续跑,会后审一遍、结掉一批再下班。现在,哪怕一整天都在开会,也能产出 10 个高质量、已合并的 PR。
可要是换成刚毕业的新人——日程没排满,也没有几十年经验攒下来的工程判断力,却真心想成长为更强的工程师——这套东西对他们意味着什么?对他们来说,这套模式好用吗?一句话:并不能完全满足需求。我们正在给这家医院加点新功能:让人类接手分诊后的 issue,自己制定方案,再交给医院的智能体评审。评审过程中,他们需要为自己的决定辩护,也会被追问还有没有别的方案,能不能给出更全面的解法。同样,当拿到一份评审过的方案后,人类也可以选择自己动手写代码——小修小补就手写,复杂些的就借助交互式 LLM 编码会话来写——这样他们能学会在代码库里摸索,实时做出实现上的取舍,并在随之而来的代码评审中检验自己的表现。我们需要培养未来的软件工程师,而且不管十年后智能体编码的世界变成什么样,我们仍然需要具备良好判断力和批判性思维的人。