
| 📢 导读 | 你有没有遇到过:明明只发了一次指令,AI却执行了两遍?消息重复处理,轻则浪费Token,重则重复下单、重复发邮件。今天我们从源码看看OpenClaw怎么给每条消息发一张“身份证”,确保只处理一次。 |
引言:网络抖动引发的“双重执行”
你在钉钉上发了条消息:“帮我给团队发一封周报邮件”。网络波动,钉钉重试了一次,你的消息被推了两遍。
如果AI傻乎乎地执行两遍,团队会收到两封一模一样的邮件,浪费时间不说,还可能引发混乱。
问题来了:系统怎么知道这两条是同一件事?
如果只是简单地把“消息内容”作为判断依据,那用户发两条内容相同的正常指令,也会被误判为重复。所以必须有一个唯一标识,既能识别重复,又不影响正常业务。
01 用户一般怎么做
假设你来设计这个“去重”机制,第一反应可能是:给每条消息发一个“身份证号”。收到消息时先看这个号有没有处理过,处理过就跳过。
身份证号怎么生成?必须保证唯一。可以用“来源+会话+消息ID”组合,因为同一个聊天平台里,消息ID是唯一的。
这就是幂等键(idempotency key)——同一个键,只处理一次。
但还有细节:
幂等键应该存多久?存太短,重试还没结束就过期了;存太长,会占用大量内存
处理中的消息如果崩溃了,下次重启怎么处理?
不同渠道的消息ID格式不一样,怎么统一?
02 OpenClaw怎么实现:统一身份+缓存控制
打开 src/messages/dedupe.ts 和相关路由代码,OpenClaw的去重机制设计得很清晰。
2.1 幂等键的生成规则
OpenClaw给每条进来的消息,都会计算一个幂等键,格式是:
{provider}|{accountId}|{sessionKey}|{messageId}provider:哪个渠道(钉钉、飞书、Web、WhatsApp)accountId:哪个账号(如果你绑了多个飞书机器人)sessionKey:哪个会话(群聊或私聊的唯一标识)messageId:消息的唯一ID(由渠道提供)
组合起来,就能唯一确定“某个人在某个会话里发的某条消息”。
为什么不用消息内容? 因为用户可能发两条一模一样的内容(比如“重复一下上条指令”),那是正常业务,不能去重。
2.2 缓存存储与过期策略
收到消息后,系统会先查缓存里有没有这个键:
const idempotencyKey = `${provider}|${accountId}|${sessionKey}|${messageId}`;const alreadyProcessed = await cache.get(idempotencyKey);if (alreadyProcessed) {logger.debug(`Duplicate message, skipping: ${idempotencyKey}`);return; // 直接丢弃,不处理}// 第一次见到,存进缓存,设置过期时间await cache.set(idempotencyKey, 'processed', { ttl: 300 }); // 5分钟
缓存默认保留5分钟(可配置)。为什么是5分钟?因为网络重试通常会在几秒到几分钟内完成,5分钟足够覆盖绝大部分场景,又不会占用太多内存。
如果消息正在处理中(比如Agent运行了很长时间),缓存会在处理开始时写入,处理完成后不会删除(由TTL自动过期)。这样即使处理过程中进程崩溃,重启后缓存还在(如果用的是Redis等持久化缓存),可以继续拦截重复。
2.3 处理中的消息怎么防重
有一个边界情况:消息A正在处理中,还没处理完,同样的消息B又来了。如果直接根据缓存判断,会拦截掉B,这是正确的。但如果在处理过程中,消息A的处理进程崩溃了,消息B又来了怎么办?
OpenClaw的处理是:幂等键的写入发生在处理开始之前。这样即使A崩溃,缓存里已经有记录,B也不会被处理。这就避免了重复执行。
但这种设计也有代价:如果A崩溃了,这条消息就永远不会被处理了。所以需要配合监控和重试机制——用户可以在超时后重新发送,或者管理员手动清理缓存。
03 动手试试:模拟重复消息
我们来模拟一下消息重复的场景,验证幂等机制是否生效。
第一步:发送一条消息
在Web控制台或钉钉/飞书里,给AI发一条指令,比如“帮我查一下今天的日期”。
第二步:查看日志,记录消息ID
观察Gateway日志,找到类似这样的行:
[debug] Processing message: provider=feishu, accountId=xxx, sessionKey=xxx, messageId=12345记下 messageId。
第三步:模拟重复消息
你可以通过WebSocket API或直接修改代码,用同一个 messageId 再发一次相同的消息。
第四步:观察结果
第二次发送时,日志里会出现:
[debug] Duplicate message, skipping: feishu|xxx|xxx|12345AI不会再次执行,直接忽略。
如果你无法直接模拟消息ID重复,可以写一个简单的测试脚本,调用OpenClaw的内部API。或者直接在 src/messages/dedupe.ts 里临时加一条日志,观察缓存命中情况。
最后:这个设计教会我们什么
幂等是分布式系统的基本功。消息可能重复,网络可能重试,设计任何接收任务的系统,都要先问自己:重复执行会不会出事?
OpenClaw的方案有几个值得借鉴的点:
幂等键的组成:不是简单用消息内容,而是用“渠道+账号+会话+消息ID”的组合,既唯一又稳定
TTL设计:5分钟的过期时间,在“防重”和“内存占用”之间做了合理平衡
写入时机:处理开始前写入,确保崩溃也不重复
可观测性:日志明确标记重复消息,方便排查
下次你在设计自己的消息队列、API接口或任务调度系统时,也可以参考这个模式:给每个请求发一个“身份证”,处理前先查档,处理过就不再碰。

夜雨聆风