文章将 Agent 失败拆分到多个层次,指出 HTTP 200 仍可能对应截断、拒绝或字段缺失。建议用代码验证输出,并使用由任务标识生成的稳定幂等键,避免重试产生重复写入。
你的 Agent 在凌晨 2 点挂了。是模型不行吗?大概率不是。
我见过的大多数运行失败,都被归错了因。明明是传输、配额或契约检查出了问题,最后却怪到了模型头上。
下面用一组简短的问答拆解几个常见误区,最后给出一个探测脚本,按五个层次梳理失败原因。
状态码只描述传输情况,并不能说明输出是否可用。
你可能收到 200,但 finish_reason: "length";可能收到 200,但模型拒绝了请求;也可能收到 200,返回的 JSON 能解析,却缺少必填字段。
这算完成了一项工作吗?不算。你只是收到了产物,但产物没有通过验收。
所以,要在代码里定义验收条件:解析、校验,再计算哈希。否则,你统计的只是收到多少次响应,而不是拿到了多少个有效结果。
从网络开销看,重试很便宜;从副作用看,重试可能很贵。
重试一次读取,什么都不会改变。重试一次写入数据行的工具调用,表里就可能多出两行。
这不是模型的问题,而是缺少幂等键。
我的规则是:每次尝试都携带一个稳定的 Idempotency-Key。根据任务 ID 生成它,绝不要根据尝试次数生成。
延迟中位数只能回答一个很窄的问题:热启动路径有多快?
但用户实际走的往往不是这条路径。免费层的托管服务经常呈现出两个延迟分组:热启动和冷启动。
应该衡量的是「拿到通过验收的产物需要多久」。这个数字包含重试和校验器的耗时。
两个模型即使 token 生成速度完全一样,这个指标也可能相差很远。原因可能只是其中一个模型更容易校验失败。
换供应商之前,先做好归因。要问的是哪一层出了问题,而不是用了哪个模型。
通常,只有最后两层的问题才归咎于你的 prompt。契约层的问题往往出在你自己的校验器上。
这个脚本只使用 Python 标准库。它会记录足够的信息,用来对每次尝试进行分类。
我没有用你的 endpoint 运行过它。任何输出都应视为示例,而不是基准测试结果。
# probe.py - failure-taxonomy probe (illustrative, unexecuted here)
import json, os, time, urllib.request, urllib.error, hashlib
BASE = os.environ["BASE_URL"] # OpenAI-compatible base, e.g. https://host/v1
MODEL = os.environ["MODEL"]
KEY = os.environ.get("API_KEY", "")
RUNS = int(os.environ.get("RUNS", "8"))
PROMPT = os.environ.get("PROMPT", 'Return JSON: {"ok": true}')
def call(idem):
body = json.dumps({
"model": MODEL,
"messages": [{"role": "user", "content": PROMPT}],
"temperature": 0,
"max_tokens": 256,
}).encode()
url = BASE.rstrip("/") + "/chat/completions"
req = urllib.request.Request(url, data=body, method="POST")
req.add_header("Content-Type", "application/json")
req.add_header("Idempotency-Key", idem)
if KEY:
req.add_header("Authorization", "Bearer " + KEY)
t0 = time.monotonic()
try:
with urllib.request.urlopen(req, timeout=120) as r:
return r.status, r.read(), (time.monotonic() - t0) * 1000, ""
except urllib.error.HTTPError as e:
return e.code, e.read(), (time.monotonic() - t0) * 1000, "http_error"
except Exception as e:
return None, b"", (time.monotonic() - t0) * 1000, type(e).__name__
def classify(status, schema_ok):
if status is None: return "transport"
if status == 429: return "quota"
if status in (401, 403): return "auth"
if status and status >= 500: return "upstream"
if status != 200: return "contract"
if not schema_ok: return "contract"
return "ok"
task_id = os.environ.get("TASK_ID", "probe-1")
ledger = []
for attempt in range(1, RUNS + 1):
idem = task_id # stable across retries, on purpose
status, raw, ms, err = call(idem)
schema_ok, reason = False, ""
try:
payload = json.loads(raw.decode() or "{}")
choice = payload["choices"][0]
reason = choice.get("finish_reason", "")
json.loads(choice["message"]["content"]) # your real contract check goes here
schema_ok = True
except Exception as e:
reason = reason or type(e).__name__
ledger.append({
"attempt": attempt, "idem": idem, "status": status,
"ms": round(ms), "bytes": len(raw), "finish": reason,
"err": err,
"sha256": hashlib.sha256(raw).hexdigest()[:12],
"layer": classify(status, schema_ok),
})
for row in ledger:
print(json.dumps(row))
print("--- layer histogram ---")
for layer in sorted({r["layer"] for r in ledger}):
print(layer, sum(1 for r in ledger if r["layer"] == layer))
设置三个环境变量,指定重复次数后运行:
BASE_URL=https://your-endpoint/v1 MODEL=your-model RUNS=12 python3 probe.py
重点就在这份运行记录里。每一行对应一次尝试,并附有一个分类标签。
大量记录被标为 contract,同时出现 finish: "length"?增加输出预算,或缩小请求范围。
transport 记录按固定时间间隔出现?应怀疑空闲超时,而不是模型。
quota 和 ok 混杂出现?你需要控制请求节奏,而不是换供应商。
在 temperature: 0 时出现相同的 sha256?把结果缓存起来,别重复运行。
没有任何 ok 记录,但传输一切正常?很可能是你的契约检查写错了。
看完这些标签,你的结论会变吗?反正我的结论通常会变。
把这张表放在脚本旁边。它能很快终结「哪个模型最好」的争论。
RUNS=1 和固定的 prompt 复现问题。finish_reason 和输出字节数。RUNS=12 重新运行,查看分类直方图。第五步和第六步总是被跳过,而真正节省时间的恰恰是这两步。
在放心把批处理任务交给任何免费 endpoint 之前,我会先运行这个探测脚本。
免费模型访问加上免费服务器选项,真正有用的地方就在这里:你可以先找个地方运行探测脚本,再决定是否配置资源。
利益披露:本文是 MonkeyCode 产品推广活动的一部分。
MonkeyCode 列出了免费模型访问和免费服务器选项。这两项说法均由运营方提供,我不会在此基础上作进一步承诺。
我不会猜测模型名称、配额、硬件或可用时长。用上面的脚本测量这些,再作决定。
把免费层用于探测和小任务。无论如何,分类表都要如实反映结果。
这个脚本假设你使用的是兼容 OpenAI 的 chat endpoint。你的 endpoint 可能并不兼容。
它不测试并发。如果你的风险在于并发,就加一个并行运行器。
它无法证明某个服务层会一直免费。应当定期重新运行。
它无法说明输出质量如何。那需要单独的 eval。
Idempotency-Key 请求头只有在服务器遵守它的情况下才有用。
如果你已经为每次尝试记录了分层标签,可以跳过。如果只是一次性的单次调用,也可以跳过。
如果你今天就需要吞吐量保证,也请跳过。探测脚本只能测量,不能作出保证。
下次 Agent 运行失败时,先问:哪一层失败了?而不是:该换哪个模型试试?
把失败层写进日志和错误信息里。每多记录一次,争论就会少一点。
如果还想采取进一步行动,可以考虑屏蔽此人和/或举报滥用行为。