先判断:这批任务该不该走 Batch
Batch API 的核心不是“更快”,而是“更便宜、更省限流”。它把一批请求写进文件提交,服务端在 24 小时内跑完,通常远快于这个上限。代价是你没法等待单条结果,也不能流式输出。
| 对比项 | 同步接口 | Batch API |
|---|---|---|
| 成本 | 按标准价格计费 | 同样的调用享受 50% 折扣 |
| 响应时间 | 秒级返回 | 24 小时内完成,通常更快 |
| 速率限制 | 消耗标准按模型的限流额度 | 使用单独一套更高的限流池 |
| 交互方式 | 一问一答,可流式 | 提交文件后异步取结果,不支持流式 |
| 适合的任务 | 聊天、补全、需要即时反馈的流程 | 离线标注、批量摘要、批量分类、批量嵌入 |
jsonl 输入到底长什么样
输入是一个 .jsonl 文件:一行一条 JSON,每行代表一次独立请求。四个字段缺一不可——custom_id 用来把结果对回原文,method 和 url 决定调哪个端点,body 就是该端点原本要接收的参数。
{"custom_id": "faq-001", "method": "POST", "url": "/v1/responses", "body": {"model": "gpt-4.1-mini", "input": "把这段话改写成一句话摘要:退款会在 3 到 5 个工作日到账。"}}
{"custom_id": "faq-002", "method": "POST", "url": "/v1/responses", "body": {"model": "gpt-4.1-mini", "input": "把这段话改写成一句话摘要:发票需要提供公司名称和税号。"}}- 一个文件里只能包含同一个模型的请求,混着写会被拒。
- custom_id 必须唯一,它是结果对齐的唯一依据,重复就无法区分。
- body 里的参数和同步调用完全一致,先在同步接口上调通再搬进 jsonl。
- 不能有 stream: true;也不要塞空行或注释,jsonl 里每一行都必须能单独解析。
- 结果顺序与文件里的行序无关,靠 custom_id 对齐,不要指望按顺序读回来。
上传与创建:五步跑完一个批次
整个流程就是“上传文件 → 创建批次 → 轮询状态 → 下载输出 → 处理失败项”。前两步用 Files API 和 Batches API,后面都是查询和下载。
- 01
上传 jsonl
用 Files API 上传,purpose 必须设为 batch,返回的 file id 就是后面的 input_file_id。
- 02
创建批次
调用创建接口,传入 input_file_id、endpoint(要和文件里的 url 一致)和 completion_window: "24h"。
- 03
轮询状态
定时 retrieve 该批次,看 status 从 validating 走到 completed;别写死 sleep,要按状态判断。
- 04
下载输出
完成后用 output_file_id 下载结果文件,仍然是 jsonl,每行对应一条请求的结果。
- 05
收尾失败项
有请求失败时,失败原因在 error_file_id 对应的文件里,同样按 custom_id 对齐后重跑。
# 1. 上传,purpose 必须是 batch
curl https://api.openai.com/v1/files \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-F purpose="batch" \
-F file="@batch-input.jsonl"
# 2. 创建批次
curl https://api.openai.com/v1/batches \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"input_file_id": "file-abc123",
"endpoint": "/v1/responses",
"completion_window": "24h"
}'
# 3. 查状态
curl https://api.openai.com/v1/batches/batch_abc123 \
-H "Authorization: Bearer $OPENAI_API_KEY"
# 4. 下载结果文件
curl https://api.openai.com/v1/files/file-output-abc123/content \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-o batch-output.jsonl用官方 SDK 会短很多,适合直接写进脚本或定时任务里。下面这段创建批次并等到结束:
import time
from openai import OpenAI
client = OpenAI()
with open("batch-input.jsonl", "rb") as fh:
upload = client.files.create(file=fh, purpose="batch")
batch = client.batches.create(
input_file_id=upload.id,
endpoint="/v1/responses",
completion_window="24h",
)
while batch.status not in ("completed", "failed", "expired", "cancelled"):
time.sleep(60)
batch = client.batches.retrieve(batch.id)
print(batch.status, batch.request_counts)
# 结果文件
content = client.files.content(batch.output_file_id).text
open("batch-output.jsonl", "w").write(content)状态机:怎么判断该等还是该放弃
正常路径是 validating → in_progress → finalizing → completed。除此之外还有几种终态,处理逻辑要覆盖到,否则脚本会一直转圈。
| 状态 | 含义 | 该怎么处理 |
|---|---|---|
| validating | 正在校验输入文件格式 | 什么都不用做,继续等 |
| in_progress | 正在执行请求 | 可以看 request_counts 估算进度 |
| finalizing | 正在汇总输出文件 | 马上结束,别急着判失败 |
| completed | 全部结束,输出可下载 | 下载 output_file_id,并检查 error_file_id 是否存在 |
| failed | 批次整体失败 | 看错误信息,多为文件格式或端点不匹配 |
| expired | 超过 24 小时未跑完 | 未完成的请求进错误文件,已完成部分照常计费 |
| cancelling / cancelled | 正在取消 / 已取消 | 取消最多约 10 分钟生效,期间状态可能停在 cancelling |
用 custom_id 把结果对回去
输出行的顺序不保证和输入一致,这是最容易踩的坑:直接按行号 zip 会把结果错配到别的请求上,而且错得很安静。永远用 custom_id 建索引再取。
import json
def load_jsonl(path):
with open(path, encoding="utf-8") as fh:
for line in fh:
line = line.strip()
if line:
yield json.loads(line)
results = {}
for row in load_jsonl("batch-output.jsonl"):
custom_id = row.get("custom_id")
response = row.get("response") or {}
if row.get("error"):
results[custom_id] = {"ok": False, "error": row["error"]}
continue
if response.get("status_code") == 200:
body = response.get("body", {})
results[custom_id] = {"ok": True, "body": body}
else:
results[custom_id] = {"ok": False, "error": response}
# 和输入比对,找出既没成功也没失败的漏项
inputs = [row["custom_id"] for row in load_jsonl("batch-input.jsonl")]
missing = [cid for cid in inputs if cid not in results]
print("总数", len(inputs), "有结果", len(results), "缺失", len(missing))- custom_id 建议直接用业务主键,比如订单号、文档 id,方便出问题时反查。
- 结果里既有成功行也有失败行,失败行同样带 custom_id,别只读 response 字段就跳过。
- error_file_id 里的内容是失败原因,通常需要按原因分类重跑,而不是整批重来。
- 先落库再处理:把原始 jsonl 存一份,方便事后核对到底哪条没跑成功。
限额、过期与计费要注意什么
Batch 有自己的配额体系,但也不是无限用。下面这些数字是当前常见上限,平台会调整,动手前请以官方文档为准。
| 限制项 | 当前上限 | 影响 |
|---|---|---|
| 单批请求数 | 最多 50,000 条 | 超过要拆成多个批次 |
| 输入文件大小 | 最大 200 MB | 长文档任务尤其容易先撞上这条 |
| 创建速率 | 每小时最多 2,000 个批次 | 别用“每条请求一个批次”的写法 |
| /v1/embeddings 批量 | 整批最多 50,000 个输入 | 嵌入任务要按输入条数算,不是按行数 |
- 输出文件在批次完成后保留 30 天,之后自动删除,要归档就当场下载。
- 24 小时内没跑完会进入 expired,未完成的请求写进错误文件(错误码 batch_expired)。
- 即使超时,已经完成的请求照常计费——所以要尽量把批次切小,别把全部希望压在一个大文件上。
- 取消只对还没开始执行的请求有效,已经跑完的部分照样收费。
- 具体单价和折扣以官方页面为准,不同模型、不同批次的计费口径可能变化。
和在线接口的分工与检查清单
实际项目里通常是混合用:用户在页面上等结果时走在线接口,后台补数据、夜间跑批、历史数据回填走 Batch。同一套提示词两边都能用,把参数抽出来复用即可。
| 任务 | 建议接口 | 理由 |
|---|---|---|
| 用户在页面上点击后等结果 | 在线接口 | 需要秒级反馈,Batch 至少要等校验和调度 |
| 每天凌晨给新文章打标签 | Batch | 不要求实时,量大,五折很划算 |
| 需要边生成边展示 | 在线接口 | Batch 不支持流式 |
| 全量历史数据重新向量化 | Batch | 一次跑完,失败项单独重跑即可 |
- 先在同步接口把单条请求调通,再批量生成 jsonl。
- 生成 jsonl 后做一次预检:行数、custom_id 是否唯一、模型是否统一、有没有 stream 字段。
- 批次大小控制在能一次跑完的范围,宁可多批几次,也不要一个 20 万行的大文件。
- 把 batch id、input_file_id、output_file_id 记进数据库,方便事后追查。
- 下载结果后先校验覆盖率,再入库,最后才跑下游逻辑。
常见问题
Batch API 一定能在 24 小时内跑完吗?
官方承诺的窗口是 24 小时内完成,通常远快于这个时间,但并不保证。超过窗口仍未完成的批次会变成 expired,未跑的请求写进错误文件,已完成的请求照常计费。所以重要的任务要留出重跑时间。
Batch 的结果顺序和输入一致吗?
不保证。必须用 custom_id 建索引再取结果,按行号直接对应会错配,而且不会报错。建议 custom_id 直接用业务主键,方便反查和重跑。
批处理失败了要不要整批重跑?
通常不用。失败请求会写进 error_file_id 对应的文件,按 custom_id 挑出来单独重跑即可。只有批次整体 failed 或输入文件格式错误时,才需要修正后重新提交。
Batch 会影响我平常调用的速率限制吗?
Batch 使用单独一套更高的限流池,不消耗标准按模型的速率限制。但它本身也有上限,比如单批请求数、文件大小和每小时创建批次数,具体数值以官方文档为准。
官方文档与参考
本文依据上述官方文档整理;价格、额度与功能会随官方更新变化,第三方中转站的行为以其自身说明为准。