Skip to content

OpenClaw 源码解读(十七)自动回复引擎

一、导读

自动回复引擎(Auto-Reply Engine)是 OpenClaw 的心脏。当一条消息从 WhatsApp、Telegram、Discord、Slack 或任何渠道进入系统时,正是这个引擎决定:要不要回复?用哪个 Agent 回复?怎么回复?回复发到哪里?

它的源码位于 src/auto-reply/,约 100+ 个文件,是整个项目中最庞大、最复杂的子系统。本文沿着一条消息从"入站"到"出站"的完整生命旅程来拆解这套引擎:

入站消息


渠道 Monitor → 防抖 → 去重 → allowFrom 检查 → @提及门控


dispatchInboundMessage()  ← 统一入口


getReplyFromConfig()      ← 引擎核心
 ├── 命令检测 & 解析
 ├── 指令处理(模型/思考级别/权限提升)
 ├── 内联动作处理
 ├── Agent Runner → AI 模型调用
 └── 回复构造


ReplyDispatcher → 回复标准化 → 渠道投递

![消息生命周期全景流水线](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(十七)自动回复引擎/01-infographic-message-lifecycle-1775150737800.png)


二、消息上下文 —— MsgContext

templating.ts 中的 MsgContext 是整个自动回复引擎的数据载体,定义了入站消息的所有维度:

分类典型字段说明
消息体Body, BodyForAgent, RawBody, CommandBody4 种不同用途的消息体
发送者From, SenderId, SenderName, SenderUsername发送者标识
会话SessionKey, AccountId, ParentSessionKey会话与账户路由
消息IDMessageSid, ReplyToId, RootMessageId消息链路追踪
引用ReplyToBody, ReplyToSender, ReplyToIsQuote引用消息上下文
转发ForwardedFrom, ForwardedFromType, ForwardedDate转发消息元数据
线程ThreadStarterBody, ThreadHistoryBody, ThreadLabel线程上下文
媒体MediaPath, MediaUrl, MediaType, Sticker多媒体附件
群聊ChatType, GroupSubject, GroupMembers群聊元数据
路由OriginatingChannel, OriginatingTo回复路由信息
权限CommandAuthorized, GatewayClientScopes命令授权状态

四种消息体的区别

  • Body:原始消息文本
  • BodyForAgent:Agent 提示词(可能包含信封/历史/上下文)
  • RawBody / CommandBody:用于命令检测(去除结构化上下文后的纯文本)
  • BodyForCommands:命令解析专用(最干净的文本)

FinalizedMsgContextMsgContext 的安全化版本——CommandAuthorized 字段被强制为 boolean(默认 false),实现"默认拒绝"的安全策略。

![MsgContext 消息上下文结构](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(十七)自动回复引擎/02-infographic-msg-context-structure-1775150738528.png)


三、入站消息分发 —— dispatch.ts

dispatchInboundMessage() 是所有渠道消息进入自动回复引擎的统一入口

typescript
async function dispatchInboundMessage(params: {
  ctx: MsgContext | FinalizedMsgContext;
  cfg: OpenClawConfig;
  dispatcher: ReplyDispatcher;
  replyOptions?: Omit<GetReplyOptions, "onToolResult" | "onBlockReply">;
  replyResolver?: typeof getReplyFromConfig;
}): Promise<DispatchInboundResult> {
  const finalized = finalizeInboundContext(params.ctx);
  return await withReplyDispatcher({
    dispatcher: params.dispatcher,
    run: () => dispatchReplyFromConfig({ ... }),
  });
}

三个关键设计:

  1. Context 安全化finalizeInboundContext()CommandAuthorized 默认为 false
  2. Dispatcher 生命周期保证withReplyDispatcher() 确保无论成功还是异常,Dispatcher 都会调用 markComplete() + waitForIdle()——这防止了回复丢失
  3. 三种分发变体
    • dispatchInboundMessage:接收外部 Dispatcher
    • dispatchInboundMessageWithDispatcher:自动创建基础 Dispatcher
    • dispatchInboundMessageWithBufferedDispatcher:创建带 Typing 指示器的 Dispatcher

![入站消息分发三步设计与三种变体](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(十七)自动回复引擎/03-infographic-dispatch-entry-1775150739592.png)


四、入站信封格式化 —— envelope.ts

Agent 看到的不是裸消息,而是带有丰富元数据的信封格式

[WhatsApp Alice +2min Thu 2026-03-25 14:30 CST] Hello, how are you?

formatAgentEnvelope() 的信封结构:

[渠道名 发送者 +距上条消息的时间差 星期 时间戳] 消息正文

安全设计——sanitizeEnvelopeHeaderPart() 对信封头部做了三重防护:

  • 换行替换为空格(防止换行注入)
  • [ ] 替换为 () (防止括号逃逸)
  • 多空格折叠

时间戳支持 4 种时区模式:local(系统时区)、utcuser(用户配置的时区)、或直接指定 IANA 时区。信封还会包含星期前缀(ThuMon 等),因为小模型在根据日期推导星期方面出了名的不可靠。

![入站信封格式与安全防护](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(十七)自动回复引擎/04-infographic-envelope-format-1775150740419.png)


五、入站防抖 —— inbound-debounce.ts

当用户快速连发多条消息时,防抖器会将它们合并为一次处理:

typescript
function createInboundDebouncer<T>(params: {
  debounceMs: number;
  buildKey: (item: T) => string | null;  // 按 session key 分组
  shouldDebounce?: (item: T) => boolean;
  onFlush: (items: T[]) => Promise<void>;
})

关键行为:

  • debounceMs > 0shouldDebounce 返回 true 时,消息进入缓冲区
  • 每次新消息到达重置定时器(经典的"滑动窗口"防抖)
  • 命令消息(/status/new 等)不参与防抖,立即处理
  • 支持 per-channel 的防抖时间覆盖

![入站防抖滑动窗口机制](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(十七)自动回复引擎/05-infographic-debounce-mechanism-1775150741308.png)


六、命令检测 —— command-detection.ts

在消息进入 AI 之前,先检测是否是控制命令:

typescript
function isControlCommandMessage(text?: string, cfg?, options?): boolean {
  // 1. 检测斜杠命令(/status, /new, /model ...)
  // 2. 检测中止触发词(如 stop, cancel)
  return hasControlCommand(trimmed, cfg, options) || isAbortTrigger(normalized);
}

hasControlCommand() 的匹配逻辑:

  • 遍历所有注册命令的 textAliases
  • 精确匹配(/status = /status
  • 前缀匹配(/model gpt-4 匹配 /model 命令,前提是 acceptsArgs 为 true)

hasInlineCommandTokens() 是一个粗粒度的预检——通过正则 /(?:^|\s)[/!][a-z]/i 快速判断消息中是否有内联命令标记(如 hey /status),让渠道 monitor 决定是否需要计算 CommandAuthorized

![命令检测三层过滤逻辑](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(十七)自动回复引擎/06-infographic-command-detection-1775150742053.png)


七、回复引擎核心 —— getReplyFromConfig()

这是整个自动回复系统最核心的函数(~400 行),位于 reply/get-reply.ts。它的处理流程可以分为 8 个阶段

阶段 1:Agent & 模型解析

agentId = resolveSessionAgentId(sessionKey, config)
{ defaultProvider, defaultModel, aliasIndex } = resolveDefaultModel(cfg, agentId)

支持三层模型覆盖:

  • 心跳模型覆盖(heartbeatModelOverride
  • 会话模型覆盖(sessionEntry.modelOverride
  • 渠道模型覆盖(channelModelOverride

阶段 2:Workspace 初始化

workspace = ensureAgentWorkspace({ dir, ensureBootstrapFiles })

确保 Agent 工作目录存在,包括引导文件(如 SYSTEM.md)。

applyMediaUnderstanding({ ctx, cfg, agentDir, activeModel })
applyLinkUnderstanding({ ctx, cfg })

将图片、音频、视频、链接等非文本内容转换为 Agent 可理解的文本描述。

阶段 4:命令授权 & 会话初始化

resolveCommandAuthorization({ ctx, cfg, commandAuthorized })
sessionState = initSessionState({ ctx, cfg, commandAuthorized })

这一步会解析会话状态(新建/恢复/重置),包括 20+ 个返回值:sessionEntrysessionStoresessionKeyisNewSessionresetTriggered 等。

阶段 5:指令解析

directiveResult = resolveReplyDirectives({ ... 30+ 参数 ... })

指令系统是一个独立的子系统,解析消息中的内联指令(如模型选择、思考级别、权限提升等)。如果指令本身产生回复(如 /status 命令),直接返回。

阶段 6:内联动作处理

inlineActionResult = handleInlineActions({ ... })

处理不需要 AI 参与的快速动作(如 /new 重置会话、/compact 压缩上下文)。

阶段 7:沙箱媒体暂存

stageSandboxMedia({ ctx, sessionCtx, cfg, sessionKey, workspaceDir })

将用户发送的媒体文件复制到 Agent 沙箱的工作目录。

阶段 8:Agent 运行

return runPreparedReply({ ... 40+ 参数 ... })

最终调用 Agent Runner 执行 AI 推理。

![getReplyFromConfig 八阶段流水线](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(十七)自动回复引擎/07-infographic-eight-stage-pipeline-1775150742792.png)


八、Agent Runner —— AI 调用引擎

reply/agent-runner.ts 中的 runReplyAgent() 是实际调用 AI 模型的地方(~250+ 行)。

8.1 Steer 机制

如果当前会话已有活跃的 AI 运行,且新消息到达:

typescript
if (shouldSteer && isStreaming) {
  const steered = queueEmbeddedPiMessage(followupRun.run.sessionId, followupRun.prompt);
  if (steered && !shouldFollowup) {
    typing.cleanup();
    return undefined;  // 消息已注入到活跃运行中
  }
}

queueEmbeddedPiMessage 尝试将新消息"注入"到正在运行的 Agent turn 中——而不是排队等待。这就是"Steer"(转向)机制:让 Agent 在运行中感知到新的用户输入。

8.2 队列策略

当 Steer 不可用时,队列策略决定消息命运:

动作含义
drop丢弃(心跳消息在 Agent 忙时被丢弃)
enqueue-followup排入后续队列,等当前 run 结束后执行
继续执行直接启动新的 Agent turn

8.3 Memory Flush

在 Agent 运行前,检查是否需要刷新记忆:

typescript
activeSessionEntry = await runMemoryFlushIfNeeded({
  cfg, followupRun, promptForEstimate,
  sessionCtx, defaultModel, agentCfgContextTokens,
  // ...
});

当上下文窗口即将溢出时,自动将历史消息压缩/存储到长期记忆。

8.4 Fallback 机制

runAgentTurnWithFallback() 支持模型降级——当主模型调用失败时,自动切换到备用模型重试。

8.5 Block Streaming

当启用分块流式回复时,Agent 的输出会被拆分成多个小块逐步发送:

typescript
const blockReplyPipeline = createBlockReplyPipeline({
  onBlockReply: opts.onBlockReply,
  timeoutMs: blockReplyTimeoutMs,
  coalescing: blockReplyCoalescing,
  buffer: createAudioAsVoiceBuffer({ isAudioPayload }),
});

分块参数包括最小/最大字符数、断行策略(paragraph/newline/sentence)、超时时间。

![Agent Runner 三大机制](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(十七)自动回复引擎/08-infographic-agent-runner-mechanisms-1775150743718.png)


九、回复分发器 —— ReplyDispatcher

reply/reply-dispatcher.ts 是回复投递的关键中间层。

9.1 三种回复类型

typescript
type ReplyDispatcher = {
  sendToolResult: (payload) => boolean;   // 工具调用结果
  sendBlockReply: (payload) => boolean;   // 流式分块回复
  sendFinalReply: (payload) => boolean;   // 最终完整回复
  waitForIdle: () => Promise<void>;
  markComplete: () => void;
};

9.2 串行化投递

所有回复通过 Promise chain 严格串行化:

typescript
sendChain = sendChain
  .then(async () => {
    if (shouldDelay) {
      await sleep(getHumanDelay(options.humanDelay));
    }
    await options.deliver(normalized, { kind });
  })
  .catch((err) => options.onError?.(err, { kind }))
  .finally(() => {
    pending -= 1;
    if (pending === 0) {
      unregister();
      options.onIdle?.();
    }
  });

9.3 Human Delay

一个有趣的设计——可以配置"仿人类延迟":

typescript
const DEFAULT_HUMAN_DELAY_MIN_MS = 800;
const DEFAULT_HUMAN_DELAY_MAX_MS = 2500;

在分块回复之间随机插入 800ms~2500ms 的延迟,让回复看起来更像真人在打字。第一个分块不延迟(避免初始等待感)。

9.4 预留计数器

Dispatcher 使用"预留"计数器防止过早触发 idle:

创建时: pending = 1 (预留位)
每次入队: pending += 1
每次完成: pending -= 1
markComplete: 释放预留位
pending === 0: 触发 idle

这保证了即使所有回复都瞬间完成,也不会在 markComplete() 之前误触发 idle。

![ReplyDispatcher 串行投递与预留计数器](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(十七)自动回复引擎/09-infographic-reply-dispatcher-1775150744527.png)


十、回复载荷 —— ReplyPayload

typescript
type ReplyPayload = {
  text?: string;           // 文本内容
  mediaUrl?: string;       // 单个媒体 URL
  mediaUrls?: string[];    // 多个媒体 URL
  replyToId?: string;      // 引用回复的消息 ID
  replyToTag?: boolean;    // 是否添加引用标记
  replyToCurrent?: boolean; // 引用当前消息
  audioAsVoice?: boolean;  // 音频作为语音气泡发送
  isError?: boolean;       // 错误消息标记
  isReasoning?: boolean;   // 思考/推理过程标记
  channelData?: Record<string, unknown>;  // 渠道特定数据
};

特别注意 isReasoning 字段——它标记了 AI 的"思考过程"内容。不同渠道的处理策略不同:Discord 可以用折叠块展示,WhatsApp/Web 则完全抑制。


十一、静默回复令牌

typescript
export const HEARTBEAT_TOKEN = "HEARTBEAT_OK";
export const SILENT_REPLY_TOKEN = "NO_REPLY";

这两个令牌是 Agent 与引擎之间的"暗号":

  • NO_REPLY:Agent 认为不需要回复(比如消息不是对它说的,或者没有需要回答的内容)
  • HEARTBEAT_OK:心跳检查回复(定时探活,不应发送给用户)

isSilentReplyText() 使用精确匹配——只有整条消息是 NO_REPLY(允许前后空白)才算静默。这防止了 #19537 问题:实质性回复恰好以 NO_REPLY 结尾被误吞。

stripSilentToken() 更宽松——支持从混合内容中剥离尾部的 NO_REPLY 令牌。

![静默回复令牌协议](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(十七)自动回复引擎/10-infographic-silent-tokens-1775150745456.png)


十二、Typing 控制器

自动回复引擎在 Agent 运行期间会模拟"正在输入"状态:

typescript
const typing = createTypingController({
  onReplyStart: opts?.onReplyStart,
  onCleanup: opts?.onTypingCleanup,
  typingIntervalSeconds: 6,  // 默认每 6 秒发一次 typing
  silentToken: SILENT_REPLY_TOKEN,
});

Typing 策略按消息来源分级:

策略适用场景行为
user_message用户直接消息完整 typing 指示
system_event系统事件触发轻量 typing
internal_webchatWeb 控制台消息轻量 typing
heartbeat定时心跳可完全抑制
auto自动检测根据上下文决定

十三、指令系统

reply/directives.tsdirective-handling.*.ts 实现了丰富的内联指令:

13.1 模型选择指令

/model gpt-4       → 切换模型
/model list        → 列出可用模型
/model status      → 显示当前模型

支持模型别名解析(alias → provider/model)。

13.2 思考级别指令

/think high        → 设置思考级别
/think off         → 关闭思考

6 个思考级别:offminimallowmediumhighxhigh

13.3 权限提升指令

/elevate           → 提升为 elevated 模式

Elevated 模式解锁更强的工具权限,但受 allowlist 控制。

13.4 快速通道

directive-handling.fast-lane.ts 实现了"快速通道"——某些指令(如 /status/help)不需要经过 AI,可以直接生成回复并提前返回。

![指令系统四类指令与快速通道](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(十七)自动回复引擎/11-infographic-directive-system-1775150746257.png)


十四、命令系统

reply/commands-core.tscommands-*.ts 实现了 30+ 个斜杠命令:

命令文件命令功能
commands-session.ts/new, /reset, /compact会话管理
commands-models.ts/model模型选择
commands-config.ts/set, /unset配置修改
commands-bash.ts/bash执行 shell 命令
commands-tts.ts/tts文字转语音
commands-subagents.ts/agents子 Agent 管理
commands-compact.ts/compact上下文压缩
commands-approve.ts/approve工具执行审批
commands-system-prompt.ts/system查看系统提示词
commands-context-report.ts/context上下文使用报告
commands-export-session.ts/export导出会话
commands-acp.ts/acpACP 控制

命令授权是双层的:

  1. CommandAuthorized —— 渠道级别的授权(由 allowFrom 决定)
  2. command-gates.ts —— 命令级别的门控(某些命令需要特定权限)

十五、回复标准化

normalize-reply.ts 在回复发送前做最后一轮处理:

  1. 静默回复过滤NO_REPLY → 跳过
  2. 心跳令牌剥离HEARTBEAT_OK → 跳过
  3. 空回复检测:无文本且无媒体 → 跳过
  4. Response Prefix 注入:在回复文本前添加可配置的前缀
  5. 模板插值:前缀支持 等变量

Skip 原因被回调通知(onSkip),让渠道可以做差异化处理(比如 Telegram 在空回复时发送默认消息)。


十六、队列系统

reply/queue/ 实现了 per-session 的消息队列:

16.1 入队

typescript
enqueueFollowupRun(queueKey, followupRun, resolvedQueue)

当 Agent 正在处理消息时,新消息进入 followup 队列。

16.2 消耗

typescript
drainQueue(queueKey, resolvedQueue)

当前 turn 结束后,从队列中取出下一条消息处理。

16.3 队列模式

模式行为
replace新消息替换队列中的旧消息(只保留最新)
append新消息追加到队列尾部
drop队列满时丢弃新消息

16.4 清理

cleanup.ts 定期清理过期的队列条目,防止内存泄漏。


十七、各渠道的接入模式

所有渠道都通过相同的模式接入自动回复引擎:

渠道 Monitor (监听消息)
    → 构造 MsgContext (填充渠道特定字段)
    → 创建 ReplyDispatcher (渠道特定的投递函数)
    → 调用 dispatchInboundMessage()
    → 处理投递结果

各渠道的差异主要在两处:

  1. MsgContext 填充:Telegram 有 sticker/topic,Discord 有 Components v2,Slack 有 thread resolution
  2. 回复投递:每个渠道有自己的 deliver-reply.ts,处理渠道特定的消息格式、分块限制、媒体上传

十八、心跳系统

src/infra/heartbeat-runner.ts 实现了定时触发的 Agent 回复:

Cron 定时器 → heartbeat-runner → getReplyFromConfig(isHeartbeat=true)
    → Agent 运行 → 如果回复非 NO_REPLY → 投递到渠道

心跳的独特之处:

  • isHeartbeat: true 标记,让引擎知道这是系统触发而非用户消息
  • 独立的模型覆盖(heartbeatModelOverride
  • 在 Agent 忙时直接 drop(不排队)
  • Typing 指示器可配置抑制

十九、安全设计

层级安全措施
上下文安全化finalizeInboundContext 默认 CommandAuthorized=false
信封注入防护换行/括号转义
命令授权双层门控(渠道级 + 命令级)
静默回复精确匹配防误吞
队列溢出模式化策略(replace/drop/append)
Dispatcher 生命周期预留计数器 + finally 保证
防抖per-session 滑动窗口

二十、设计模式总结

模式应用位置效果
PipelinegetReplyFromConfig 8 阶段清晰的处理流水线
Strategy队列模式(replace/append/drop)可配置的队列行为
Chain of Responsibility指令 → 内联动作 → Agent层层过滤,提前返回
ObserverReplyDispatcher callbacks投递事件通知
Debounceinbound-debounce防止消息风暴
SteerqueueEmbeddedPiMessage运行中注入新消息
Reservation CounterDispatcher pending防止过早 idle
Human Delay分块回复间随机延迟仿人类节奏
Token ProtocolNO_REPLY / HEARTBEAT_OKAgent-引擎暗号
EnvelopeformatAgentEnvelope结构化消息上下文

![设计模式全景](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(十七)自动回复引擎/12-infographic-design-patterns-1775150747219.png)


二十一、推荐阅读顺序

  1. src/auto-reply/tokens.ts — 静默回复令牌
  2. src/auto-reply/types.ts — 核心类型(GetReplyOptions, ReplyPayload)
  3. src/auto-reply/templating.ts — MsgContext 消息上下文
  4. src/auto-reply/envelope.ts — 信封格式化
  5. src/auto-reply/command-detection.ts — 命令检测
  6. src/auto-reply/inbound-debounce.ts — 入站防抖
  7. src/auto-reply/dispatch.ts — 分发入口
  8. src/auto-reply/reply/reply-dispatcher.ts — 回复分发器
  9. src/auto-reply/reply/normalize-reply.ts — 回复标准化
  10. src/auto-reply/reply/inbound-context.ts — Context 安全化
  11. src/auto-reply/reply/get-reply-directives.ts — 指令解析
  12. src/auto-reply/reply/get-reply-inline-actions.ts — 内联动作
  13. src/auto-reply/reply/get-reply.ts — 引擎核心(8 阶段流水线)
  14. src/auto-reply/reply/get-reply-run.ts — 准备好的回复执行
  15. src/auto-reply/reply/agent-runner.ts — Agent Runner
  16. src/auto-reply/reply/agent-runner-execution.ts — 带 Fallback 的执行
  17. src/auto-reply/reply/block-streaming.ts — 分块流式回复
  18. src/auto-reply/reply/queue.ts — 消息队列

二十二、思考题

  1. Steer 机制(运行中注入消息)和队列机制(排队等待)各有什么优缺点? 在什么情况下 Steer 可能导致 Agent 混乱?

  2. ReplyDispatcher 的"预留计数器"设计解决了什么竞态条件? 如果没有这个预留,会发生什么?

  3. 信封格式中包含星期前缀是因为"小模型推导星期不可靠"——这个问题在大模型时代还存在吗? 这个设计决策是否值得保留?

  4. Human Delay(仿人类延迟)功能的存在暗示了什么使用场景? 这在什么情况下是必要的,什么情况下反而有害?

  5. getReplyFromConfig() 接收 40+ 个参数——这是否违反了"函数参数不宜过多"的原则? 如果重构,你会怎么设计参数传递?

读文档、看源码、写代码,理解 AI Agent 本质 🤖