大家好,欢迎收听我的演讲。今天我们将探讨如何为 AI 规模下的海量提交重建持续集成(CI)。这源于我过去十年以上在构建和合并基础设施方面的工作经验。我想带大家了解我们在软件工厂以及受高度监管的行业(如银行)中遇到的新瓶颈,以及如何在此基础上保持合规性和可审计性的同时实现快速迭代。让我们深入探讨。

我叫 Gautam,是 QAR 的联合创始人兼 CTO,近期以首席工程师身份加入 Sonar。在此之前,我在 Uber 工作了近十年,负责单体仓库的构建和合并基础设施工作。从 CI/CD、ID 到任何你能想到的东西,我都参与过,以确保开发者的生产力。现在在 Sonar(QAR 的一部分),我致力于确保提交代码经过验证,并由 AI Agent 编写的高质量代码在合并前得到确认。

我想先回顾一下历史背景。早在 AI Agent 出现之前,Uber 就面临着海量提交量的挑战。我们拥有一个巨大的单体仓库,每天进行数千次更改,且构建时间非常长。如果我们将所有提交串行化并逐一合并以防止破坏主分支,整个过程可能需要长达 20 天才能确保一切正常。

如果我们跳过检查并允许所有人直接合并到主分支,我们会发现主分支经常变红。因为更改之间会发生冲突,即使 CI 在单个提交中通过,合并后也可能导致破坏。这会导致我们的 iOS 监控经常显示红色状态,这对开发者的生产力造成了极大影响。

为了解决这个问题,我们构建了一个名为 Submit Cube 的系统(我们有相关论文)。该系统测试所有可能的提交组合,以减少等待时间并实现并行测试。通过测试各种组合,我们可以预测哪些更改可能导致破坏。一旦知道某个 PR 可能会失败,我们会将其从队列中移除并通知作者,要求他们在重新变基后再次提交。

最终,只有当所有提交的组合都通过测试时,代码才会直接进入主干(Trunk),以确保保持主干清洁的开销是值得的。该系统至今仍在 Uber 运行。虽然 GitHub 等代码托管平台后来推出了合并队列等功能,但在当时并没有现成的系统可用,因此我们自行开发了这一解决方案。

然而,这种方法的当前问题在于所有的队列(无论是 CI 还是代码审查)都成为了软件开发生命周期中的集中式瓶颈。这些队列是为人类的速度设计的,而人类每小时只能生成几次更改,因此我们可以逐一进行测试和审查。

现在有了 Agent 后,开发者每天会编写多个 PR,并且他们并不总是等待队列中的顺序,而是希望同时提交。这意味着 CI 和审查队列会堆积如山,造成巨大压力。加上软件工厂模式和后台 Agent 在各种工作流中触发任务的情况,一切都会变得极其缓慢。

这对今天的团队意味着什么?为了让大家有个概念,自三年前我们开始使用 QAR 以来,我们的提交率几乎增长了十倍。在团队规模依然很小的情况下,我们能够推送更多的代码。行业趋势也证实了这一点:GitHub 的数据显示 PR 数量呈指数级增长,每月增长率约为 25%。

这种趋势不仅限于我们这样的小团队,随着各大企业和银行推进 AI 采用之旅,他们面临的 PR 审查、CI 修复等压力程度只会更高。如果 Agent 打开的 PR 单独通过 CI,合并回主干时仍可能导致系统崩溃。

如何解决这个问题?关键在于更改本身没有相互测试。我们需要改变 CI 系统的架构,并使队列更智能地解决此问题。此外,随着大家采用软件工厂模式(由 Agent 根据规范编写代码,人类仅负责规范和审查),现有的流水线是为人类速度设计的。

当 Agent 推送更多更改时,流水线的每个部分都会变慢。例如在构建和测试阶段,编译失败可能导致大量重建;集成测试中可能存在本地无法复现的不稳定(Flaky)测试,导致需要重试并耗时数小时。

再加上审查和批准环节:虽然很多人使用 AI 进行初步代码审查,但如果缺乏相应系统,人工审查者将成为瓶颈。即使经过审查,由于 PR 数量激增,许多人在行业中只是“橡皮图章”式地批准代码,无法真正跟上高频率的审查需求。

他们现在依赖其他信号来勉强通过,因此流程必须改变。目前我们面临的另一个问题是大量检查正在运行,且我们要保留的大量记录会过期失效,旧日志在过期后便不复存在。由于只有批准印章,缺乏实际检查内容的记录,这导致无法追溯具体进行了哪些验证。因此,为了进行审计和合规性检查,必须建立这些事项的记录机制。

我们发现的另一个问题是,随着代码量的增加,修复所需的时间也带来了巨大的开销。至少在我们的平台上,13% 的拉取请求(PR)记录了 CI 失败的情况。这意味着在所有这些案例中,都需要进行额外的往返流程:要么回到开发人员机器上,要么让智能体理解日志、定位失败的根本原因并修复它,然后推送新的提交。

接着需要不断迭代直到状态变绿。这仅仅是 CI 部分,因为在大多数情况下,如果人工审查者想要查看 PR,除非 CI 是绿色的(这是第一道防线),否则他们不会去查看。这也拖慢了审查流程,因为无法并行进行;通常我们希望在代码定稿后一次性查看。

由于每次审查都在产生成本,如果审查意见返回而你又进行另一轮修改导致再次失败,就会重复这个循环。那么如何改进呢?随着智能体推动更多代码提交,我们如何让这个问题更具可管理性?我们在 Back at Uber 以及现在的 Guitar Stream 中都发现了这一点。

最明显的方法当然是让 CI 变得非常快速,这意味着你可能需要移除许多昂贵的检查项。尝试将它们卸载到合并后执行,并使 CI 步骤本身尽可能快速,因为这样修复效率会更高。你可以并行处理事物、进行图表化分析或使用缓存等技术,稍后我们会讨论我们使用的一些技术。

你还需要确保审查快速进行。不仅要有深度审查,还要有多个快速的反馈环节告知开发人员,使他们能够了解在何种场景下需要针对高风险变更进行深入审查。对于低风险变更,甚至可以让 AI 批准并合并它们。

对于那些已经在 CI 中运行的检查项,如果能将它们“左移”(提前检测),成本会低得多。这也非常适用于 CI 之后的检查,例如昂贵的回归测试或可能在生产中失败的情况。虽然并非总能将所有内容左移,但尽可能这样做意味着下游的瓶颈会更少。

在本地环境中,智能体已经拥有更多上下文来修复面临的问题。此外,你还需要制定政策,这对于受监管的行业至关重要。我们在 Uber 的经历是:由于软件中的一个漏洞允许司机在未完成所有必要检查的情况下上平台,且这是由于功能标志变更引起的,我们差点失去了伦敦的经营许可。

因此,我们必须建立一套流程:任何通过软件开发生命周期(SDLC)且影响伦敦地区的变更,都需要某种形式的血缘关系分析,以确定该变更是否会影响伦敦地区。如果是,需确认已采取的控制措施是什么?最终我们成功收回了许可证。

许多听众可能正面临类似的挑战,即智能体使理解代码库变得更加困难。如何确保你的政策得到尊重?你需要有一种在 CI 之外执行这些规则的方法,因为代码编译通过并不意味着它遵循特定的规范。这可能涉及 HIPAA 或 TORA 等合规要求,有许多其他政策是智能体所不知道的。

除非你的另一端强制实施这些规则,否则智能体不会意识到它们。同时,你需要记录已执行的情况以供审计。简要分享一个我们加快 CI 速度的例子:我们的基线速度约为10分钟,虽然还算快,但在尝试提交更多代码时非常令人烦恼。我们希望显著缩短这一时间。

我们开始思考如何缓存输出,我们的构建系统使用 Rust。因此,我们尽可能多地缓存并复用它们。我们还研究了减少编译器变动的方法,添加了一些修复措施以确保编译部分更具可缓存性且失效频率更低。

通过在此基础上增加更多修复、分片处理以及消除这些开销,我们将增量步骤的 CI 时间缩短至不到一分钟。这意味着一旦冷启动后的 CI 被“预热”,增量构建将变得非常快,我们可以推送更多的代码。

另一个问题是审查环节。仅仅因为 CI 是绿色的还不够。有趣的是,当我们衡量 PR 进入生产环境的 P90 时间时,绝大多数时间都花在审查上。在 Uber 以及今天的大多数团队中,人们在将代码合并到主分支前花费大量时间进行代码审查和其他类型的测试。

这需要在 CI 之外解决,因为目前的情况是:智能体在生成代码的同时,AI 代理也在你的代码审查系统中发现问题。这些自动审查发现的问题现在需要你去解决。因此,你必须要么在本地为智能体拉取上下文,要么让 CI 中的智能体迭代修复。

人类审查者只需在 AI 完成首轮检查后,对剩余部分进行审查。这必须成为整体流程的一部分,以免浪费宝贵的人力去处理越来越多的由智能体生成的 PR。举一个我们的例子:为了加快 CI 速度,我们重构了代码库,使用 NextText 来并行化我们的测试。

我们启动了一个共享数据库,以确保只需设置一次,所有数据库测试都可以复用它。但问题在于,我们没有放置一些防护机制,这意味着当 CI 非常繁忙时,会出现交叉污染,导致测试变得不稳定。Guitar(作为代码审查者)捕获了这个问题。

在合并之前,我们会先审查我们的拉取请求(PR),这样就不必事后浪费时间。GAR 也为我们修复了问题,因为它建议了解决方案、提交了代码并将工程师列为共同作者。通过这种方式,我们能够在代码最终进入主分支之前防止昂贵的错误发生。我们在 Qar 做的另一件事,以及随着越来越多的客户开始这样做的是:我们使用策略和标准来决定何时适合批准更改。

许多更改属于常规或低风险类别,例如文档更新、依赖项升级(当然前提是不破坏任何 API)、安全补丁,甚至是一些对内部工具的低风险更改,这些都可以自动批准。Gitar 开箱即用地支持这些功能,您还可以添加额外的自定义规则,使用纯英语编写来创建批准和合并逻辑。我们看到许多非常原生地采用 AI 的团队开始越来越多地使用这些功能。

我们观察到,在我们平台上,团队采纳自动批准和自动合并的速度增长非常快,从大型团队到拥有超过 10 万名开发者的超大团队都是如此。以下是一个批准策略的示例:如果是依赖项升级和文档更改,我们应该予以批准;涉及敏感内容(如支付或 OAuth)的任何更改可能需要额外的人工审批。

当然,您不希望生成更改的代理自己批准其更改,因为那样不符合零信任原则。因此在这种情况下,当代理生成 PR 时,PR 会自动获得批准;如果出现不符合标准的新更改,我们需要将其升级给另一位审批人。这当然可以使用您自己的代理来完成,但 Gitar 也开箱即用地提供了此功能供您尝试。

今天仅依赖 CI(持续集成)的另一个问题是,正如我提到的那样,CI 绿灯并不一定意味着代码符合合规性要求。在这种情况下,虽然 CI 通过了测试,但我们有一些关于它的元数据表明:如果重新运行某个不稳定的测试,它将会失败。这意味着当您强制推送以修复该问题时,分支指向的是另一段代码。

这个问题的后果是,随着时间的推移,大多数 CI 数据都会过期失效。除非您有记录发生了什么以及修复了什么的内容,否则您将无法保持合规性,也无法审计实际发生的情况。因此我认为代理轨迹以及每次推送后的结果非常重要。无论您能在 PR 或任何其他记录系统中记录什么信息,都将成为当您的工件过期时的唯一事实来源。

这里的控制点在于确保更改是由未编写该代码的某人批准和测试过的。如果您没有这些信息,可能会成为一个问题。以下是一个示例:我们有一个失败的流水线(左侧)和一个通过的流水线。但区别在于,如果您正确记录了失败的原因,并且由于这是已知故障模式,其他人能够批准它(发出鼻音),且这一信息随更改一起保存下来。

这种情况实际上通过了您的检查,而对于那些没有记录发生情况、只是盲目信任代理推送代码的情况则不然,因为那可能是一个质量不高的测试。没有记录所有合规性检查的结果。因此您希望确保在更敏感的行业中,除了代码审查外,还要将所有这些决定记录为 PR 之上的规则或合规性检查。

我们要保留哪些类型的记录呢?这里有一些我们保留的记录,我认为这只是一部分示例,对其他人来说可能不同。例如:是哪个代码代理创建了 PR?PR 源自何处?引发它的会话是什么?以及是谁实际启动了它?它是来自自动化工作流的触发,还是仅仅由工程师进行提示操作引发的?

对于运行的每个 CI 测试,什么是被信任的提交记录并保存了测试报告(而不仅仅是状态)?并确保每个发现项都有如何解决它的记录:是被忽略了吗?是有后续跟进吗?还是在 PR 上修复了它?并确保审批人是除由同一人提示的机器人之外的其他人。

确保任何覆盖操作都明确命名了原因。例如,如果构建失败或代码覆盖率检查被跳过,您需要确保有理由说明为什么该检查会失败以及在此场景下我们为何可以接受。并确保合并的代码实际上包含了所有这些内容,而不是在顶部合并了一个小更改。因此所有检查每次都要完成。

在这种情况下,我们要确保代理不能批准自己的更改;提示它的工程师也不是作者。如果您现在实施了这些实践,我们实际上可以开始消除一些瓶颈,因为更快的 CI 将解除您构建的阻塞。因此您的流程会更加顺畅,并且确保您可以像左移一些检查一样操作,也将显著减少测试负载。

在 CI 方面,如果您能自动化大部分审查工作,仅在例外情况下升级给人工处理,甚至自动批准低风险或内部更改,那么您就消除了进一步的瓶颈。这样就有更少的内容需要触及人类去执行软件工厂的潜力。当然,在每次批准和合并后,您都想记录发生的情况,因为随着工厂的扩张,如果您不了解过去发生了什么,这些上下文对于如何迭代和改进软件将非常有用。

以上就是我的分享。我很希望大家也能尝试 Gitar,看看您如何改进您的软件工厂。在此非常感谢大家的……