Agent 写代码把 CI 压垮:Anthropic 测试影响分析服务的三次续命与重构
Anthropic 的 CI 在六个月内承受了 25 倍任务量增长:Claude 编写 80% 的代码,工程师人均季度产出是 2021–2025 均值的 8 倍,测试总量涨了 10 倍而人数几乎没变。瓶颈从写代码转到 PR 审核,再落到测试影响分析服务上。该服务由 listener 和 selector 两个确定性组件构成,因为需要单写入者维护每个测试的历史,v0 只能单进程运行,无法水平分片。作者记录了三轮续命:加核心数撑了 70 天,按包分片撑了 29 天,每日重启撑了不到一天;期间内存见底、只找到四个 bug、换内存分配器无效,重启还让 listener 越追越远。最终方案是把历史状态搬进内存数据存储:任意 listener worker 把结果追加进 journal 即可无状态退出,单独的 consumer 每几秒把 journal 汇总成 per-test 历史,selector 再查询。单人三周完成,积压事件归零。作者的建议是:假设两个季度内负载达 25 倍,把状态移出进程,别把关键服务跑成单实例。
Anthropic engineers on average ship 8x as much code per quarter as they did from 2021-2025. Claude authors 80% of that code and it also plays a large role in reviewing and approving PRs as well.
Anthropic 的工程师平均每季度交付的代码量是 2021 至 2025 年的 8 倍。其中 80% 的代码由 Claude 编写,它还在审查和批准 PR 方面扮演重要角色。


Writing code is no longer the constraint, and once PR review gets accelerated, CI starts feeling the pressure. On top of that, the amount of tests across our codebase grew 10x and we added a nominal amount of engineers. This all led to a 25x increase in CI jobs over a six month period (in case you are trying to do the math, not every test runs on every PR as I will explain).
编写代码不再是瓶颈,而一旦 PR 审查加速,CI 便开始承压。此外,我们代码库中的测试数量增长了 10 倍,而工程师人数只增加了寥寥几个。这导致 CI 任务量在六个月内增长了 25 倍(如果你想算一下,并非每个测试都会在每个 PR 上运行,我稍后会解释)。
This threatened to overload our test impact analysis service several times. To avoid becoming the next bottleneck, we blew up the whole thing and reimagined what the service's architecture looks like. But getting there was a bumpy path that started with three quick fixes, which lasted 70 days, then 29 days, and then less than a day respectively.
这多次险些压垮我们的测试影响分析服务。为了避免成为下一个瓶颈,我们彻底推倒重来,重新构想了服务的架构。但这条路并不平坦,始于三次快速修复,它们分别只撑了 70 天、29 天和不到一天。
Scaling CI is a challenge more engineering teams are likely to soon face as agents continue to accelerate code generation and review. I anticipate horizontally scaled test selection architecture will become industry standard as teams running agents create both more PRs and more tests.
随着 agent 持续加速代码生成和审查,扩展 CI 是更多工程团队即将面临的挑战。我预计,横向扩展的测试选择架构将成为行业标准,因为运行 agent 的团队会创建更多 PR 和更多测试。


In this article, I’ll discuss how we scaled our test impact analysis service at Anthropic and the lesson I learned the hard way: always plan for the exponential. The specific scaling techniques–buying bigger machines, parallelizing processes, or restarting the service (yeah, this one still works surprisingly well) – are common and not the insights to take from this article. The point is that each of these techniques bought a fraction of the time they did a year ago. On the other hand, overhauling and completely redesigning a service also takes a fraction of the time and is much more sustainable now that writing code is no longer the bottleneck. The more you can anticipate this strain and plan how your architecture will evolve with it, the less time you will waste on half-measures.
本文将讨论我们如何在 Anthropic 扩展测试影响分析服务,以及我付出代价才学到的教训:永远为指数增长做规划。具体的扩展技巧——买更大的机器、并行化进程、重启服务(没错,这招依然出奇地有效)——都很常见,并非本文要传达的洞见。关键在于,这些技巧如今能争取到的时间只有一年前的零头。另一方面,彻底重构并重新设计一个服务所需的时间同样只有零头,而且在编写代码不再是瓶颈的当下,这种方式可持续得多。你越能预见这种压力并规划架构如何随之演进,浪费在半吊子措施上的时间就越少。
Many of my peers work at organizations where every test is still run on every change. This works up to a point, but doesn’t scale: CI gates get increasingly long, expensive, and untrustworthy. Additionally, humans are great at determining which test failures don’t apply to them while agents will require more context and direction. When they get a specific set of valid tests, they can self-verify and iterate more effectively.
我的许多同行所在的组织仍然在每次变更时运行所有测试。这在一定程度上可行,但无法扩展:CI 门禁变得越来越长、昂贵且不可信。此外,人类很擅长判断哪些测试失败与自己无关,而 agent 则需要更多上下文和指导。当它们拿到一组具体的有效测试时,就能更有效地自我验证和迭代。
At Anthropic, we built a deterministic test impact analysis or test selection service that determines which tests run on each change based on past performance and package relevance. This isn’t an uncommon practice, and there is a category of vendors with offerings in this area. Our service depends on two deterministic components staying in sync: A “listener” records the test results from every CI run. A “selector” reads the test result history and determines which tests run on which opened PRs.
在 Anthropic,我们构建了一个确定性的测试影响分析(或测试选择)服务,它根据历史表现和 package 相关性决定每次变更运行哪些测试。这种做法并不罕见,市面上也有一类厂商提供相关产品。我们的服务依赖两个保持同步的确定性组件:一个“listener”记录每次 CI 运行的测试结果;一个“selector”读取测试结果历史,并决定哪些测试在哪些已打开的 PR 上运行。
This is effective, but when there are multiple CI jobs running every second, the listener starts to increasingly fall behind the PR queue. For an AI-native SDLC, a small lag can have a big impact. For example, 20 minutes of listener lag can translate into tens of thousands of test updates not being applied to the selector.
这很有效,但当每秒都有多个 CI 任务运行时,listener 开始越来越落后于 PR 队列。对于 AI 原生的 SDLC,小小的延迟也可能造成巨大影响。例如,listener 延迟 20 分钟,就可能意味着数万条测试更新未能应用到 selector。
If a bad change gets merged, then a test will start failing for everyone else causing multiple unnecessary investigations. If a dependency starts flaking, then flaky reds start blocking merges. If a test gets fixed or a new one gets added, it won't run until the listener catches up risking a regression. All of this ran as a single process because keeping a running history per test meant a single writer needed to apply the results. This v0 design prevented us from being able to horizontally shard.
如果某个坏变更被合并,那么某个测试就会开始对其他人失败,引发多次不必要的调查。如果某个依赖开始不稳定,那么 flaky 的红色测试就会开始阻塞合并。如果某个测试被修复或新增,在 listener 赶上之前它都不会运行,从而带来回归风险。所有这些都运行在单个进程中,因为要为每个测试维护运行历史,就需要一个单一的写入者来应用结果。这种 v0 设计阻碍了我们进行水平分片。
By October of last year the service was already showing signs of strain, and we got paged two days straight. The first fix was easy: we doubled the cores running the service. We also knew it would be fleeting.
去年 10 月,该服务已经显现出压力迹象,我们连续两天被 on-call 呼叫。第一个修复很简单:我们把运行服务的核心数翻倍。我们也知道这只是权宜之计。


Conversation recreated. Based on real events. Even when the trend line was clear, ownership was murky. No one wanted to own another piece of infrastructure. Also, the CI team had bigger fish to fry. At this point we were getting paged pretty frequently by the lag building up in the listener of this service. To drive some long-term fixes, I started a long-running session in an internal version of Claude Tag dedicated to monitoring the service. Anytime the listener lag would get more than 50,000 jobs behind, Claude would ping me and resume our conversation on next steps. This would go on for months, and it was helpful not having to constantly remind it of past efforts or context. Claude often argued for an overhaul, but we usually settled on another patch.
对话重现。基于真实事件。即便趋势线已经清晰,责任归属依然模糊。没人愿意再接管一块基础设施。而且 CI 团队还有更棘手的事要处理。此时,我们因为该服务的 listener 积压延迟而频繁被 on-call 呼叫。为了推动一些长期修复,我在内部版本的 Claude Tag 中启动了一个长期会话,专门监控该服务。每当 listener 延迟超过 50,000 个任务时,Claude 就会 ping 我,并继续我们关于下一步的对话。这持续了几个月,不用反复提醒它过去的努力或上下文,非常有用。Claude 经常主张彻底改革,但我们通常还是选择了又一个补丁。


Verbatim conversation on an internal version of Claude Tag with some redactions. In February, the exponential growth of CI jobs started to strain the service once again. This time, we decided to parallelize. The listener didn’t need a single writer to order test results correctly, it needed a single writer per package to order the test results for each section of our codebase correctly. Claude generated the code for us to split each package’s state into a shard with its own worker. We also knew this fix would be fleeting, but we didn’t realize it would only buy us 29 days.
内部版本 Claude Tag 上的逐字对话,部分内容已隐去。二月,CI 任务的指数增长再次给服务带来压力。这次我们决定并行化。listener 并不需要一个单一写入者来正确排序测试结果,它需要的是每个 package 一个单一写入者,以正确排序代码库各个部分的测试结果。Claude 为我们生成了代码,将每个 package 的状态拆分到拥有自己 worker 的分片中。我们也知道这个修复只是暂时的,但没想到它只为我们争取了 29 天。


In March, the process reached its memory limit by mid-afternoon on most weekdays. Again, we looked for quick fixes but: We only found four bugs. Swapping the memory allocator as a quick-hack did nothing. We were trying to optimize garbage collection but that wasn’t really the solution. We didn’t want to risk memory profiling a singleton already under a heavy load. Restarting bought us less than a day. We also discovered daily restarts were resulting in the service gradually falling further behind. When it fell behind for more than an hour, which happened several times, a ton of job results weren’t recorded by the listener. To be clear, this doesn’t mean CI never ran on those PRs, or that untested code was pushed to production. What it meant was that the listener didn’t pick up some results, which meant our test-selection component was using stale data to decide what to run and what not to on PRs. Mostly this translated into us running tests that were already super flaky or widespread-failing across the board.
三月,进程在大多数工作日的下午三点左右就达到内存上限。我们再次寻找快速修复,但:只找到了四个 bug。作为快速 hack 更换内存分配器毫无作用。我们试图优化垃圾回收,但那并非真正的解决方案。我们不想冒险对一个已经高负载的单例进行内存性能分析。重启只为我们争取了不到一天。我们还发现,每日重启导致服务逐渐落后得越来越远。当它落后超过一小时(这种情况发生过好几次),大量任务结果没有被 listener 记录。需要说明的是,这并不意味着 CI 从未在这些 PR 上运行,也不意味着未经测试的代码被推送到生产环境。它意味着 listener 没有获取到部分结果,导致我们的测试选择组件使用过期数据来决定在 PR 上运行什么、不运行什么。大多数情况下,这导致我们运行那些早已极度 flaky 或大面积失败的测试。
It was (past) time to redesign the service, and we took Claude’s advice: we gave the test selection service a database, or an in-memory data store to be exact. By doing so, we effectively offloaded a huge chunk of in-memory processing that the singleton used to do. Now, any listener worker can process any result, append it to a journal in the in-memory store, and move on without holding anything in memory - stateless and hence, horizontally scalable. A small separate consumer process rolls the journal up into per-test history every few seconds, and the selector can look up relevant result history quickly.
是时候(早就该)重新设计服务了,我们采纳了 Claude 的建议:为测试选择服务配备一个数据库,确切地说是一个内存数据存储。这样一来,我们有效地卸下了单例过去承担的大量内存内处理。现在,任何 listener worker 都可以处理任何结果,将其追加到内存存储中的日志,然后继续工作,无需在内存中保留任何东西——无状态,因而可水平扩展。一个独立的小型消费者进程每隔几秒将日志汇总为每个测试的历史,selector 可以快速查找相关结果历史。


This distributed architecture is more expensive to run, but it is much easier to scale and memory profile than a shaky singleton. This project took three weeks for a single engineer. A year ago it would have been closer to a quarter.
这种分布式架构运行成本更高,但它比摇摇欲坠的单例更容易扩展和进行内存性能分析。这个项目一名工程师花了三周完成。一年前,这可能需要接近一个季度。


Queued, unprocessed job-result events, hourly max. Before: a backlog built up most days and grew week over week. After cutover and tuning: flat. There was some fine tuning (sizing the journal and number of workers) which Claude did largely autonomously, but our service has remained stable since.
排队等待处理的未完成任务结果事件,每小时最大值。之前:大多数日子都会积压,且逐周增长。切换和调优后:持平。进行了一些微调(调整日志大小和 worker 数量),主要由 Claude 自主完成,此后我们的服务一直保持稳定。
If I was sent back in time to October 2025, I would have approached this and other projects differently with what I now know. The first difference is that I would account for the AI exponential. CI jobs increase exponentially as the average number of agents per engineer rises and as accelerated PR approval becomes more sophisticated. This has changed the shape of PRs over time at Anthropic as Claude prefers smaller, more granular PRs (another good reason not to run every test against every PR). This has translated into more CI jobs in a given day. Also, the activity level floor is raised as agents push overnight and on weekends, but it remains bursty as human engineers still drive and approve a significant amount of PRs.
如果我能回到 2025 年 10 月,以我现在所知,我会以不同的方式处理这个项目和其他项目。第一个不同是,我会考虑到 AI 的指数增长。随着每位工程师平均拥有的 agent 数量上升,以及加速的 PR 审批变得更加成熟,CI 任务会呈指数增长。这在 Anthropic 改变了 PR 的形态,因为 Claude 偏好更小、更细粒度的 PR(这也是不要对每个 PR 运行所有测试的另一个好理由)。这导致每天产生更多 CI 任务。此外,agent 在夜间和周末推送代码,抬高了活动水平的下限,但由于人类工程师仍然驱动和批准相当数量的 PR,活动仍然是突发性的。
My advice to engineering teams is, whether you build or buy, assume your architecture will be at a 25x load within two quarters. Over-engineering as a concept is starting to slightly fade away, or at least the bar is moving much higher. You can now start to account for 10-20x the perceived scale in your v0 designs as long your budget allows for it. Instrument your services to act as Claude’s eyes and ears. It allows Claude to hill-climb and fix problems incrementally much better and faster than we could manually. In particular, ensure that the same number of CI jobs coming in equals the same going out. Keep state out of the process from the start. I’d also avoid running any critical service as a single instance unless you can measure it and any canary changes. CI is evolving too quickly to proceed any other way.
我对工程团队的建议是,无论自建还是购买,都要假设你的架构在两个季度内会承受 25 倍的负载。过度设计这个概念正在逐渐淡去,或者至少门槛已经大大提高。只要预算允许,你现在就可以在 v0 设计中考虑 10-20 倍的预期规模。为服务埋点,让它们成为 Claude 的眼睛和耳朵。这让 Claude 能够以远好于我们手动的方式,逐步爬山并更快地解决问题。特别要确保进来的 CI 任务数量与出去的数量相等。从一开始就把状态挡在进程之外。我还建议避免将任何关键服务作为单实例运行,除非你能度量它并进行金丝雀变更。CI 演进得太快,不这样做不行。
I’ve also written how we accelerated CI on call using Claude Tag (beta).
我还写了我们如何使用 Claude Tag(beta)加速 on-call 期间的 CI。