跳到正文
Cloudflare Blog·· 1 天前精选AI 评分76

Cloudflare 通过极致优化将 AI 代码审查单次成本降低 68% 至 $0.77

Cutting our inference bill by 68% by being obsessive about optimization

AI 导读

Cloudflare 分享其 AI 代码审查器的成本优化复盘:平均单次审查成本从 $2.43 降至 $0.77(降 68%),中位耗时从 4m 09s 缩至 1m 32s。

推荐理由

原文复盘了 AI 代码审查成本从 $2.43 降到 $0.77 的具体手段,包括缓存前缀共享和模型切换,方法可迁移到其他 Agent 场景。

正文 · AI 翻译

我们之前在四月的博客文章中介绍过我们的 AI 代码审查器,当时平均每次审查的成本是尚可接受的 1.19 美元。到八月底,这个数字飙升到了约 2.46 美元。我们并没有做出任何重大的架构调整:协调器仍然把工作分派给同一组专业审查器,同样还是由相同的风险等级来决定审查的深入程度。

九月中旬前后,我们让智能体去查明那些 token 都花到哪里去了,并大幅削减它们。在代码质量设为高强度的情况下,现在平均每次审查的成本仅为 0.77 美元,相比开始之前我们最昂贵的两周里平均的 2.43 美元下降了 68%。我们的主要目标是削减成本,但作为这项工作的一部分,我们也成功优化了审查器,因此现在审查耗时的中位数只需 1 分 32 秒,而之前需要 4 分 09 秒。

大约 40% 的节省来自对审查器代码的更改和优化,其余则来自对使用哪些模型以及它们投入多少精力的实验。

一次审查的成本是如何翻倍的

下图显示了自三月以来每个工作日平均每次审查的成本。它固定了各审查等级的构成比例,因此某个大型审查较多的日子不会使数据产生偏差。

每次审查的成本从 1.22 美元(三月)稳步上升到 1.44 美元(六月)→ 1.74 美元(七月),最终达到 2.32 美元(八月)。其中一小部分原因是更多审查落入了最昂贵的等级(三月为 59% 的审查,八月为 65%)。我们相信对此有一个简单的解释:模型正变得好得多,也强大得多。自然而然地,工程师们产出的变更越来越大、越来越复杂,这意味着其中更多会落入我们的“完整审查”等级。不过我们发现,大部分成本增长实际上来自这几个月间的模型更换。

八月初,我们把每个审查器都迁移到了 Anthropic 的模型上,这使每次作业的成本从 1.38 美元提高到 2.03 美元。我们做出这一选择,是因为在评估中发现 Anthropic 的模型在这一特定工作负载上的表现优于其他模型。

我们的内部标准审查器 Codex最终采用了 Claude Opus 4.7,这是出于性能和可靠性的综合考量。这让它每次审查的成本从约 0.25 美元涨到了八月的 0.88 美元,使它比协调器智能体本身还要昂贵。

缓存命中率也有所提高,从四月的 85.7% 上升到九月的 97%,而每次审查的 token 数量增长了两倍多,从 91 万增加到 299 万。即使缓存读取成本很低,在 token 数量翻三倍的情况下保持高命中率仍然要贵得多。

自我们上一篇文章以来还发生了什么变化?

最大的变化之一是审查器现在运行在 OpenCode v2 上,这是对 v1 的彻底重写。在把所有审查从 v1 切换到 v2 之前,我们在对比影子模式下,对每个金丝雀仓库中的每一次审查都同时运行了 v1 和 v2。在我们测试的合并请求中,两者在批准决定上的一致率为 98%,因此我们在十月初一次性完成了全部迁移,并移除了 v1 的代码。比较使用相同模型的两天,v2 上的完整等级审查完成速度快约 13%。我们目前还在试验其他运行内部 AI 审查器的方式,包括仅使用 pi-durable 在 Durable Objects 上运行。我们希望很快能带来更多相关内容,敬请关注!

那么,那些 token 都花到哪里去了?

就在我们开始专注于成本优化之前,我们的 AI 审查支出按以下方式划分:输出 token 约占 23%,缓存写入成本占 43%,缓存读取成本占 34%。这意味着大部分费用花在了模型已经看过或即将再次看到的上下文上。

如果你深入了解单个会话,这就完全说得通了。代码质量审查器每次审查平均发起 16.6 次大语言模型(LLM)调用,在这些调用中它向缓存写入了 72k token,并读回了 1.08m,因为每次调用都会把整个对话作为上下文发送。一个会话的成本会随着调用次数和上下文大小一起增长,而在过去几个月里,我们已慢慢地让两者都在增加。

值得花几分钟读一读 Anthropic 的 prompt cache 是如何工作的。它的原理是缓存大语言模型(LLM)已经计算过的提示词部分。这些部分被称为前缀,因为它们只对对话开头的消息生效,而不是对话中间或结尾的消息。这意味着未来的请求可以复用较早的前缀,直到对话中与已缓存内容不同的部分为止。在我们的场景中,AI 审查器会话开头的 harness 工具、系统提示词和消息很少变化,因此它们是前缀缓存的绝佳候选。只有缓存断点结束的地方才会存储前缀。写入缓存的成本是普通输入价格的 1.25 倍,读取成本是 0.1 倍,并且条目会在五分钟后过期,除非有东西读取它。

这意味着我们有两件事要做:让每个会话的第一次请求从缓存读取而不是写入,并让每个会话使用更少的调用。

在每个仓库之间共享缓存前缀

从 9 月 14 日开始,审查器记录每个审查器首次 LLM 调用的 token 使用量,以及第一个系统块的哈希值,完全按照发送给提供商时的原样。

数据显示,会话的第一次请求只有 14% 到 26% 的情况找到缓存前缀。每个会话都以相同的工具定义和相同的 agent 提示词开头,这些在我们审查的每个仓库中都完全相同,有 11k 到 21k token,所以几乎所有这些都本应是缓存命中。

问题最终出在 OpenCode v1 构建系统提示词的方式上:它构建了一个包含我们 agent 提示词的块,后面跟着一个包含工作目录和日期的环境部分,并把唯一的缓存断点放在该块的末尾。共享部分从未被单独存储,因此在一个仓库中的审查永远无法复用另一个仓库中审查器运行所写入的内容。

9 月 16 日,我们在每个 agent 提示词的末尾添加了一个标记,并添加了一个插件钩子,在该标记处将块一分为二。前半部分现在以每个作业都共享的字节结束,并拥有自己的断点。为了保持这一点,我们不得不移除其前面任何与仓库相关的内容,因此可用技能列表被禁用,工具描述不能在不同作业之间变化。

Chart showing the share of reviewers' first requests that read from the cache rising from 14–26% to 91–99.5% after September 16.

同一天,我们还将内部标准(Codex)审查器的参考索引(一张内部标准表、一个技能文件和一个参考文件列表)移入了它的提示词中。在此之前,每个会话都花了前三轮通过工具调用读取这些文件。它每次运行的 LLM 调用从 15.9 次减少到 9.6 次,每次运行的成本从 $1.12 降至 $0.72。

这些改动加起来把每次审查的成本从 $2.47 降到了 $2.02,而且我们在发布一启动时就立刻感受到了这一下降。

更少的调用,更少的钱

下一批改动于 9 月 21 日和 23 日上线。这些改动单看都不算特别惊艳,但合在一起,每次审查大约省下了 $0.20。

改动

当时的情况

改动的效果

移除审查者无法使用的工具

子审查者能看到发布最终审查评论的那个工具。在 57% 到 76% 的会话中,它们尝试调用它,被服务器上的检查拒绝,然后又花了一次调用来恢复。

少了一次浪费的调用,并且每个子审查者请求中的工具定义少了约 2.9k 个 token

并行读取文件

在代码质量审查者 64% 的轮次中,模型只进行了一次工具调用,通常是为了读取单个文件。

审查者在第一次响应中就读取共享上下文和所有补丁,最多可达 150 KB

在重新审查时跳过部分审查者

大约三分之二的作业是对同一个合并请求的第二次或更晚的审查,而且通常其中三个审查者没有什么新内容可看。

在完整层级的重新审查中,AGENTS.md 审查被跳过的比例为 56%,性能审查被跳过 23%,文档审查被跳过 13%。

在审查发布后停止

OpenCode 把结果传回模型再多跑一轮,模型又把整个对话重新读一遍,写出一份没人看的摘要。

每个作业大约节省 $0.07

区别对待慢审查者和故障提供方

一个触及时间上限的审查者被当作提供方故障处理,并以相同的时间上限重新开始。

再加上更长的时间限制和更低的 effort,安全审查者的超时率从 1.8% 的运行占比降到了几乎为零

取消已取消的作业

GitLab 的 Kubernetes executor 在作业被取消时不会向作业脚本发送信号。在 446 个已取消作业的样本中,有 358 个继续运行,直到审查完成。

作业每 15 秒检查一次自身状态,如果被取消就停止

最复杂的一项改动是在三个审查者之间共享同一个前缀。代码质量、安全和性能审查者现在发送完全相同的首次请求,带有相同的工具、一个共享规则文件,然后是包含所有补丁的合并请求。每个审查者各自的指令紧随其后,我们确保其中一个写入缓存,另外两个从缓存读取。

Diagram showing three sub-reviewers sharing one cached first request: the first writes it and the other two read it, so only each reviewer's own instructions differ. The tools and shared rules at the start are shared across every repository.

缓存条目只有在写入它的响应开始流式传输后才能被读取,而这三个审查者同时启动。因此第二个和第三个要等待第一个的响应开始,最长 20 秒,然后才发送它们自己的首次请求。这会让它们稍微慢一点,但每个审查者大约节省九美分。

更换模型和 effort

下一张图展示了从 9 月初开始的同一条曲线,并标出了每一项改动。

Chart showing cost per review falling in steps from about $2.40 to $0.75, each step matching a specific change.

当我们按每次发布和每次配置变更来切分这段时间时,代码改动反而不是节省中的较小部分。

Chart splitting the drop from $2.44 to $0.77 by change. The shared cached prefix was the biggest single saving, but model and effort changes together saved more than code changes.

在 $1.67 中,代码改动占 $0.65,模型和 effort 改动占 $1.06。每一步都比较了改动前的工作日时段和改动后的时段,所以这并不是一个统计上严谨的对照实验。

协调器自八月以来一直在运行三种模型与投入程度的组合。在同一模型上,中等投入的完整层级任务提前 20% 完成,因此我们放弃了另外两种。在随后的一次随机拆分中,中等投入的 Opus 5.5 在评审结果或关键发现上与 Opus 5 相比没有可测量的变化,所以我们进行了切换。高投入的 Opus 5.5 成本高于中等投入的 Opus 5.5,但并未带来发现数量的有意义增加。

Opus 5.5 每 token 比 Opus 5 更便宜,但价格只占降幅中约 0.20 美元。其余部分来自发送和生成更少的 token。自九月初以来,每次评审的缓存读取下降了 82%,缓存写入下降了 54%,输出下降了 67%。

最近的一次变更是引入新模型。10 月 3 日,我们将每个 Sonnet 评审器以 50/50 的比例在 Sonnet 5 和 Sonnet 5.5 之间拆分。两天半后,Sonnet 5.5 在代码质量上每次运行便宜 62%,性能上便宜 58%,安全上便宜 47%。这两个模型每 token 成本相同,所以这一切都来自使用更少的 token。在完整层级评审中,5.5 评审器发布的关键和警告发现数量与 Sonnet 5 评审器大致相同,因此在 10 月 5 日我们将所有评审器都迁移到了 5.5。

在中等投入的 Sonnet 5.5 上,代码质量评审器每次运行进行 1.7 次 LLM 调用,而不是 6.2 次,并且其超过一半的运行在单次调用中完成,这意味着它们不会打开仓库中的任何其他文件。在高投入下,它每次运行进行 3.4 次调用。我们担心这会让评审变得肤浅,这一点我们在下面的质量部分中讨论。

为了了解更高的投入是否能在更大层级上带回发现,自 10 月 5 日以来,代码质量评审器在 5.5 上以 50/50 的比例在中等投入和高投入之间拆分。一天后,高投入每次运行成本高出 61%,并发布多出 58% 的关键和警告发现。这大约是每次评审两美分,而下面的质量部分表明这些额外的发现是值得的,因此我们正在将代码质量转向高投入。本文中的成本数字基于这一假设。

延迟

我们并非旨在让评审更快,但更少的调用也意味着更少的等待,这意味着更快的评审和更满意的工程师。

Chart showing reviews getting faster at every percentile, the slowest most. The median fell from 4m 09s to 1m 32s and the 99th percentile from 15m 20s to 4m 41s.

最慢的评审改善最大。在 9 月 14 日之前的四周内,37.9% 的评审耗时超过五分钟,而现在只有 0.7%。如今的第 99 百分位比四月份四分之三的评审还要快。每个层级一路上都在变快,完整层级评审的中位数从 5 分 16 秒降至 1 分 54 秒,轻量层级从 2 分 58 秒降至 1 分 14 秒,琐碎层级从 77 秒降至 30 秒。

到八月,评审中位数比我们四月份报告的慢了 14%,第 99 百分位慢了 48%,所以这项工作的很大一部分只是让我们回到了原来的水平。

评审质量

如果更便宜的评审也发现更少的问题,那就没有多大价值,而这里的第一个图表看起来正是如此。

Chart showing the code quality, security and performance reviewers posting far fewer critical and warning findings, while compliance posted more.

在高投入的 Sonnet 5.5 上进行代码质量评审时,它发布的关键和警告发现比八月下旬少了 40%。保持中等投入的安全和性能评审分别少了 56% 和 71%。严重发现的下降本身并不能告诉你真正的问题是被遗漏了还是噪音被削减了,所以我们从两个方向进行了考察。

首先,我们检查了作者是否会根据审阅者指出的问题采取行动。对于严重和警告级别发现项的样本,我们将被审阅的提交与作者的下一次推送进行比较,如果发现项指向的行发生了改动,就将其计为已采取行动。每个发现项周围30到40行在每一时期大约有15%的概率发生改动,这为巧合发生的编辑提供了一个基线。

Chart showing authors now act on 59% of the code quality reviewer's serious findings, up from 34%, so it leads to about as many changes as before despite flagging fewer.

在变更之前,作者对代码质量审阅者提出的严重发现项采取了34%的行动。现在是59%。审阅者标记的问题更少了,但被标记的问题更有可能导致改动,因此每1,000次审阅中导致改动的发现项数量大致相同。我们现在看到的是132,之前是145,这在误差范围内。在全部四个审阅者中,导致改动的严重发现项从每1,000次审阅229个增加到321个。

其次,我们让来自不同提供商的两个模型对来自三个Sonnet审阅者的657个发现项的盲样本进行评判,附上每个发现项所指向的差异,且不告知其来自哪个时期。分歧交由第三个模型裁决。每个发现项被标记为重要、次要、重复、错误或无法验证,其严重程度被标记为合理、过高或过低。

Chart showing the drop in findings was mostly minor or unverifiable ones, while important findings per review held steady (0.28 before, 0.30 now).

消失的大部分是次要发现项,还有一些重复项以及评判者无法从差异中验证的发现项。每次审阅的重要发现项从之前的0.28变为现在的0.30,置信区间重叠。更大的变化在严重程度方面。之前,评判者认为每次审阅有0.46个严重或警告发现项的严重程度高于其应得的水平。现在这一数字是0.14,下降了70%,是这项研究中最清晰的结果。严重发现项数量下降的很大一部分原因是审阅者不再把次要问题称为严重问题。

所以答案取决于审阅者。对于高工作量的代码质量审阅,消失的主要是噪声和夸大的严重程度,它发现的真实问题数量与之前大致相同。中等工作量确实失去了真实发现项。在随机拆分中,中等工作量的代码质量所导致的已采取行动的发现项大约是高工作量的四分之一,这就是我们将其移至高的原因。性能审阅也失去了真实发现项:评判者发现每次审阅的重要性能问题减少了约64%,实际上大约每24次完整层级审阅就少一个重要发现项。安全审阅下降了约四分之一,不过这并不具有统计显著性。两者仍处于中等工作量,接下来将在高工作量下测试它们。

两种方法都不完美。作者改动了被标记的行,表明他们做了些什么,但并不意味着该发现项是正确的。评判者也是模型,而且每组只有35到66个发现项。“现在”窗口也仅有一天。即便如此,两种方法相互吻合,且都没有表明代码质量审阅发现的真实问题变少了。

可靠性也保持住了。未给出结论就结束的审阅从0.79%降至0.27%,安全审阅者重试从3.5%降至0.04%。

我们的结论

现在,一次平均审查的费用是 0.77 美元,而不是 2.43 美元;中位数审查的完成时间是 1 分 32 秒,而不是 4 分 9 秒。最大的一笔节省来自检查每次会话首个请求实际发送到提供方的内容,并找到一种方法,让这些消息在提交、合并请求和仓库之间尽可能一致地排序。其余的成本节省来自不断尝试不同的模型和投入组合,同时确保抛开对特定模型的任何先入为主的偏见。

成本更低的审查并不自动意味着质量更差,但也不自动意味着没问题。高投入下的代码质量发现的真实问题与之前差不多,但噪音少得多,严重性被夸大的情况也少得多。性能在中等投入下丢失了真实发现,安全可能也是如此,因此接下来要在高投入下对两者进行测试。如果你以任何规模运行代理,我们首先要检查的两件事是:每次会话的首个请求是否真的从缓存读取,以及你的审查器在你所付费的投入水平下是否仍能发现真实问题。

如果你在旧金山参加 Cloudflare Connect,我会在 10 月 20 日星期二太平洋夏令时下午 12:30 的会议上,讲述我们是如何构建审查器而不是购买一个的,是时候构建,而不是购买。下午 1:10 我也会在 Speaker Connection Lounge,如果你想询问本文中的任何数字。

来源:Cloudflare Blog · blog.cloudflare.com