如果你平时在做数据处理、内容生成或者知识库构建,大概率已经跟各家大模型的 API 打过交道。很多人一开始都是老老实实一条条实时调用,等任务量上来之后才发现,钱包消耗的速度比任务进度还快。今天聊的批量 API(Batch)就是专门治这个毛病的:把大量请求打包成一个文件提交上去,让平台异步处理完,你再一次性把结果拉回来。官方通常直接按实时价格的五折计费,我自己在真实项目里跑下来,大批量场景确实能省差不多一半成本,同时还能把系统从同步等待里解放出来,稳定性肉眼可见地变好。
这篇文章会从批量 API 的工作原理、完整实操流程、成本测算、结果对账,再到我踩过的一堆坑,一次性讲清楚。想用批量接口省钱、想把线上同步调用改造成异步批处理的读者,可以直接照着做,每一步都给你写明白了。
1. 先搞懂 Batch API 的原理,才知道钱省在哪里
1.1 一次性搞清同步接口与批量接口的差别
日常用的实时 API 调用,本质上是同步请求:你发一条 HTTP 请求过去,服务器排队处理,然后把这条请求的结果返回给你。这种模式适合聊天、客服、实时审核这类必须立刻出结果的场景。缺点是每条请求都要占用一次完整往返,服务器需要为你维持连接、调度资源,不管你有没有用到满,这部分开销都已经花出去了。
批量 API 的思路完全反过来。它不要求每条请求立刻返回,而是让你先把一堆请求写到同一个文件里,一次性上传上去,平台拿到之后会在自己的调度系统里排队,等计算资源有空闲了再逐个处理。处理完以后,平台把结果也打包成一个文件,你下载下来自己解析就行。
类比一下,实时 API 就像是专门为你叫的专车,随叫随走,但单价高;批量 API 更像是定时班车或者拼车,你得等它按点发车,但票价便宜一半甚至更多。对不着急出结果的任务来说,这笔账怎么算都划算。
1.2 平台凭什么给你打五折:成本逻辑和适用边界
平台愿意给批量接口打折,不是因为做慈善,而是因为批量任务能帮平台填平算力低谷。大模型服务的算力需求波动很厉害,白天高峰时段 GPU 可能满载,凌晨可能闲置一大片。实时请求没法挑时间,平台只能随时准备好资源伺候着。但批量任务可以缓存排队,平台就能把这类任务安排在算力空闲的时间段处理,把原本闲置的 GPU 利用起来。
所以批量 API 的定价逻辑很简单:用户牺牲实时性,平台换取调度弹性,省下来的算力成本双方对半分。多数平台直接给到五折,少部分平台还会根据任务窗口给更深的折扣。
明白了这个逻辑,你就能判断自己的业务适不适合用批量 API 了。适合的场景很清晰:批量翻译、文章批量润色、知识库向量化之前的文本清洗、大规模数据打标、离线评测集跑分、定时生成日报周报、历史日志分析,这些任务的特点是量大、不要求秒回、容忍几小时甚至一天内有结果。不适合的场景也很清晰:用户发消息之后的实时对话、在线客服应答、需要立刻反馈的交互式操作,这些还是老老实实走实时接口,不要为了省钱牺牲产品体验。
2. 实操:把第一批任务真正跑通
2.1 准备输入文件:JSONL 格式和几个隐藏要求
批量 API 的输入文件格式基本是业界统一的 JSONL,每行是一个独立的 JSON 对象,代表着一次完整的请求。下面是个具体例子:
{"custom_id": "request-0001", "method": "POST", "url": "/v1/chat/completions", "body": {"model": "gpt-4o-mini", "messages": [{"role": "user", "content": "请把这句话翻译成英文:批量API怎么用?"}], "max_tokens": 200}} {"custom_id": "request-0002", "method": "POST", "url": "/v1/chat/completions", "body": {"model": "gpt-4o-mini", "messages": [{"role": "user", "content": "总结下面这段文本的核心要点,不超过100字:..."}], "max_tokens": 150}}第一眼看上去很简单,但有几个隐藏要求值得注意。custom_id必须是一个字符串,而且在同一个批量任务内必须全局唯一,最终下载结果的时候,你全靠它来识别每一行对应的结果。我建议从一开始就建立一套命名规则,比如用“业务名-批次-序号”的格式,既方便排查问题,也方便后续和业务数据做关联。
url字段通常写的是接口路径而不是完整地址,比如/v1/chat/completions或/v1/embeddings。域名部分由平台自动拼接,不用你管。body里的内容和你平时调用实时接口传的参数一模一样,模型名、消息内容、temperature、max_tokens 都可以写进去。
文件编码统一用 UTF-8,不要带 BOM。有同事曾经用 Windows 记事本编辑过 JSONL 文件,存出来带了个 BOM 头,平台解析第一行的时候直接报格式错误,排查了半天才发现是编码问题。另外还要注意,每行必须是一个完整的 JSON 对象,不能用数组把所有请求包起来,也不能在行尾多加逗号。
2.2 上传、建任务、查状态:三步操作的完整流程
第一步,把 JSONL 文件上传到平台的存储系统,拿到一个文件 ID。第二步,用这个文件 ID 创建一个批量任务,指定要访问的接口和期望完成时间。第三步,定期查任务状态,等状态变成 completed 之后下载结果文件。
以 OpenAI 兼容接口为例,用官方 Python SDK 写出来大概是这样的:
import time from openai import OpenAI client = OpenAI(api_key="你的API Key") # 1. 上传输入文件 with open("batch_input.jsonl", "rb") as f: file_resp = client.files.create(file=f, purpose="batch") file_id = file_resp.id print("file_id:", file_id) # 2. 创建批量任务 batch_resp = client.batches.create( input_file_id=file_id, endpoint="/v1/chat/completions", completion_window="24h", ) batch_id = batch_resp.id print("batch_id:", batch_id) # 3. 轮询任务状态 while True: status = client.batches.retrieve(batch_id) print(status.status) if status.status in ["completed", "failed", "expired", "cancelled"]: break time.sleep(60) # 4. 任务完成后下载结果 if status.status == "completed": result_file = client.files.content(status.output_file_id) with open("batch_output.jsonl", "wb") as f: f.write(result_file.content) print("结果已保存到 batch_output.jsonl")如果不想用官方 SDK,直接拿 requests 请求接口也可以。核心就三个接口:上传文件、创建批次、查询批次状态。很多国内平台的批量接口也兼容这套格式,SDK 版本不同字段名可能略有差异,但大方向是一样的。
轮询的时间间隔不要写死成每 5 秒跑一次,尤其任务数量大的时候,频繁轮询除了浪费自己服务器资源没有别的好处。建议任务刚提交时 1 分钟查一次,运行一段时间后改成 2 到 5 分钟查一次,如果平台提供了回调或者 webhook 功能,优先用回调,比轮询优雅得多。
2.3 completion_window 不是乱填的:怎么选才稳妥
创建批量任务时有个参数叫completion_window,常见值是24h,部分平台也支持1h。这个字段代表你给平台的期望完成时间:在这个时间内处理完就行。你可能会觉得选个短窗口拿结果更快,但实际用下来发现,窗口越短,平台调度的约束越强,任务排进紧俏时段的概率反而降低,并不一定更快。而且价格通常不因窗口长短而改变,所以没必要为了心理上的快感选短窗口。
我的建议很简单:业务要求多久出结果,就选多大的窗口。能做 T+1 的任务统一用 24h,让平台自己慢慢排;只有被产品经理盯着必须尽快出结果的才缩窗口。另外特别提醒一句:这个窗口是硬约束,如果平台在窗口内来不及处理,任务会直接变成 expired 状态,结果没跑完还白花了上传文件的功夫。所以选窗口之前,先评估一下你的任务量和平台当前的调度压力。
2.4 常见疑问:一个批量任务能塞多少请求
单个批量任务能放多少请求,不同平台限制不一样。不少平台限制一次任务最多几万条请求,文件大小也有上限,超出部分需要拆成多个提交。我自己习惯是单个文件控制在 50000 行以内,文件大小不超过 200MB,这样不管切到哪个平台都能兼容。
如果你的数据量特别大,比如要处理 1000 万条文本,拆成 200 个小批量文件分批提交,每个文件独立一个任务,这样即使某一个小批失败,也不会拖垮全局,排查问题也只定位到具体文件,比一次性塞一个巨型文件稳妥得多。
3. 成本测算与大规模改造的关键判断
3.1 算一笔账:批量 API 到底能省多少
纸上谈兵没有感觉,直接算一笔账。假设当前使用的模型实时价格是:输入 1 美元/百万 tokens,输出 3 美元/百万 tokens;批量接口价格是五折,即输入 0.5 美元/百万 tokens,输出 1.5 美元/百万 tokens。
假设你的业务每天要处理 100 万输入 tokens,产出 20 万输出 tokens:
| 计费项 | 实时接口价格 | 批量接口价格 | 每日实时成本 | 每日批量成本 |
|---|---|---|---|---|
| 输入 tokens(100万) | 1.0 美元/百万 | 0.5 美元/百万 | 1.0 美元 | 0.5 美元 |
| 输出 tokens(20万) | 3.0 美元/百万 | 1.5 美元/百万 | 0.6 美元 | 0.3 美元 |
| 合计 | - | - | 1.6 美元 | 0.8 美元 |
每天省 0.8 美元看起来不多,但把规模放大到 1000 万输入 tokens、200 万输出 tokens,每天就能省 8 美元,一个月就是 240 美元,一年接近 3000 美元。对于做 to B 服务或者长期跑数据管线的团队来说,这不是小钱。
还有一个容易被忽略的好处:批量任务的限流阈值通常比实时接口宽松得多,即使你的业务量短时间内翻倍,也不会马上触发 429 限流错误。这一点在应对业务突发流量时尤其重要,省钱之外还省了运维压力。
3.2 架构怎么改:从同步请求到异步批处理管线
把同步实时调用改造成批量任务,不是简单换个接口就行,整个调用链路的思维模式都要变。实时调用的思路是“请求-响应”,批量调用则是“投递-回执”。
我建议的架构方案是:上游业务把需要处理的文本写进消息队列或者业务表,一个定时任务定时拉取待处理数据,组装成请求行,写入 JSONL 文件,上传后创建批量任务。任务完成之后,再有一个下载任务把结果落库,同步更新业务表里的对账状态。
这个流水线里最关键的设计是状态机。每个业务批次至少要有这几个状态:待提交、已提交、运行中、已完成、部分失败、彻底失败。用一张批次表记录批量任务 ID、文件 ID、结果文件 ID、提交时间、完成时间。这样才能做到失败可重试、进度可追踪,而不是把一切交给大脑记忆。
整个改造成本并不高,一个小团队一两周就能搭建完。但收益是实打实的:线上系统不再被长耗时的大模型调用拖住,主链路响应更快,离线任务在大半夜慢慢跑也没人管。
3.3 什么时候不该用批量 API
省钱是好事,但不能为了省而省。如果单次业务量每天只有几十条请求,建议别折腾批量 API。因为批量 API 有固定的管理成本:准备文件、上传、轮询、下载、对账,这些步骤的开发和维护工作量不可忽视。每天几十条请求用实时接口,可能花不到 1 美元,省下五毛钱却要维护一条完整管线,不划算。
实时交互场景更不能用批量 API。用户问一句话等 3 秒还能接受,等 3 个小时肯定连产品都保不住。批量 API 替代的是那些“等得起、量大、重复性高”的任务,找准自己的业务边界,才能发挥它最大的价值。
4. 结果文件解析与错误定位
4.1 拿到输出文件后,怎么正确对账
批量任务完成后,你下载到的输出文件也是 JSONL 格式,每一行对应输入文件中的一条请求。一个成功的结果行大概长这样:
{"id": "batch_req_8a1c2d", "custom_id": "request-0001", "response": {"status_code": 200, "request_id": "req_9f3e2a", "body": {"id": "chatcmpl-xxx", "choices": [{"message": {"role": "assistant", "content": "..."}}], "usage": {"prompt_tokens": 32, "completion_tokens": 78, "total_tokens": 110}}}, "error": null}response.status_code是 HTTP 状态码,200 表示成功;response.body里的内容和实时接口返回的格式一致,choices里就是模型生成的正文,usage里有 token 消耗数据。error字段在成功时是 null,失败时则包含错误码和错误信息。
对账的时候有个非常容易踩的坑:输出文件里的行顺序不一定和输入文件一致。平台多线程并发处理,谁先跑完谁先落盘,所以绝对不要按行号去对应输入输出。正确做法是把输出文件按custom_id建立索引,再和输入文件的custom_id做关联,这样哪怕顺序打乱也能准确对上。我见过不止一个同事想当然按顺序逐行读取,结果数据全部错位,教训深刻。
还有一点要提醒:如果批量任务里有一部分请求失败,平台可能不会把失败记录放在正常输出文件里,而是单独生成一个错误文件。创建任务返回结果里通常有error_file_id字段,记得检查这个文件,否则你会以为任务全成功了,实际上有漏网之鱼。
4.2 常见的 API 错误码和应对策略
批量任务的结果文件和实时调用的错误码是同一套体系,但处理策略不太一样。实时调用出错,你当场就能重试;批量任务出错,可能几十分钟后才发现。我把常见的错误码整理成了一张表:
| 状态码/错误 | 可能原因 | 处理建议 |
|---|---|---|
| 200 | 请求正常 | 直接解析 body 用结果 |
| 400 | 请求参数错误,比如模型名不对、消息格式有问题、JSON Schema 校验失败 | 检查对应请求行的 body 参数,修完重新提交 |
| 401 | API Key 无效或过期 | 检查鉴权配置,换了 Key 后重跑失败批次 |
| 403 | 账户权限不足,或者模型未开通 | 检查平台控制台的权限和模型白名单 |
| 404 | 接口路径或者模型不存在 | 核对 Endpoint 路径和 model 名,别自己瞎拼 |
| 429 | 触发限流,或者账户余额不足 | 增大退避时间,充值后再重试 |
| 5xx | 平台临时故障 | 稍后重试该批次,一般都能恢复 |
批量接口相对实时接口要宽容一些,单条请求失败不会让整个任务崩掉。但是你要建立一条处理链路:拿回输出文件后,先过滤所有非 200 的状态码,把失败请求单独存起来,做成可重试的队列。我习惯做法是给失败请求打上错误类型标签,如果是参数错误就修数据,如果是限流就排到下个批次,分级处理,效率高很多。
4.3 一个隐蔽坑:custom_id 设计与幂等去重
custom_id不只是用来关联结果,它还承担着幂等去重的职责。如果你的数据在业务层面本身有唯一 ID,比如订单号、用户 ID、文档 ID,我强烈建议直接把业务 ID 拼进custom_id里,比如order-20250101-0001或者doc-xxxxx-part-3。
为什么强调这一点?因为批量任务失败后最常见的行为就是整批重跑。如果custom_id是随机生成的,重跑后你根本分辨不了哪些结果已经入库了,只能全部删掉重新处理。但如果你用业务 ID,下载结果后直接 upsert 到数据库,即使任务重跑,也不会产生重复数据,实现天然幂等。
我见过最惨的例子是同事用毫秒级时间戳做 custom_id,某个批量任务因为平台故障重跑了三次,结果库里多了两倍的数据,最后只能靠生成时间倒推去重,折腾了一整天。前车之鉴,大家一定别偷懒。
5. 我在实战里踩过的几个重坑
5.1 模型名写错导致 400,接口文档并不会替你把关
批量接口对模型名称的校验非常严格,你在 body 里填的 model 必须是当前平台支持的模型名,一个字符都不能差。我就多次收到这种报错:400 the supported api model names are deepseek-flash, deepseek-v4,字面意思很清楚,你填的模型名不在支持列表里,但问题在于不同平台的模型命名规则并不统一,同一家平台不同时期的模型名还会变。
你别指望报错信息会给出完整支持列表,有些平台只在文档里写,报错信息里不提示。正确做法是提交批量任务之前,先用实时接口小规模试调用一次,确认模型名、参数格式都能通,再放心去组批量。这一步能挡掉一大半低级错误。
5.2 鉴权信息冲突:一套代码里塞了两个 Key
有一段时间我调用某个第三方兼容接口时始终报认证失败,日志显示服务端认为是鉴权信息冲突。后来排查才发现,配置文件里同时配置了一个主 Key 和一个备用 Key,SDK 自动同时加载了两套认证头,其中一个是给 Anthropic 风格的,另一个是 OpenAI 风格的,服务端一检测到两个认证字段同时存在就直接拒绝。
这类报错通常带着auth conflict: both a token (anthropic_auth_token) and an api key这类信息,看到之后别怀疑平台,先检查自己的代码和配置。一个问题排查原则:认证相关的报错,90% 的根源在客户端配置,不在服务端。
5.3 function calling 的 schema 校验失败,正则差点把我搞疯
有一次给批量任务加函数调用功能,提交后收到一条 400 错误:invalid schema for function 'artifact': "^(?!.*$)[^\p{cc}\p{c,batch..."。第一次看到这个报错,说实话我愣了半天,因为报错信息被截断得非常奇怪,看起来像是正则表达式里混入了二进制字符。
排查到最后发现,问题出在参数定义的 JSON Schema 里写的正则表达式不合法。我原本想在字符串校验里用\p{Cc}这类 Unicode 属性来排除控制字符,但平台使用的 JSON Schema 校验器并不支持这种简写形式,而且字符串里反斜杠需要双重转义,稍不注意就会把 JSON 结构弄坏。
这个坑让我总结了三条经验。第一,函数参数定义尽量用 JSON Schema 的标准关键字,如minLength、maxLength、pattern,不要一上来就写复杂正则。第二,凡是涉及正则在提交前先用本地代码解析一遍 JSON,确保语法正确。第三,小批量试跑永远值得做,你永远不知道平台的 schema 校验会比本地严格多少。
5.4 批量任务过期了才想起来去取结果
批量任务的窗口是硬约束,24 小时窗口就是最多 24 小时,超时任务直接进入 expired 状态。我有一个项目因为团队休假,提交完任务没人盯状态,等回来发现任务早就过期了,所有请求要重新提交一遍,白白浪费了时间。
从这里我学到两个教训。一是批量任务提交后要有自动告警,任务完成或者失败要第一时间通知到人,不能只靠人去轮询。二是窗口用完之前,如果发现任务可能跑不完,可以先手动取消再重新提交,不要傻等着它过期。取消之后再提交,至少能保留一部分已完成的结果,比全部作废强。
5.5 本地 Docker 环境连接失败,和批量接口无关的干扰项
排查问题时要学会区分前后端问题。有次我在本地调试批量任务脚本,一运行就报failed to connect to the docker api at npipe:////./pipe/dockerdesktoplinuxen,直觉以为是 API 调用链路出问题了,折腾了半天才发现是我本地的 Docker Desktop 服务没启动,脚本里有个依赖服务跑在 Docker 容器里,和云端 API 一点关系都没有。
这类连不上本地服务、认证失败、端口占用的问题,建议先排查环境再怀疑接口。给自己定个规矩:看到连接类型报错先看服务状态,看到认证类型报错先查配置,看到参数类型报错再查代码逻辑。按这个顺序排查,效率会高很多。
6. 成本再优化:三个进阶玩法
6.1 控制输出 token 与请求体积
批量 API 只打了五折,但真正的省钱大头是在源头控制请求体积。输出 token 的单价通常比输入 token 贵好几倍,所以在构造请求时,一定要给max_tokens设置一个合理上限。很多人为了让模型一次性输出更完整的内容,习惯把max_tokens调到很大,结果模型真的生成了大量冗余内容,费用直线上升。
我建议根据业务需求估算每个请求需要的输出长度,留出 20% 的余量作为上限。比如要求生成 100 字以内的摘要,max_tokens设 150 到 200 就足够了,没必要设成 1000。另外 prompt 本身也不要写太多无关的思考链和示例,能在提示词里压缩的内容尽量压缩,输入 token 积少成多也是一笔不小的开支。
6.2 对重复请求做缓存
很多批量任务里天然存在大量重复请求。比如知识库构建时,同一份文档可能被多个上游任务重复触发预处理;内容打标时,同一段新闻文本会被多个不同标签任务重复调用。对这些重复请求,最好的优化就是做缓存。
缓存的粒度可以按custom_id对应的业务主键来:处理前先查库里有没有这个主键的已有结果,有就直接跳过。还可以对请求内容做哈希,相同哈希的请求直接复用之前的结果。这两个策略实现成本很低,但能显著减少实际消耗的 token 数,效果往往比批量五折更明显。
6.3 实时与批量混合:一个务实的架构方案
最后说一个组合拳的思路。如果业务里既有必须实时返回的请求,又有可以等待的离线请求,不要一刀切全部走实时,也不要全部切到批量。我的推荐方案是:用户主动触发、需要即时反馈的单独走实时接口;后台任务、定时任务、数据预处理的全部走批量接口。实时接口的并发能力留给真正重要的交互场景,批量接口负责消化大量非紧急请求,两者互不干扰。
这个方案还有一个额外的好处:实时调用出现的 429 限流会明显减少,因为流量被批量任务分流了。整体成本能控制在原来的 60% 左右,系统稳定性还比原来好,一举两得。
我在实际项目里连续跑了半年多批量任务,最大的体会是:批量 API 带来的不只是账面上的成本折半,它更重要的是改变了你设计系统的方式,把一个一个的同步请求变成了一条可观测、可重试、可追溯的异步管线。最后再分享一个保存了很久的经验:不管任务看起来多简单,都先提交一个 10 条以内的小文件试跑,确认结果结构和预期一致,再放开全量。这一步能帮你躲掉至少一半的返工时间,我每次都是这么干的。