靠极致优化,我们把推理成本砍掉了 68%
今年 4 月我们第一次写 AI 代码审查器时,平均每次审查成本是 1.19 美元,还算能接受。到了 8 月底,这个数字涨到了约 2.46 美元。这期间我们并没有大改架构:协调器还是把任务分发给同一批专业审查器,还是那套风险等级决定审查的深度。9 月中旬,我们让智能体去查这些 token 到底花在哪儿,然后大刀阔斧地砍。在代码质量设为高强度的情况下,现在平均每次审查只要 0.77 美元,比开始优化前最贵的两周平均 2.43 美元下降了 68%。我们的主要目标是降成本,但顺带也把审查器优化了一遍,审查中位数时间从 4 分 09 秒缩短到了 1 分 32 秒。约 40% 的节省来自审查器代码的改动和优化,其余来自尝试不同模型、调整模型投入的精力。
一次审查的成本是怎么翻倍的
下图展示了 3 月以来每个工作日的平均审查成本。统计时固定了审查档位的构成,所以某天大型审查多也不会让数据失真。单次审查成本一路走高:1.22 美元(3 月)→ 1.44 美元(6 月)→ 1.74 美元(7 月),最终在 8 月达到 2.32 美元。
其中一小部分原因是更多审查落到了最贵的档位(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 数量翻了三倍多,从 91 万涨到 299 万。命中率再高,token 总量翻了三倍,成本还是会大幅增加,哪怕缓存读取的价格很低。
自上一篇文章以来,还有哪些变化?最大的变化之一是审查器现在运行在 OpenCode v2 上,这是对 v1 的彻底重写。在把所有审查从 v1 切换到 v2 之前,我们在每个金丝雀仓库上用 v1 和 v2 并行运行了所有审查,做对比影子测试。在我们测试的合并请求中,两者的批准决定有 98% 是一致的,于是我们在 10 月初一次性全量切换,并删除了 v1 的代码。在相同模型下对比两天数据,v2 上的完整层级审查快了约 13%。我们目前还在尝试其他运行内部 AI 审查器的方式,包括仅使用 pi-durable 在 Durable Objects 上运行。希望很快能分享更多进展,敬请关注!
那么,token 都花到哪里去了?就在我们开始专注成本优化之前,AI 审查的支出构成是这样的:输出 token 约占 23%,缓存写入占 43%,缓存读取占 34%。这意味着大部分钱花在了模型已经见过、或者即将再次看到的上下文上。深入看一次会话就完全能理解。代码质量审查器平均每次审查会发起 16.6 次大语言模型(LLM)调用,在这些调用中,它向缓存写入 7.2 万 token,读回 108 万 token,因为每次调用都会把整个对话作为上下文发送。一次会话的成本随调用次数和上下文大小同时增长,而在过去几个月里,我们慢慢把这两个数字都推高了。值得花几分钟了解一下 Anthropic 的提示缓存(prompt cache)是怎么工作的。它的原理是缓存大语言模型(LLM)已经计算过的提示部分。
这些缓存叫做「前缀(prefix)」,因为它们只对对话开头的消息生效,聊天中途或末尾的内容不算。也就是说,后续请求可以复用之前的前缀,一直复用到对话发生变化的那部分为止。在我们的场景里,代码框架工具、系统提示词、以及 AI 审查会话开头的那几条消息几乎不变,非常适合做前缀缓存。
前缀只有在一个缓存断点(cache breakpoint)把它截断的地方才会被存下来。写缓存的价格是普通输入价的 1.25 倍,读缓存是 0.1 倍。一条缓存记录如果没人读取,五分钟后就会过期。所以我们有两件事要做:让每个会话的第一次请求走读缓存,而不是写缓存;让每个会话尽量少调用几次。
让所有仓库共用同一段缓存前缀
从 9 月 14 日起,审查器开始记录每个 reviewer 第一次调用 LLM 时的 token 用量,外加第一个系统块的哈希值——按发送给服务商的原始内容计算。数据显示,会话的第一次请求只有 14% 到 26% 的概率命中了缓存前缀。
每个会话开头都是同一套工具定义和同一段 agent 提示词,大小在 11k 到 21k token 之间,在所有被审查的仓库里完全一致。按理说这些几乎都该命中缓存。问题最终出在 OpenCode v1 组装系统提示词的方式上:它把我们的 agent 提示词和一个环境区块(包含工作目录和日期)拼成同一个块,而唯一的缓存断点放在这个块的末尾。于是共享的那部分从来没被单独存下来,一个仓库的审查无法复用另一个仓库的 reviewer 写下的缓存。
9 月 16 日,我们在每段 agent 提示词的末尾加了一个标记,并挂上一个插件钩子,在该标记处把块切成两半。这样前半段结束于所有任务都相同的字节上,并且有了自己的断点。为了维持这一点,我们还得把前半段里所有跟具体仓库相关的内容清出去,于是可用的 skill 列表被禁用,工具描述也不能在不同任务间发生变化。
同一天,我们又把内部规范(Codex)审核器的参考索引(一张内部规范表、一个技能文件,外加一份参考文件清单)直接放进了它的提示词里。在这之前,每次会话的头三轮都在用工具调用读这些文件。改完之后,它每次运行的大语言模型(LLM)调用次数从 15.9 降到 9.6,单次运行成本从 1.12 美元降到 0.72 美元。这两项加起来,把单次审核的成本从 2.47 美元压到 2.02 美元,效果在发版当天就能明显感觉到。
调用更少,花钱更少
下一批改动在 9 月 21 日和 23 日上线。单看每一项都不算惊艳,但合起来每次审核省下了约 0.20 美元。
| 改动 | 当时的问题 | 改动效果 |
|---|---|---|
| 移除审核器用不到的工具 | 子审核器能看到发布最终审核评论的那个工具。在 57% 到 76% 的会话里,它们会尝试调用,被服务端的校验拦下,然后再花一次调用去恢复。 | 省掉一次无效调用,而且每个子审核器的请求里少了约 2900 个 token 的工具定义。 |
| 并行读取文件 | 在代码质量审核器 64% 的轮次里,模型只发起一次工具调用,通常只读一个文件。 | 审核器在第一次响应里就读完共享上下文和它负责的全部补丁,最多 150 KB。 |
| 复审时跳过部分审核器 | 我们约三分之二的作业是对同一个合并请求的第二次或更晚的审核,而且往往有三个审核器根本没有新东西可看。 | 在完整档的复审中,AGENTS.md 审核被跳过 56%,性能审核跳过 23%,文档审核跳过 13%。 |
| 审核发布后立即停止 | OpenCode 会把结果再交回模型跑一轮,模型于是重读整段对话,写一份没人看的总结。 | 每个作业省下约 0.07 美元。 |
| 区分“审核器太慢”和“服务商故障” | 审核器撞到时间上限时,被当成服务商故障处理,于是用同样的时间上限重新开始。 |
配合更长的时间限制和更低的投入等级,安全审查超时从占运行次数的 1.8% 降到几乎为零。
取消那些已被取消的作业
GitLab 的 Kubernetes 执行器在作业被取消时,并不会给作业脚本发送信号。在抽样的 446 个已取消作业中,有 358 个一直运行到审查结束才停下来。
现在作业每 15 秒检查一次自身状态,发现被取消就立即停止。
最费周折的改动是让三个审查器共享同一个前缀。代码质量、安全、性能三个审查器现在发送的首次请求完全相同:工具相同、规则文件相同,再加上包含所有补丁的合并请求。之后才是各自的具体指令。我们确保其中一个写入缓存,另外两个从缓存读取。
缓存条目必须等到写入它的响应开始流式传输后才能被读取,而三个审查器又是同时启动的。所以第二个和第三个要等第一个的响应开始,最多等 20 秒,再发送自己的首次请求。这会稍微拖慢它们,但每个审查器能省约 9 美分。
更换模型和投入等级
下一张图展示的是 9 月初开始的同一条成本曲线,每个改动点都做了标记。把整个周期按每次发布和每次配置变更切分后可以看出,代码改动其实只占节省中的一小部分。
在总计 1.67 美元中,代码改动贡献 0.65 美元,模型和投入等级变更贡献 1.06 美元。每一步比较的都是改动前的工作日时段与改动后的时段,所以这算不上统计上严谨的对照实验。
协调器从 8 月起一直在运行三种模型与投入等级的组合。在相同模型下,中等投入的完整层级作业完成速度快 20%,于是我们放弃了另外两种。在后来的一次随机分组中,Opus 5.5 在中等投入下的审查结果和关键发现,与 Opus 5 相比没有可测量的差异,于是我们切换了模型。Opus 5.5 在高投入下比中等投入更贵,但发现的问题并没有明显增多。Opus 5.5 的每 token 价格比 Opus 5 便宜,但价格因素只贡献了约 0.20 美元的降幅,其余来自发送和生成更少的 token。
从 9 月初到现在,每次评审的缓存读取量下降了 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 上,代码质量审查器每次运行只调用大语言模型(LLM)1.7 次,而不是 6.2 次;超过一半的运行只调用一次就结束,也就是说,这些运行不会打开仓库里的其他文件。高强度下则是每次运行调用 3.4 次。我们担心这样会让评审变浅,下面的质量部分会讨论。为了确认提高强度能否在更大的档位里找回问题,从 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 上以高投入度运行时,它标出的严重和警告级问题比八月下旬少了 40%。安全与性能审查保持中等投入度,标出的问题分别减少了 56% 和 71%。
严重问题数量下降,本身并不能说明到底是漏掉了真问题,还是砍掉了噪音,所以我们从两个方向做了检查。
第一,我们看作者是否会针对审查者标出的问题采取行动。我们抽取了严重和警告级问题的样本,把被审查的那次提交与作者的下一次推送做对比:如果问题指向的代码行发生了变化,就计为“已处理”。在每一个时间段里,距离问题所在位置约 30 到 40 行的代码行都有大约 15% 的概率发生变化,这可以作为“碰巧被改动”的基线。改动之前,作者会处理代码质量审查者标出的严重问题中的 34%;现在这个比例是 59%。审查者标出的问题变少了,但被标出的问题更有可能引发改动,所以每 1000 次审查中,“最终导致改动的问题数”基本持平:现在是 132,之前是 145,处于误差范围之内。把四个审查者合起来看,导致改动的严重问题从每 1000 次审查 229 个上升到 321 个。
第二,我们让两家不同厂商的模型对来自三个 Sonnet 审查者的 657 条问题做盲评。样本中附上了每条问题对应的代码差异(diff),但不告诉评审模型这条问题来自哪个时间段。如果两个模型意见不一致,就交给第三个模型裁决。每条问题被标注为重要、次要、重复、错误或无法验证,其严重程度则标为合理、偏高或偏低。消失的问题大多是次要问题,另有一些重复项,以及评审模型无法从 diff 中验证的问题。每次审查中的重要问题从之前的 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,如果你想询问本文中的任何数字,欢迎来聊。