文章

LLM Batch Inference 生产实战:用异步批处理跑稳离线大模型任务

本文系统讲解 LLM Batch Inference 如何用于离线摘要、分类、数据标注和批量评测任务,覆盖任务切分、幂等重试、结果归并、成本控制、失败恢复与上线检查清单。

背景问题

很多团队第一次接入大模型时,会把所有任务都塞进在线接口:用户请求走在线接口,后台摘要走在线接口,日志分类走在线接口,数据标注也走在线接口。短期看实现简单,长期看会出现三个问题。

第一,离线任务挤占实时流量。 批量摘要、历史工单归类、知识库条目清洗、评测集回放等任务通常不需要秒级返回,却会和真实用户请求竞争同一组在线限流与并发资源。

第二,失败恢复粗糙。 如果 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 BatchingBatch Inference
定位在线推理服务内部的调度技术离线异步任务模式
目标提升在线吞吐并控制延迟在可接受窗口内提高吞吐、降低成本
机制把多个生成中的请求按 token 迭代动态组合准备输入 → 提交作业 → 轮询/回调 → 下载结果
响应时间毫秒到秒级分钟到小时级
适用场景在线 Chat、实时对话离线摘要、批量分类、数据标注、评测回放

这意味着 Batch Inference 的设计重点不是”如何让单个用户更快看到回答”,而是”如何让 10 万、100 万条离线任务稳定、可恢复、可对账地跑完”。

2. 作业输入通常是文件化记录

主流平台的批处理接口通常要求把每条请求组织成独立记录。例如 OpenAI Batch API 使用 JSONL 文件,每一行是一条请求,并通过 custom_id 关联输入和输出。Anthropic Message Batches 也建议在结果处理中根据 custom_idresult 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 都有独立状态:createduploadedsubmittedrunningcompleted / failed / expiredimported。这样做的好处是,某个分片失败时只需要重试该分片,而不是重跑整个任务。

分片大小需要结合平台限制、任务复杂度和下游导入能力决定。例如 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 的示例也展示了根据 succeedederroredexpired 等结果类型分别处理。生产系统应把这些状态映射为自己的统一错误模型。

5. 建立批处理任务台账

建议单独建立 llm_batch_jobllm_batch_shardllm_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_idshard_idcustom_idsource_idprompt_versionmodel_name

误区三:失败后整批重跑

整批重跑会增加成本,并可能导致重复写入。正确做法是按结果状态拆分重试集合,只处理失败、过期或缺失记录。

误区四:忽略输出顺序问题

输出文件不应按行号和输入文件强行一一对应。结果归并必须基于 custom_id

误区五:不做成本预估

批处理虽然可能更便宜,但并不等于没有成本风险。提交前应估算输入 token、最大输出 token、分片数量和失败重试比例。对于大批量作业,还应设置预算阈值和审批门槛。

上线检查清单

上线前至少检查以下事项:

  • 是否明确区分在线任务和离线任务
  • 是否为每条输入生成稳定 custom_id
  • 是否记录 job_idshard_idsource_idprompt_versionmodel_name
  • 是否按平台限制控制单批请求数和文件大小
  • 是否支持分片级提交、取消、重试和状态查询
  • 是否支持结果导入幂等
  • 是否能按失败类型选择性重试
  • 是否能识别缺失结果和重复结果
  • 是否记录 input_tokensoutput_tokenstotal_tokens 和成本估算
  • 是否有抽样质检和异常输出审查流程
  • 是否对敏感数据、隐私数据和跨区域存储做合规检查
  • 是否为大批量任务设置预算阈值和人工确认

推荐的生产架构

可以按以下链路建设:

业务数据源 → 任务筛选与快照 → Prompt / Model 版本绑定
    → JSONL 分片生成 → 对象存储上传 → Batch Job 提交
    → 状态轮询或事件通知 → 输出文件下载
    → 结果归并与幂等写入 → 失败记录重试 → 抽样质检与报表

这条链路的关键是**“快照”**。批处理任务往往跑得久,如果任务执行过程中业务数据被修改,必须能解释输出基于哪一版输入、哪一版提示词、哪一版模型生成。

参考资料

  1. OpenAI Batch API
  2. Anthropic Message Batches API
  3. Google Gemini Batch Inference
  4. Amazon Bedrock Batch Inference

常见问题

LLM Batch Inference 适合实时聊天吗?
不适合。它面向可延迟完成的离线任务,实时聊天、语音交互和低延迟 API 仍应走在线推理链路。
Batch Inference 和 Continuous Batching 是一回事吗?
不是。Batch Inference 是离线异步任务模式,Continuous Batching 是在线推理服务内部的动态调度技术。
批处理失败后应该整批重跑吗?
不建议。更稳妥的方式是用 custom_id 或业务任务 ID 归并结果,只重试失败、过期或可恢复的子任务。
Batch Inference 可以替代在线 API 吗?
不能。它面向可延迟完成的离线任务,延迟通常在分钟到小时级别,不适合需要即时响应的在线场景。