OpenClaw 的 Gateway 对 Agent 请求做了幂等性处理。为什么 Agent 系统特别需要幂等性?工具已经产生副作用时怎么办?
参考答案
幂等性就是”同一个操作执行一次和执行一百次,效果完全一样”。
Agent 的一次运行可能涉及多个工具调用,花费几十秒到几分钟。在这个过程中,网络断连、客户端重试、消息队列重投都很常见。
如果没有幂等保护,同一个请求可能触发 Agent 重复跑一遍,轻则重复发消息,重则重复执行命令、重复扣费,后果非常严重。
OpenClaw 通过 idempotencyKey + 内存 dedupe Map 来做幂等。每个请求带一个 UUID 作为幂等键,Gateway 先在 dedupe Map 里查一把:有缓存就直接返回,没有才真正执行,执行完再把结果存进去。缓存有 TTL 和最大条目数限制,不会无限膨胀。
工具已经产生副作用时怎么办?这个问题其实没有完美方案。
OpenClaw 的策略是防止重复触发,不做回滚。已经执行的副作用就让它留着,重点保证后续不会再重复执行。
扩展知识
dedupe Map 的实现细节
看代码 src/gateway/server-methods/agent.ts:
1 | ▼ |
dedupe Map 的维护在 src/gateway/server-maintenance.ts,定期清理过期条目,超过 DEDUPE_TTL_MS 就删掉,超过 DEDUPE_MAX 就按时间戳淘汰最老的。
实际占用很小,一个条目就是一个 UUID 加一份响应快照,几万条也就几十 MB。
消息发送的双层防护
消息发送场景更复杂。send.ts 里除了 dedupe Map,还有一个 inflightMap 防止并发发送同一条消息。
两层防护叠加:dedupe 管”历史上发过没有”,inflight 管”当前是不是正在发”。
Chat 方法还有一层额外保护,用 transcriptHasIdempotencyKey() 检查 transcript 中是否已存在某个幂等键,避免重复追加消息到对话历史。三层保护各管一段,组合起来基本堵死了重复执行的可能。
处理已产生副作用的策略
实践中一般从三个层面去缓解:
1)让工具本身尽量设计为幂等的。比如”写入文件”天然幂等,同样的内容写两遍结果一样;”发送消息”就不行,发两遍用户收到两条。
2)对不可幂等的工具,在工具侧做去重。比如发消息前先检查”这条消息是不是已经发过了”,用消息 ID 做幂等键。
3)接受”至多一次”语义。宁可漏执行也不重复执行,对很多业务场景来说,漏发一条通知比重复发十条要好得多。
面试官追问
提问:内存里的 dedupe Map 挂了怎么办?重启后幂等保护不就失效了?
回答:确实,内存 dedupe 在进程重启后就丢了。对于 OpenClaw 这种单实例部署的场景,重启窗口很短,实际风险不大。如果要做到更强的保证,得把幂等状态持久化到 Redis 或数据库里,不过这会引入额外的 IO 开销和复杂度,得根据业务容忍度权衡。
提问:分布式环境下多个 Gateway 实例,幂等怎么保证?
回答:单实例的内存 Map 搞不定多实例场景,因为请求可能落到不同节点。这时候得换成集中式存储,比如 Redis 的 SETNX,用幂等键做 key,设一个和 TTL 一致的过期时间。或者在请求入口加一致性哈希,保证同一个幂等键的请求总是路由到同一个实例。
提问:幂等键用 UUID,那客户端怎么知道该用同一个 UUID 重试?
回答:客户端在第一次发起请求时生成 UUID 作为幂等键,然后本地缓存住。如果需要重试,带上同一个 UUID 就行。消息平台的 Webhook 场景更简单,平台本身每条消息都有唯一 ID,直接拿来当幂等键,天然支持重试去重。
提问:dedupe 缓存的 TTL 设多长合适?
回答:TTL 得跟 Agent 的最大执行时间挂钩。如果一个 Agent 最长跑 5 分钟,TTL 起码设到 10 分钟,留出足够的重试窗口。内存方面靠 DEDUPE_MAX 限制最大条目数,超了就按时间戳淘汰最老的,不用担心撑爆内存。