# Peri Agent 3.35% 工具错误率复盘

> **[Peri Code](https://github.com/konghayao/peri)** — 用 Rust 写的开源 Coding Agent，兼容 Claude Code 生态。<https://github.com/KonghaYao/peri>

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

第一次跑 Agent 工具（SubAgent 调度）的专项分析，1344 次调用里 45 次返回内容以「Error:」开头，错误率 3.35%。数字本身不算高，但这 45 次错误里九成是参数没传对，而且全部对运行时监控不可见——这才是真正的问题。

## 45 次错误中 91% 是参数使用不当

把 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 次可以避免。问题不在系统健壮性，在参数传达。

## agent-defect-analyzer 靠内容前缀匹配发现错误

数据来源是本地 SQLite 数据库，存的是每次会话的完整消息序列，包括 assistant 发起的每次工具调用和对应的返回结果。analyzer 扫描这些 tool_use 和 tool_result 配对，统计每个工具的调用次数和失败次数。

第一版 analyzer 直接按 is_error 字段筛选错误，结果 Agent 工具的错误数是 0。改用正则匹配返回内容里以「Error:」开头的条目，才兜住 45 次。一个分析器要靠字符串匹配而非结构化字段来判断错误，这个绕路本身就是信号——is_error 字段不可信，问题出在数据源本身，根因在工具的返回方式。

## 45 次错误未触发任何失败标记

把 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 次也不会触发纠正。

第三道是前面说的离线分析器。三套系统共用一个字段，一个字段错了，三道防线同时失效，问题出在数据源层面。

## Edit 工具 284 次失败未触发任何失败标记

这个 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/peri](https://github.com/konghayao/peri)
