在 OpenClaw 中,一条用户消息从进入系统到收到回复,完整链路是怎样的?

魏远标 Lv7

参考答案

整条链路可以分成 入站、路由、执行、出站 四个阶段,一共 8 步。

用户在 Telegram、Discord、Slack 这些渠道发了一条消息,首先命中的是对应的渠道适配器,它负责接收原始消息,然后把平台私有格式转换成统一的 MsgContext 结构,包含发送者信息、渠道类型、群组 ID 这些字段,屏蔽掉各渠道的格式差异。

转换完成后进入 dispatchInboundMessage() 做入站上下文补全,然后 resolveAgentRoute() 按优先级匹配到目标 Agent 和 Session Key。

路由匹配完,系统先检查消息是不是斜杠命令,像 /new/reset 这类指令直接在这一层处理掉,不进 Agent。

如果是普通消息,就进入 Agent 执行循环:runReplyAgent()runAgentTurnWithFallback()runEmbeddedPiAgent()

Agent 执行的核心是一个 LLM + 工具调用的循环:先构建系统提示和历史上下文,调用 LLM 拿到输出,解析输出看有没有工具调用请求,有的话执行工具拿到结果再喂回 LLM,如此往复直到 LLM 给出最终回复。

最后通过 ReplyDispatcher 把回复投递回原渠道。

一条用户消息的完整链路,从左到右流转:

用户发送消息 → 渠道适配器接收原始消息 → 转换为统一 MsgContext → dispatchInboundMessage 入站补全 → resolveAgentRoute 路由匹配 → 斜杠命令检查(是命令则直接处理返回)→ runReplyAgent 启动 Agent → LLM + 工具循环(构建提示 → 调 LLM → 解析输出 → 执行工具 → 结果回传,循环直到最终回复)→ ReplyDispatcher 投递回复到原渠道

扩展知识

幂等性保护

所谓幂等,就是”同一个操作执行一次和执行多次,效果一样”。分布式系统里最怕的就是重复执行。用户网络抖了一下,同一条消息可能被渠道推送 2-3 次过来。如果不做幂等处理,Agent 会对同一条消息执行多次,可能产生副作用,比如重复扣费、重复发消息。

OpenClaw 在 Gateway 层给每个请求分配了 idempotencyKey,重复请求直接返回缓存结果,Agent 压根不会被二次触发。

这个设计在 Stripe、支付宝这类支付系统里也很常见,核心思路就是”同一个 key 只执行一次”。

排队机制

Agent 运行不是来一条消息就立刻执行的,中间有两层排队:enqueueSessionenqueueGlobal

enqueueSession 保证同一个 session 内的消息串行执行。想象一下用户连续发了 3 条消息,如果 Agent 同时处理这 3 条,上下文会乱套,回复可能互相矛盾。串行执行确保每条消息都能看到前一条的完整对话历史。

enqueueGlobal 是全局限流,防止突发流量把 LLM API 打爆。比如同时有 200 个 session 都在排队,全局队列控制并发数在一个合理范围内,避免触发 OpenAI 的 rate limit。

排队机制的两层结构:

用户消息进入后,先进 enqueueSession(session 级别队列,保证同一 session 串行),再进 enqueueGlobal(全局队列,控制总并发数)。

Session A 的消息 1、2、3 在 session 队列里排队等待串行执行。Session B 的消息同理。

多个 session 的任务汇入全局队列,受全局并发数限制。最终从全局队列出来的任务才真正调用 LLM API。

op1.drawio.png

Fallback 机制

runAgentTurnWithFallback() 这个函数名已经暗示了它的核心能力。

主模型调用失败时,系统自动切换到配置的 fallback 候选重试。

注意 fallback 只切换 provider 和 model,系统提示词、工具列表等 Agent 配置保持不变,确保切换后的行为语义一致。比如主模型用 GPT-5,fallback 切到 Claude Opus 4.6。

这在生产环境非常实用。OpenAI 偶尔抽风、某个 region 的 API 超时,如果没有 fallback,用户就只能看到一个错误提示。

有了自动切换,用户几乎感知不到后端出了问题,回复可能慢了 1-2 秒,但至少不会断。

Hook 插入点

链路中埋了多个 Hook 可以介入执行过程,类似 Webpack 的 plugin 机制:

1)before_model_resolve 在模型选择之前触发,可以根据用户身份、消息内容动态切换模型。比如复杂的走 Claude Opus 4.6,简单的走免费模型。

2)before_prompt_build 在构建提示词之前触发,可以注入额外的上下文信息。比如从外部知识库拉取相关文档塞进去,做 RAG 增强。

3)llm_input 在 LLM 调用之前触发,可以拦截并修改最终发给 LLM 的完整输入。适合做日志审计、敏感词过滤这类横切逻辑。

流式输出

对于支持流式的渠道,Agent 的回复是边生成边推送的,通过 WebSocket 实时下发 token,不用等全部生成完再发。

用户看到的效果就是”打字机”一样一个字一个字蹦出来,体验比等 5-10 秒后突然弹出一大段文字好得多。

不过不是所有渠道都支持流式。

Telegram 没有原生的 WebSocket 流式推送,OpenClaw 用了两种模拟策略:优先使用 Telegram Bot API 较新的草稿消息接口(sendMessageDraft),如果不可用则 fallback 到先发一条消息再用 editMessageText 循环更新内容,逐步追加生成内容来模拟流式效果。

面试官追问

提问:如果用户连续快速发了 5 条消息,Agent 会怎么处理?会不会每条都触发一次完整的 LLM 调用?

回答:不会每条都独立跑一遍。session 级别的排队机制保证同一个 session 串行执行,第 1 条消息在 Agent 里跑的时候,后面 4 条在队列里排着。等第 1 条处理完,第 2 条才会进入 Agent,这时候第 2 条已经能看到第 1 条的完整对话历史了。不过具体策略可以优化,比如设一个 500ms 的 debounce 窗口,把短时间内的多条消息合并成一条再处理,减少 LLM 调用次数。

提问:fallback 切换模型后,系统提示词和工具列表会不会跟着变?

回答:不会变。OpenClaw 的 fallback 是 model-level 的,只切换 provider 和 model,系统提示词、工具列表等其余 Agent 配置全部保持不变。这个设计是有意为之,fallback 的目的是应对 provider 故障,不应该改变 Agent 的行为语义,否则用户会感知到前后不一致。

提问:幂等性 key 是怎么生成的?如果两个不同用户恰好发了一模一样的消息内容,会不会被误判为重复?

回答:不会。idempotencyKey 不是根据消息内容生成的,通常是用渠道推送过来的消息 ID,比如 Telegram 的 message_id、Discord 的 message snowflake。这些 ID 在渠道层面就是全局唯一的,跟消息内容无关。两个用户发了一模一样的文字,message_id 完全不同,不会触发幂等拦截。

目录
在 OpenClaw 中,一条用户消息从进入系统到收到回复,完整链路是怎样的?