Cloudflare 团队通过优化推理将 AI 代码评审成本降低 68%
Cutting our inference bill by 68% by being obsessive about optimization
Cloudflare 团队通过精细化优化代码评审智能体,将平均单次评审成本从 2.43 美元降至 0.77 美元,降幅达 68%,同时将中位评审耗时从 4 分 09 秒缩短至 1 分 32 秒。优化手段主要包括跨仓库共享提示词前缀以提高缓存命中率、减少无效工具调用以及切换至更高效的 Anthropic 模型。
文章详细拆解了代码评审智能体在提示词缓存、模型切换与工具调用上的优化细节,为高频调用大模型的工程团队提供了可落地的降本增效方法。
当我们之前在 4 月的博客中介绍我们的 AI 代码审查工具时,平均每次审查的成本还算容易接受,为 1.19 美元。到了 8 月底,这个数字飙升至约 2.46 美元。我们并没有做任何重大的架构调整:协调器仍然将工作分派给同一组专家审查员,并且由相同的风险等级来决定我们进行多大强度的审查。
大约在 9 月中旬,我们给代理(agents)下达了任务,要求它们找出这些 Token 的去向并进行激进的削减。在代码质量保持高投入的情况下,现在平均每次审查的成本仅为 0.77 美元,比开始优化前成本最高的那两周的平均值 2.43 美元下降了 68%。我们的主要目标是削减成本,但作为这项工作的一部分,我们还成功优化了审查员,使得中位审查时间从之前的 4 分 09 秒缩短到现在的 1 分 32 秒。
大约 40% 的节省来自于对审查员代码的修改和优化,其余则来自于对我们使用的模型以及它们投入的精力大小进行实验。
审查成本是如何翻倍的
下图展示了 3 月以来每周工作日平均每次审查的成本。它固定了审查等级的组合比例,因此审查规模较大的日子不会扭曲数据。
单次审查成本稳步上升:从 1.22 美元(3 月)→ 1.44 美元(6 月)→ 1.74 美元(7 月),最后到 2.32 美元(8 月)。其中一小部分原因在于,落入最高成本等级的审查更多了(3 月占 59%,8 月占 65%)。我们认为原因很简单:模型变得越来越好、能力越来越强。自然地,工程师们正在产出更大、更复杂的变更,这意味着更多的变更落入了我们的“全面审查”等级。不过,我们发现大部分成本增长实际上来自于这几个月中的模型变更。
在 8 月初,我们将所有审查员都迁移到了 Anthropic 模型上,这使得每个任务的成本从 1.38 美元上升到 2.03 美元。我们做出这个选择是因为我们在评估中发现,在这一特定工作负载下,Anthropic 模型的表现优于其他模型。
由于性能和可靠性的综合考量,我们的内部 标准审查员 Codex 最终使用了 Claude Opus 4.7。这使其单次审查成本从大约 0.25 美元飙升至 8 月的 0.88 美元,甚至比协调器代理本身的成本还要高。
缓存命中率也有所提高,从 4 月的 85.7% 提升至 9 月的 97%,而每次审查的 Token 数量则增加了一倍多,从 0.91 百万增加到 2.99 百万。在 Token 数量为三倍的情况下,即使缓存读取成本很低,高命中率仍然会带来高昂的总体成本。
自我们上一篇博客以来还发生了哪些变化?
最大的变化之一是,审查员现在运行在 OpenCode v2 上,这是从 v1 彻底重写而来的。在将所有审查从 v1 切换到 v2 之前,我们在每个灰度仓库(canary repo)中同时在 v1 和 v2 上以比较影子模式(shadow mode)运行了每一次审查。在测试的所有合并请求中,两者在批准决策上有一致性达到了 98%,因此我们在 10 月初一次性将所有人迁移过去,并删除了 v1 代码。在使用相同模型的两天内进行对比,v2 上的全面审查速度快了约 13%。我们目前还在尝试运行内部 AI 审查员的其他方法,包括完全使用 pi-durable 在 Durable Objects 上运行。我们希望很快能分享更多相关内容,敬请期待!
那么,那些 Token 都去哪儿了?
就在我们开始专注于成本优化之前,我们的 AI 审查支出比例为:输出 Token 约占 23%,缓存写入成本占 43%,缓存读取成本占 34%。这意味着大部分资金都花在了模型已经看过或即将再次看到的上下文上。
当深入研究单个会话时,这就完全说得通了。代码质量审查员平均每次审查进行 16.6 次大语言模型(LLM)调用,在这些调用中,它向缓存写入了 72k 个 Token 并读取了 1.08m 个,因为每次调用都会将整个对话作为上下文发送。会话的成本随着调用次数和上下文大小的增加而增长,在过去的几个月里,我们慢慢增加了这两者。
花几分钟了解一下 Anthropic 的提示词缓存 是如何工作的,是非常值得的。它的工作原理是缓存大语言模型(LLM)已经计算过的一部分提示词。这些被称为前缀,因为它们仅对对话开始时的消息生效,对中途或结尾的则无效。这意味着未来的请求可以重用更早的前缀,直到对话中发生变化的部分(即超出已缓存的部分)。在我们的情况下,AI 审查员会话开始时的封装工具、系统提示词和消息几乎不会改变,因此它们是前缀缓存的极佳候选者。前缀仅存储在缓存断点结束的地方。写入缓存的成本是正常输入价格的 1.25 倍,读取成本是 0.1 倍,并且除非有读取操作,否则条目会在五分钟后过期。
这意味着我们有两个需要改进的方向:让每个会话的第一个请求从缓存中读取而不是写入,并让每个会话使用更少的调用。
在每个仓库之间共享缓存的前缀
从 9 月 14 日开始,审查员记录了每个审查员第一次 LLM 调用的 Token 使用量,以及发送给提供商的第一个系统块的哈希值。
数据显示,会话的第一个请求仅有 14% 到 26% 的概率找到缓存的前缀。每个会话都以相同的工具定义和相同的代理提示词开始,在我们审查的每个仓库中,这 11k 到 21k 个 Token 是完全相同的,因此几乎所有这些请求都应该是缓存命中。
问题最终出在 OpenCode v1 构建系统提示词的方式上:它构建了一个包含我们代理提示词的块,后面跟着一个带有工作目录和日期的环境部分,并将唯一的缓存断点放在该块的末尾。共享部分从未单独存储,因此在一个仓库中的审查永远无法重用另一个仓库中的审查员所写入的内容。
9 月 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 美元 |
区别对待响应迟钝的评审员与崩溃的服务提供商 | 触及时间限制的评审员被视为服务提供商故障,并带着相同的时限重新开始。 | 结合更长的时限和更低的努力程度,安全评审员的超时率从运行总数的 1.8% 降至几乎为零 |
取消已被取消的任务 | GitLab 的 Kubernetes 执行程序在任务取消时不会向任务的脚本发送信号。在 446 个被取消任务的样本中,358 个任务一直在运行直到评审完成。 | 任务每 15 秒检查一次自身状态,如果被取消则停止 |
涉及最多的更改是在三个评审员之间共享一个前缀。代码质量、安全和性能评审员现在发送相同的第一次请求,包含相同的工具、一个共享的规则文件,然后是包含每个补丁的合并请求。每个评审员自己的指令紧随其后,并且我们确保其中一个写入缓存,另外两个从中读取。
在写入缓存的响应开始流式传输之前,缓存条目是无法被读取的,而这三个评审员是同时启动的。因此,第二和第三个评审员会等待第一个评审员的响应开始(长达 20 秒),然后才发送他们自己的第一次请求。这会稍微减慢他们的速度,但每个评审员大约能节省九美分。
更换模型和努力程度
下一张图表展示了从 9 月初开始的相同折线,并标出了每一项更改。
当我们把时间段按每次发布和每次配置更改进行分割时,代码更改反而是节省成本中较小的一部分。
在 1.67 美元的节省中,代码更改占了 0.65 美元,模型和努力程度的更改占了 1.06 美元。每个步骤都比较了更改前后的工作日小时数,因此这并不是一个统计学上严谨的对照实验。
该协调器自 8 月以来一直在运行三种模型和工作量组合。在同一模型上,中等工作量的全量级(full-tier)任务完成速度快了 20%,因此我们弃用了另外两种。在后来的随机拆分测试中,中等工作量的 Opus 5.5 在审查结果或关键发现上与 Opus 5 相比没有表现出可测量的变化,因此我们进行了切换。高工作量的 Opus 5.5 成本比中等工作量的 Opus 5.5 高,但发现的问题并没有实质性增加。
Opus 5.5 的每 Token 价格比 Opus 5 更便宜,但价格因素只占下降幅度的约 0.20 美元。其余节省来自于发送和生成更少的 Token。自 9 月初以来,每次审查的缓存读取量下降了 82%,缓存写入下降了 54%,输出下降了 67%。
最近的一次变更是引入了新模型。10 月 3 日,我们将每个 Sonnet 审查员在 Sonnet 5 和 Sonnet 5.5 之间进行了 50/50 的均分。两天半后,在代码质量审查方面,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 次 calls。我们曾担心这会使审查变浅,我们将在下文的质量部分对此进行探讨。
为了观察在更大层级上投入更多工作量是否能带回更多的发现,自 10 月 5 日以来,代码质量审查员在 5.5 上按 50/50 的比例平分了中等和高工作量。一天之后,高工作量的每次运行成本增加了 61%,并多发布了 58% 的关键和警告发现。每次审查大约只需两美分,下文的质量部分表明这些额外的发现是值得拥有的,因此我们将代码质量提升到了高工作量。本文中的成本数字都是基于这一假设计算的。
延迟
我们并非旨在让审查变得更快,但更少的调用也意味着更少的等待,这意味着更快的审查和更快乐的工程师。
最慢的审查改善最为明显。在 9 月 14 日之前的四周里,37.9% 的审查耗时超过五分钟,而现在只有 0.7% 的审查如此。如今的第 99 百分位速度比 4 月份四分之三的审查还要快。在此过程中,每个层级都变得更快,全量级审查的中位数从 5 分 16 秒降至 1 分 54 秒,精简版(lite)从 2 分 58 秒降至 1 分 14 秒,微型版(trivial)从 77 秒降至 30 秒。
到 8 月时,审查中位数比我们在 4 月份报告的时长慢了 14%,第 99 百分位慢了 48%,因此这项工作很大程度上只是让我们回到了先前的水平。
审查质量
如果更便宜的审查同时发现的问题也更少,那么它就没有多大价值,而这里的第一个图表看起来就像是发生了这种情况。
当代码质量使用高工作量的 Sonnet 5.5 时,其发布的关键和警告发现比 8 月下旬减少了 40%。保持在中等工作量的安全和性能审查,其发现分别减少了 56% 和 71%。严重问题的减少本身并不能说明是遗漏了真实的问题还是消除了噪音,因此我们从两个方向进行了观察。
首先,我们检查了作者是否会根据评审员指出的问题采取行动。对于关键和警告发现的样本,我们比较了被评审的提交与作者的下一次推送,如果它所指向的行发生了变化,则计为已采取行动。在每个时期,距离每个发现 30 到 40 行的代码大约有 15% 的时间会发生变化,这为偶然发生的修改提供了一个基准。
改动前,作者会处理代码质量评审员 34% 的严重发现。现在这一比例为 59%。评审员标记的问题虽然减少了,但被标记的问题更有可能引发改动,因此每 1,000 次评审中促成改动的问题数量大致保持不变。我们现在看到的是 132 个,之前是 145 个,这在误差范围内。在所有四个评审员中,促成改动的严重发现从每 1,000 次评审的 229 个上升到 321 个。
其次,我们请来自不同提供商的两个模型,对来自三个 Sonnet 评审员的 657 个发现的匿名样本进行评估,其中包含了各自指代的代码差异(diff),且不了解这些发现来自哪个时期。分歧则交由第三个模型裁决。每个发现都被标记为重要、次要、重复、错误或无法核实,其严重程度则被标记为合理、过高或过低。
消失的大部分是次要发现,加上一些重复发现以及评审员无法从代码差异中核实的发现。每次评审的重要发现从之前的 0.28 个变为现在的 0.30 个,且置信区间重叠。更大的变化在于严重程度。此前,裁判认为每次评审中有 0.46 个关键或警告发现的严重程度高于应有水平。现在这一数字为 0.14,下降了 70%,这也是这项研究中最明确的结果。关键发现的大幅减少,主要是因为评审员不再将一些小问题称为关键问题。
因此,答案取决于评审员。对于高工作量的代码质量评审,消失的大部分是噪声和夸大的严重程度,它仍然能发现和以前差不多数量的真实问题。中等工作量确实丢失了真实的发现。在随机拆分中,中等工作量的代码质量所带来的被处理发现大约是高工作量的四分之一,这就是为什么我们要将其提升到高工作量。性能评审也丢失了真实的发现:裁判发现每次评审的重要性能问题减少了约 64%,在实践中大约是每 24 次完整层级(full-tier)评审减少一个重要发现。安全性下降了约四分之一,尽管这在统计上并不显著。这两者目前仍处于中等工作量,下一步是对其进行高工作量测试。
这两种方法都不是完美的。作者更改了被标记的行只表明他们采取了行动,并不代表该发现是正确的。裁判也是模型,且每组只有 35 到 66 个发现。“现在”的时间窗口也只有一天。即便如此,这两种方法的结果是一致的,而且没有任何一种方法表明代码质量发现的真实问题变少了。
可靠性也同样保持了下来。没有给出结论的评审从 0.79% 下降到 0.27%,安全评审员的重试次数从 3.5% 下降到 0.04%。
我们目前的处境
现在平均每次代码审查的成本从 2.43 美元降至 0.77 美元,中位耗时从 4 分 9 秒缩短至 1 分 32 秒。单项最大的节省来自于检查每个会话的第一次请求实际发送给提供商的内容,并找到一种方法,在多次提交、合并请求和仓库之间尽可能保持这些消息顺序的一致性。其余的成本节省则来自于对不同模型和工作量组合进行不懈的实验,同时确保抛开对特定模型的固有偏见。
审查成本降低并不自动意味着质量下降,但也不自动意味着一切都好。高工作量下的代码审查发现的真实问题与以前差不多,但噪音大为减少,被夸大的严重程度也大幅降低。中等工作量下的性能审查漏掉了一些真实问题,安全审查可能也是如此,因此下一步是在高工作量下对这两项进行测试。如果你以任何规模运行 Agent,我们建议首先检查的两件事是:每个会话的第一次请求是否确实从缓存中读取,以及你的审查人员是否仍然在你所支付的工作量水平下发现真正的问题。
如果你将参加旧金山的 Cloudflare Connect,我将在 10 月 20 日星期二太平洋夏令时下午 12:30 的会议“是时候自己构建,而不是购买了”中,分享我们如何自己构建审查器而不是购买现成产品。如果你想询问本文中的任何数据,我也会在下午 1:10 出现在演讲者交流休息室(Speaker Connection Lounge)。
来源:Cloudflare AI · blog.cloudflare.com