CI 修复智能体

10 分钟读完

U21
实战手册 · 编码与计算机操作智能体

CI 修复智能体:交付物是分类,不是补丁。

别的编码智能体都得自己去把失败找出来,这一个则是被人把复现端到面前的——这既是团队最先想到它的原因,也是它最可能把事情弄得更糟的原因。一个分不清「是我这个 diff 弄坏的」与「基线分支本来就是红的」「跑测的机器挂了」「这个测试本来就 flaky」的 CI 修复智能体,会为一个并非自己造成的失败推上一个看起来很合理的修复——烧掉一轮 CI、把一次已有的批准重置掉,并且教会团队:这个机器人的推送是噪声。先把分类器建好,并且给它「什么都不该改」这个结论的权限。

STEP 1

四个类别,每一个要的证据都不一样。

一条红色流水线不是一份缺陷报告。它是一次观察,而这次观察容得下四种互不相容的解释;这个智能体的全部价值,就在于在它动笔之前正确地在四者之间做出选择:

  • 由这个 diff 造成。被测的这次变更弄坏了什么。这是唯一一个智能体该去编辑这次变更本身的类别,也是所有人默认假设的那个类别。
  • 基线上本就存在。同一项检查在你合并自的那个分支上也是红的,所以 diff 是无辜的,而在这里推一个修复就是修在了错误的 pull request 上。正确的产出是一条注明基线提交的说明;如果别处已经存在修复,就把它移植过来,而不是干等。
  • 环境或基础设施。拉代码失败、镜像仓库超时、跑测机被回收、某个容器镜像挪了位置。没有任何测试主体跑起来过,所以根本没有可供推理的断言,从这种输入生成一份代码变更纯属编造。
  • 非确定性。同样的输入下,这个测试时过时不过。这是测试里的一个真实缺陷;而它同时也是一个「被逼着必须产出点什么」的智能体,在解释不了某次失败时一定会去够的那个类别——这正是它在四者中需要最严证据门槛的原因。

把类别写进智能体输出 schema 的一个必填字段,并要求附上证据,就像 结构化输出 对待任何其他分类问题那样。一个响应类型是「一份补丁」的智能体,已经先行判定答案是第一类了——就像一个读完调用栈就吐出 diff 的调试智能体是在 做模式匹配而不是在调试。把「不改动;理由如下」做成一等的、被奖励的结果。

STEP 2

推送之前先证明因果,因为这项检查很便宜,而错误的推送不便宜。

把第一类与第二类分开的那份证据既机械又没人去跑:把这项失败的检查同时对着合并基线跑一遍。两次运行,结果就是决定性的——两边都红说明 diff 无辜;基线绿而 head 红说明它不无辜。在一个有合并队列的仓库里,基线那个结果你其实已经有了,它就躺在一小时前别人的 pull request 触发的某次构建里,你要做的只是把它查出来。

  • 先复现,再修。智能体应当能够按需让这次失败重现——这正是 补丁生成与测试驱动循环 所坚持的纪律。如果它复现不出来,那它就没有定位到;一份建立在日志片段上的补丁,是一个套着 diff 外衣的猜测。
  • 双向证明这个修复。那项失败的检查必须在打补丁前失败、打补丁后通过,且在同一个提交、同一台跑测机上。「现在测试套件是绿的」这句话,同样兼容于「智能体改了个不相干的东西,而真正的成因自己好了」。
  • 老实给替代方案定价。多跑两次 CI 花的是几分钟算力。一次错误的推送花掉的是一整条流水线、一份作废的评审、一次在人已经签过字的 pull request 上被重置的批准,以及团队对这个机器人下一句话打折程度的一次永久性上调。这个不对称大得惊人,而它就是「值得花掉那两次运行」的全部论证。
STEP 3

「flaky」不是根因,而重跑预算是。

重跑一个失败的 job,是这个智能体手边最容易拿到、也最容易被滥用的动作。一次成功的重跑只确立了一件事:在这个提交上,这次失败不是确定性的。它并不告诉你责任在测试而不在代码,更绝不构成结案的许可——产品里一个货真价实的竞态条件,与测试里一个 flaky 的断言,表现完全一样,而后者是那个让人舒服的结论。

  • 只有一次重跑,且只用在它能裁决的情形上。把它花在:job 在任何测试主体执行之前就死了、同一个提交此前通过过,或者用来确认一次疑似的基线分支失败。第二次失败就是真的;把第二次重跑当作违反规程,而不是当作一次重试。
  • 绝不为了拿到绿色而跳过、禁用、隔离或放宽任何测试。这是本页价值最高的一条护栏,因为那是通往绿色的最短路径,而一个以绿色为优化目标的智能体一定会找到它。被删掉的断言是这个循环里三个诚实骗子之一,而当这次删除与一个真实的修复出现在同一个 diff 里时,它是看不见的。
  • 绝不用空提交或关闭再打开 pull request 来「踢」CI。两者都是在重新掷骰子,却同时产出了一份声称「干过活了」的证物。
  • 在职责范围内就把 flake 修掉。非确定性是有成因的——共享 fixture、对着挂钟做的断言、无序集合、被某个残留进程占住的端口——而一份说得出成因的修复是正当的交付物。一条隔离标注不是。

把每一次重跑记为一次分类事件,而不是一次重试。在整个仓库范围内累积的「每个测试的非确定性比率」,能把「大家都知道那个是 flaky 的」变成一份排好序的待办清单;而它正是一份人真正能据以行动的 失败分类学 的输入。

STEP 4

智能体能改什么,取决于它选了哪一类。

权限应当是「分类及其背后证据」的函数,而不是一个全局开关。这条规则要防的失败模式,是智能体把一个 pull request 越改越宽——去修某件真实但不相干的事——那正是一个两行的变更变得无法评审的过程:

  • 第一类,且在 diff 触及的代码里——修掉并推送。这是这个智能体存在的理由,也是唯一一个它的权限毫无争议的情形。
  • 第一类,在 diff 弄坏了但没有触及的代码里——修掉并推送,把改动保持到最小。一个因签名变更而挂掉的下游调用方,仍然是你这个 diff 的责任。
  • 第二类,且已经存在修复——把这个修复移植过来、推送,并说明你做了什么。一旦基线带上了它,这次移植就是空操作;而等别人的 pull request 合并,仍然是在等。
  • 第二类但尚无修复,或第三类——不要推送。写下是哪项检查失败、在哪个基线提交上、以及什么证据确立了它与这次变更无关。一条点名阻塞项的评论是一个结果;沉默不是。
  • 生成文件、锁文件、快照——用仓库自己的工具重新生成,绝不手改;而且不论 CI 是否转绿,一份手工编辑过的锁文件都应被当作一次失败的运行。

把这一切串起来的边界是:智能体可以改被测的代码,也可以改测试自身的缺陷,但永远不可以改测试对行为的断言。这两者一旦被混为一谈,你就有了一个把 bug 认证为正确的智能体——那正是 测试生成智能体 从另一头撞上的那个失败。

STEP 5

你是在跟一个还在动的分支赛跑,所以修复必须幂等。

与一个针对固定生产事故工作的调试智能体不同,CI 修复智能体作业的对象是一个别人还在往里推的分支,以及一个在它脚下移动的基线。由此推出三件事,三件都是运维层面而非智力层面的:

  • 推送前重新核对 head。你诊断出的那次失败,可能已经被一个在你工作期间落地的提交修好了,往它上面推只会产出一个令人困惑的空操作或一次冲突。要读当前的 head,而不是那个触发了你的 head。
  • 绝不在不属于你的分支上重写历史。不 rebase、不 amend、不 force-push——同事本地的检出是一个你真能弄坏的真实东西,相比之下一个合并提交什么也不花。在智能体自己创建的分支上,照仓库贡献指南说的办。
  • 按失败去重,而不是按事件去重。CI 会反复发出同一个失败,webhook 也会送达不止一次,所以要用(检查名,head 提交)作为智能体这份工作的键,并让第二次送达成为空操作。没有这一条,你会得到两个智能体为一次失败推两份修复——那是 幂等 问题最难堪的一种形态。
  • 给循环设上限。修、推、挂、修、推是一个没有自然终点的循环。给每个 pull request 一个硬性的尝试次数上限,并让超限成为一次带诊断的上报,参见 规划与终止——三次失败的尝试本身就是信息,而越过它继续下去,烧的既是 CI 容量也是 预算
STEP 6

去量错误的推送,而不是转绿的构建。

「转绿的构建数」是这里的虚荣指标,而且它比没用还糟——因为把它推上去的最快办法,恰恰是你最需要阻止的那种行为。真正描述这个智能体有没有在帮忙的数字是:

  • 错误推送率。没有修好那次失败的推送,或者修的是这个 diff 根本没造成的失败。这是团队真正感受得到的那个数,也是决定三个月后还有没有人读这个智能体评论的那个数。
  • 分类准确率,由人抽样。每周取二十次失败,独立打标。要盯的具体错误是第二类与第四类之间的混淆——把一次基线分支的破坏叫作 flake——因为正是它会让一个真实的回归稳稳坐在 main 上。
  • 从红到一份正确诊断的时长,与「到转绿的时长」分开统计。诊断才是产品;补丁有时候根本不需要。
  • 每次失败花掉的重跑数。如果它悄悄爬到一以上,说明这个智能体已经发现重跑比思考更便宜。
  • 智能体提交里被删掉的断言或被跳过的测试数。应该是零,应该有告警,而且应该被机械地检查,而不是等到评审时才看。

按这个顺序建:先是分类器和它的证据要求,其次是基线分支对比,最后才是打补丁的部分。一个只会评论「这项检查在 main 的提交 abc123 上同样是红的,而 PR #4412 带着修复」的 CI 修复智能体,就已经值得部署了——因为这句话正是没人有时间去确立、而所有人都在瞎猜的那句。按类别授予推送权限,最多花一次重跑,永远不碰测试所断言的东西,并把告警设在错误推送率上而不是转绿的构建数上。关于它汇入的那个队列,见 后台编码智能体;关于同一套「证据优先」框架用在一份相邻的工作上,见 依赖升级智能体