你的智能体忘记发送的幂等键
你的智能体中最昂贵的 Bug 不是幻觉。而是重试。
在你的技术栈中,总有一个工具负责扣款、发送邮件、关闭工单或写入数据行。智能体调用它,调用耗时过长导致超时,于是智能体——作为一个优秀的、具有弹性的软件——再次调用它。棘手之处在于,第一次调用其实已经成功了,只是响应从未返回。结果你扣了客户两次款,无论你如何提示“请在支付时保持谨慎”,都无法阻止这种情况发生。
这是分布式系统中最古老的故障模式,只是披上了一件新装。十年前,我们通过幂等键(idempotency keys)为 HTTP API 解决了这个问题。但大多数智能体技术栈通过将原本为读取设计的重试逻辑直接用于执行写入的工具,且从未发送那个能让重试变得安全的字段,从而重新引入了这个问题。
这个问题之所以不断出现,是因为智能体产生重复调用的频率远高于普通客户端,且来源也更加多样。在传统的 REST 客户端中,重试来源只有一个:你的重试包装器(retry wrapper)。而在一个智能体循环中,至少有四个来源。提供商 SDK 会重试 HTTP 请求;你的工具包装器会在 5xx 错误时重试;智能体运行时会重试该步骤;而模型本身——最不可预测的一层——也会因为之前的执行结果从上下文中被截断、多步计划中断,或者仅仅是对操作是否成功缺乏信心,而重新发出已经执行过的工具调用。实践者报告显示,智能体工具调用的重试率在 15-30% 左右,这比你的支付代码设计的容忍度高出了一个数量级。
故障模式是“成功后丢失”,而非“失败”
几乎所有人都会犯的错误是将重试视作仅在失败时触发。事实并非如此。最危险的是模棱两可的情况:请求到达了服务器,服务器完成了工作,然后网络在返回途中丢弃了响应。从调用者的角度来看,这与请求从未到达是无法区分的。同样的超时,同样的缺失确认,同样的重试本能。
如果你的重试逻辑无法区分这两种情况——而且在网络层面上,它根本无法区分——那么只有在操作是幂等(idempotent)时,重试才是安全的。读取天然是幂等的:获取两次客户资料返回的是相同的结果,且不会改变任何数据。写入则不然:预订两次预约会产生两个预约。幂等性的整个学科就是为了让第二类行为的表现与第一类一致。
幂等键就是实现这一目标的方法。客户端为特定工作单元生成一个唯一标识符,并随请求发送。服务器将该键与结果一同记录。如果服务器再次看到相同的键,它会跳过执行过程,直接返回原始结果,而不是执行第二次。Stripe 为每个端点保留这些键 24 小时;在此窗口内的重试是一个空操 作(no-op),直接返回第一次的响应。无论请求到达多少次,扣款都只会发生一次。
将键放在工具层,而非提示词中
这是初识智能体的团队最容易绊倒的地方:幂等性不是一个推理问题,因此它不属于提示词范畴。你无法通过指令来实现正确性。“不要重复扣款”并不是模型具备的能力——它看不见网络,不知道之前的调用响应是否丢失,即使是表现完美的模型也会在上下文被截断时重新发起调用。要求 LLM 管理幂等性,是要求一个对故障模式毫无感知的组件去防止故障。
键应该放在工具包装器(tool wrapper)中——即介于模型调用决策与实际副作用之间的确定性代码。这一层能看到每次调用,知道工具参数,并且无论模型在想什么,其运行方式始终一致。它是唯一一个既拥有信息又具备可靠性来强制执行“精确一次”(exactly-once)语义的地方。
具体来说,将你的工具分为三类并区别对待:
- 自然幂等的读取 —— 获取个人资料、检查状态、搜索知识库。随意重试,无需键。这些是安全的,因为操作没有副作用可以被复制。
- 产生副作用的写入 —— 扣款、预订时段、发送消息、创建工单。每一个在重试时都需要幂等键。这是最容易让你受损的类别。
- 长时间运行的操作 —— 生成文档、启动工作流。为触发器设置键以防启动两次,并暴露一个独立的状态端点,以便重试时是轮询完成 情况而非重新启动。
失败的原因并不是团队不知道幂等键。而是他们将每个工具都归类为“一次 API 调用”,并为所有工具配置了统一的重试包装器——这个包装器对读取是正确的,对写入则是隐蔽错误的。
生成一个能在重试中幸存的 Key
Key 只有在重试产生与原始调用 相同 的 Key 时才有效。这听起来显而易见,却是该模式失效最常见的原因。如果你用时间戳或随机值作为 Key 的种子,每次重试都会生成一个新的 Key,服务器会把每一个都看作新任务,这样你构建的复杂机制就毫无意义。Key 必须是操作的稳定函数,而不是时刻的稳定函数。
对于智能体(Agent)的工具调用,自然的素材包括模型的 tool-call ID(每个生成的调用都是唯一的)、会话或 Session ID、工具名称以及序列化参数的哈希值。将这些组合起来并进行哈希处理,你就能得到一个 Key,它在同一次逻辑操作的所有重试中保持一致,但在真正不同的操作之间会有所区别。确定性的输入,确定性的输出。
作用域也很重要。Key 应该涵盖你无法承受重复代价的工作单元——即带有外部副作用的操作——而不是产生该操作的推理请求。如果你以 LLM 调用为 Key,你会愉快地去重两个相同的 推理,却仍然触发了两次 扣费,这完全搞反了。要为扣费(Charge)设 Key,而不是产生扣费的想法。
你还需要服务器端真正遵循它。当下游 API 原生支持幂等 Key(如 Stripe、Adyen、Square)时,直接透传该 Key 让它们去重。当它们不支持时(许多内部服务和像 Twilio 这样的第三方服务),你需要自己构建去重逻辑:使用像 Redis 这样快速的存储来保存 Key 到结果的映射,再加上一个短期的处理锁(使用 SET 命令的 NX 选项),这样两个并发的相同调用就不会在其中任何一个完成前都溜过去。检查存储,获取锁,执行一次任务,缓存结果,释放锁。第二个调用者会找到缓存的答案并返回。
持久化执行(Durable execution)的适用场景与局限
更沉重的解决方案是持久化执行:在像 Temporal、Restate 或 Inngest 这样的引擎上运行你的智能体。这些引擎会记录每一步的日志,在外部持久化状态,并在恢复时回放日志,从而跳过已完成的步骤而不是重复执行。如果做得好,这能为工具调用提供“精确一次”(exactly-once)的语义,而无需在应用代码中穿插幂等 Key——引擎会记得该步骤已经运行过。持久化执行在 2025 年和 2026 年进入主流应用,正是因为智能体基础设施让可靠性差距变得无法忽视。
但持久化执行不能替代边界上的幂等性,原因有二。首先,日志只能保护引擎控制 内部 的步骤。一旦步骤触及第三方 API,引擎的“运行且仅运行一次”保证就会降级为“至少运行一次,并不断重试直到收到确认为止”——在这种情况下,出站调用上的幂等 Key 才是保证诚实的关键。其次,多层重试会产生自身的风险:模型重试、SDK 重试、工作流引擎重试、服务商内部重试。在非幂等的写入操作上堆叠四个独立的重试循环,无异于在某个空闲的下午自找故障。边界上的幂等性让所有这些冗余重试变得安全,而不是演变成灾难。
因此,这两种技术是互补的。持久化执行处理编排层的恢复;幂等 Key 处理编排器无法撤销的外部副作用。你两者都需要,如果只能选一个,选幂等 Key——它更便宜,而且是离钱最近的那一层。
能捕获该问题的测试
大多数团队在生产环境中发现这个 Bug,是因为他们的“开心路径”(happy-path)测试从未演练过这种模糊的情况。修正这一点。对于每一个写入工具,在交付前编写三个测试:
- 成功后超时——操作在服务器上完成,但响应丢失。重试。断言副作用只发生了一次。
- 到达服务器前出错——请求从未到达。重试。断言操作发生了一次(这次是真正运行)。
- 并发重复——两个相同的调用同时到达。断言一个获胜,一个返回缓存结果,且副作用仅触发一次。
如果一个写入工具不能通过这三项测试,它就不配交给智能体,因为智能体 肯定 会遇到这种模糊的情况——它的重试频率远超你的测试套件所假设的。还要对“获胜”情况进行监测:每当重试被去重时,发送一个事件,并在该频率飙升时发出告警。去重命中率的突然上升是你的早期预警,表明网络不稳定或模型正处于死循环重新规划中,这远比收到客户关于重复扣费的投诉要早得多。
令人不安的事实是,智能体并没有发明这个问题,它们只是让这个问题从罕见变成了大概率事件。修复方法是古老、成熟且枯燥的:为每个产生副作用的操作提供一个稳定的 Key,在模型不可见的代码中强制执行它,并测试“成功”与“丢失”看起来一模一样的故障模式。你的智能体忘记发送的幂等 Key,能将重试从负担变回它原本应有的样子——一种安全机制,而不是第二次扣费。
- https://stripe.com/blog/idempotency
- https://docs.adyen.com/development-resources/api-idempotency
- https://www.channel.tel/blog/idempotent-tool-calls-agent-retry-safety
- https://www.buildmvpfast.com/blog/idempotent-ai-agent-retry-safe-patterns-production-workflow-2026
- https://www.inngest.com/blog/durable-execution-key-to-harnessing-ai-agents
- https://temporal.io/
- https://dev.to/mukundakatta/make-your-agents-api-calls-idempotent-before-you-need-to-2994
- https://zuplo.com/learning-center/implementing-idempotency-keys-in-rest-apis-a-complete-guide
