智能体工具调用中的幂等性问题
这种场景每次都如出一辙。你的智能体正在预订酒店房间,支付API调用返回200后、确认信息存储之前发生了网络超时。智能体框架发起重试,支付再次执行,客户被扣了两次款。支持团队升级处理,某位高管说AI"幻觉出了重复扣款"——这种说法是错的,但听起来有道理,因为没人愿意承认他们的重试逻辑从一开始就是坏的。
这不是AI问题,而是分布式系统问题——被AI层全盘照搬,却没有带来分布式系统工程师几十年苦心钻研出的应对之道。标准的智能体重试逻辑假设操作是幂等的,而大多数工具调用并非如此。
为什么智能体重试在结构上就是坏的
每个主流智能体框架——LangChain、LlamaIndex、OpenAI的Agents SDK、Anthropic的Claude——都内置了针对瞬时故障的自动重试行为。这是正确的。分布式系统中瞬时故障 随时发生,静默丢弃请求比重试更糟糕。
问题在于重试逻辑所处的位置。智能体框架在LLM层重试:模型看到工具调用失败,决定再试一次,于是再次调用工具。这个循环里没有任何机制追踪工具是否已经执行了其副作用。框架看到超时,推断:"我没有收到结果,所以应该重试。"而工具可能早已写入了数据库、扣了款、发了邮件或创建了工单。
这种故障模式遍布各行各业。管理CRM系统的智能体会为同一客户投诉创建重复的支持工单;库存管理智能体会对同一订单重复扣减库存;金融智能体会发送重复退款。每个案例都遵循同样的模式:超时或瞬时错误触发重试,工具再次执行,系统最终进入智能体从未预期的状态。
这时,"修复提示词"的本能就会发作。工程师添加诸如"只调用一次支付工具"或"创建之前先检查订单是否存在"之类的指令。这没用。产生重复扣款的智能体当时是在正确地执行指令——它真的不知道第一次调用已经成功了。问题不在于模型的推理,而在于缺少外部状态让工具报告"我已经做过了"。
幂等性对工具调用究竟意味着什么
幂等性意味着多次调用一个操作与调用一次产生相同的结果。GET请求天然是幂等的:读取一条记录不会改变它,读十次也是安全的。DELETE在实践中是幂等的:删除一条不存在的记录与删除一条存在的记录返回相同的逻辑结果。而创建记录或扣款的POST默认不是幂等的——每次 调用都会创建新东西。
让非幂等操作变得安全的模式,在支付API中已经相当成熟。客户端发送请求时附带一个幂等键——由客户端生成并持有的唯一标识符。服务端将该键连同操作结果一起存储。用同一个键重试时,服务端检查存储并返回缓存结果,而不重新执行。客户端第三次重试时得到的响应与第一次一致。
对于智能体工具调用,这一模式需要在三个层面做出明确的设计决策:
智能体运行时层必须为每个工作流步骤生成并维护幂等键。实践中,从持久状态派生的键效果最好:{workflowRunId}:{stepId}。这确保键在重启后仍然有效且是确定性的——恢复时重新生成相同的键,而不是重试时生成新键。
工具执行层必须将幂等键传递给下游服务,在执行前检查去重存储,并以足够的TTL缓存结果。如果键已存在且上次调用成功,返回缓存响应;如果键已存在且上次调用以永久错误失败,返回该错误而不重新执行。
工具接口本身必须接受幂等键并实现去重逻辑。调用外部API的工具应将键透传给这些API;向内部数据库写入的工具应将键作为唯一约束的一部分。
当三个层面协同配合时,智能体框架可以尽情重试。经济结果——一次扣款、一条记录、一封邮件——与智能体的意图相符。
多步骤工作流的Saga模式
单工具幂等性是更简单的问题。更难的情况是多步骤工作流:智能体依次调用多个工具,中途某处发生故障。
设想一个处理订单的智能体:预留库存、向客户扣款、发送确认邮件。每个步骤单独看都是幂等的。但如果支付成功而确认邮件失败,客户已被扣款却没有收到确认。如果智能体重试整个流程,支付会再次执行(假设你在那一层实现了幂等性,扣款会被去重),但库存已经被预留了——第二次预留尝试可能会因库存归零而失败。
这正是Saga模式要解决的问题。Saga是一系列步骤,每个步骤都有对应的补偿动作,当后续步骤失败时可以撤销其效果。与原子回滚(需要分布式事务及其相关代价)不同,Saga通过显式补偿实现最终一致性。
对于订单处理工作流,Saga如下所示:
- 预留库存 → 补偿动作:释放预留
- 扣款支付 → 补偿动作:发起退款
- 发送确认 → 补偿动作:发送取消通知
如果确认步骤永久失败,Saga执行器以相反顺序运行补偿动作:发起退款、释放库存。客户看到的是订单失败,而非扣款成功但无确认的状态。
关键的实现细节是:补偿动作本身必须是幂等的。重试退款时不应该发起第二次退款。这听起来显而易见,直到你在凌晨两点调试退款循环时才真正体会到。
Saga执行存在两种实现模式。编排模式使用中央协调器——通常是持久化工作流引擎或专用的编排智能体——指挥每个步骤并在失败时触发补偿。这提供了对工作流状态的清晰可见性,但产生了单点协调。协同模式让每个步骤发出事件触发下一个步骤,补偿由失败事件触发。这种耦合更松散,但观察和调试难度显著更高。
对于智能体工作流,编排模式通常更优。智能体需要追踪调用了哪些工具以及结果如何。持久化这些状态的工作流引擎为确定性重试和补偿提供了基础,无需在 智能体的提示词中重新构建这些逻辑。
设计幂等的工具接口
负担不应完全落在智能体运行时上。工具本身也应该以幂等性为设计目标,而这些设计决策并不显而易见。
接受客户端生成的幂等键。 不要在服务端生成它们。当客户端持有键时,可以从相同的输入状态重新生成相同的键进行重试。服务端生成的键需要客户端存储第一次响应中的键——如果第一次请求在响应到达前就超时了,这个键就不存在了。
返回明确的错误信号。 当智能体工具调用失败时,智能体需要知道是否应该重试。并非所有错误都可重试。来自不稳定网络的500状态码可以重试;来自格式错误请求的400状态码不行——重试也会再次失败;来自重复键冲突的409状态码可能意味着操作已经成功。每种错误类型都应该明确表达其重试语义,而不是让智能体去猜测。
为破坏性操作实现预览端点。 在智能体删除记录、取消订阅或运行批量更新之前,它应该能够询问"这会做什么?"而不实际提交。预览端点返回受影响资源的描述;执行端点需要预览响应中的令牌。这种模式——在基础设施工具中很常见——防止智能体通过不可撤销的执行来发现副作用。
尽可能使用自然业务标识符。 对于与业务实体相关联的操作——订单、订阅、客户——将实体ID作为幂等键的一部分,可以自然实现去重。"为订单ord_12345创建通知"如果工具在创建新通知前检查该订单是否已有通知,则构造上就是幂等的。这种方法比基于UUID的键更健壮,因为它将去重逻辑与业务语义对齐。
为异步工作返回操作ID。 如果工具启动了长时间运行的操作——报告生成、数据迁移——它应该立即返回操作ID,让调用方轮询完成状态。这将"操作是否已启动?"与"操作是否已完成?"分离开来,使智能体能够从超时中恢复,而不会冒重复执行的风险。
投递语义与"精确一次"的幻觉
分布式系统工程师区分三种投递保证:
至多一次:操作执行零次或一次。没有重复,但可能丢失数据。当执行两次比不执行更糟时使用。
至少一次:操作执行一次或多次。没有数据丢失,但可能重复。这是智能体重试逻辑默认提供的保证。
精确一次:操作精确执行一次。这是所有人都想要的,在分布式系统中也是出了名地难以实现。
在实践中近似精确一次语义的方法是:将至少一次投递与幂等执行相结合。投递机制不断重试直到收到确认;执行层使用幂等键确保重复投递没有额外效果。两者结合,从系统角度看产生了精确一次的效果,即使调用可能多次到达。
这正是Kafka等消息队列声称"精确一次语义"时的架构。它们并没有在网络层消除重复消息,而是将消费者层的去重与事务提交相结合,确保每条消息产生一个逻辑效果,无论到达多少次。
智能体框架正在向同样的架构收敛。智能 体运行时以至少一次语义投递工具调用(失败时重试);工具执行层实现幂等性(执行前检查键)。结果是精确一次的业务效果,这正是智能体的意图。
生产级智能体基础设施的样子
2025-2026年在生产环境运行智能体的团队所呈现的模式,跨公司和技术栈高度一致:
每个产生副作用的工具调用都携带从持久化工作流状态派生的幂等键。智能体框架在持久存储中保存步骤结果,以便重启时智能体能够重建哪些已执行、哪些仍待执行。每个有下游依赖的步骤都存在补偿事务。错误响应包含明确的重试指导——retryable: true/false和retry_after_seconds——使智能体不会对不同的故障模式应用统一的重试逻辑。
跳过这些基础设施的团队最终会遭遇看起来像AI故障实则是分布式系统故障的生产事故。"编造"了重复订单的智能体并没有幻觉——它重试了非幂等操作并丢失了结果记录。"无法取消"订阅(因为无法确认)的智能体并没有错过指令——它缺乏补偿逻辑,工作流陷入了不一致状态。
框架的定义决定了修复方向。归咎于模型会导向提示词工程;归咎于架构则会导向幂等键、Saga执行器和工具接口重新设计——这些改变才真正解决问题。
