7.OpenClaw源码解析——可靠消息投递

1. 知识回顾

上节课我们主要学习了 OpenClawheartbeatcron 两种异步任务机制,有如下特点:

  • 异步任务在后台自动运行。
  • 当有用户提问时,系统会优先响应用户 ,阻塞 heartbeat等主动任务。
  • cron 任务在后台默默执行,不影响主流程。

本节课我们来解决一个OpenClaw的核心痛点,当发送消息时,可能会因为网络故障进程崩溃 等原因导致回复丢失 而影响体验,所以OpenClaw的做法就是引入可靠消息投递机制,确保消息不会丢失。


2. 总体设计思路

OpenClaw的做法就是消息必须先写到磁盘,然后再尝试发送

  • 消息必须先持久化到本地磁盘,确保掉电/崩溃后依然可恢复。
  • 随后再通过后台线程异步尝试发送,失败则自动重试

实现细节

3.1 消息分片

大模型生成的回复可能过长,需要根据不同平台的消息长度限制进行切分:

  • 如果回复较短,则不分片
  • 如果回复过长,则按平台规则切割为多个片段,依次投递。

3.2. 生成任务对象并持久化入队 💾

具体做法是:

  • 生成唯一的 delivery_id(UUID 截取前 12 位)。
  • 构造 QueuedDelivery 任务对象,包含通道、接收方、文本内容、入队时间、下次重试时间等。
  • 调用 _write_entry() 写入磁盘。

下面的代码演示了原子写入的过程,防止写入过程中断导致文件损坏

python 复制代码
def _write_entry(self, entry: QueuedDelivery) -> None:
    """通过 tmp + os.replace() 保证原子写入,避免数据损坏"""
    final_path = self.queue_dir / f"{entry.id}.json"
    tmp_path = self.queue_dir / f".tmp.{os.getpid()}.{entry.id}.json"

    data = json.dumps(entry.to_dict(), indent=2, ensure_ascii=False)
    with open(tmp_path, "w", encoding="utf-8") as f:
        f.write(data)
        f.flush()
        os.fsync(f.fileno())   # 强制落盘
    os.replace(str(tmp_path), str(final_path))  # 原子替换

通过将消息落入磁盘进行持久化,防止消息丢失。

python 复制代码
delivery_id = uuid.uuid4().hex[:12]
entry = QueuedDelivery(
    id=delivery_id,
    channel=channel,
    to=to,
    text=text,
    enqueued_at=time.time(),
    next_retry_at=0.0,        # 立即重试
)
self._write_entry(entry)      # 落盘后才返回
return delivery_id            # 返回 ID 给上层

3.3 后台线程扫描

系统启动时,后台线程会扫描 pending(待发送)和 failed(失败)队列,确保没有遗留任务。

python 复制代码
pending = self.queue.load_pending()
failed = self.queue.load_failed()

# 打印恢复状态
if pending:
    print_delivery(f"Recovery: {len(pending)} pending")
if failed:
    print_delivery(f"Recovery: {len(failed)} failed")

后台线程进行轮询,遍历pending队列。

python 复制代码
for entry in pending:
    # 1. 检查是否到达下一次重试时间
    if entry.next_retry_at > now:
        continue

    # 2. 尝试投递
    try:
        self.deliver_fn(entry.channel, entry.to, entry.text)
        self.queue.ack(entry.id)           # 投递成功,确认删除
        self.total_succeeded += 1
    except Exception as exc:
        # 3. 投递失败,记录失败并更新重试次数
        self.queue.fail(entry.id, str(exc))
        self.total_failed += 1

        # 4. 判断是否超过最大重试次数(MAX_RETRIES)
        if entry.retry_count + 1 >= MAX_RETRIES:
            print_warn(f"Delivery {entry.id} -> failed (最终失败)")
        else:
            # 进入指数退避等待
            backoff = compute_backoff_ms(entry.retry_count + 1)
            print_warn(f"next retry in {backoff / 1000:.0f}s")

为了防止某个消息一直发送不成功而抢占其他消息的时间,OpenClaw使用了带随机抖动的指数退避算法,具体代码如下:

python 复制代码
# 基础退避时间序列(毫秒)
BACKOFF_MS = [5_000, 25_000, 120_000, 600_000]  # 5s, 25s, 2min, 10min

def compute_backoff_ms(retry_count: int) -> int:
    if retry_count <= 0:
        return 0

    # 根据重试次数选择对应的基础退避档位(超出则取最大)
    idx = min(retry_count - 1, len(BACKOFF_MS) - 1)
    base = BACKOFF_MS[idx]

    # 加入随机抖动(±20%),避免大量消息同时重试造成网络风暴
    jitter = random.randint(-base // 5, base // 5)
    return max(0, base + jitter)

4. 知识总结

模块 作用
消息分片 适配不同平台的长度限制,避免发送失败
原子写入 利用 tmp+ os.replace,防止崩溃导致文件损坏
持久化队列 内存队列 + 磁盘存储,确保重启后任务不丢失
指数退避重试 失败后等待时间递增(含随机抖动),避免服务端过载
后台线程扫描 服务启动时自动恢复未完成的任务,实现高可用
相关推荐
拾光人1 小时前
tool calling 没那么玄:我用 Prompt 手搓了一遍
agent
Oliver_NI1 小时前
Agent 理论(一):Tool Call —— 大模型如何使用工具?
agent
数据智能老司机1 小时前
检索前处理
agent
劈星斩月1 小时前
机器学习之 定义与三大范式
人工智能·机器学习·监督学习·强化学习·无监督学习
触底反弹1 小时前
🎨 通义万相实战:用 Qwen 多模态 API 实现 AI 换装换姿势,10 行代码搞定!
vue.js·人工智能
属鼠哥1 小时前
一场正在发生的范式转变:Loop Engineering(循环工程)
人工智能·aiops
码农小旋风1 小时前
Claude Code 基础用法大全:对话、分析、修改、测试、Git 和工作流
人工智能·git·chatgpt·claude
Solis程序员1 小时前
MCP (Model Context Protocol):AI应用连接外部世界的标准协议
人工智能·microsoft·agent·skill·mcp
贵慜_Derek1 小时前
《从零实现 Agent 系统》连载 29|多 Agent 研究 Harness:Lead、Worker 与 Spawn
人工智能·架构·agent