AI Agent 批量处理时模型返回空响应(HTTP 200 但零 token)会被静默跳过,需在编排层和执行层分别建立成功判断标准,否则 80% 任务悄然消失。
你的批量 Agent 处理 10 个条目。日志显示成功。但实际上只运行了 2 个。另外 8 个呢?它们消失了,没有报错,没有痕迹,系统从未告诉你出了问题。
批量处理是静默失败滋生的温床。以下是其机制:
你的 Agent 遍历一个输入列表。它为每个条目调用一个模型(或一串工具)。如果模型返回空响应——HTTP 200,但零输出 token——那么 Agent 面临一个选择:显式处理这个空情况,或者跳过它并处理下一个条目。
大多数代码库选择跳过。循环继续。没有异常抛出。运行以"成功"状态完成,因为循环本身完整执行而没有崩溃。从外部看,一切正常。从内部看,80% 的工作从未发生。
陷阱:编排层面的成功不等于执行层面的成功。
你不需要监控基础设施来捕捉这个问题。你需要的是推理能力加上你现有的日志。
Input: [item_1, item_2, item_3, item_4, item_5, item_6, item_7, item_8, item_9, item_10]
Expected executions: 10
现在用 grep 在日志中搜索真实的模型调用——即真正产生 token 的事件。查找如下模式:
OpenAI API call 或 Anthropic API call 或你的模型提供商的日志标记
tokens_sent, output_tokens,或者你的 SDK/客户端记录的任何内容
任何表明"模型被问到并回答了"的内容
统计它们。如果你看到 2 次真实的模型调用,但有 10 个输入条目,你就已经找到了问题所在。
从成功的模型调用中提取条目标识符。例如:
Call 1: processed item_2 → 120 output_tokens
Call 2: processed item_7 → 98 output_tokens
Items processed: {item_2, item_7}
Items missing: {item_1, item_3, item_4, item_5, item_6, item_8, item_9, item_10}
现在查看每个缺失条目周围的日志。搜索:
循环是否到达了 item_1?(查找类似"processing item_1"的日志行。)
如果到达了该条目,是否调用了模型?
如果调用了模型,响应是什么?是空的吗?是否包含输出 token?
你在寻找逻辑断裂的地方。通常是以下几种情况之一:
场景 A:循环从未到达该条目。你的批量输入在迭代开始前就被截断、过滤或部分失败了。不太常见,但请检查你的批量构建代码。
场景 B:循环到达了该条目,但跳过了模型调用。你的代码有一个条件判断:if item.valid(): call_model(),而多个条目在验证时静默失败了。或者触发了一个回退逻辑但没有明显地记录下来。
场景 C:循环调用了模型,但模型返回了空输出。这是静默失败。模型返回 HTTP 200,但 output_tokens == 0 或响应体为空/空白。你的代码看到空响应后,要么原样返回它,要么跳过它而没有发出警报。
对于每个实际运行了的条目,记录输入和输出 token:
item_2 input: 450 tokens → output: 120 tokens (good, output exists)
item_7 input: 480 tokens → output: 0 tokens (empty response)
如果一个条目有输入但输出等于 0,那就是静默失败。模型被调用了,它返回了成功,但它什么都没产生。
检查你的代码:它是否显式处理了这种情况?
# Without explicit handling (the dangerous version):
for item in batch:
response = model.call(item)
results.append(response) # if response is empty, it still gets appended as "nothing"
# With explicit handling (safer):
for item in batch:
response = model.call(item)
if not response or response.tokens == 0:
log_alert(f"Empty response for {item}")
results.append({"error": "empty_response", "item": item})
else:
results.append(response)
第一个版本吞掉了失败。第二个版本将其暴露出来。
一旦你确定了哪些条目失败了(以及如何失败的),问自己:
为什么模型返回空?
触发了速率限制?(在日志中查找 429 错误或长延迟间隔。)
输入格式有问题?(实际发送给该条目模型的是什么?记录下来。)
模型特定的行为?(某些模型在特定输入下返回 200 但输出为空;检查你的模型的行为。)
超时或部分读取?(连接是否在响应中途断开了?)
为什么代码没有捕获到?
output_tokens == 0?大多数时候,根本原因是:你的代码假设 HTTP 200 意味着成功,从不检查是否实际产生了输出。
添加显式输出验证。在每次模型调用后,检查 output_tokens > 0(或者对你的用例来说意味着"真实输出"的任何指标)。记录失败。
统计你的批量完整性。在发布之前,比较输入数量和输出数量。如果不匹配,你就有了静默失败。
记录每个条目的决策。每当一个条目被跳过、处理或返回空时,用条目 ID 记录它。然后日后用完全可见性 grep 你的日志。
对空响应进行重试。如果一个条目返回空输出,在放弃之前重试一到两次。记录重试次数。大多数瞬时失败在重试后会清除。
批量处理是高杠杆、高风险的:处理 100 个条目,一个失败会隐藏在眼皮底下。处理 1,000 个,你直到客户投诉或指标突然下降才会注意到。
其机制始终相同:错误层面的成功。编排层(循环)成功了。执行层(模型调用)静默失败了。这种不匹配永远不会冒泡上来,因为没有人同时关注这两个层。
这就是为什么那些及早捕获这些失败的构建者不依赖"如果出了问题系统会告诉你"。他们在交叉点进行检测:输入计数、输出计数、每个条目的 token 核算,以及对空响应的显式检查。
一旦你推理出这个机制,你就可以在几分钟内从日志中发现批量静默失败。它并不神秘。它只是核算。
你在批量作业中遇到过这种情况吗?是什么让你注意到的?欢迎留言,我很想听听是什么暴露了它。