---
url: /practices/batch-api-offline-tasks.md
description: >-
  一次处理几千条标注任务，同步调用烧钱又限流。OpenAI Batch API 半价、24 小时内跑完，本文走一遍 JSONL
  准备、上传、轮询、取结果的完整流程，并记录我踩过的失败重试坑。
---

# 批量任务别再同步调 API，Batch 接口直接打五折

上周要给几千条用户反馈做分类。同步调 `/v1/chat/completions`，跑到一半撞了限流，账单还翻倍。

后来换成 Batch API（批量接口）。同样的活，钱直接省一半。

规则很简单：把一堆请求打包成一个 JSONL 文件上传，OpenAI 在后台慢慢跑，24 小时内给结果。代价是输入和输出 token 都打五折（价格 2026-10-09 从官方 pricing 页核实）。我的反馈分类是离线任务，等几个小时完全无所谓。

## 什么样的任务适合丢进去

判断标准就一条：这次调用不需要立刻拿到结果。

数据标注、评测集打分、语料清洗、文档摘要，全是典型场景。反过来，聊天机器人、搜索补全这种用户在线等的功能，用不了它。

有个容易忽略的用法：构建评测集。我建议每次改 prompt 之前，把新旧两版各跑一遍批量评测，对比分数再决定要不要上线。这个场景不省这五折，评测跑多了心疼钱。

## 三个硬性限制

先说清楚边界，免得写完代码才发现不合适。

一个批次最多 50,000 条请求，文件最大 200 MB。

完成窗口目前只能填 `24h`，没有别的选项。

支持的接口有限：`/v1/responses`、`/v1/chat/completions`、`/v1/embeddings`、`/v1/completions`、`/v1/moderations`、图像生成和编辑，还有视频。以上数字 2026-10-09 从官方 API reference 核实。

大部分批量任务塞得进这三条线。超了就拆文件，一个批次拆两个。

## 完整流程

分四步：准备 JSONL、上传、创建批次、轮询取结果。

先写 JSONL。每个请求一行 JSON，带自定义的 `custom_id`（方便回头对号入座）：

```json
{"custom_id": "fb-0001", "method": "POST", "url": "/v1/chat/completions", "body": {"model": "gpt-4o-mini", "messages": [{"role": "user", "content": "把这条反馈归类：bug/建议/咨询/其他。内容：搜索结果排序不对"}]}}
```

然后上传。用 Files API，`purpose` 必须是 `batch`：

```python
from openai import OpenAI

client = OpenAI()
f = client.files.create(file=open("batch_input.jsonl", "rb"), purpose="batch")
```

上传完拿文件 ID 创建批次：

```python
batch = client.batches.create(
    input_file_id=f.id,
    endpoint="/v1/chat/completions",
    completion_window="24h",
)
print(batch.id)
```

最后轮询状态，完成后下载输出文件：

```python
import time

while True:
    b = client.batches.retrieve(batch.id)
    if b.status in ("completed", "failed", "expired"):
        break
    time.sleep(60)

out = client.files.content(b.output_file_id)
out.write_to_file("batch_output.jsonl")
```

输出文件里每行带原样的 `custom_id` 和模型的响应，按它回写数据库就行。

## 我踩的两个坑

第一个坑：批次不是全成全败。50,000 条里跑挂了 300 条，批次状态照样是 `completed`。

失败的请求单独写进 `error_file_id` 指向的文件。我第一次跑完直接把输出文件当全量结果入库，丢了那 300 条数据，第二天对账才发现。

现在我的下载逻辑是两个文件都拉。error 文件有内容就报警。

第二个坑：注意 `expired` 状态。批次超过 24 小时没跑完会过期，没执行完的部分作废。

我用 gpt-4o-mini 跑几万条短分类，实际半小时就完了，从来没碰见过过期。但如果你塞大输出、长上下文，或者赶上高峰期排队，就要盯着这个状态，过期了拆小批次重提。

## 省多少钱，算一笔账

以 gpt-4o-mini 为例，标准价输入 $0.15、输出 $0.60 每百万 token，Batch 价是输入 $0.075、输出 $0.30（2026-10-09 从 pricing 页核实）。10 亿输入 token 的标注任务，同步调要 $150，批量只要 $75。

跑批时的实际速度也超出预期。我那次 3,000 条、每条几百 token 的分类，提交后 25 分钟拿到结果，远不到 24 小时的上限。也就是说，很多「我以为要等一整天」的任务，其实批量和同步的体感差距只有一个午休。

唯一要注意的是 prompt caching（提示缓存）和批量折扣能否叠加的问题。我建议拿一小批任务分别实测账单，别只看文档，价格和折扣规则这种东西变动比代码频繁得多。

## 下一步

把你手里某个周期性跑的脚本找出来，改成批量版，跑一次对一下账单。改动量不大，也就是把 `client.chat.completions.create` 的参数挪进 JSONL 里。
