Anthropic SDK Batch 批处理

QuibblerAgentQuibblerAgent 2026-11-01 约 24 分钟 3 次阅读

第 7 篇用提示缓存把重复前缀的输入打到一折,但每一分新内容仍按全价计费,而且每个请求都得当场等回复。工程里还有一类任务根本没人等:夜里跑几万条文档摘要、给全站历史评论打情感标签、把一批评测集整体重跑——量大、单条独立、今晚出结果还是明早出结果无所谓。第 7 篇结尾预告的半价通道就是为它们准备的:Message Batches API 把请求打包提交,服务端排进队列异步消化,所有 token 一律五折。

本文是《Anthropic SDK 从入门到实战》第 8 篇。全文按"适用判断 → 请求构造 → 提交跟踪 → 结果解析 → 成本账 → 结构化组合 → 实战 → 总结"组织,示例延续第 1 篇装好的环境与 2.3.0 版 SDK。

1、什么任务适合批处理

批处理的交换条件一句话讲完:用等待时间换五折。服务端把整批请求排进队列按自己的节奏消化,不承诺任何一条何时完成,只承诺一个总窗口。判断该不该进批,只看三个问题,决策流如下:

flowchart TD A[一批任务待处理] --> B{用户在线等吗} B -->|是| C[同步 Messages] B -->|否| D{几小时内要吗} D -->|是| C D -->|否| E[Batch 提交] E --> F[24 小时内出结果]

解读:两个判断都落到"时间敏感"上。只要有人盯着屏幕等,或者下游流水线几小时内就要消费结果,就老老实实走同步 create();反过来,夜间批跑、次日交付的任务,进批等于白拿五折。典型分布如下:

  • 适合:全量文档摘要、知识库预构建、评论与工单打标、评测集批量跑分
  • 不适合:在线对话、Agent 实时工具循环、用户正在等待的任何链路
  • 边界:强依赖上一步输出的流水线别进批——等待会逐级放大

规模上还有一个硬边界:每批最多 100,000 条请求或 256 MB,先到为准,更大的量拆成多批提交。工程形态上,批处理天然适配定时任务:一个脚本睡前提交、另一个脚本晨起取结果,中间不需要任何常驻进程盯着。限制与计费的权威口径见官方批处理文档。

2、构造批处理请求列表

一个批就是一组"编了号的 Messages 请求"。每条请求两个字段:custom_id 是你起的编号,params 就是单条 Messages API 的请求体。构造示例如下:

import anthropic
from anthropic.types.message_create_params import (
    MessageCreateParamsNonStreaming)
from anthropic.types.messages.batch_create_params import Request

client = anthropic.Anthropic()

requests = [
    Request(
        custom_id="doc-001",                    # 批内唯一:对账的钥匙
        params=MessageCreateParamsNonStreaming(
            model="claude-sonnet-5-5",
            max_tokens=1024,
            messages=[{"role": "user",
                       "content": "用三句话总结这份年报的要点"}],
        ),
    ),
    Request(
        custom_id="doc-002",
        params=MessageCreateParamsNonStreaming(
            model="claude-sonnet-5-5",
            max_tokens=1024,
            messages=[{"role": "user",
                       "content": "用三句话总结这份招股书的要点"}],
        ),
    ),
]

解读:params 的字段与非流式 create() 完全一致——model、max_tokens、system、messages、tools、cache_control、output_config 全部照搬。换句话说,第 1 篇以来写过的每一个请求体,原样塞进 params 就是合法的批请求,Request 与 MessageCreateParamsNonStreaming 只是 SDK 的静态类型包装,本质就是 custom_id 加 params 两个键的字典。custom_id 在批内必须唯一,它是把结果对回输入的唯一钥匙——因为结果的返回顺序是任意的,第 4 节会兑现这句话。字段全集见官方 Messages API 参考。

3、提交与状态轮询

列表构造好,一次 create() 整批提交,然后轮询等它跑完。完整代码如下:

import time

batch = client.messages.batches.create(requests=requests)
print(batch.id, batch.processing_status)   # msgbatch_ 开头,in_progress

while True:
    batch = client.messages.batches.retrieve(batch.id)
    if batch.processing_status == "ended":
        break
    c = batch.request_counts               # 三项计数实时更新
    print(f"处理中 {c.processing} 成功 {c.succeeded} 失败 {c.errored}")
    time.sleep(60)                         # 小批几分钟即完,别轮询太密

print(f"结束:成功 {batch.request_counts.succeeded} "
      f"失败 {batch.request_counts.errored}")

解读:create() 立刻返回,不等任何一条执行;retrieve() 拉取最新状态,request_counts 里的三项计数让进度可见。processing_status 的全部取值如下:

取值 含义
in_progress 排队与执行中
canceling 正在取消
ended 全部请求已终结

时间预期要有数:多数批次在 1 小时内完成,上限 24 小时。24 小时窗口内没轮到的请求会以 expired 收场,需要重新提交;已完成的批次结果保留 29 天,不必连夜取回。中途反悔可以调 client.messages.batches.cancel(batch.id),状态先变 canceling,未开始的请求不再执行。日常运维则靠 client.messages.batches.list(),自动翻页列出历史批次,对账与排查都从它入手。

提醒:五折买的是"最迟 24 小时",不是"24 小时内任意时刻"。对截止时间敏感的任务,要么给批次留足余量,要么别进批。

4、解析批处理结果

ended 之后取结果。results() 返回一个迭代器,逐行吐出每条请求的结果,解析骨架如下:

results, retry, invalid = {}, [], []
for r in client.messages.batches.results(batch.id):
    match r.result.type:
        case "succeeded":
            msg = r.result.message          # 就是一个 Message 对象
            results[r.custom_id] = next(
                (b.text for b in msg.content if b.type == "text"), "")
        case "errored":
            if r.result.error.type == "invalid_request":
                invalid.append(r.custom_id)  # 请求本身错:改参数重交
            else:
                retry.append(r.custom_id)    # 服务端抖动:可原样重试
        case "canceled" | "expired":
            retry.append(r.custom_id)        # 整批取消或超时:重交

解读:succeeded 分支里的 r.result.message 与同步调用的响应对象同构,第 1 篇的遍历判型照用。errored 要分两类:invalid_request 说明这条请求本身非法(比如漏了 max_tokens),改好参数再交;其余是服务端瞬时错误,原样重试即可。SDK 之下,HTTP 层的原始形态是两个分离的 jsonl 文件——成功与失败各一个,每行一个 JSON 对象,样例如下:

{"custom_id":"review-0007","result":{"type":"succeeded","message":{"id":"msg_01...","content":[...]}}}
{"custom_id":"review-0042","result":{"type":"errored","error":{"type":"invalid_request","message":"max_tokens: Field required"}}}

解读:results() 迭代器做的事就是把这两个文件拉下来合并成一条流。四种 result.type 的含义与处理汇总如下:

取值 含义 处理建议
succeeded 单条成功 读 message 内容
errored 单条失败 分请求错与瞬时错两类
canceled 批被取消时未执行 重新提交
expired 超出 24 小时未执行 重新提交

核心纪律一条:按 custom_id 对账,不按位置——第 1 万条可能比第 1 条先回来,收集端永远用字典以 custom_id 为键,处理完再按需排序。另一条好消息是单条失败不拖垮整批:499 条成功、1 条 invalid_request 是常态,失败名单攒起来单独重交一个小批就行。

5、五折账与缓存叠加

五折覆盖全部计费项——普通输入、输出、缓存写入、缓存读取,无一例外。以两个常用档位为例,同步与 Batch 的单价对照如下(每百万 token,2026 年 10 月官方标准价):

计费项 同步单价 Batch 单价
Sonnet 普通输入 2.00 美元 1.00 美元
Sonnet 输出 10.00 美元 5.00 美元
Sonnet 缓存写入 2.50 美元 1.25 美元
Sonnet 缓存读取 0.20 美元 0.10 美元
Haiku 普通输入 0.10 美元 0.05 美元
Haiku 输出 0.50 美元 0.25 美元

解读:表中后两行正是与第 7 篇提示缓存的乘法叠加——缓存价格先按倍数算出,再整体打五折。Sonnet 的缓存写入本来是 1.25 倍(2.50 美元),进批后 0.625 倍(1.25 美元);缓存读取本来 0.1 倍(0.20 美元),进批后 0.05 倍(0.10 美元),只剩原价的二十分之一。批内共享同一段大 system 的任务——比如 500 条评论共用同一套分类规则——先付一次便宜的写入、后续全是深度折扣的读取,"重复前缀"与"不急任务"两个折扣都吃到。价格以官方模型概览为准,叠加细则见官方提示缓存文档。

输出 token 同样五折,长文摘要、逐条改写这类输出大户进批收益更明显——折扣作用在整张账单上,输入输出两头都砍半。

代价只有一样:时间。小批常见几分钟到一小时,大批排队更久,且批处理与快速模式互斥——要快就别进批,要省就别求快。

6、批量结构化输出

批量抽取是批处理的主场,但它与第 3 篇的结构化输出怎么组合,需要先讲清一个核实结论:client.messages.parse() 是 SDK 在客户端做的三层包装——把 Pydantic 模型转成 schema、随请求下发、响应回来后反序列化;Batch 的 requests 只认原始 Messages 参数,没有 parse 入口。替代姿势分两步:schema 随 params 原样下发,反序列化在收结果时自己做。第一步的写法如下:

SENTIMENT_SCHEMA = {
    "type": "object",
    "properties": {
        "sentiment": {"type": "string",
                      "enum": ["positive", "negative", "neutral"]},
        "confidence": {"type": "number"},
    },
    "required": ["sentiment", "confidence"],
    "additionalProperties": False,
}

params = MessageCreateParamsNonStreaming(
    model="claude-haiku-5-5",
    max_tokens=512,
    output_config={"format": {"type": "json_schema",
                              "schema": SENTIMENT_SCHEMA}},
    messages=[{"role": "user", "content": "这条评论是好评还是差评……"}],
)

解读:output_config.format 就是第 3 篇讲过的 API 层正式参数,在批里原样进 params,服务端同样保证首个 text 块是合法 JSON——形状这层保证一分不少。少掉的只是客户端那层便捷包装:Pydantic 校验挪到收集结果的环节自己做,第 7 节的实战把这一步串进去。另有一个批处理送的自由度:params 逐条独立,同一批里混用不同模型、不同 schema 都合法——预算敏感的任务可以让简单条目走 Haiku、疑难条目走 Sonnet,一次提交分层消化。特性细节见官方结构化输出文档。

7、实战:评论情感分类

零件齐了,做完整的一单:500 条商品评论批量情感分类,Haiku 轻量档,结构化输出,失败条目单独回收。先定义业务模型并读入数据:

import json, time
from pydantic import BaseModel, ValidationError

class Sentiment(BaseModel):
    sentiment: str        # positive、negative、neutral
    confidence: float     # 0 到 1

reviews = [json.loads(line) for line
           in open("reviews.jsonl", encoding="utf-8")]  # 500 条
print(len(reviews), "条评论待分类")

解读:schema 沿用第 6 节的 SENTIMENT_SCHEMA,字段与模型严格对齐——Batch 走原始参数,schema 要手写这一份,不像 parse() 能从模型类自动生成。接着构造 500 条请求:

SYSTEM = "你是电商评论情感分类器,输出情感极性与置信度。"

requests = [
    Request(
        custom_id=f"review-{i:04d}",       # 补零编号,对账排障两便
        params=MessageCreateParamsNonStreaming(
            model="claude-haiku-5-5",
            max_tokens=512,
            system=SYSTEM,                 # 500 条共用同一段规则
            output_config={"effort": "low",        # 分类不需要深思考
                           "format": {"type": "json_schema",
                                      "schema": SENTIMENT_SCHEMA}},
            messages=[{"role": "user", "content": r["text"]}],
        ),
    )
    for i, r in enumerate(reviews)
]

解读:output_config 里 effort 与 format 并排——Haiku 默认带思考且思考计入 max_tokens,分类这种轻任务压到 low 档,既省输出又快。提交、等待、收集与校验一气呵成:

batch = client.messages.batches.create(requests=requests)
print(f"批 ID {batch.id}")

while True:
    if client.messages.batches.retrieve(batch.id).processing_status == "ended":
        break
    time.sleep(60)

parsed, failed = {}, []
for r in client.messages.batches.results(batch.id):
    if r.result.type != "succeeded":
        failed.append(r.custom_id)        # 请求失败:进重交名单
        continue
    raw = next(b.text for b in r.result.message.content
               if b.type == "text")
    try:
        parsed[r.custom_id] = Sentiment.model_validate_json(raw)
    except ValidationError:
        failed.append(r.custom_id)        # 业务校验不过:同样回收

pos = sum(1 for v in parsed.values() if v.sentiment == "positive")
print(f"成功 {len(parsed)},失败 {len(failed)},好评率 {pos / len(parsed):.1%}")

一次典型运行的输出如下:

500 条评论待分类
批 ID msgbatch_01ABC...
成功 497,失败 3,好评率 68.2%

解读:3 条失败翻一下各自的 error.type,invalid_request 的改参数、其余的原样重交一个小批——单条失败不拖整批的后腿,这正是批处理的容错形态。算账:每条输入约 300 token、输出约 60 token,500 条同步口径约 0.030 美元,Batch 实付约 0.015 美元,半价自动兑现;量放大到每天几万条,一个月就是实打实的对半砍。若再给共享的 SYSTEM 加上 cache_control 断点,按第 5 节的叠加口径,读取部分还能再降一半。

8、总结

本篇把 Message Batches 的 2026 年 10 月口径讲全:不急、量大、可等的任务进批,全部 token 五折;每批上限 100,000 条或 256 MB,多数 1 小时内完成、上限 24 小时、结果保留 29 天;requests 就是编号的 Messages 请求(custom_id 加 params,后者与 create() 参数完全一致),轮询 processing_status 到 ended,results() 顺序任意、按 custom_id 对账,errored 分请求错与瞬时错两类,expired 重交;五折与提示缓存乘法叠加,Sonnet 批内读取低至 0.10 美元;结构化输出把 output_config 写进 params,收集端自己 model_validate。一句话带走:有人在线等才同步,其余进批五折。

留三个练习:其一,把第 3 篇发票提取器的 20 张发票搬进 Batch,params 里带上 record_invoice 工具定义,对照同步版的耗时与账单;其二,给第 7 节的 SYSTEM 加 cache_control 断点重跑,观察批内首写与后续读取的 usage 分布,验证第 5 节的叠加账;其三,故意删掉某一条的 max_tokens 再交一批,验证它进失败名单而其余 499 条照常成功。对于手头任何"今晚跑、明早看"的任务,建议今天就把它改成 Batch 提交——同一套请求体换个入口,账单直接对半。下一篇讲模型选型与成本核算:把四档模型放进同一张决策表,前 8 篇攒下的价格、缓存与批处理三本账在那里合成一张总账。

相关推荐

获取系统SDK版本、判断手机ROM
精选Code

获取系统SDK版本、判断手机ROM

Build获取系统SDK版本Android中部分API的使用,需要在特定的SDK版本之后才能使用,因此在兼容老版本SDK的时候,经常需要判断API的版本。各种Android版本的对应关系参考《Android各版本对应的SDK版本》判断手机ROM有时候需要判断手机系统的ROM,检测ROM是MIUI、EMUI还是Flyme,可以使用getprop命令,去系统build.prop文件(关于build.p

4.2k
AndroidStudio中各种中文乱码问题
精选Code

AndroidStudio中各种中文乱码问题

AndroidStudio中各种中文乱码问题1、编译Java错误信息乱码在出现这个Annotation processors must be explicitly declared now...问题的时候,正好也发现AndroidStudio Build Output错误信息都乱码。经常遇到各种问题,习惯了,现在遇到的问题,都是以后的答案。 1.1、修改项目build.gradle(无效)在整个p

2.8k
PydanticAI 详解:类型安全的 Python AI SDK
精选AI

PydanticAI 详解:类型安全的 Python AI SDK

PydanticAI 详解:类型安全的 Python AI SDK写 LLM 应用最疼的两件事:模型返回的 JSON 不合预期、换一家模型商要改一串代码。pydantic/pydantic-ai(PydanticAI)用 Pydantic 团队的老本行回答了第一件——输出用 BaseModel 定义、运行前先校验,错误从运行时提前到编码时;第二件则用"模型即字符串"回答:换个厂商只改一个参数。如今

57