大模型 Batch API 打五折,算下来反而比平时更贵的账本复盘
最近在给招聘监控项目处理 4 万多条历史岗位数据,需要用大模型把格式五花八门的招聘信息清洗成统一的结构化画像。
哪怕单次调用的价格按厘计算,乘以 4 万这个基数后,账单依然不可小觑。
刚好模型厂商提供了 Batch API(离线批处理任务):把请求打包成 jsonl 文件上传,服务端承诺 24 小时内异步跑完,输入和输出 Token 单价直接全场五折。
什么是 Batch API?它是各大主流模型服务商(如 OpenAI、Anthropic、Google)为不要求实时返回的离线任务提供的批量处理接口。由于允许厂商在算力波谷时调度计算,通常会提供 50% 甚至更高的价格折扣,但通常会有几小时到 24 小时的延迟。
不赶时间、离线吞吐、白纸黑字的五折优惠——按直觉来看这根本不用犹豫,切成 Batch 闭着眼都能把预算砍掉一半。
在动手重写后台调度任务之前,我拉出实际请求的 Token 结构认真算了笔账,结果却被真实数字泼了一盆冷水。
被忽略的 94%
提取岗位画像时,发给模型的并不只有几百字的岗位描述。
为了让模型输出结构严谨的 JSON,并且把“资深后端”、“Golang 架构师”准确归类到统一的行业标准,每个请求开头都必须携带一大段沉重的系统前缀:输出字段规范、JSON Schema 约束,以及一套几千行的岗位体系分类字典。
它们负责教模型“什么叫三年经验”、“什么叫校招”,真正每次请求都在变化的,其实只有末尾那一小截几十到几百字的岗位详情。
拉出日志看了一眼单次请求的真实 Token 分布:平均一个请求占 11,400 个输入 Token,其中这段固定的前缀规则就占了 10,700 多个,比例高达 94%!
单次请求的 Token 结构
- 固定前缀(约 94%): 统一抽取规则、Schema 校验、全量分类标准字典
- 动态内容(约 6%): 当前岗位的标题、职责要求和公司字段
通俗比喻:全场五折的复印店 vs 模板只印一次的优惠为什么五折的离线批处理会输给平时的实时调用? 想象你要印 1000 份合同,每份合同前 94 页全是完全一样的通用条款,只有最后 1 页是具体甲方签字:
- Batch API(全场五折):相当于复印店老板说“你把 1000 份合同全给我,我给你算半价”,但他每次都把那 94 页通用条款一页不落地重新印刷 1000 次;
- 实时调用 + 前缀缓存:相当于复印店老板把前 94 页做成了固定模具,只印了一次,后续 999 次印刷直接复用底板,前 94 页只收一折的电费,你只要为最后那 1 页付全价。
只要固定前缀足够长,“固定模具打折”省下来的钱,能把“整包打半价”按在地上摩擦。
如果走实时接口,各大厂商目前基本都支持上下文缓存(Context Caching):只要请求开头的字节保持一致,后续请求命中缓存后,前缀部分的输入单价通常只有两折甚至更低。
而厂商的 Batch API 虽然标着五折,但不支持前缀上下文缓存。
隐式缓存的波动与显式锁定
在得出最终结论前,我还必须确认实时调用的缓存收益是真实的,而不是纸面富贵。
最开始我依赖厂商的“隐式缓存”(由服务端自动比对前缀)。但在 1,000 条岗位的先期试跑中,命中的表现非常不稳定:总命中率只有 48% 左右,且忽高忽低。刚开始几个请求还能命中 7,800 Token,跑着跑着就掉成了 0;如果任务中间停顿几分钟,缓存更是直接蒸发。
隐式缓存与显式缓存的区别
- 隐式缓存(Implicit Cache): 服务端根据最近请求自动嗅探相同前缀,命中策略对开发者完全是个黑盒,稍微有请求抖动或并发中断就会失效;
- 显式缓存(Explicit Cache): 开发者通过 API 主动声明“这段前缀请在显存中保活 N 分钟”,前缀带有明确的 Cache Key,命中率极高且确定。
于是我重构了请求结构,把模式改成显式缓存:把固定的 Schema 和分类体系单独抽出来,在末尾打上缓存声明锚点;动态的岗位正文死死固定在末尾,绝不往前缀里掺杂任何当前时间戳、随机 UUID 或批次 ID。
这一改,效果立竿见影:固定的 10,743 个 Token 在后续请求中几乎 100% 稳定复用,部署环境真实跑下来,整批任务的综合缓存命中率达到了 93.8%。
现在,天平两端的砝码彻底变了:不再是简单的“原价 vs 五折”,而是变成了**“全量打五折”与“94% 的内容打两折/一折”的较量。**
重新算一遍账
我把两条链路折算成每 1,000 条岗位的实际费用:
| 方案路径 | 计费组成方式 | 每千条实际开销 |
|---|---|---|
| 实时调用 + 显式缓存 | 约 94% 输入走超低缓存价,仅 6% 走原价 | 0.81 元 |
| Batch API | 全部 100% 输入统统打五折 | 1.37 元 |
标着“特惠半价”的 Batch API,实际跑完居然比直接调实时接口贵了将近 70%!
这个算术很容易想明白。假设一个请求有 100 个 Token,其中 94 个是固定 Prompt:
- Batch 方案: 100 个 Token 全部按 50% 结算;
- 实时缓存方案: 94 个 Token 按 10%~20% 的缓存价结算,只有 6 个动态 Token 付 100% 全价。
前者表面上打了五折很诱人,但后者需要付全价的部分只有一点点。
算账别只看标价折扣评估大模型接口成本时,不能只盯标价上的几折。一定要抓包分析你的 Prompt 结构:到底是动态内容占大头,还是固定的规则、Few-shot、知识库占大头?
省下的不只是真金白银
成本差距已经足以把 Batch 方案枪毙,而工程维护成本则让这个决定更加坚定。
如果用实时接口,代码可以无缝沿用现有的消息队列、重试补偿、并发控制和结果落库逻辑,没有任何心智负担。
如果硬切 Batch,我需要新建一套复杂的离线流程:写本地文件 → 调 API 上传 → 提交 Batch 任务 → 写 Worker 定时轮询状态 → 任务结束后下载结果文件 → 逐行反序列化并按外部 ID 映射回数据库。不仅平白多了一两天的开发工作,还得忍受结果延迟返回的滞后性。
那 Batch API 什么时候才真正有用?并不意味着 Batch 一无是处。如果是做长篇不同文档的翻译、长小说摘要、或者每次输入都截然不同的离线评测任务(没有公共前缀),Batch 的半价优势就会显现出来。根据当时的计费公式反推,只有当请求的前缀缓存命中率低于 40% 时,Batch 才会开始比实时调用划算。
最终落地非常干脆:继续走实时接口,加上显式前缀缓存,把并发任务拆成均匀的微批次持续推送,既保住了缓存的生命周期,又把账单压到了极致。
很多时候,云厂商在计费页面上用红色大字标注的“50% OFF”,只是一张诱人的表象。在重度依赖特定业务规范和复杂 Schema 的抽取任务中,单价打折永远跑不过命中缓存。