Anthropic工程师代码量翻8倍,CI任务六个月涨25倍
AI正在改变CI
Anthropic工程师平均每季度交付的代码量是2021-2025年期间的8倍。Claude编写了其中80%的代码,并在审查和批准PR方面也扮演了重要角色。编写代码不再是瓶颈,一旦PR审查加速,CI就开始感受到压力。
此外,我们代码库中的测试数量增长了10倍,而我们只增加了微乎其微的工程师人数。这一切导致CI任务在六个月内增加了25倍(如果你想算一算的话,并非每个测试都会在每个PR上运行,我后面会解释)。
这多次险些压垮我们的测试影响分析服务。为了避免成为下一个瓶颈,我们彻底推翻了原有设计,重新构想了该服务的架构。但一路走来颇为坎坷,我们先尝试了三个快速修复,分别维持了70天、29天,然后不到一天。
随着智能体持续加速代码生成和审查,扩展CI很可能很快成为更多工程团队面临的挑战。我预计,随着运行智能体的团队创造更多的PR和测试,水平扩展的测试选择架构将成为行业标准。
在本文中,我将讨论我们在Anthropic如何扩展测试影响分析服务,以及我通过惨痛教训学到的东西:永远为指数增长做计划。具体的扩展技巧——购买更大的机器、并行化进程或重启服务(是的,这个仍然出奇地有效)——都很常见,并不是本文要分享的洞见。
关键在于,这些技巧每种都只买来了一年前几分之一的时间。另一方面,彻底检修和完全重新设计一个服务所需的时间也少了很多,而且由于编写代码不再是瓶颈,这样做也更具可持续性。
你越能预见到这种压力,并规划好你的架构将如何随之演变,你浪费在权宜之计上的时间就越少。
测试影响分析架构
我的许多同行所在的组织仍然对每次变更运行所有测试。这在一定程度上有效,但无法扩展:CI门禁变得越来越冗长、昂贵,而且不可信。
此外,人类很擅长判断哪些测试失败与自身无关,而智能体则需要更多的上下文和指导。当它们获得一组具体的有效测试时,就能更有效地自我验证和迭代。
在Anthropic,我们构建了一个确定性的测试影响分析(即测试选择)服务,根据过去的性能和包相关性来决定每次变更运行哪些测试。这并非罕见做法,而且有一类供应商提供该领域的解决方案。
我们的服务依赖于两个确定性组件保持同步:一个“监听器”(listener)记录每次CI运行的测试结果。一个“选择器”(selector)读取测试结果历史,并决定在哪些打开的PR上运行哪些测试。
这很有效,但当每秒有多个CI任务运行时,监听器开始越来越跟不上PR队列。对于AI原生的SDLC,一个小的延迟可能产生重大影响。例如,监听器延迟20分钟可能意味着成千上万的测试更新没有应用到选择器上。
如果一次糟糕的变更被合并,那么某个测试就会开始对其他人失败,导致多次不必要的调查。
如果某个依赖开始不稳定,那么不稳定的失败结果就会开始阻塞合并。
如果某个测试被修复或新增了一个测试,在监听器赶上之前不会运行,从而面临回归的风险。
所有这些都作为单个进程运行,因为要保持每个测试的运行历史,就需要一个单一写入者来应用结果。这个v0设计使我们无法进行水平分片。
通往重新设计的坎坷之路
到去年10月,该服务已经显示出紧张迹象,我们连续两天被召唤。
修复1:更大的机器
第一个修复很简单:我们加倍了运行该服务的核心数。我们也知道这不会持续太久。
对话为重现,基于真实事件。
即使趋势线清晰,归属权却不明确。没有人愿意再拥有另一块基础设施。而且,CI团队有更重要的事情要做。
修复2:分片
此时,我们经常因为该服务监听器中的延迟积累而被召唤。为了推动一些长期修复,我在内部版的Claude Tag中启动了一个长期运行的会话,专门监控该服务。每当监听器延迟超过50,000个任务时,Claude就会通知我,并继续我们就下一步的对话。
这种情况持续了几个月,而且不必不断提醒它过去的努力或上下文是很有帮助的。Claude经常主张彻底检修,但我们通常还是会选择另一个补丁。
内部版Claude Tag上的逐字对话,部分内容已删减。
到了二月,CI任务的指数增长开始再次给该服务带来压力。这一次,我们决定并行化。
监听器并不需要单一写入者来正确排序测试结果,它需要每个包一个写入者来正确排序我们代码库每个部分的测试结果。Claude生成了代码,让我们将每个包的状态拆分为一个带有自己工作进程的分片。
我们也知道这个修复不会持久,但我们没意识到它只为我们争取了29天。
修复3:每日重启
到三月,该进程在大多数工作日的下午三点左右就会达到内存上限。我们再次寻找快速修复,但:我们只找到四个bug。作为快速黑客手段更换内存分配器毫无作用。我们试图优化垃圾回收,但那并不是真正的解决方案。我们不想冒险对一个已经处于重负载下的单例进行内存剖析。重启只给我们争取了不到一天的时间。
我们还发现每日重启导致服务逐渐落后更多。当它落后超过一个小时(这种情况发生了好几次)时,大量任务结果没有由监听器记录。
需要说明的是,这并不意味着CI从未在这些PR上运行,或者未测试的代码被推送到生产环境。这意味着监听器没有获取某些结果,从而我们的测试选择组件在使用过时数据来决定在PR上运行什么和不运行什么。这大多导致我们运行那些已经非常不稳定或全面失败的测试。
重新设计
是时候(早就该)重新设计该服务了,我们采纳了Claude的建议:我们给测试选择服务加了一个数据库,准确地说是一个内存数据存储。这样一来,我们有效地卸下了单例过去所做的大量内存处理工作。
现在,任何监听器工作进程都可以处理任何结果,将其追加到内存存储中的日志中,然后继续运行,无需在内存中保留任何内容——无状态,因此可以水平扩展。一个独立的小型消费者进程每隔几秒将日志汇总为每个测试的历史,选择器可以快速查找相关的结果历史。
这种分布式架构运行起来更昂贵,但比不稳定的单例更容易扩展和进行内存剖析。
这个项目由一位工程师花了三周完成。一年前可能需要接近一个季度。
排队未处理的任务结果事件,每小时最大值。之前:大多数日子积压堆积,且周复一周增长。切换并调优后:平稳。
我们进行了一些细微调优(调整日志大小和工作进程数量),这些主要由Claude自主完成,但此后我们的服务一直保持稳定。
我会做出哪些不同
如果让我回到2025年10月,我会用现在的认知以不同方式处理这个和其他项目。
第一个不同是,我会把AI的指数增长考虑在内。随着每位工程师平均拥有的智能体数量增加,以及加速PR审批变得更加成熟,CI任务呈指数增长。
随着Claude偏好更小、更细粒度的PR,这逐渐改变了Anthropic中PR的形态(这也是不对每个PR运行所有测试的另一个好理由)。这转化为一个给定日期内更多的CI任务。此外,由于智能体在夜间和周末推送代码,活动水平下限被提高,但由于人类工程师仍然驱动和批准大量PR,活动仍然是突发性的。
我对工程团队的建议是,无论你是自建还是购买,都要假设你的架构将在两个季度内承受25倍的负载。过度工程这个概念开始逐渐消退,或者至少门槛正在大幅提高。只要预算允许,你现在可以在v0设计中考虑感知规模的10-20倍。
为你的服务插桩,使其成为Claude的耳目。这让Claude能够比我们手动操作更好、更快地进行爬山式搜索并逐步解决问题。特别是,要确保进入的CI任务数量与离开的相等。
从一开始就不要在进程内保留状态。我还会避免任何关键服务以单实例运行,除非你能测量它以及任何金丝雀变更。CI发展太快,没有其他路可走。
其他CI资源
我还写过一篇文章介绍我们如何使用Claude Tag(测试版)加速CI待命。