LLM 测试的隐形账单:调用频次放大成本
文章以每次运行调用模型 100 次、每天运行 30 次为例,估算每月测试会产生 6.6 万次付费请求。CI 和本地自动重跑会持续放大开销,提示缓存也无法免除输出生成费用。
AI 深度解读
还没有用户,AI 功能的测试就可能先成为 API 的大客户。作者算了一笔账:100 个测试每次各调用一次模型,每个工作日跑 30 轮,一个月就是 66,000 次付费请求。按文中的 GPT-5.6 标价和用量假设,费用约为 290 美元。
作者给出的办法很直接:把模型回答录下来,日常测试反复回放,只在定时检查时调用真实模型。这套做法值得借鉴,但省下费用的同时,必须弄清楚测试还在验证什么。
为什么没有用户,测试也能烧出账单
模型 API 不区分测试请求和用户请求。同一道题问第 500 遍,仍然是一次新的计费调用。
计费单位是 token,大约相当于四分之三个英文单词。输入和输出分别收费,输出通常更贵。供应商的提示词缓存可以降低部分重复输入的费用,回答仍要重新生成、重新计费。
作者使用的假设是:每轮 100 次调用,每次 1,000 个输入 token、200 个输出 token,每个工作日运行 30 轮,每月 22 个工作日。
因此,月调用量是 100 × 30 × 22 = 66,000。按文中 GPT-5.6 每百万输入、输出 token 分别 2 美元和 12 美元的标价,账单是 290.40 美元。
这些数字是作者设定的算例,不是团队实测账单。
真正容易失控的是运行频率。开发时本地跑一次,推送后 CI 再跑一次,监听文件保存的测试工具还可能继续触发。费用跟着开发活动增长,未必跟着用户增长。
换便宜模型能省钱,但如果生产用另一款模型,测试结果与线上行为之间又多了一层距离。
把回答存下来,日常测试就不用反复付费
作者采用的是录制与回放(record and replay):首次请求真实模型,把回答保存为文件;之后遇到相同请求,直接读取文件。通用 HTTP 工具 vcrpy 也采用类似思路。
下面这段代码演示了如何用请求内容生成文件名,并复用已经保存的回答:
import hashlib, json, pathlib
def recorded(call, folder="llm_cassettes"):
def wrapper(**request):
key = hashlib.sha256(json.dumps(request, sort_keys=True).encode()).hexdigest()
path = pathlib.Path(folder) / f"{key[:16]}.json"
if path.exists(): # replay: no API call, no bill
return json.loads(path.read_text())
reply = call(**request) # record: one paid call
path.parent.mkdir(exist_ok=True)
path.write_text(json.dumps(reply))
return reply
return wrapper
paid = []
ask = recorded(lambda **r: paid.append(r) or {"label": "positive"})
for _ in range(3): # three test runs
ask(model="gpt-5.6", messages=[{"role": "user", "content": "Great phone!"}])
print(f"3 test runs, paid API calls: {len(paid)}")
关键看 if path.exists():命中文件时,程序在调用模型之前就返回了。
这里的 lambda 是真实 API 的替身。首次运行会输出 3 test runs, paid API calls: 1,再次执行则输出调用次数为 0,因为回答已经落盘。它证明了回放机制,没有实际调用模型。
接入 OpenAI SDK 时,作者给出的包装对象是 lambda **r: client.chat.completions.create(**r).model_dump()。
请求通过 json.dumps(request, sort_keys=True) 序列化,再计算 SHA-256,取前 16 位作为文件名。修改提示词后,请求内容改变,会生成新文件,只有对应测试需要重新调用。
作者建议把 llm_cassettes 目录提交到仓库,让 CI 也能读取。回放还有一个直接收益:模型服务变慢、宕机或繁忙时拒绝请求,不会再拖垮这些测试。
每晚调用真实模型,究竟补上了什么
回放验证的是自己的代码如何处理已有回答。供应商调整模型行为,或者团队更换模型,旧文件都无法反映新的输出。
作者因此建议,每晚把 folder 指向空目录,让整套测试真正访问 API。文中按每月 3,000 次调用计算,费用降至标价下的 13.20 美元,调用量不足原来的 5%。
这里有个口径需要拆开看:66,000 次按 22 个工作日计算;3,000 次对应每轮 100 次、每月 30 轮。两者使用了不同的运行日历,套用到自己的 CI 时要重新计算。
真实模型测试也不宜死盯回答原文。保存的回答只是一次采样,下次措辞可能变化。作者建议检查合法 JSON、必要字段等结构条件,而不是要求文本完全相同。
降单价的作用则有限。作者任职的网关给出 gpt-5.6 每百万输入、输出 token 为 1.70 / 10.20 美元,相比标价 2 / 12 美元,原账单从 290.40 降到 246.84 美元。回放节省更多,是因为减少了重复调用。
我的判断
我认可把日常代码回归与真实模型检查分开。固定回答能让解析、状态处理等逻辑得到稳定输入,排查失败原因也更容易。
但结构正确不等于业务正确。JSON 字段齐全,分类仍可能分错。原文没有展开如何评估回答质量,不能把夜间测试通过当成模型表现合格的充分证据。
与手写 mock 相比,录制文件保留了真实响应;与每次调用真实模型相比,它又牺牲了对最新行为的即时感知。选哪一种,取决于测试想回答什么问题。
此外,这段 Python 适合解释原理,原文没有交代并发写入、流式响应等工程问题。文件还会保存测试数据,提交前必须排除真实客户信息。
作者没有提供团队落地后的对照数据,所贴原文也缺少标题所说的五模型价格表。因此,成本下降可以按假设复算,实际维护收益仍属于经验建议。
能拿来做什么
- 给现有 AI 测试列一张调用账:每轮请求数、每次输入输出量、本地与 CI 的运行次数,再计算月费用,先找出重复调用最多的位置。
- 在 Node 服务的模型调用边界引入录制与回放,让日常回归复用固定响应;提示词和模型参数应参与请求匹配,避免修改后继续误用旧回答。
- 保留定时真实调用,并分别检查输出结构与业务结果。对分类功能,除了字段是否存在,还要检查分类是否符合预期。
- 提交录制文件前检查输入和响应中的客户数据,并明确哪些改动需要更新录制结果,避免旧样本长期掩盖模型变化。