背景问题
很多团队第一次接入大模型时,会把所有任务都塞进在线接口:用户请求走在线接口,后台摘要走在线接口,日志分类走在线接口,数据标注也走在线接口。短期看实现简单,长期看会出现三个问题。
第一,离线任务挤占实时流量。 批量摘要、历史工单归类、知识库条目清洗、评测集回放等任务通常不需要秒级返回,却会和真实用户请求竞争同一组在线限流与并发资源。
第二,失败恢复粗糙。 如果 10 万条任务中有 800 条失败,很多系统只能整批重跑,造成重复费用、重复写入和结果对账困难。
第三,成本与吞吐不可控。 离线任务通常天然适合排队、削峰、分片和低优先级处理。如果仍按在线任务处理,团队很难把成本、吞吐和完成时间窗口分开治理。
LLM Batch Inference 的核心价值,就是把”不需要立即返回”的大模型调用变成可排队、可追踪、可恢复的异步作业。OpenAI Batch API 明确面向异步请求组,提供独立的批处理接口、较高的独立限流池、24 小时完成窗口和成本优惠;Anthropic Message Batches 也面向大规模异步 Messages 请求,适合无需即时响应的大批量任务;Google Gemini Batch Inference 和 Amazon Bedrock Batch Inference 同样把批量提示词作为异步作业处理。
核心原理
1. Batch Inference 不是在线批处理
这里要先区分两个容易混淆的概念。
| 概念 | Continuous Batching | Batch Inference |
|---|---|---|
| 定位 | 在线推理服务内部的调度技术 | 离线异步任务模式 |
| 目标 | 提升在线吞吐并控制延迟 | 在可接受窗口内提高吞吐、降低成本 |
| 机制 | 把多个生成中的请求按 token 迭代动态组合 | 准备输入 → 提交作业 → 轮询/回调 → 下载结果 |
| 响应时间 | 毫秒到秒级 | 分钟到小时级 |
| 适用场景 | 在线 Chat、实时对话 | 离线摘要、批量分类、数据标注、评测回放 |
这意味着 Batch Inference 的设计重点不是”如何让单个用户更快看到回答”,而是”如何让 10 万、100 万条离线任务稳定、可恢复、可对账地跑完”。
2. 作业输入通常是文件化记录
主流平台的批处理接口通常要求把每条请求组织成独立记录。例如 OpenAI Batch API 使用 JSONL 文件,每一行是一条请求,并通过 custom_id 关联输入和输出。Anthropic Message Batches 也建议在结果处理中根据 custom_id 和 result type 做不同处理。Bedrock Batch Inference 则要求把输入文件上传到 S3,提交批处理作业后再从 S3 取回输出。
一个适合生产使用的输入记录至少应包含三类 ID:
{
"custom_id": "ticket-summary:2026-07-02:000001",
"method": "POST",
"url": "/v1/responses",
"body": {
"model": "gpt-4.1-mini",
"input": "Summarize this support ticket...",
"metadata": {
"job_id": "job_20260702_ticket_summary",
"source_id": "ticket_000001",
"pipeline_version": "summary_v3"
}
}
}
custom_id 不应只是随机 UUID。更好的做法是把业务对象、任务类型、日期或批次号编码进去,便于失败恢复、重复提交检测和结果归并。
3. 输出不是按输入顺序天然可靠归并
批处理结果通常是一个输出文件,每行对应一条成功或失败结果。工程上不能假设输出顺序和输入顺序完全一致,也不能只按数组下标归并。更安全的方式是始终使用 custom_id / source_id / job_id 做结果关联。
结果表可以这样设计:
CREATE TABLE llm_batch_result (
id BIGSERIAL PRIMARY KEY,
job_id TEXT NOT NULL,
custom_id TEXT NOT NULL,
source_id TEXT NOT NULL,
status TEXT NOT NULL,
model_name TEXT,
input_tokens INTEGER DEFAULT 0,
output_tokens INTEGER DEFAULT 0,
result_json JSONB,
error_code TEXT,
error_message TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(job_id, custom_id)
);
UNIQUE(job_id, custom_id) 是关键。它可以避免重复导入、重复补偿和重复计费统计。
工程落地
1. 先判断任务是否适合批处理
适合 Batch Inference 的任务通常有三个特征:
- 用户不等待结果,允许分钟级或小时级完成。
- 输入集合明确,可以拆分成很多互相独立的记录。
- 结果可以异步回填,例如写入数据库、对象存储、搜索索引或评测报告。
典型场景:
| 场景 | 说明 |
|---|---|
| 历史客服工单摘要 | 批量生成摘要、情绪、问题类别 |
| 评论/反馈/邮件分类 | 对大量文本进行标签分类 |
| 文档元数据抽取 | 批量抽取结构化字段 |
| 数据标注与弱标签生成 | 自动生成训练数据标签 |
| RAG 知识条目清洗 | 批量清洗和标准化知识库 |
| 离线评测回放 | 模型或提示词升级后的批量评测 |
| 低优先级内容生成 | 批量生成非实时内容 |
不适合的场景: 实时聊天、客服坐席实时辅助、语音 Agent、用户正在等待的表单提交、需要多轮交互的工具调用链路。Bedrock 文档也明确指出其批处理不支持 tool calling / function calling 或 structured output 这类需要客户端来回交互的功能。
2. 把大任务拆成可恢复的小分片
不要把所有输入直接拼成一个巨大批次。更稳妥的结构是:
batch_job
├── shard_0001.jsonl
├── shard_0002.jsonl
├── shard_0003.jsonl
└── shard_0004.jsonl
每个 shard 都有独立状态:created → uploaded → submitted → running → completed / failed / expired → imported。这样做的好处是,某个分片失败时只需要重试该分片,而不是重跑整个任务。
分片大小需要结合平台限制、任务复杂度和下游导入能力决定。例如 OpenAI 文档说明单个 batch 最多可包含 50,000 个请求,输入文件大小上限为 200 MB;Gemini Batch Inference 文档说明单个 batch job 最多可包含 200,000 个请求。实际工程中不必总是卡满上限,先用更小分片验证失败率、耗时和导入速度更安全。
3. 建立幂等提交与结果导入
批处理链路至少需要两层幂等。
第一层是提交幂等。 同一个 job_id + shard_id 不应重复提交多个平台作业,除非旧作业明确失败、取消或过期。
第二层是结果导入幂等。 同一个 job_id + custom_id 多次导入时只能更新同一条记录,不能插入重复结果。
def import_batch_results(job_id: str, output_lines: list[dict]) -> None:
for line in output_lines:
custom_id = line["custom_id"]
result = normalize_result(line)
upsert_result(
job_id=job_id,
custom_id=custom_id,
source_id=parse_source_id(custom_id),
status=result.status,
model_name=result.model,
input_tokens=result.input_tokens,
output_tokens=result.output_tokens,
result_json=result.body,
error_code=result.error_code,
error_message=result.error_message,
)
4. 失败分类不要一刀切
批处理失败通常可以分为四类:
| 失败类型 | 说明 | 处理策略 |
|---|---|---|
| 输入校验失败 | 请求格式、模型名、字段不合法 | 修复生成逻辑后重跑,不能盲目重试 |
| 可恢复服务错误 | 临时服务异常、限流、内部错误 | 按失败记录重试 |
| 过期未完成 | 批处理窗口到期,部分请求未执行 | 只重试 expired 记录 |
| 业务不可处理 | 输入为空、文本过长、敏感内容不允许 | 进入人工或规则兜底 |
OpenAI Batch API 会把失败请求错误写入 error file,并可通过 custom_id 找回过期请求;Anthropic 的示例也展示了根据 succeeded、errored、expired 等结果类型分别处理。生产系统应把这些状态映射为自己的统一错误模型。
5. 建立批处理任务台账
建议单独建立 llm_batch_job、llm_batch_shard、llm_batch_result 三张表。
CREATE TABLE llm_batch_job (
job_id TEXT PRIMARY KEY,
task_type TEXT NOT NULL,
model_name TEXT NOT NULL,
prompt_version TEXT NOT NULL,
status TEXT NOT NULL,
total_items INTEGER NOT NULL DEFAULT 0,
succeeded_items INTEGER NOT NULL DEFAULT 0,
failed_items INTEGER NOT NULL DEFAULT 0,
expired_items INTEGER NOT NULL DEFAULT 0,
created_by TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
finished_at TIMESTAMPTZ
);
CREATE TABLE llm_batch_shard (
shard_id TEXT PRIMARY KEY,
job_id TEXT NOT NULL REFERENCES llm_batch_job(job_id),
input_uri TEXT NOT NULL,
provider_batch_id TEXT,
output_uri TEXT,
error_uri TEXT,
status TEXT NOT NULL,
retry_count INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
台账不是为了好看,而是为了回答几个生产问题:
- 这批任务是否已经全部完成?
- 哪些分片失败,失败原因是什么?
- 哪些
source_id没有结果? - 重跑会不会重复写入?
- 某个
prompt_version的成本和失败率是多少?
适用场景
离线摘要
把过去一年的客服工单生成摘要、情绪、问题类别和建议动作。这类任务量大,但用户不需要实时等待结果,非常适合批处理。
批量分类
对评论、邮件、客服对话、舆情文本进行标签分类。重点是保证输入版本、提示词版本、模型版本和输出结果能对齐,否则后续很难解释分类口径为什么变化。
大规模评测回放
当提示词或模型升级时,可以把历史样本、标准答案、打分规则打包成批处理任务,低成本地跑一轮离线评测。这里要注意不要和 LLM-as-a-Judge 混为一谈:本文关注的是批处理任务编排,而不是自动评分方法本身。
数据标注与清洗
批处理可用于生成弱标签、抽取结构化字段、清洗长文本、补充元数据。但这类任务必须保留人工抽检和版本记录,因为批量生成的错误会被系统性放大。
常见误区
误区一:把 Batch Inference 当成更便宜的在线接口
批处理不是低延迟接口。它适合可延迟完成的任务。如果用户正在等待响应,批处理只会让体验变差。
误区二:只记录平台 batch_id,不记录业务 ID
平台 batch_id 只能说明平台侧作业,不能直接解释业务对象。生产系统必须保存 job_id、shard_id、custom_id、source_id、prompt_version、model_name。
误区三:失败后整批重跑
整批重跑会增加成本,并可能导致重复写入。正确做法是按结果状态拆分重试集合,只处理失败、过期或缺失记录。
误区四:忽略输出顺序问题
输出文件不应按行号和输入文件强行一一对应。结果归并必须基于 custom_id。
误区五:不做成本预估
批处理虽然可能更便宜,但并不等于没有成本风险。提交前应估算输入 token、最大输出 token、分片数量和失败重试比例。对于大批量作业,还应设置预算阈值和审批门槛。
上线检查清单
上线前至少检查以下事项:
- 是否明确区分在线任务和离线任务
- 是否为每条输入生成稳定
custom_id - 是否记录
job_id、shard_id、source_id、prompt_version、model_name - 是否按平台限制控制单批请求数和文件大小
- 是否支持分片级提交、取消、重试和状态查询
- 是否支持结果导入幂等
- 是否能按失败类型选择性重试
- 是否能识别缺失结果和重复结果
- 是否记录
input_tokens、output_tokens、total_tokens和成本估算 - 是否有抽样质检和异常输出审查流程
- 是否对敏感数据、隐私数据和跨区域存储做合规检查
- 是否为大批量任务设置预算阈值和人工确认
推荐的生产架构
可以按以下链路建设:
业务数据源 → 任务筛选与快照 → Prompt / Model 版本绑定
→ JSONL 分片生成 → 对象存储上传 → Batch Job 提交
→ 状态轮询或事件通知 → 输出文件下载
→ 结果归并与幂等写入 → 失败记录重试 → 抽样质检与报表
这条链路的关键是**“快照”**。批处理任务往往跑得久,如果任务执行过程中业务数据被修改,必须能解释输出基于哪一版输入、哪一版提示词、哪一版模型生成。