Skip to content
Published on

AI 智能体在生产环境中是如何失败的 — 14 种失败模式,以及重试为何不安全

分享
Authors

引言 — 生产环境智能体的三个真问题

在演示里跑得好好的智能体,一旦放上生产环境,问题就变了。从「这能行吗?」变成「它为什么没成、烧了多少钱、能不能重试?」。本文按顺序回答这三个问题。

先说结论。失败大多不是出在模型,而是出在系统设计。 UC 伯克利的研究者对多智能体系统的 1642 条执行轨迹做了分类,结果是:44.2% 的失败属于系统设计问题,32.3% 是智能体之间的错位,23.5% 是任务验证失败。而最常见的两种失败模式是步骤重复(15.7%)和未意识到终止条件(12.4%) — 加起来占全部的 28.1%。这两者不只是「失败」。它们意味着智能体在重复同一件事、并且不知道何时停下,这既是一张 token 账单,也意味着副作用会发生两次

第三个问题由此而来。当非确定性的调用方(LLM)在驱动真实副作用(支付、发邮件、建工单)时,重试安全吗?答案是「除非你亲手把它做成安全的,否则不安全」。在智能体调用工具的标准协议 MCP 里,有声明幂等性的提示,却没有保证幂等性的机制。而这个问题上称得上行业标准的 IETF Idempotency-Key 头部草案,2020 年启动至今仍不是 RFC — 目前处于 2026 年 4 月已过期的状态。

智能体究竟在哪里失败 — MAST 的 14 种模式

最值得引用的资料是 MAST(Multi-Agent System Failure Taxonomy)。这是 UC 伯克利与 Intesa Sanpaolo 的研究者(Cemri、Pan、Yang 等,arXiv:2503.13657)发表在 NeurIPS 2025 Datasets & Benchmarks 轨道的论文,是对多智能体系统失败做经验性分类的首次尝试。

先看方法论。研究者用扎根理论(Grounded Theory)分析了来自 5 个开源框架的 150 余条轨迹(平均每条超过 1 万 5 千行),据此建立分类体系,共有 6 名专业标注员参与。先由 3 名标注员对 15 条轨迹独立打标,确保标注者间一致性达到 kappa 0.88,然后以该定义为基准,搭建基于 OpenAI o1 的 LLM-as-a-judge 流水线,为全部 1642 条打标。

分类结果如下。括号里是各模式相对于全部 1642 条的出现比例。

[范畴 1] 系统设计问题 (System Design Issues)              44.2%
  1.1 违反任务规范        (Disobey Task Specification)      11.8%
  1.2 违反角色规范        (Disobey Role Specification)       1.50%
  1.3 步骤重复            (Step Repetition)                 15.7%   <- 最多
  1.4 丢失对话历史        (Loss of Conversation History)     2.80%
  1.5 未意识到终止条件    (Unaware of Termination Conditions) 12.4%

[范畴 2] 智能体间错位 (Inter-Agent Misalignment)           32.3%
  2.1 对话重置            (Conversation Reset)               2.20%
  2.2 未能请求澄清        (Fail to Ask for Clarification)    6.80%
  2.3 任务脱轨            (Task Derailment)                  7.40%
  2.4 隐瞒信息            (Information Withholding)          0.80%
  2.5 忽视其他智能体的输入 (Ignored Other Agent's Input)      1.90%
  2.6 推理-行动不一致     (Reasoning-Action Mismatch)        13.2%

[范畴 3] 任务验证 (Task Verification)                      23.5%
  3.1 过早终止            (Premature Termination)            6.20%
  3.2 无验证/验证不完整   (No or Incomplete Verification)    8.20%
  3.3 错误验证            (Incorrect Verification)           9.10%

不妨核对一下数字是否对得上:11.8 + 1.5 + 15.7 + 2.8 + 12.4 = 44.2,2.2 + 6.8 + 7.4 + 0.8 + 1.9 + 13.2 = 32.3,6.2 + 8.2 + 9.1 = 23.5。三个范畴相加恰好是 100%。

这里要提个醒。总结这篇论文的二手资料里,流传着「42% 是规范问题、37% 是协调崩溃、21% 是验证不足」这样的说法,但这三个数字都不在论文里。范畴名称也对不上 — 论文的第一个范畴不是「规范问题」,而是「系统设计问题」(看起来是早期版本改过名字留下的痕迹)。要引用的话,建议直接看论文的 Figure 1。

基线失败率也值得记住。论文评估的 7 个 SOTA 开源系统,失败率在 41% 到 86.7% 之间。最差的是 AppWorld 的 Test-C(失败率 86.7%),最好的是 AG2 的 OlympiadBench(失败率 41%)。这就是你要放上生产环境的东西的基准线。

每个失败范畴向你收取什么

先诚实地交代一件事:没有任何公开数字写明「这种失败值多少钱」。MAST 数的是出现频率,没有测成本;我找到的任何一手资料,也都没有公布按失败模式划分的金额。所以与其在这里编造数字,不如区分每个范畴向你收取的成本类型。这个区分在实务中比金额更有用 — 类型不同,应对就不同。

范畴 1(系统设计,44.2%)以 token 的形式向你收费。 步骤重复(15.7%)和未意识到终止条件(12.4%)占了这个范畴的一半以上,两者的症状都是「智能体一直在转」。循环每转一圈,整个上下文都会重新进入模型,所以成本不是线性增长,而是比线性更陡。这个范畴的好处是抓起来最便宜 — 轮数上限、预算上限、重复检测,都可以在不碰模型的前提下加上。

范畴 2(智能体间错位,32.3%)以错误的副作用向你收费。 这个范畴里最多的模式是推理-行动不一致(13.2%),按论文的定义,指智能体的推理与实际行动不相符的情况。这不是 token 问题。智能体嘴上说「我要做 A」,实际执行了 B,账单就留在 B 碰过的那个系统里。任务脱轨(7.40%)也是同一性质。

范畴 3(任务验证,23.5%)以悄悄出错的产物向你收费。 无验证/验证不完整(8.20%)加上错误验证(9.10%)合计 17.3%,两者的共同点是失败看起来不像失败。论文举的例子令人印象深刻 — ChatDev 生成的国际象棋程序,通过了流于表面的测试。这个范畴发现得最晚,所以通常也最贵。

整理起来是这样:

范畴 1 (44.2%)  ->  token 账单        ->  抓起来最便宜 (上限·检测)
范畴 2 (32.3%)  ->  错误的副作用      ->  用幂等性·权限来防御
范畴 3 (23.5%)  ->  悄悄出错的结果    ->  发现得最晚,代价最高

为什么重试不安全

现在进入核心。把范畴 1 的两个最多模式(重复·不终止)和范畴 2 的最多模式(推理-行动不一致)叠在一起,会得出一句话:非确定性的调用方,在反复调用会产生真实副作用的工具。

在传统分布式系统里,这不是什么陌生问题。网络一断,客户端就不知道请求有没有被处理,一重试就产生重复。所以我们用幂等性键。但智能体还多出一个问题 — 传统客户端由代码决定要不要重试,而智能体由模型决定。 模型一旦判断工具失败了(或者误判它失败了),就会直接再调一次。重试策略等于写在提示词里,而那不是策略,是概率。

这里有一个非常重要的区分。重试的危险不只在工具失败时。更危险的是工具成功了、响应却丢了的时候。此时模型看到的是「没有结果」,模型的理性行为就是重试。而副作用已经发生了。

MCP 能保证幂等性吗 — 有提示,没有机制

那么,作为智能体调用工具的标准协议,MCP 是怎么处理这个问题的?直接打开当前最新规范修订版(2025-11-25)的 schema,答案很明确。

MCP 里有工具注解(tool annotations),其中之一就是幂等性提示。照搬 schema 的定义:为真时表示「用相同参数重复调用该工具,不会对环境产生额外影响」。四个提示的默认值如下。

readOnlyHint      默认值 false   (假定工具会修改环境)
destructiveHint   默认值 true    (假定修改是破坏性的)
idempotentHint    默认值 false   (假定重复调用会产生额外影响)
openWorldHint     默认值 true    (假定与外部世界交互)

值得注意的是,默认值全都取在悲观的一侧。意思是:没有注解时,所有工具都应当被当作破坏性的、非幂等的写操作来对待。这是好的设计。

但规范对这些提示钉下了这样的话(以下是 schema 注释原文的译文)。

ToolAnnotations 的所有属性都是提示。不保证它们忠实地描述了工具的行为(包括 title 这类描述性属性)。客户端绝不应根据来自不可信服务器的 ToolAnnotations 来决定工具的使用。

真正的问题在这里显形。在整份 2573 行的 schema 里,idempotentHint 只出现在它被声明的那一处。而「retry」这个词在 schema 里一次也没有出现。没有幂等性键,没有请求去重机制,也没有重试语义。

也就是说,关于幂等性,MCP 给了词汇,却没给工具。服务器可以声称「我是幂等的」,但这个声称 (a) 不被强制执行,(b) 规范直接警告说来自不可信服务器的就别信,(c) 而对于幂等的工具,协议里根本没有任何办法把它变得可以安全重试。

你可能会问:传输层不是有重传吗?Streamable HTTP 规范里确实有可恢复性(resumability)和重传(redelivery) — 服务器给 SSE 事件附上 id,客户端断连后带着 Last-Event-ID 头部发 GET,服务器就可以重放该点之后的消息。但这重放的是从服务器发往客户端的消息,并且仅限于断掉的那条流(规范明确说不得重放其他流的消息),它只是流内的游标。它不是为客户端发出的工具调用做去重的装置。 关于 MCP 规范本身的走向,我会在MCP 正在拆掉会话 — 解读无状态内核一文里另行展开。

那行业标准走到哪儿了 — 写了 5 年半的草案

「直接用幂等性键不就行了」是很自然的反应。没错。但令人惊讶的是,幂等性键并没有标准

在 Datatracker 上直接查 IETF HTTPAPI 工作组的 Idempotency-Key HTTP 头部草案的历史,是这样的。

draft-idempotency-header-00                  2020-11-17  (个人草案)
draft-ietf-httpapi-idempotency-key-header-00 2021-07-01  (WG 采纳)
                                        -01  2022-05-08
                                        -02  2022-11-09
                                        -03  2023-07-04
                                        -04  2023-11-16
                                        -05  2024-05-27
                                        -06  2025-02-24
                                        -07  2025-10-15  (最新)

当前状态: Expired (2026-04-18 过期)
IESG 状态: "I-D Exists" — IESG 从未开始处理

自 2020 年 11 月启动以来修订了 8 次,最新修订版于 2025 年 10 月提交,按互联网草案 6 个月生命期的规则,于 2026 年 4 月 18 日过期。IESG 状态是「I-D Exists」 — 按 Datatracker 的说明,意思是「IESG 尚未开始处理这份草案,或已中止处理而未出版」。5 年半过去,它没能成为 RFC。

所以在现实中,「幂等性键」不是标准,而是惯例。把这套惯例文档化得最好的是 Stripe,实际上,上面那份 IETF 草案想要成文化的,也大体就是这套惯例。从 Stripe 文档能确认的行为如下。

  • 对给定的幂等性键,保存第一次请求的状态码和响应体,无论成功还是失败都保存。
  • 用同一个键发来的后续请求,返回同样的结果 — 连 500 错误也原样重放
  • 幂等性层会把进来的参数与原请求比对,不一致就报错。
  • 最少过 24 小时后就可能被清理(prune),清理之后再复用同一个键,会创建一条新请求
  • 结果只有在端点开始执行之后才会保存。参数校验失败和并发执行冲突不会被保存,因此可以重试。

从智能体的视角看,其中两条尤其扎心。一条是连 500 也重放 — 幂等重试不是「再试一次」,而是「把原来的结果再给你看一遍」。模型看到失败后重试,就会再看到一次失败。这不是 bug 而是设计,而模型若理解不了这一点,就会陷入无限循环(而那恰恰就是失败模式 1.3 步骤重复)。另一条是 24 小时。跑上好几天的长时间运行智能体,若拿昨天的键来重试,那不是重试,是新的副作用。

怎么衡量智能体的成本 — OTel 里没有成本指标

接下来是可观测性。想跟着标准走的话,你会去看 OpenTelemetry 的 GenAI 语义约定(semantic conventions),但最好先准确了解它的现状再开始。

第一,GenAI 约定搬家了。它原本在 open-telemetry/semantic-conventions 仓库的 docs/gen-ai 路径下,但现在那个路径下的文件只剩一句提示:「本页面已迁移,不再在本仓库维护」。新家是 open-telemetry/semantic-conventions-genai

第二,这个新仓库创建于 2026 年 5 月 5 日,至今一个发布版本都没有。(主语义约定仓库的最新发布是 v1.43.0,2026 年 7 月 3 日。)而且智能体 span 文档和 MCP 文档的文档状态都是「Development」。开发并没有停 — 最后一次推送就在两天前。只是这意味着:你称之为「标准」并据此搭建仪表盘的那个东西,是一份从未发布过的、开发中的规范

第三,也是实务中最重要的一点 — 没有成本指标。在 GenAI 语义约定的指标文档和属性注册表全文里搜索 costpriceusddollar,结果是 0 条。已定义的指标就是下面这些。

gen_ai.client.token.usage
gen_ai.client.operation.duration
gen_ai.client.operation.time_to_first_chunk
gen_ai.client.operation.time_per_output_chunk
gen_ai.server.request.duration
gen_ai.server.time_to_first_token
gen_ai.server.time_per_output_token

标准给你 token,钱得你自己算。要把 token 换算成金额,需要一张按模型划分的单价表,而那张表由厂商说改就改。各家厂商的成本仪表盘各有各的规格、数字彼此对不上,原因就在这里。

直接把 token 相加为什么会错

既然决定自己算成本,就有一个必须知道的陷阱。属性注册表里定义的 token 属性有五个。

gen_ai.usage.input_tokens
gen_ai.usage.output_tokens
gen_ai.usage.cache_creation.input_tokens
gen_ai.usage.cache_read.input_tokens
gen_ai.usage.reasoning.output_tokens

问题在于,它们不是平铺的,而是嵌套的。注册表的注释写得明白 — 缓存创建 token 和缓存读取 token 应当(SHOULD)包含在 gen_ai.usage.input_tokens 里,而输入 token 应当包含包括缓存 token 在内的所有种类的输入 token。推理 token 同理,应当包含在 gen_ai.usage.output_tokens 里。

所以下面这种算法是错的。

错误的算法:
  成本 = input_tokens x 输入单价 + output_tokens x 输出单价

为什么错:
  input_tokens 里已经包含了 cache_read.input_tokens
  缓存读取的计费比新输入便宜 (这正是用缓存的理由)
  -> 缓存命中得越好,你算出来的就比实际越贵

正确的方向:
  新输入 = input_tokens - cache_read - cache_creation
  成本 = 新输入 x 输入单价
       + cache_read x 缓存读取单价
       + cache_creation x 缓存写入单价
       + output_tokens x 输出单价
  (单价因厂商·模型而异,这里不写具体数字)

在智能体这里,这个误差不小。智能体循环每一轮都会重发同样的系统提示词和工具定义,缓存命中率在结构上就偏高,因此 input_tokens 里缓存读取的占比很大。天真地相加,缓存做得越好,账面成本就被吹得越大。你本想在仪表盘上确认缓存的成本效果,却可能得出完全相反的结论。

还有一层。上面五个属性是 span 属性,而指标 gen_ai.client.token.usage 的维度 gen_ai.token.type 的 well-known 值只有 inputoutput 两个。也就是说,只看聚合后的指标,是分不清缓存 token 和新 token 的。要算出准确的成本,就得下到 span 层级。(自定义值不是不能用,但用的那一刻就不再是标准了。)

该埋点什么 — 以及 Opt-In 陷阱

工具调用的埋点用 execute_tool span 来做。span 名称的形式是在 execute_tool 后面接上工具名,属性如下。

gen_ai.operation.name        Required            (Development)
gen_ai.tool.name             Required            (Development)
error.type                   Recommended         (Stable)
gen_ai.agent.name            Conditionally Req.  (Development)
gen_ai.tool.call.id          Recommended         (Development)
gen_ai.tool.description      Recommended         (Development)
gen_ai.tool.type             Recommended         (Development)
gen_ai.tool.call.arguments   Opt-In              (Development)   <- 默认不采集
gen_ai.tool.call.result      Opt-In              (Development)   <- 默认不采集

最后两行是陷阱。智能体用什么参数调用了工具、拿回了什么,是「Opt-In」 — 也就是默认不采集。这有其正当理由(隐私和存储成本)。但结果就是,调试「智能体为什么干了蠢事」恰恰需要的那两个字段,默认是关着的。要追踪范畴 2(推理-行动不一致)就得打开它们,而打开的那一刻,敏感信息就可能进入 trace。这不是免费午餐 — 是一个必须连同采样·脱敏一起有意识决定的权衡。

用 MCP 的话还得知道一件事。MCP 语义约定文档指出,HTTP 的 trace 上下文传播只覆盖 HTTP 请求,覆盖不到请求/响应流里往来的单条消息。因此埋点要把上下文注入 MCP 请求的 params._meta 口袋里(SEP-414 定义了 traceparenttracestatebaggage 这几个键的处理),接收方再把它用作远程父级。不做这一步,智能体的 trace 就会在 MCP 边界处断掉。日志·追踪的一般性讨论,请参考LLM 日志与追踪一文。

修了真的会变好吗 — 诚实的干预结果

这是全文最需要诚实的部分。MAST 论文没有止步于分类,而是以分类为依据实际做了干预。结果(论文 Table 5)如下。

AG2 / GSM-Plus (GPT-4)      基线 84.75 +- 1.94 | 改进提示词 89.75 +- 1.44 | 变更拓扑 85.50 +- 1.18
AG2 / GSM-Plus (GPT-4o)     基线 84.25 +- 1.86 | 改进提示词 89.00 +- 1.38 | 变更拓扑 88.83 +- 1.51
ChatDev / ProgramDev-v0     基线 25.0          | 改进提示词 34.4          | 变更拓扑 40.6
ChatDev / HumanEval         基线 89.6          | 改进提示词 90.3          | 变更拓扑 91.5

被广泛引用的「+9.4%」和「+15.6%」就出自这里。在 ChatDev 的 ProgramDev-v0 上,25.0 → 34.4 是 +9.4,25.0 → 40.6 是 +15.6。这不是百分比,是百分点

但如果你要引用这些数字,有几件事必须一并说清。

第一,ProgramDev-v0 共 32 个任务。 32 个的 25.0% 是 8 个,34.4% 是 11 个,40.6% 是 13 个(计算严丝合缝)。也就是说,广为流传的「+9.4%p」意味着多通过了 3 个任务,「+15.6%p」意味着多通过了 5 个任务。样本很小。

第二,即便在最好的干预之后,成功率也只是 40.6%。 换句话说,仍有约 59% 在失败。照搬论文的表述:「早期阶段的干预虽然带来了性能提升,但并未消除所有失败模式,任务完成率依然偏低,这表明还需要更实质性的改进」。

第三,效果并不普适。 在 AG2 上,变更拓扑在 GPT-4 下的 Wilcoxon 检验 p 值为 0.4,在统计上不显著;同样的变更在 GPT-4o 下 p 值 0.03,是显著的。论文的结论是:「这些策略并不普适,其效果取决于底层 LLM 等因素」。

第四,方向性是有的。 论文报告,在两个系统上,基于拓扑的变更都比基于提示词的变更更有效。而且作者们钉下结论:要获得稳固的可靠性,需要的「不止是孤立的修补」,而应当走向「根本性的 MAS 重新设计」。

最后是这份资料本身的局限。1642 条中的绝大部分,是由作者们搭建的基于 o1 的 LLM 标注器打的标。经人工验证的只是小得多的子集(kappa 0.88 是 3 名标注员对 15 条轨迹的一致性)。作者们自己也写道:「不主张 MAST 覆盖了所有潜在的失败模式」。这不是一张决定性的地图,而是迄今为止最具经验性的一张。

那么,该做什么 — 决策规则

下面这些规则,是从上述资料里直接推出来的。

1. 先按副作用给工具分类。 MCP 的默认值已经把正确答案告诉你了 — 没有注解就按破坏性·非幂等对待。不要把只读工具和写入工具混在一起,还只用一套重试策略。

2. 幂等性要在你自己的层里实现。 MCP 的 idempotentHint 是提示不是保证,规范也直说了别信不可信服务器的注解。IETF 标准也不存在。所以对产生副作用的工具,要在你的服务器一侧接收键、保存首次结果并重放。Stripe 的惯例是经过验证的参考实现 — 从保存结果(包括错误)、比对参数指纹,到写明保留期限。

3. 别让模型来生成键。 决定是否重试的主体是非确定性的,所以幂等性键必须从确定性的地方派生 — 编排器的步骤 ID 或任务 ID。模型每次都编一个新键的话,幂等性键就成了摆设。

4. 把上限当预算挂上去。 既然 28.1% 的失败是重复和不终止,轮数上限和 token 预算就不是优化,而是断路器。这是用最低的成本拦住最常见失败范畴的办法。

5. 成本要在 span 层级计算。 只靠指标分不出缓存 token。也不要直接把 token 相加 — 缓存读取已经在 input_tokens 里面了。

6. 有意识地决定要不要采集参数和结果。 默认是关的。不开就没法调试范畴 2,开了敏感信息就会进入 trace。请把采样和脱敏一起设计进去。

什么时候不该做这一切

诚实的另一面。上面这些,大多只有在智能体驱动真实副作用时才值回票价。

  • 如果是只读智能体,幂等性基础设施就是过度设计。 对只做检索·摘要·分析的智能体,重复调用只是多花钱,不会弄坏状态。挂个上限就可以翻篇了。
  • 如果所有写操作都由人来审批,重试风险大部分就消失了。 审批门比幂等性层实现起来便宜,在早期阶段通常也更好。
  • 如果工作流就够用,就别上智能体。 MAST 的失败大多是系统设计问题,而这种问题「把多少决定交给了模型,就长出多少」。路径既然是定死的,就用代码写死 — 构建高效的 AI 智能体一文里讲的工作流/智能体之分,在这里原样适用。
  • 单智能体够用就别上多智能体。 MAST 的失败有 32.3% 发生在智能体之间。多接一个智能体的那一刻,你就重新买入了那 32.3% 的暴露面。

结语

归纳起来是这样。智能体的失败大多不是模型的失败,而是系统设计的失败(按 MAST 是 44.2%)。最常见的两种模式是重复和不终止(合计 28.1%),它们在烧 token 的同时,也在让副作用重复发生。而能拦住这种重复的协议层装置,不存在 — MCP 只给幂等性提示,IETF 的幂等性键标准写了 5 年半还是草案,而且现在处于过期状态。可观测性这边也差不多 — OTel GenAI 约定是连一个发布版本都没有的开发中规范,成本指标压根没有,调试所需的参数·结果默认是关的。

这份清单不是让你悲观,而是一张标明哪些地方得由你亲手补上的地图。你以为标准会替你填、于是空着的那些格子,恰恰就是生产环境里账单飞来的格子。

最后,再看一眼干预的结果。即便在最好的结构性干预之后,代码生成成功率也只是从 25.0% 到 40.6% — 仍有超过一半在失败。运营生产环境的智能体,与其说是消灭失败,不如说是知道每种失败的代价。哪种失败只烧 token,哪种失败会弄坏状态,哪种失败会悄悄交出错误的答案 — 从分清这三者的那一刻起,才算是在做运营。拿什么、怎么衡量智能体的问题,在被模拟的顾客永远不会离开一文中继续;把智能体隔离起来跑的问题,在Lambda 微虚拟机智能体沙箱一文中继续。

参考资料