Agent 系统可靠性的第一道门槛:幂等性

每次超时后的重试都是一场赌博——幂等性是打破"重复 vs 丢失"僵局的关键。从分布式系统的底层视角,拆解 Agent 工具调用中的幂等性设计与实现。

每个搭过 Agent tool execution 层的人,都会在某个时刻撞上同一个问题:

Agent 决定调用一个 API。HTTP POST 发出去了。然后——超时了

你面临的是一个经典的网络困境。服务端可能根本没收到请求,可能收到了执行了但在返回时超时了,也可能收到了一半就崩溃了。你没法区分这三种情况。所以你只能做一个选择:

  • 重试 → 可能重复执行(发两封邮件、扣两次费)
  • 不重试 → 可能任务失败(该做的事没做成)

在不确定的网络里,你要么冒着重复的风险保证完成,要么冒着丢失的风险避免重复。

幂等性是打破这个僵局的钥匙。


Idempotency Key:一把钥匙一把锁

解法不复杂:每次发出请求时,生成一个全局唯一的 ID(UUID),通过 HTTP 头或者请求体里的 Idempotency-Key 发给服务端。

服务端的逻辑只有三步:

收到请求 → 检查 idempotency store 中有没有这个 key
  ├─ 有 → 直接返回上次存储的响应结果(不执行)
  └─ 无 → 执行请求 → 存 key→response 到 store → 返回结果

你可以把它想象成酒店的寄存服务。你寄存行李时拿到一张行李牌(idempotency key),回来时凭牌取行李。如果你拿着一张牌来取两次,第二次柜员看到牌已经取过了,直接把第一次的单子给你看:"这个你上次就拿走了"。


在 Agent 系统里,这个问题被放大了

普通 API 调用要考虑幂等性,Agent 系统里更是躲不开。有三个原因让它在 Agent 场景下变得尤其尖锐。

1. 并行 tool call 带来的竞争

你在一个 ReAct 步骤里同时发了 5 个 tool call。第 3 个超时了,但其他 4 个已经成功返回。你只重试第 3 个——如果它没有 idempotency key,重试时服务端可能已经处理到一半,导致下游状态不一致。

而且 Agent 的 tool call 往往是有副作用的——写数据库、发消息、下单订购。这些操作重复执行的影响比"多查一次天气"严重得多。

2. 多步 workflow 中的级联效应

假设 Agent 分三步完成一个任务:
1. 创建订单(返回 order_id)
2. 用 order_id 扣款
3. 用 order_id 发送确认邮件

如果第一步"创建订单"在成功执行后超时返回,Agent 重试它——没有幂等性保护的话,就会创建两个订单。第二步拿着两个 order_id 扣两次款。第三步发两封确认邮件。

一次超时,酿成整个 workflow 的状态污染。 而且这种错误不容易发现——数据看着是对的,只是多了一倍。

3. Agent 的长生命周期

传统 API 调用的超时和重试在秒级别完成,idempotency key 存几分钟就够了。但 Agent 的 workflow 可能持续几分钟甚至几小时——一个 Agent 在思考过程中调用了 10 次 API,每次间隔几十秒。如果中间某步失败,Agent 的 retry 逻辑可能在 30 分钟甚至更久之后才触发。

这意味着 Agent 系统的 idempotency store 的 TTL 要覆盖整个 workflow 的生命周期,而不是单个请求的 retry 窗口。


底层本质:分布式事务的视角

把 Agent 系统的 tool execution 抽象出来,它本质上是一个分布式事务问题:

  • Agent(客户端)发出命令
  • Tool Service(服务端)执行命令
  • 中间隔着不可靠的网络

分布式系统里解决这个问题的方法论叫做 exactly-once semantics,公式很简单:

At-least-once delivery + Idempotent processing = Exactly-once semantics

"至少一次投递"是重试机制保证的——失败就重试。"幂等处理"是 idempotency key 保证的——重复请求不重复执行。两者加起来,就得到了"恰好一次执行"。

再往深挖,idempotency store 本身是一个分布式的 KV 数据库。如果 Agent 系统本身是分布式的(多个节点共享同一个 tool execution 层),这个 store 就要面对 CAP 的权衡:

  • CP(一致性优先):idempotency key 要写入所有副本才能返回成功——保证不重复,但写入延迟高
  • AP(可用性优先):idempotency key 写入主节点后立即返回,异步复制到其他副本——写入快,但网络分区时可能出现重复

大多数 Agent 系统选择 AP,因为"偶尔重复但总在运行"比"绝不重复但可能卡住"更适合助手场景。


实践中怎么设计

如果你在自己的 Agent 框架里实现幂等性支持,核心组件就三个。

一个 Idempotency Store

一个 key→response 的 KV 存储。选型取决于你的场景:

方案 优点 缺点 适合场景
Redis(TTL) 快、自带过期 持久化不够强 单机 Agent / 轻量场景
PostgreSQL 强一致、持久化 比 Redis 慢一点 多 Agent 共享、需要审计
内存 Map 零依赖、极快 重启丢失 开发环境 / 单次 session

TTL 建议:至少是 workflow 最大预期持续时间的 2 倍

一个 Key 生成策略

Idempotency key 必须全局唯一。Agent 系统的天然选择是:用 (session_id, step_index, call_index) 三重前缀生成 deterministic UUID。这样即使 Agent 崩溃后恢复,相同的步骤也会产生相同的 key,不会因为重启而产生新的 key。

一个重试策略

重试不能是盲目的。好的重试策略考虑三点:

  1. 指数退避——第一次等 1s,第二次 2s,第三次 4s……不要狂轰滥炸
  2. 抖动(Jitter)——在退避时间上加随机噪声,避免多个 Agent 同时重试造成惊群效应
  3. 最大重试次数——超过次数后放弃或者走人工兜底,不要无限重试

给 Agent 开发者的建议

如果你只从这篇文章带走一个概念,那就是:

每一个有副作用的 tool call,都应该有一个 idempotency key。

这不是一个锦上添花的 feature,而是 Agent 系统从实验性玩具走向生产级工具的必要条件。没有幂等性,Agent 在不确定网络下就是不可靠的——你今天能做的事越多,明天能搞出来的 bug 就越多。

在设计上,tool execution 层的幂等性保障应该是一个基础设施级的抽象,而不是每个 tool 各自实现的东西。把 key 生成、store 选型、重试策略封装成可替换的组件,让每个 tool 开发者只需要关心业务逻辑。


延伸阅读

  • Stripe API Idempotency 文档 — 幂等性在支付 API 中的工业级实践,也是这个模式的鼻祖参考
  • "Exactly-Once Semantics in Distributed Systems" (Pat Helland, 2010) — 分布式事务领域的经典论述
  • "A Note on Distributed Computing" (Waldo et al., 1994) — 用最清晰的语言解释了为什么网络是不可靠的
  • AWS idempotent API design best practices — 云服务商如何在实践中解决这个问题

评论

此博客中的热门博文

我写了半年 prompt,最后发现最好的技巧就三个