双跳工具链:为什么 95% 的工具组合会变成 80% 的流水线
你的可观测性技术栈中的单工具仪表盘讲了一个令人宽慰的谎言。search_listings 的成功率是 96%,显示为绿色。book_appointment 是 95%,也是绿色。而连续调用这两个工具的智能体(agent)三周以来的成功率一直只有 78%,却没人能解释原因。原因不在任何一个工具内部,而是在它们之间的缝隙里——那个没有任何仪表盘面板覆盖的地方。
组合不是加法。当工具 A 的输出流入工具 B 的输入时,故障面并不是 B 对“有效调用”的狭隘定义下的 1 - (0.96 × 0.95)。它是 A 在 B 的标准下所有微妙偏差方式的完整笛卡尔积:A 返回的日期格式是 MM/DD/YYYY,而 B 期望的是 ISO 8601;返回的价格单位是分,而 B 解析的是元;分页游标指向了最后一个结果之后的一项;或者上游服务昨天重命名了一个实体 ID。这些情况都能顺利通过 A 自身的契约测试(contract tests),但每一个都会导致 B 崩溃。团队的单工具可靠性指标永远看不到这一点,因为按各自的标准来看,每个工具都运行良好。
在智能体的深度调用中,这种复利效应非常严重。行业对多步流水线的分析发现,一个每步可靠性为 95% 的 20 步链路,其端到端的成功率仅约为 36%。即使每步可靠性达到 99%,10 跳之后成功率也会降至 90%。而且这些数字是基于标准的独立故障模型计算的——这还是乐观的情况。现实中的工具链故障是相关的:同样的上游 API 故障每次都会级联导致相同的下游空值处理(null-handling)错误。真实的曲线比乘积模型更糟糕。
单工具指标无法察觉的故障分类
当你专门审视组合故障(而非单个工具故障)时,它们会聚集为一些在任何工具的独立单元测试中都不会出现的形态:
损耗性契约转换 (Lossy contract translation)。工具 A 返回的对象比工具 B 接受的对象更丰富。胶水代码(或者作为胶水代码使用的 LLM)丢弃了某些字段。有时,丢弃的字段在没人列举出的边缘案例中对 B 的行为起着关键支撑作用。经典案例是 A 返回了一个权限/范围令牌,而 B 默认不透传,导致 B 在下游以较低权限运行,无法访问 A 能看到但 B 看不到的记录。
隐式假设不匹配 (Implicit assumption mismatch)。A 返回“客户最近的地址”,而 B 期望“客户的账单地址”。大多数时候它们是同一个记录,所以预发布环境的测试显示绿色。但对于那 4% 地址不同的客户,这就变成了一个长尾故障模式,看起来像是随机发生的,因为它与两个工具的开发者都没想过要记录的客户属性相关。
单位和编码漂移 (Unit and encoding drift)。货币单位是分还是元。时间戳是 UTC 还是本地时间。UUID 是字符串还是二进制。ID 在 A 中是 64 位整数,在 B 中是字符串(以及 JSON 在传输过程中丢失精度的各种方式)。这些 bug 之所以能上线,是因为每个查看追踪(trace)的开发人员在阅读数值时都会在脑海中自动进行归一化处理。
分页和游标语义 (Pagination and cursor semantics)。A 返回了 312 个项中的前 50 个以及一个 next_cursor。B 拿到列表后将其视为完整列表进行处理。智能体基于片面的信息进行推理,给出了一个自信但错误的答案。追踪显示对 A 的调用和对 B 的调用都很干净,两者都没有报错。分页导致的 off-by-one 错误、“这个游标是耗尽了还是仅仅当前页为空”的歧义,以及“如果在获取页面的间隙底层数据发生了变化会怎样”,这些都是组合特有的问题;你无法通过单独测试 A 或 B 来发现它们。
错误信息被下游工具吞掉 (Error-message-swallowed-by-downstream-tool)。A 返回了一个结构化的错误对象,在非标准字段中包含解释性消息。胶水代码在将其交给 B 之前将其映射为通用的 500 错误。B 的重试逻辑启动,重试成功是因为底层状态已经改变,而原始错误从追踪和日志中都丢失了。团队看到的是一个“间歇性”故障,而实际上它是一个确定性的契约转换 bug。
跨缝隙的陈旧标识符 (Stale identifier across the seam)。A 返回一个实体 ID。智能体进行了一些推理,问了一个澄清性问题,几轮对话后最终将 ID 传给 B。在此期间,上游服务轮换了 ID 格式(或者实体被归档了)。B 报错“未找到”。这既是功能上的组合,也是时间上的组合——这是一个被低估的生产故障来源,因为复现它需要回放一个过时的世界快照。
为什么单工具仪表盘会掩盖这些问题
看看使用工具的智能体的可观测性通常是如何检测的:每个工具调用一个 span,包含输入参数、输出、延迟和成功/失败。每个工具都有一个面板。面板显示该调用自身的成功定义——通常是“工具是否返回了等同于 2xx 的结果且未抛出异常”。
对于组合问题来说,这恰恰是错误的度量单位。工具成功返回了,但组合仍然失败了。正确的单位是端到端任务,这是“B 从 A 获取了错误输入”作为故障暴露出来的唯一层级。大多数团队不衡量这一点,或者仅在一个无法反映组合边缘案例的长尾输入的薄弱离线评估集上衡量。
第二个检测差距:单工具面板不会向你展示 B 接收到的输入的分布。所以当 A 开始返回更多之前罕见类型的记录时(例如因为上游目录增长了),B 的失败率上升,但 B 的面板仍然显示“工具实现无变化,无事故”。变化不在 B 内部,而是在 A 的输出和 B 的输入的联合分布中,单工具视角捕捉不到任何这类信息。
契约测试应针对衔接处,而非工具本身
修复方法在概念上很简单,但在操作上却很难 :你需要契约测试来将两个工具之间的衔接处作为一个单元进行练习,并将 A 的完整输出范围作为显式示例输入到 B 中。其模式如下:
- https://redis.io/blog/why-multi-agent-llm-systems-fail/
- https://www.mindstudio.ai/blog/reliability-compounding-problem-ai-agent-stacks
- https://arxiv.org/pdf/2503.13657
- https://stackoverflow.blog/2025/06/30/reliability-for-unreliable-llms/
- https://orq.ai/blog/why-do-multi-agent-llm-systems-fail
- https://galileo.ai/blog/why-multi-agent-systems-fail
- https://docs.bswen.com/blog/2026-03-20-llm-tool-call-contracts/
- https://circleci.com/blog/building-llm-agents-to-validate-tool-use-and-structured-api/
- https://agenta.ai/blog/the-guide-to-structured-outputs-and-function-calling-with-llms
