Skip to content

第五章 消息循环与事件驱动:Agent 为什么不会乱

核心问题:消息会乱序、任务会并发,OpenClaw 靠什么保证不断线、不串线,还能主动做事?


上一章解决的是“Agent 能不能动手”。这一章解决的是“动手以后会不会乱”。

只要 Agent 会持续推进任务,就一定会碰到这些情况:消息几乎同时到达、用户中途改方向、长任务没结束又来了补充、没人说话时系统还要定期自查。所以这一章讲的不是普通聊天,而是一个长期运行的 Agent 怎样保持节奏。

OpenClaw 主要靠两套机制:

  • 消息循环:先排队,再执行,守住顺序。
  • 事件驱动:不只处理用户消息,也处理 heartbeat、cron、子任务结果等事件。

第一节 为什么需要这套机制

这一节只回答一个问题:为什么 Agent 不能继续沿用普通聊天系统那套“收到消息就回一条”的做法。

1.1 从“一问一答”变成“持续协作”

普通聊天系统通常是:

text
收到一句话
→ 回一句话
→ 结束

这很适合问答,但不适合持续推进任务。

OpenClaw 更像这样:

text
接到任务
→ 判断下一步
→ 需要时调用工具
→ 看结果继续推进
→ 直到完成、卡住或需要你拍板

这时系统维护的不是“一句回复”,而是一个正在进行的现场。只要现场还在继续,消息就不可能永远规规矩矩地一条条来。

比如你先说“跑一下测试”,Agent 刚开始执行,你又补了一句“先切到 feature/login”,这时定时任务也正好触发。如果系统还是“谁先来就先处理谁”,顺序就会乱。

所以关键问题变成了:系统能不能守住顺序、状态和上下文。

1.2 没有队列和隔离,会发生什么

我们先来看一些场景:

场景没有调度时会怎样
先创建文件,再写内容写入可能先跑,结果报文件不存在
多人同时用同一个 Agent上下文混在一起,回复串线
一个任务跑很久后来的消息一直堵住
任务做到一半改方向系统没接住,继续按旧方向跑

这些问题背后其实都指向同一件事:Agent 是有状态的。 它不是每次都从零开始,而是带着“现在做到哪一步、桌上已经有什么材料”继续往下走。所以消息一乱,坏掉的就不只是当前这一句,而是整段任务现场。

于是 OpenClaw 不能把消息处理理解成“收件箱里有新消息,我就马上答”,而必须先解决一个更基础的问题:消息到达以后,先整理,再执行。 这个“先整理”的过程,就是队列;这个“别互相打扰”的机制,就是隔离。

1.3 为什么是事件驱动

OpenClaw 处理的不只是用户消息,还包括:

  • 用户新消息
  • heartbeat 巡检
  • cron 定时任务
  • 子任务返回结果
  • 某次运行状态变化后的后续动作

这些东西来源不同,但本质很像:有一件新事情发生了,系统要决定要不要处理、什么时候处理、在哪个现场里处理。 这就是事件驱动的视角。它不再把系统理解成“有人请求,我就同步回一个结果”,而是理解成:

text
发生一个事件
→ 进入统一入口
→ 调度器决定去哪
→ 系统按规则推进

这样做有两个直接好处:

  • 入口统一:用户消息、heartbeat、cron 可以走同一套框架。
  • 时间解耦:事件先进入系统,再由调度层决定何时执行。

一句话说,这一章讲的是:当 Agent 从“回一句话”变成“持续协作”,系统就必须先整理消息,再推进任务。


第二节 它是怎么跑起来的

这一节要把整条链路讲清楚:消息进来以后,OpenClaw 到底怎么让它有序地跑起来。

2.1 先分清三个词:messagesessionlane

理解消息系统,最容易混的就是这三个词。先一句话分清:

含义
message一条新输入
session一段持续合作的会话现场
lane这段会话对应的一条执行通道

这里把同一会话的执行通道叫 lane(泳道)。核心规则可以先这样理解:

text
同一个 session lane 串行执行
不同 session 可以并行推进
整体并发再由 global lane 收口

这条规则很关键,因为它同时解决了两件事:

  • 同一会话里的前后顺序不乱
  • 不同会话不会互相堵死

所以重点不是“系统里有队列”,而是:系统先把现场分开,再把每个现场的顺序守住。

2.2 同一个 lane 里来了新消息,怎么接

分了 lane 以后,还剩下一个特别现实的问题:同一个 lane 里,如果 Agent 正在忙,这时又来了一条消息,怎么办?

这时就轮到 queue mode 出场了。所谓 queue mode,你可以先把它理解成:系统接到“忙中插入的新消息”时,该采取什么态度。 按当前 OpenClaw 文档,最容易记的心智模型是:四个常见模式 + 一个 backlog 变体

模式大意适合什么情况
collect先收集,等当前运行结束后合并处理普通补充,默认最稳
followup当前运行结束后,再开启下一轮明确的先后顺序
steer把新消息注入当前运行,立刻改方向需要边做边纠偏
interrupt中止当前运行,先处理最新消息当前方向已经错了
steer-backlog先把当前运行拉回正轨,同时把后续补充保留下来既要立刻纠偏,又不想丢后面的细节

这里要顺手记两个细节:steer 在非 streaming 场景下可能会回退成 followup;另外旧文档里的 queue 现在更像 steer 一侧的 legacy alias,不建议作为新读者的第一个术语。

如果想用一句话去记:

  • collect:先做完手上的。
  • followup:这件事排在后一步。
  • steer:不用停,但现在拐弯。
  • interrupt:先刹车,再处理新的。
  • steer-backlog:先纠偏,后面的补充别丢。

这里最容易误解的一点是:这些模式不是“谁更高级”,它们只是对应了不同的协作意图。很多时候,默认的 collect 反而是最稳的,因为它不会在任务还没收尾时频繁打断现场。只有在确实需要立即纠偏时,steerinterrupt 才更有价值。

2.3 高峰期为什么不会挤爆

光有 queue mode 还不够,因为真实系统里,消息不一定是一条一条慢慢来的。它可能突然一口气冲进来很多条。

比如:

  • 用户连续发了五句补充;
  • 某个群聊一下子刷了很多消息;
  • 某个自动化场景在短时间内触发了多次事件。

如果系统对每条都立刻开一次运行,结果通常不会更快,反而更乱。所以消息系统还有一层更“后台”的兜底策略。
而且这层和 queue mode 还不是一回事:最新 OpenClaw 会先在入站阶段做 dedupe 和 messages.inbound debouncing,避免同一条消息重复触发 run;等 run 已经活起来以后,才轮到 queue mode 决定新消息怎么接。

进入 queue 这一层以后,最常见的是这三个概念:

机制它在解决什么
debounceMs短时间内连发很多消息时,先等一小会儿再合并处理
cap给等待队列设上限,避免越堆越长
drop如果已经超了上限,决定丢旧的、丢新的,还是先压缩再保留

先说 debounceMs。它就是我们平时说的“防抖”。意思是:如果很短时间内连续来了几条消息,系统先别急着每条都单独开工,而是等一个很小的窗口,看会不会还有补充。

这样做的好处很直接:

  • 减少无意义的频繁启动;
  • 把几条本来就属于一组的消息合起来理解;
  • 降低“刚开始跑就又被新消息改方向”的概率。

再说 cap。它本质上是一个上限,因为任何系统都不可能无限堆消息。没有上限,就意味着高峰期可能把内存、上下文和运行时间一起拖垮。

最后是 drop。这个词听起来有点可怕,但它其实是在做一个很现实的选择:如果队列已经满了,系统总得决定到底保留什么。

常见思路一般有三类:

  • 丢最旧的;
  • 丢最新的;
  • 先把旧消息压缩成摘要,再把重点留下来。

2.4 Heartbeat:没人发消息时,Agent 为什么还会动

如果没人发消息,系统仍然可以按节奏做巡检。这就是 heartbeat。

最直接的理解是:

text
定时唤醒
→ 快速检查
→ 没事就静默结束
→ 有事再提醒用户

heartbeat 的检查内容通常写在 HEARTBEAT.md 里。它更像一张短清单,而不是一段长提示词,例如:

markdown
# Heartbeat checklist

- 看一下有没有超时没回的消息
- 检查后台任务有没有卡住
- 如果今天有要跟进的事情,顺手提醒一次

而 heartbeat 也不是“时间一到就无脑吵醒你”。默认情况下,它甚至可以只在内部跑一轮而不对外发送,因为默认 target 就可以是 none。运行时还会结合主队列是否正忙、activeHours、目标路由、HEARTBEAT.md 是否有效等条件来决定要不要真正执行或对外发送。目标只有一个:主动,但别烦人。

可以把这些条件理解成“心跳的礼貌”:

  • 主队列正忙时,先别硬插;
  • 不在允许主动提醒的时间段时,先安静待着;
  • 没有有效外部目标时,也可以只做内部巡检,不主动发消息。

这里还有一个非常关键的约定必须记住:HEARTBEAT_OK。它的意思可以直接翻译成:“我检查过了,没什么值得打扰你的。” 更严格一点说,只有当它出现在回复开头或结尾、而且剩余内容很短时,运行时才会把它当作真正的 heartbeat ACK。

2.5 Heartbeat 和 Cron 的分工

它们都能“定时触发”,但解决的问题不同:

维度HeartbeatCron
作用巡检、跟进、轻提醒准点执行某件事
风格没事就安静到点就执行
上下文关系更贴近当前会话常常可独立运行
例子看看有没有该跟进的事每天 9 点发周报

仓库里的使用文档还给出了一种常见做法:cron 任务可以跑在独立会话里,例如 cron:<job.id>
不过这只是常见的 isolated 形态之一;当前文档里也能看到 currentsession:custom-id 这类 session 目标。核心不是记住某个前缀,而是理解:cron 可以选择复用当前会话,也可以故意隔离出去

一句话说:

  • 想“定期看看有没有新情况”,用 heartbeat。
  • 想“某天某时一定做一件事”,用 cron。

第三节 实际配置时怎么用

前两节讲的是原理。这一节讲更实际的:如果你真的开始用这套系统,应该怎么配、怎么用,才不容易把自己绕进去。

3.1 先求稳,不要一上来就追求最灵活

很多人第一次碰到这些配置项,直觉都会是:“既然系统这么强,是不是应该把最灵活的模式都打开?”

通常不是。对刚开始使用的人来说,最重要的不是“它能不能立刻聪明地改路”,而是:它的行为是不是稳定、好猜、可复盘。

所以更稳的起步方式通常是:

项目起步建议
默认 queue mode先用 collect
并发先保守,不要把全局并发开太高
heartbeat先写很短的检查清单
cron只给真正需要准点执行的任务

为什么先用 collect?因为它最符合大多数人的直觉:当前任务先做完,新的补充先记着,做完以后再统一接上。这样更稳定,也更容易观察。等你熟悉以后,再根据具体场景引入 steerinterrupt

3.2 四个常用模式怎么选

如果你只想记一个最实用的判断法,可以记下面这张表:

你的真实意图更适合的模式
“我只是补充一点信息,你先做完手上的”collect
“顺序别错,这件做完再做下一件”followup
“别停,但请马上按我说的新方向调整”steer
“先停,现在这条路已经不该走了”interrupt

这里面最容易混的是 steerinterrupt。区别其实很朴素:steer 是“边走边拐弯”,interrupt 是“先刹车,再重来”。

如果当前运行还有保留价值,用 steer。如果当前运行已经明显错了,继续跑只会浪费时间,用 interrupt

followup 的价值在于“把顺序说死”。它适合那种非常明确的任务链,比如:

text
先跑测试
→ 测试跑完以后再整理失败用例
→ 整理完以后再写修复建议

这种时候,followup 往往比 collect 更明确,因为它不只是“等会儿再说”,而是“这件事一定排在后一步”。

3.3 让 heartbeat 和 cron 分工

一个很实用的组合方式是这样的:

text
heartbeat:
  负责轻量巡检、跟进、提醒

cron:
  负责准点执行、报表、例行任务

比如一个学生项目场景:heartbeat 每隔一段时间看一下,有没有实验跑完、有没有同学发来新消息、有没有待跟进事项;cron 每天晚上固定整理一次实验结果摘要。

这时两者就各干各的:heartbeat 保持“活着”,cron 保持“准时”。

反过来,如果你拿 cron 去做持续巡检,通常会很僵,因为 cron 更像一个固定时刻的锤子。它擅长“到点就做”,但不擅长“围着当前会话持续观察有没有新变化”。

同样,如果你拿 heartbeat 去承担所有定时任务,也会变得混乱,因为 heartbeat 的本意是巡检,不是时间表。所以更好的思路不是二选一,而是:该巡的交给 heartbeat,该准点的交给 cron。

3.4 排障时先看调度,再看模型

很多人遇到问题时,第一反应是:“是不是模型理解错了?”

有时候确实是,但在消息系统这一层,更常见的原因其实是前面几步就已经乱了。所以更稳的排障顺序通常是:

  1. 先看这条消息是不是进了正确的 session;
  2. 再看它是不是进了正确的 lane;
  3. 再看当前 queue mode 是否符合你的真实意图;
  4. 然后看 heartbeat 或 cron 有没有被跳过、延后或静默处理;
  5. 最后再去怀疑模型本身的判断。

你可以把它记成一句顺口的话:先看队排没排对,再看人做没做好。

比如:

  • 你以为系统“没回你”,其实它还在前一个长任务后面排队;
  • 你以为 heartbeat “失效了”,其实它可能只是正常返回了 HEARTBEAT_OK,或者这一轮只做了内部巡检、没有对外发送;
  • 你以为 cron “没执行”,其实它跑在独立会话里,结果不在你当前聊天窗口里。

这些都不是模型理解问题,而是运行机制问题。一旦把这个排障顺序建立起来,你会发现很多问题都能更快定位。

如果把第三节收成一句话,就是:对于大多数人来说,最好的用法不是一开始把系统调到最灵活,而是先让它稳定、安静、可预期,再一点点放开更主动的能力。


本章小结

这一章的重点只有两个。

第一,OpenClaw 不能再按“收到一句就回一句”的方式工作,因为持续协作一定会遇到并发、顺序、上下文和时间问题。

第二,OpenClaw 处理这些问题,不靠一个神奇提示词,而靠一套运行机制:

机制作用
command queue先整理消息,再执行
session把不同现场分开
lane守住同一现场里的顺序
queue mode决定忙时怎么接新消息(collect / steer / followup / interrupt / steer-backlog
heartbeat定期巡检
cron准点执行

所以消息循环和事件驱动的意义,不只是“更工程化”,而是让 Agent 真能长期稳定地协作。

下一章: chapter6/index.md