教程 / 订阅与计费

OpenAI Batch API 教程:半价跑批量任务

当你要处理几千条文本,又不急着在几秒内拿到结果时,同步接口是最贵也最浪费的用法。Batch API 把这类任务打包成文件异步跑,价格打五折,还占用一套单独的限流池。这篇讲清它适合什么、输入怎么写、结果怎么对回来。

更新于 2026-10-24由 GPTHubs 团队 整理约 9 分钟读完订阅与计费

先判断:这批任务该不该走 Batch

Batch API 的核心不是“更快”,而是“更便宜、更省限流”。它把一批请求写进文件提交,服务端在 24 小时内跑完,通常远快于这个上限。代价是你没法等待单条结果,也不能流式输出。

对比项同步接口Batch API
成本按标准价格计费同样的调用享受 50% 折扣
响应时间秒级返回24 小时内完成,通常更快
速率限制消耗标准按模型的限流额度使用单独一套更高的限流池
交互方式一问一答,可流式提交文件后异步取结果,不支持流式
适合的任务聊天、补全、需要即时反馈的流程离线标注、批量摘要、批量分类、批量嵌入

jsonl 输入到底长什么样

输入是一个 .jsonl 文件:一行一条 JSON,每行代表一次独立请求。四个字段缺一不可——custom_id 用来把结果对回原文,method 和 url 决定调哪个端点,body 就是该端点原本要接收的参数。

batch-input.jsonl:两行就是两条请求
{"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,后面都是查询和下载。

  1. 01

    上传 jsonl

    用 Files API 上传,purpose 必须设为 batch,返回的 file id 就是后面的 input_file_id。

  2. 02

    创建批次

    调用创建接口,传入 input_file_id、endpoint(要和文件里的 url 一致)和 completion_window: "24h"。

  3. 03

    轮询状态

    定时 retrieve 该批次,看 status 从 validating 走到 completed;别写死 sleep,要按状态判断。

  4. 04

    下载输出

    完成后用 output_file_id 下载结果文件,仍然是 jsonl,每行对应一条请求的结果。

  5. 05

    收尾失败项

    有请求失败时,失败原因在 error_file_id 对应的文件里,同样按 custom_id 对齐后重跑。

用 curl 走完上传与创建
# 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 会短很多,适合直接写进脚本或定时任务里。下面这段创建批次并等到结束:

用 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 建索引再取。

按 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 使用单独一套更高的限流池,不消耗标准按模型的速率限制。但它本身也有上限,比如单批请求数、文件大小和每小时创建批次数,具体数值以官方文档为准。

官方文档与参考

本文依据上述官方文档整理;价格、额度与功能会随官方更新变化,第三方中转站的行为以其自身说明为准。