把 AI 代码审查的账单砍掉 68%:真正起作用的那些数字
2026 年 4 月,Cloudflare 写过一篇自建 AI 代码审查的文章,当时一次审查平均花 1.19 美元,还算能接受。到了 8 月底,这个数字涨到约 2.46 美元。中间他们没做任何架构改动:调度 Agent 还是把活分给同一批专家审查器,还是同一套风险分级决定审查强度。9 月中旬,他们让 Agent 去查这些 token 到底流去哪儿了,然后狠狠地砍。结果是:代码质量审查以高强度运行时,平均每次审查现在只要 0.77 美元,比优化前两周的平均 2.43 美元降了 68%;顺带把中位耗时从 4 分 09 秒压到 1 分 32 秒。这篇文章把六项改动按「花钱的地方」拆开讲,方便你照着自己账单找。
架构一行没改,单次审查成本五个月翻了近一倍
一、先看清楚钱花在哪儿
优化之前,Cloudflare 的支出结构是这样的:输出 token 约占 23%,缓存写入占 43%,缓存读取占 34%。换句话说,三分之二的钱花在模型已经看过、或者马上要再看一遍的上下文上。往单个会话里看就懂了:代码质量审查器平均每次审查发起 16.6 次 LLM 调用,这些调用一共往缓存写了 72k token,却读回了 108 万 token。原因很朴素——每次调用都把整段对话当上下文重新发一遍。所以账单是「调用次数 × 上下文大小」的乘积,而过去几个月他们在这两个变量上都在悄悄变大:token 总量从 0.91M 涨到 2.99M,缓存命中率虽然从 4 月的 85.7% 升到 9 月的 97%,但三倍的 token 量乘上很高的命中率,依然很贵。
# Step 1: find out where the money goes BEFORE optimizing anything.
# Cloudflare's split before they started:
# output tokens ~23%
# cache writes 43%
# cache reads 34%
# Two thirds of the bill was context the model had already seen.
# Pull per-review usage from your provider and group it.
python3 summary.py reviews.jsonl
# avg_calls_per_review 16.6
# cache_write_tokens 72k
# cache_read_tokens 1.08M
# If reads >> writes and cost is still high, your prefix is too long,
# not your prompt.二、真凶:首个请求根本没命中缓存
9 月 14 日起,审查器开始记录每次会话第一个 LLM 调用的 token 用量,以及首个 system block 原样的哈希。数据出来很难看:会话的第一个请求只有 14% 到 26% 的概率命中了缓存。而每个会话开头都是同一份工具定义和同一份 Agent prompt,11k 到 21k token,在他们审查的每一个仓库里都一模一样——几乎全部都该命中。问题出在 OpenCode v1 构造 system prompt 的方式:它把 Agent prompt 和环境信息(工作目录、日期)拼成一个 block,而唯一的缓存断点放在这个 block 的末尾。结果就是共享的那半截从来没被单独存过,A 仓库写下的缓存,B 仓库永远读不到。9 月 16 日他们加了个标记和一个插件钩子,把 block 一分为二,第一个半截结束在所有任务共享的字节上,并拥有自己的断点。
# Step 2: check whether your FIRST request actually reads the cache.
# This was the real bug: 14%-26% hit rate on the first call of a session,
# while 11k-21k tokens of tool defs + agent prompt were identical everywhere.
import hashlib, json
seen = {}
hits = total = 0
for line in open("sessions.jsonl"):
rec = json.loads(line)
first = rec["calls"][0]
block = first["system"][0]["text"] # exactly as sent
key = hashlib.sha256(block.encode()).hexdigest()
total += 1
if first["usage"]["cache_read_input_tokens"] > 0:
hits += 1
seen.setdefault(key, 0)
seen[key] += 1
print(f"first-call cache hit rate: {hits/total:.1%}")
print(f"distinct first blocks: {len(seen)} (want: 1)")
print("top block appears", max(seen.values()), "times")三、拆 block 的代价:冻结工具描述
拆 block 不是免费的。为了让共享前缀保持字节一致,任何仓库相关的信息都不能出现在它前面:可用的 skill 列表被禁用了,工具描述也不能在任务之间发生变化。这是一个真实的权衡——你换来了跨仓库的缓存复用,交出了每个仓库定制工具集的能力。同一天他们还做了另一件事:把内部规范审查器(代号 Codex)的参考索引——一张内部规范表、一个 skill 文件、一份参考文件清单——直接塞进 prompt 里。之前每个会话都要花前三轮用工具调用把这些文件读一遍。效果立竿见影:每次运行的 LLM 调用次数从 15.9 降到 9.6,单次成本从 1.12 美元降到 0.72 美元。这两项合起来,把每次审查的成本从 2.47 美元拉到 2.02 美元。
# Step 3: split the block so the shared prefix can be cached on its own.
# Cloudflare added a marker at the end of the agent prompt and a plugin
# hook that splits there. Everything repository-specific moved AFTER it.
# Before: one block, one breakpoint at the very end
# [agent prompt][cwd + date][repo details] <-- breakpoint
# => the shared part was never stored alone, so no repo reused another's cache
# After: two blocks, breakpoint on the shared half
# [agent prompt] <-- breakpoint (identical across all repos)
# [cwd + date + repo details]
# Cost of the split: skills list disabled, tool descriptions frozen.
# Win: standards reviewer calls per run 15.9 -> 9.6, cost $1.12 -> $0.72四、删掉不可能成功的调用
9 月 21 日和 23 日发的那批改动单看都不惊艳,合起来每次审查省了约 0.20 美元。最典型的一条:子审查器能看到「发布最终评论」这个工具,在 57% 到 76% 的会话里它们真的去调了,被服务端拦下,然后又多花一次调用去恢复。正确做法不是在运行时过滤,而是把这个工具从子审查器的定义里彻底删掉——顺带每次请求少了约 2.9k token 的工具定义。第二条:代码质量审查器有 64% 的轮次只发一个工具调用,通常是读一个文件。改成让它在第一次响应里就把共享上下文和全部 patch 一起读完,上限 150 KB。第三条:约三分之二的作业是同一个 MR 的第二次或更晚的审查,往往有三个审查器根本没新东西可看,于是全量复审时 AGENTS.md 审查跳过 56%、性能审查跳过 23%、文档审查跳过 13%。
# Step 4: stop paying for calls that cannot succeed.
# 57%-76% of sub-review sessions tried to call a tool they were not
# allowed to use (posting the final comment), got rejected, then spent
# another call recovering.
# Delete the tool from sub-reviewer definitions, do not filter at runtime.
DEFERRED = {"post_review_comment", "assign_reviewer", "merge_ready"}
def tools_for(role: str) -> list:
if role == "sub_reviewer":
return [t for t in ALL_TOOLS if t["name"] not in DEFERRED]
return ALL_TOOLS
# Also: 64% of turns made exactly one tool call (usually one file read).
# Batch reads into the first response -- up to 150 KB of context + patches.
# And cancel cancelled jobs: GitLab's Kubernetes executor sends no signal,
# so 358 of 446 cancelled jobs kept running to completion.
# Poll your own status every 15s and exit.五、还有两个人人都该修的坑
第一,审查发布完之后还在烧钱:OpenCode 会把结果再回传给模型跑一轮,模型于是把整段对话重读一遍,写出一份没人看的总结。停掉它,每个作业省约 0.07 美元。第二,被取消的作业没真的取消:GitLab 的 Kubernetes executor 在作业被取消时不会给脚本发信号,他们抽查的 446 个被取消作业里有 358 个一直跑到了审查结束。解法很土但有效——作业每 15 秒查一次自己的状态,发现被取消就退出。另外,把「审查器超时」和「厂商提供方故障」分开处理也很关键:之前超时被当成提供方故障,于是用同样的时限重试一遍,白烧一遍钱;放宽时限并降低 effort 之后,安全审查器的超时率从 1.8% 降到几乎为零。
# Step 5: share ONE cache prefix across three reviewers.
# Code quality, security and performance now send identical first requests:
# same tools, same shared rules file, then the merge request with all patches.
# Then each reviewer's own instructions. One writes the cache, two read it.
SHARED = [system_tools_block(), shared_rules_block(), mr_with_patches_block()]
reviews = {}
for role in ("code_quality", "security", "performance"):
messages = SHARED + [role_instructions(role)]
reviews[role] = call_model(
messages=messages,
cache_breakpoints=[len(SHARED) - 1], # breakpoint on the shared tail
)
# Prompt cache economics (Anthropic): write = 1.25x input price,
# read = 0.1x, entry expires after 5 minutes unless read again.
# Result: $2.43 -> $0.77 avg review, median 4m09s -> 1m32s.六、最后一步:三个审查器共用一个前缀
最费劲的一招,是让代码质量、安全、性能这三个审查器共享同一个缓存前缀:它们现在发送完全相同的第一份请求——同样的工具、同一份共享规则文件,然后是包含全部 patch 的 MR;各自的专属指令放在这之后,并确保其中一个写缓存、另外两个读缓存。Prompt 缓存的经济学是这样的:写入按输入价格的 1.25 倍计费,读取按 0.1 倍计费,条目在五分钟内没人读就过期。所以这个设计成立的前提是三家的前缀字节完全一致。最终成绩:平均每次审查 0.77 美元(原来是 2.43),中位耗时 1 分 32 秒(原来是 4 分 09 秒)。可靠性也顺带变好了:没有结论就结束的审查从 0.79% 降到 0.27%,安全审查器的重试率从 3.5% 降到 0.04%。但 Cloudflare 自己提醒了一句要紧的话:便宜不等于质量没问题,性能审查在中等 effort 下确实丢了真问题,安全审查可能也一样,所以下一步是让这两个也回到高强度跑。如果你在规模化跑 Agent,该先检查的两件事是:每个会话的第一个请求到底有没有读缓存,以及你现在付的 effort 档位还能不能找出真问题。
钱主要花在模型早就看过的上下文上
📌 常见问题 FAQ
为什么成本会在架构没变的情况下自己涨上去?
Cloudflare 的拆解是两个原因叠加。一是模型变强之后,工程师产出更大更复杂的改动,落进最贵档位的比例从 3 月的 59% 升到 8 月的 65%。二是模型本身换了:8 月初他们把全部审查器切到 Anthropic 模型,单作业成本从 1.38 美元涨到 2.03 美元;内部规范审查器最终跑在 Claude Opus 4.7 上,从约 0.25 美元涨到 0.88 美元,比调度 Agent 自己还贵。
省钱的主要来源是换便宜模型吗?
不是。Cloudflare 说大约 40% 的节省来自审查器代码本身的改动和优化,剩下来自对模型组合与 effort 档位的反复试验。单看换模型这一项,它反而是把成本推高的原因之一。
缓存命中率 97% 了,为什么还这么贵?
因为 token 总量涨得比命中率更快。命中率从 4 月的 85.7% 升到 9 月的 97%,但每次审查的 token 从 0.91M 涨到 2.99M,超过三倍。即使缓存读取单价只有输入的 0.1 倍,三倍体量乘上去依然很贵,这也是为什么他们把重点放在「减少调用次数」和「缩小前缀」而不是只盯命中率。
这些做法能直接搬到自己的 Agent 上吗?
能,而且检查顺序很清楚:先确认每个会话的第一个请求真的读到了缓存——Cloudflare 当时只有 14% 到 26% 命中,而 11k 到 21k token 的前缀本该全部命中;再把仓库相关信息从共享前缀后面移走;最后删掉那些注定会被拒绝的工具,并让不会成功的调用不再发生。
成本降了 68%,质量有没有掉?
Cloudflare 的说法是有分化:代码质量审查在高强度下找到的真实问题和以前差不多,噪声更少、虚高严重度也少很多;但性能审查在中等 effort 下确实丢了真问题,安全审查可能也一样,所以他们下一步要把这两项也调回高强度再验证。