Claude Batch API 乱序返回导致 59% 标签错配:一个血泪教训

背景:一个看似完美的标注流水线
作者手头有 5000 条历史客服工单,目标是训练一个轻量分类器,将新工单路由到六个队列(账单、登录、物流等)。计划很清晰:先用 Claude Batch API 半价批量标注全部工单,再用标注结果训练小模型,让 Claude 远离实时推理路径。
标注批次花费不到 4 美元,约 40 分钟完成。标签分布看起来完美——38% 账单、22% 登录、15% 物流,与预期一致。分类器验证准确率也令人满意。几乎要上线了。
真相:59% 的标签贴错了工单
作者手动标注了 200 条工单作为金标准集,与 Claude 的标签对比后发现仅有 41.5% 一致率。Claude 本身读工单能力不差,问题出在流水线本身。
Claude Batch API 不保证按提交顺序返回结果。 文档明确写了这一点,但作者没看到那行字。他用 zip(tickets, labels) 按位置匹配,结果 5000 条中有 3861 条结果位置错乱,最终 2940 条标签(59%)被贴到了错误的工单上。
为什么所有看板都没报警?
作者检查了所有能想到的指标,全部绿灯:
- 标签直方图:分布完全正确
- 各类别计数与占比:没变化
- "其他"标签比例:正常
- 平均标签长度、解析失败率、成本:均无异常
根本原因是:所有检查都是排列不变的(permutation-invariant)。打乱标签列不会改变分布、计数或比例。作者在测量"堆的形状",却从未检查"每张卡片是否放进了正确的信封"。
更糟的是,验证准确率也在撒谎——训练集和验证集来自同一批乱序标注数据,模型实际上是在用同一份打乱的"答案键"自测。
代码里藏着的两个 Bug
# Bug 1: zip 假设结果流与输入对齐(实际不对齐)
# Bug 2: 过滤掉 14 条失败请求后,即使顺序正确也会整体偏移
labels = [
r.result.message.content[0].text.strip()
for r in client.messages.batches.results(batch.id)
if r.result.type == "succeeded" # 丢弃了 14 条失败请求
]
for ticket, label in zip(tickets, labels): # 静默截断,最后 14 条无标签
ticket["label"] = label
作者确实填了 custom_id = f"req-{i}",但回来时直接丢弃了它。zip 还会静默截断到较短列表,最后 14 条工单压根没有标签,也没有任何报错。
正确做法
- 用稳定主键关联:将
custom_id设为工单自身的唯一 ID(如数据库主键),返回后按custom_idjoin,而非按位置 zip。 - 断言 ID 集合一致:确保返回的 ID 集合与提交时完全匹配。
- 显式处理异常状态:对
errored、expired、canceled的结果单独处理,不要简单过滤掉。 - 做逐行校验:抽样与金标准逐条对比,而非只看聚合指标。
对开发者的启示
- 批量 API 不是队列:它是一堆独立请求,Anthropic 并行处理,短请求先完成、长请求后完成,返回顺序完全不可预测。
- 聚合指标会骗人:直方图、分布、计数对排列不敏感,无法发现标签错配类问题。必须做逐行 ground truth 校验。
- 过滤失败结果要小心:丢弃失败项后再 zip,即使顺序碰巧正确也会导致后续标签整体偏移。
- 读文档的那一行:文档明确写了结果可能乱序、应使用
custom_id匹配——忽略这一行,代价是 59% 的数据污染和一个几乎上线的假模型。
本文目录(6 节)
本文基于 dev.to AI 的公开内容,由 AI 辅助整理改写后发布。
原标题:Claude Batch API Returned Results Out of Order: 59% of My Labels Wrong
阅读原文