智能体工具调用中的幂等性问题
这种场景每次都如出一辙。你的智能体正在预订酒店房间,支付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执行存在两种实现模式。编排模式使用中央协调器——通常是持久化工作流引擎或专用的编排智能体——指挥每个步骤并在失败时触发补偿。这提供了对工作流状态的清晰可见性,但产生了单点协调。协同模式让每个步骤发出事件触发下一个步骤,补偿由失败事件触发。这种耦合更松散,但观察和调试难度显著更高。
- https://docs.stripe.com/api/idempotent_requests
- https://stripe.com/blog/idempotency
- https://brandur.org/idempotency-keys
- https://microservices.io/patterns/data/saga.html
- https://learn.microsoft.com/en-us/azure/architecture/patterns/saga
- https://temporal.io/blog/idempotency-and-durable-execution
- https://blog.bytebytego.com/p/at-most-once-at-least-once-exactly
- https://composio.dev/content/apis-ai-agents-integration-patterns
- https://medium.com/@alexander.ekdahl/why-agent-frameworks-break-at-scale-ab01bf588b40
- https://techcommunity.microsoft.com/blog/educatordeveloperblog/ai-didn%E2%80%99t-break-your-production-%E2%80%94-your-architecture-did/4482848
- https://www.inferable.ai/blog/posts/distributed-tool-calling-message-queues
- https://dev.to/klement_gunndu/your-api-wasnt-designed-for-ai-agents-here-are-5-fixes-2oem
- https://www.vellum.ai/blog/the-ultimate-llm-agent-build-guide
