Peri 每次会话的完整 trace——每条消息、每次工具调用、每个返回结果——都存在本地 SQLite 数据库里。我们写了一个 agent-defect-analyzer(缺陷分析器)定期扫这些历史数据,看 Agent 在真实任务里到底犯什么错。

第一次跑 Agent 工具(SubAgent 调度)的专项分析,1344 次调用里 45 次返回内容以「Error:」开头,错误率 3.35%。数字本身不算高,但这 45 次错误里九成是参数没传对,而且全部对运行时监控不可见——这才是真正的问题。
把 45 次错误逐条分类,分布高度集中。
| 错误类型 | 次数 | 占比 |
|---|---|---|
| subagent_type 缺失(前台模式) | 21 | 46.7% |
| subagent_type 缺失(后台模式) | 15 | 33.3% |
| 超过后台并发上限 | 5 | 11.1% |
| agent 定义不存在 | 3 | 6.7% |
| prompt 参数缺失 | 1 | 2.2% |
subagent_type(指定要调用哪个子 Agent 的参数)缺失占 80%。前后台两种模式加起来 36 次,是绝对主力。LLM 在调用 Agent 工具时反复遗漏这个必填参数,要么漏传,要么传了空值。
加上超过并发上限的 5 次(其实也是参数使用不当,后台模式没等已有任务结束就又发起),参数问题合计 91.1%。真正的系统异常——agent 定义文件不存在和 prompt 缺失——只有 4 次,占 8.9%。也就是说,如果工具描述把参数要求讲清楚,这 45 次错误里 41 次可以避免。问题不在系统健壮性,在参数传达。
数据来源是本地 SQLite 数据库,存的是每次会话的完整消息序列,包括 assistant 发起的每次工具调用和对应的返回结果。analyzer 扫描这些 tool_use 和 tool_result 配对,统计每个工具的调用次数和失败次数。
第一版 analyzer 直接按 is_error 字段筛选错误,结果 Agent 工具的错误数是 0。改用正则匹配返回内容里以「Error:」开头的条目,才兜住 45 次。一个分析器要靠字符串匹配而非结构化字段来判断错误,这个绕路本身就是信号——is_error 字段不可信,问题出在数据源本身,根因在工具的返回方式。
把 45 次错误的 is_error 字段(标记这次调用是否失败)拉出来看,没有一个为真。按 is_error 口径,Agent 工具的错误率是 0%,而不是 3.35%。3.35% 是靠内容匹配事后算出来的,运行时根本看不到这个数字。
这 45 次失败对运行时监控完全隐形。错误率统计、告警、失败重试逻辑全都基于 is_error 字段,字段为假就跳过。工具调用失败后 Agent 收到的是一条标记为成功的返回,照常往下走,不知道上一步出了问题。
原因在工具的返回类型。Agent 工具和所有工具的 invoke 方法返回一个 Result 类型,成功带返回值,失败带错误,框架拿到结果后按类型分流。
Ok(返回内容) → 标记为成功,is_error = 假
Err(错误信息) → 标记为失败,is_error = 真
Agent 工具在参数缺失时返回的不是 Err,而是 Ok("Error: ...")——把错误塞进字符串、用成功状态返回。框架拿到 Ok 就按成功路径处理,is_error 设成假。错误确实发生了,内容也对,但从类型系统看这是一次成功的调用。
框架在每次工具调用后检查 is_error,为真才发一条 tool.error 指标——这 45 次全部漏报。这是被假标记击穿的第一道防线。
第二道是连续失败纠正。框架追踪同一错误连续出现的次数,到 5 次就往对话里注入一条纠正消息,提示 Agent 换个方法。由于 is_error 恒假,计数器从不递增,哪怕同一个参数错误连犯 10 次也不会触发纠正。
第三道是前面说的离线分析器。三套系统共用一个字段,一个字段错了,三道防线同时失效,问题出在数据源层面。
这个 Ok 返回错误字符串的写法不只出现在 Agent 工具。扫了一遍工具实现,Edit(文件编辑)工具也是同一套写法,而且体量更大。
Edit 工具 6233 次调用,失败 284 次,失败率 4.6%。这 284 次的 is_error 字段全部为假,原因同样是返回 Ok("Error: ...") 而非 Err。错误类型集中在 old_string(要替换的原文)找不到和不唯一,前者 213 次,后者 70 次。其中 62% 的「找不到」发生在文件已被同会话的 Edit 成功修改之后,Agent 拿着过期的旧内容去匹配。错误性质和 Agent 工具不同,但返回方式是同一个病根。写工具的人习惯把所有情况都塞进返回字符串,包括错误,框架类型系统于是无法区分成功和失败。
修复分两步。第一步,把以 Ok 返回的错误全部改走 Err。Agent 工具涉及前台参数校验、后台参数校验、并发上限、agent 定义加载共 7 处,Edit 工具涉及 5 处匹配失败。改完之后 is_error 在这些场景下正确为真,三道防线全部恢复。
第二步,修工具描述。Agent 工具的 subagent_type 参数原来写的是「不提供时 fork 当前 Agent」,这反而暗示可以不传。改成明确标注 REQUIRED unless fork=true(除非用 fork 模式否则必填),并说明不传会直接报错。参数描述从「可选,不传也行」变成「必填,不传就失败」。
测试同步更新。原来 4 个测试断言调用结果字符串里包含 Error,用的是 .unwrap()(取成功值)。改成 .unwrap_err()(取错误值),验证错误现在确实通过 Err 返回。817 个测试通过,clippy 无警告。
如果凭感觉优化 Agent 工具,大概率会去加固 agent 定义查找、加重试逻辑,但这些只覆盖 8.9% 的错误。真实分布告诉我们 91% 是参数没传对,该修的是工具描述,没有数据优化方向就是猜。而一个 Ok 和 Err 的选择,让 metrics 上报、连续失败纠正、离线分析三道防线同时失效,说明工具返回错误时必须走 Err,让类型系统替你守住失败语义。那 45 个错误本来不该是隐形的。
项目地址:github.com/konghayao/p…