文章从工具端以HTTP 200返回限流错误、导致Agent静默失败的事故出发,设计本地故障注入框架验证异常响应处理。测试可在免费环境持续运行,避免消耗生产配额并污染线上遥测。
两周前,我们的 coding-agent worker 开始返回 HTTP 200,但 diff 是空的。没有错误日志。队列深度正常。延迟正常。SLO 仪表盘一片绿色,但每一项任务都在悄无声息地生成空结果。
根本原因平平无奇:一个 MCP 工具端点开始返回 {"error": "rate_limited"},HTTP 状态码却仍是 200,而我们的 Agent 循环把所有 HTTP 200 都当成工具调用成功。随后,模型在缺失输出的情况下产生了幻觉,而不是让任务失败。此前从来没有人测试过:当工具欺骗 Agent 时,它会怎么做。
这篇文章介绍的是我本应在事故发生前进行的演练:为 Agent 工具边界故障搭建一套本地故障注入框架。它可以运行在免费基础设施上,因此持续放在 CI 中也不会产生成本。
针对付费生产模型端点进行故障演练有三个问题:消耗配额、污染生产遥测数据,而且账单一来,人们就会悄悄禁用这些演练。一个你负担不起、无法持续运行的演练,等同于没有演练。
披露声明:本文是 MonkeyCode 产品推广活动的一部分。
MonkeyCode 目前提供免费的模型访问和免费服务器选项,足以承载这类工作负载,因为演练的成本主要取决于故障注入期间的工具调用次数,而不是模型质量。你测试的是自己的编排代码——超时、重试、验证和熔断——而不是模型的推理能力。实际上,较小的免费模型在这里或许更合适:如果你的测试框架能够应对较弱模型生成的、更混乱的工具调用格式,它也就能应对更强的模型。如果你正在评估适合这种生产前准入检查的免费方案,可以从 monkeycode.dev 的免费套餐入手;下面的测试框架与 provider 无关,因此也可以把端点替换成你已经在使用的任何服务。
┌────────────┐ tool calls ┌──────────────────┐
│ Agent loop │ ─────────────▶ │ Fault-injecting │ ──▶ real tool (echo/fs mock)
│ (harness) │ ◀───────────── │ tool proxy │
└─────┬──────┘ └──────────────────┘
│ model API
▼
┌──────────────────┐
│ Free model │
│ endpoint │
└──────────────────┘
这个 proxy 位于 Agent 与工具之间,负责注入预先声明的故障。整个过程完全不会触碰生产环境。
1 个 Agent 循环,3 个工具:read_file、write_file、run_tests
每种故障模式运行 40 项任务,每项任务都是一次小规模的编辑并测试任务
模型:免费套餐提供的任意模型;temperature 设为 0
超时预算:每次工具调用 30 秒,每项任务 5 分钟
成功标准:任何未获得已验证工具输出的任务,都不得报告成功
# fault_proxy.py — sits between agent loop and tool implementations
import random, time, json
FAULTS = {
"none": lambda r: r,
"http200_error": lambda r: {"error": "rate_limited"}, # the incident
"empty_body": lambda r: {},
"slow": lambda r: (time.sleep(45), r)[1], # exceeds 30s timeout
"truncated_json": lambda r: json.dumps(r)[:12], # unparseable
"wrong_schema": lambda r: {"result": None, "extra": "x"},
}
class ToolProxy:
def __init__(self, tools, fault="none", seed=0):
self.tools, self.fault = tools, fault
random.seed(seed)
def call(self, name, args):
raw = self.tools[name](**args)
return FAULTS[self.fault](raw)
这场演练并不是简单地“让 Agent 使用故障工具运行”。演练的关键在于断言:你的编排层能够检测出每一种故障模式。
# drill.py — expected behavior under each fault
EXPECT = {
"http200_error": "job_failed_tool_error", # must NOT be treated as success
"empty_body": "job_failed_validation",
"slow": "job_failed_timeout",
"truncated_json": "job_failed_parse",
"wrong_schema": "job_failed_validation",
}
def run_drill(agent, fault, jobs=40):
outcomes = {"verified_success": 0, "silent_success": 0, "correct_failure": 0}
for _ in range(jobs):
result = agent.run_job(fault=fault)
if result.ok and result.tool_outputs_verified:
outcomes["verified_success"] += 1
elif result.ok: # 200 + garbage = the incident class
outcomes["silent_success"] += 1
elif result.failure_class == EXPECT.get(fault):
outcomes["correct_failure"] += 1
return outcomes
准入条件只有一行:在每一种故障模式下,silent_success 都必须为 0。其他一切都只是调优细节。
def call_tool_verified(proxy, name, args, timeout=30):
try:
resp = proxy.call(name, args) # raises on timeout
except TimeoutError:
return ToolResult.fail("timeout")
if isinstance(resp, str):
try:
resp = json.loads(resp)
except json.JSONDecodeError:
return ToolResult.fail("parse")
if "error" in resp:
return ToolResult.fail("tool_error", detail=resp["error"])
if not SCHEMAS[name].is_valid(resp):
return ToolResult.fail("validation")
return ToolResult.ok(resp)
四项检查:超时、解析、错误字段和 schema。整个修复就这么简单。进行这场演练,是为了证明这个 shim 始终正确接入系统,没有被绕过。
记录以下字段,确保 CI 中发生回归时能够调试:
job_id, fault_mode, tool_name, latency_ms, http_status,
parse_ok, schema_ok, error_field_present, outcome_class, verified
当 http_status=200 时出现 outcome_class=silent_success,正是我们最初那次事故的精确特征。
fault=http200_error verified=0 silent=0 correct_failure=40 PASS
fault=empty_body verified=0 silent=0 correct_failure=40 PASS
fault=slow verified=0 silent=0 correct_failure=40 PASS
fault=truncated_json verified=0 silent=0 correct_failure=40 PASS
fault=wrong_schema verified=0 silent=0 correct_failure=40 PASS
fault=none verified=40 silent=0 correct_failure=0 PASS
换用其他免费模型后,你得到的数字会有所不同——尤其是在 truncated_json 模式下,较弱的模型有时会以颇具创造性的方式重试。这种差异没有问题;唯一不可妥协的指标是 silent=0。
这场演练针对 mock 和免费端点运行;回滚方式就是删除 CI job,除此之外,生产环境中不存在任何相关改动。
把验证 shim 和演练代码放在同一个 repo 中,并让它们遵循同一套 PR 流程。没有配套演练的 shim 迟早会腐化失效。
如果你把它接入共享的免费服务器,请把并发上限设为 1~2。免费套餐使用的是共享容量,而故障注入循环恰恰是最容易触发 rate limit 的那类工作负载。
免费模型不能替代生产模型的实际行为。这场演练验证的是编排机制,而不是输出质量。质量评估需要使用真实模型和真实评估数据。
免费套餐会发生变化。固定测试框架的行为,确保 provider 侧的变更会让 CI 明确失败,而不是悄悄跳过演练。
如果 Agent 的工具会对共享系统产生副作用,例如部署或写入工单,请使用 mock——对真正具有副作用的工具进行故障注入,正是演练变成事故的方式。
不要把它当作唯一的安全保障。针对队列时长和截止期限余量实施准入控制——我此前写过相关内容——能够阻止系统过载;这场演练则用于阻止静默产生错误结果。两者缺一不可。
我们的事故并不是模型故障,而是缺少一个四行代码的验证 shim。之所以从未有人测试它,是因为大家认为测试它“要花钱”。把演练迁移到免费基础设施上之后,这个借口也就不存在了。对于任何在生产环境中运行 Agent 的团队,我都会问这样一个问题:当工具返回 HTTP 200,但响应体中包含错误时,你的 pipeline 能否做到静默成功次数为零——而且你能否在本周通过 CI 证明这一点?
对于后续操作,你可以考虑屏蔽此人和/或举报滥用行为。