Skip to content

OpenClaw 的 Gateway 对 Agent 请求做了幂等性处理。为什么 Agent 系统特别需要幂等性?工具已经产生副作用时怎么办? ​

  • 标签:AI、OpenClaw、大模型应用开发、AI应用开发、Agent开发

🏛 业务架构师 ​

Agent 系统特别需要幂等性,因为它与普通系统有三点结构性差异:

  1. 重试常态化:LLM 调用超时率不低、工具偶发失败,Agent 框架和网络层都会自动重试——没有幂等,一次重试就是一次重复下单;
  2. 多路径触达:消息可能经 webhook 重发、用户重复发送、多渠道同时进来,Gateway 去重之外,动作层还要有第二道防线;
  3. 不可预测的循环:Agent 死循环或反思重试时,可能对同一工具用相同参数反复调用——幂等是最后的保险丝。

幂等实现:为每个"意图"生成唯一幂等键(消息 ID/任务 ID/Agent 决策指纹),工具执行前查键——已执行则直接返回上次结果;未执行则加锁执行并落结果。写操作工具(下单、发消息、扣款)必须内建幂等语义。

工具已经产生副作用怎么办——补偿思路分三级:

  1. 可查询确认:先查后做——重试前先调查询接口确认动作是否已发生(订单是否已创建),已发生就直接取结果;
  2. 可补偿回滚:业务设计时预留逆向操作(取消订单、撤回消息),检测到重复执行立即补偿,并告警人工复核;
  3. 不可逆操作:必须前置拦截——这类工具(对外付款、删库)设计成"两阶段":Agent 只能发起预提交,真正执行要人工确认,幂等键绑定审批单。

业务本质:幂等性是 Agent 的"财务纪律"——自主系统的每一次外部动作都必须可追溯、可去重、可补偿,否则 AI 的效率红利会被资损事故清零。

🏗 系统架构师 ​

为什么 Agent 系统特别需要幂等:因为它的重试源比传统系统多得多——渠道 webhook 会重试(Telegram 至少重投 2~3 次)、用户重复点击、Agent 循环自身重试、网络超时后框架自动重发、多实例部署的消息重复投递。而 Agent 的动作往往带副作用(发消息、调工具、写数据),重复执行 = 重复副作用(给用户发三遍消息、下三单),所以幂等是 Agent 系统的生产底线而非优化项。

Gateway 的幂等处理:请求进网关时提取幂等键(消息 ID / 客户端提供的 idempotency key)→ 查重(Redis/DB 记录处理状态)→ 重复请求直接返回缓存结果或丢弃,新请求登记后放行;处理结果回写,未完成的请求按状态决定等待或重新调度。

工具已产生副作用时怎么办(进阶难点):副作用发生在幂等检查之后、完成回写之前(进程崩溃、超时)——重放会二次执行。我的分层处理:

  • 优先用工具自身的幂等性:调用带幂等键(支付接口、DB 唯一约束、upsert),副作用层兜底;
  • 执行日志(Write-Ahead Log):动作执行前先落"意向日志",重放时比对——已执行的工具步骤跳过,从断点继续(Saga 式恢复);
  • 可补偿设计:不可幂等的动作配套补偿动作(发错了撤回),最终一致;
  • 人工兜底:高危操作发现重复痕迹时告警人工裁决,而不是自动重放。

架构原则:幂等键贯穿全链路(消息 ID → 任务 ID → 工具调用 ID),任何一层可独立重试。

💻 全栈开发专家 ​

为什么 Agent 系统特别需要幂等:重试无处不在且都合理——渠道 webhook 至少一次投递、网络超时客户端重发、用户重复点击、模型层重试工具调用。没有幂等,一条「执行部署」消息重投两次就是双倍生产变更,这是真实事故形态。

幂等实现分层:

  1. 消息入口幂等:Gateway 按 message_id(或渠道事件 ID)去重,处理结果缓存(Redis SETNX key value EX ttl),重复投递直接返回缓存响应;
  2. 操作级幂等键:每个工具调用生成/透传 idempotency_key(工具调用 ID),服务端按键去重——关键设计:工具已经执行成功过,重放时返回上次结果而不是再执行;
  3. 副作用工具自身幂等:业务侧支撑——订单号做唯一约束、UPDATE 带版本号(乐观锁)、upsert 语义。

工具已经产生副作用时怎么办(核心难点):

  • 执行中(副作用未完成):不能贸然重试——先查状态(工具服务提供 status(op_id)),进行中就等待/订阅完成,绝不并发重入;
  • 已完成:返回缓存结果+标记 already_applied: true,让模型知道这是重放;
  • 结果未知(超时断连):最危险状态。可恢复的操作(先查再做的查询式写法)自动恢复;不可恢复的(外部支付)转人工核对,绝不自动重试——宁可慢,不可双。
python
res = store.get(key)
if res: return res                    # 已完成:重放结果
if store.locked(key): return wait_or_query_status(key)
with store.lock(key):                 # 分布式锁防并发重入
    res = execute(); store.put(key, res); return res

审计要求:每次重放单独记日志(谁的重试、命中哪个键),重放率异常升高说明上游有问题。