大多数智能体失败,并非因为幻觉
智能体干了蠢事,大家的第一反应通常是「模型幻觉了」,于是往系统提示词里再加一行。本月的一篇论文认为,这往往是误诊。
Tommy Tran 在 ACM Queue 上发表的《Failure Is a Feature》(2026 年 10 月 15 日)从一个朴素的观察出发。智能体就是一个循环:模型提出一个动作,某个执行框架(harness)拿它去调用工具,结果再回传给模型。这本质上是一个协调者在通过会丢包的网络,对并不由自己掌控的服务发起远程调用。它是一个分布式系统,中间夹着一个不确定性的部件。这类问题,分布式系统已经有四十年的现成答案。我们却总是一再伸手去改提示词。
智能体真正出问题的地方
论文按故障进入循环的位置对失败进行分类:
- 工具边界:超时、429 限流、连接中断。
- 返回内容:调用“成功”了,但返回的是被截断的 JSON、一个 HTML 错误页,或者结构已经漂移的 schema。
- 模型:选错工具、编造参数、没做检查就宣称成功。
- 副作用:写操作已经生效,确认却丢了,于是这一步被重试。
- 循环本身:两个工具来回踢皮球、对着一个死掉的 API 重试一整晚、上下文耗尽。
这里面只有一个是模型的问题。其余的都有无聊但已知的解决办法。
一个被截断的响应,把 "balance" 写成 1 而不是 1000,模型照样会兴高采烈地拿它去推理,因为模型什么都能解析。整条管道里没有任何一环说过“不”。
实验
论文附了一个小型的开源执行框架,方便你自己验证这些说法。在固定的故障配置下,结果如下:
| 配置 | 任务完成率 | 副作用 |
|---|---|---|
| 裸智能体 | 33.3% | 未提及 |
| 仅加重试 | 83.3% | 产生重复记录 |
| 全部七种模式 | 89.6% | 无重复、无孤儿写入 |
单看重试,成绩好得不得了,直到你注意到那些重复。对一次已经成功的写操作再重试,就等于退了两笔款。其他模式多出来的 6 个百分点,相比它们省下的善后工作,其实没那么重要。
值得借鉴的三件事
- 幂等键由执行框架持有,而不是模型。 如果你让模型去填
idempotency_key参数,重试的那一步可能带着一个不同的键回来,去重就会悄无声息地失效。
从框架能控制的东西里推导这个 key:
def run_side_effect(task_id, step_id, tool, args):
key = f"{task_id}:{step_id}:{tool}" # 这一步每次重试都相同
return call_with_retries(tool, {**args, "idempotency_key": key})
如果模型重新规划,又提出同一个动作,那会拿到一个新的 step id,这没问题——那是一次新的决策,不是重发。
2. 把 token 预算当成截止时间。 给每个任务设时间上限、token 上限和步数上限,每进入新一步之前先检查一遍。论文里管步数上限叫“最便宜的保险”。几行代码,就能把“智能体对着一个挂掉的 API 跑了一整夜”变成“任务在第 20 步失败,原因明确”。
3. 别让模型待在重试循环里。 瞬时错误交给框架去重试,用退避加随机抖动,只把最终结果或一个带类型的失败信息给模型看。否则每次尝试都要烧 token,还把上下文窗口塞满失败过程的描述。遇到依赖彻底挂掉的情况,应该由熔断器返回一个模型能据以规划的东西,比如 {"error": "circuit_open", "retry_after_s": 30}。
几点诚实的提醒
论文自己也说得很清楚:这是一个注入了故障的玩具智能体。故障是随机发生的时候,熔断器反而降低了任务成功率,只有在真实依赖确实宕机时才有价值。所以这不是一份可以照抄的配方,而是一种不再靠猜的做法。
更重要的结论,和论文对 tau-bench 这类基准的评论是一致的:单次运行的成功率会美化智能体的表现。一个每次尝试“大多数时候能成”的智能体,当你需要它连续成功八次时,失败率会高得多。决定一个东西是停留在演示、还是能长期无人值守跑下去的,是可靠性,而不是原始能力。
如果你在做智能体,我的建议是:先读论文、跑一遍那套测试框架,再去写下一条提示词修补。链接在下面。