第五章 消息循环与事件驱动:Agent 为什么不会乱
核心问题:消息会乱序、任务会并发,OpenClaw 靠什么保证不断线、不串线,还能主动做事?
上一章解决的是“Agent 能不能动手”。这一章解决的是“动手以后会不会乱”。
只要 Agent 会持续推进任务,就一定会碰到这些情况:消息几乎同时到达、用户中途改方向、长任务没结束又来了补充、没人说话时系统还要定期自查。所以这一章讲的不是普通聊天,而是一个长期运行的 Agent 怎样保持节奏。
OpenClaw 主要靠两套机制:
- 消息循环:先排队,再执行,守住顺序。
- 事件驱动:不只处理用户消息,也处理 heartbeat、cron、子任务结果等事件。
第一节 为什么需要这套机制
这一节只回答一个问题:为什么 Agent 不能继续沿用普通聊天系统那套“收到消息就回一条”的做法。
1.1 从“一问一答”变成“持续协作”
普通聊天系统通常是:
收到一句话
→ 回一句话
→ 结束这很适合问答,但不适合持续推进任务。
OpenClaw 更像这样:
接到任务
→ 判断下一步
→ 需要时调用工具
→ 看结果继续推进
→ 直到完成、卡住或需要你拍板这时系统维护的不是“一句回复”,而是一个正在进行的现场。只要现场还在继续,消息就不可能永远规规矩矩地一条条来。
比如你先说“跑一下测试”,Agent 刚开始执行,你又补了一句“先切到 feature/login”,这时定时任务也正好触发。如果系统还是“谁先来就先处理谁”,顺序就会乱。
所以关键问题变成了:系统能不能守住顺序、状态和上下文。
1.2 没有队列和隔离,会发生什么
我们先来看一些场景:
| 场景 | 没有调度时会怎样 |
|---|---|
| 先创建文件,再写内容 | 写入可能先跑,结果报文件不存在 |
| 多人同时用同一个 Agent | 上下文混在一起,回复串线 |
| 一个任务跑很久 | 后来的消息一直堵住 |
| 任务做到一半改方向 | 系统没接住,继续按旧方向跑 |
这些问题背后其实都指向同一件事:Agent 是有状态的。 它不是每次都从零开始,而是带着“现在做到哪一步、桌上已经有什么材料”继续往下走。所以消息一乱,坏掉的就不只是当前这一句,而是整段任务现场。
于是 OpenClaw 不能把消息处理理解成“收件箱里有新消息,我就马上答”,而必须先解决一个更基础的问题:消息到达以后,先整理,再执行。 这个“先整理”的过程,就是队列;这个“别互相打扰”的机制,就是隔离。
1.3 为什么是事件驱动
OpenClaw 处理的不只是用户消息,还包括:
- 用户新消息
- heartbeat 巡检
- cron 定时任务
- 子任务返回结果
- 某次运行状态变化后的后续动作
这些东西来源不同,但本质很像:有一件新事情发生了,系统要决定要不要处理、什么时候处理、在哪个现场里处理。 这就是事件驱动的视角。它不再把系统理解成“有人请求,我就同步回一个结果”,而是理解成:
发生一个事件
→ 进入统一入口
→ 调度器决定去哪
→ 系统按规则推进这样做有两个直接好处:
- 入口统一:用户消息、heartbeat、cron 可以走同一套框架。
- 时间解耦:事件先进入系统,再由调度层决定何时执行。
一句话说,这一章讲的是:当 Agent 从“回一句话”变成“持续协作”,系统就必须先整理消息,再推进任务。
第二节 它是怎么跑起来的
这一节要把整条链路讲清楚:消息进来以后,OpenClaw 到底怎么让它有序地跑起来。
2.1 先分清三个词:message、session、lane
理解消息系统,最容易混的就是这三个词。先一句话分清:
| 词 | 含义 |
|---|---|
message | 一条新输入 |
session | 一段持续合作的会话现场 |
lane | 这段会话对应的一条执行通道 |
这里把同一会话的执行通道叫 lane(泳道)。核心规则可以先这样理解:
同一个 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 反而是最稳的,因为它不会在任务还没收尾时频繁打断现场。只有在确实需要立即纠偏时,steer 和 interrupt 才更有价值。
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。
最直接的理解是:
定时唤醒
→ 快速检查
→ 没事就静默结束
→ 有事再提醒用户heartbeat 的检查内容通常写在 HEARTBEAT.md 里。它更像一张短清单,而不是一段长提示词,例如:
# Heartbeat checklist
- 看一下有没有超时没回的消息
- 检查后台任务有没有卡住
- 如果今天有要跟进的事情,顺手提醒一次而 heartbeat 也不是“时间一到就无脑吵醒你”。默认情况下,它甚至可以只在内部跑一轮而不对外发送,因为默认 target 就可以是 none。运行时还会结合主队列是否正忙、activeHours、目标路由、HEARTBEAT.md 是否有效等条件来决定要不要真正执行或对外发送。目标只有一个:主动,但别烦人。
可以把这些条件理解成“心跳的礼貌”:
- 主队列正忙时,先别硬插;
- 不在允许主动提醒的时间段时,先安静待着;
- 没有有效外部目标时,也可以只做内部巡检,不主动发消息。
这里还有一个非常关键的约定必须记住:HEARTBEAT_OK。它的意思可以直接翻译成:“我检查过了,没什么值得打扰你的。” 更严格一点说,只有当它出现在回复开头或结尾、而且剩余内容很短时,运行时才会把它当作真正的 heartbeat ACK。
2.5 Heartbeat 和 Cron 的分工
它们都能“定时触发”,但解决的问题不同:
| 维度 | Heartbeat | Cron |
|---|---|---|
| 作用 | 巡检、跟进、轻提醒 | 准点执行某件事 |
| 风格 | 没事就安静 | 到点就执行 |
| 上下文关系 | 更贴近当前会话 | 常常可独立运行 |
| 例子 | 看看有没有该跟进的事 | 每天 9 点发周报 |
仓库里的使用文档还给出了一种常见做法:cron 任务可以跑在独立会话里,例如 cron:<job.id>。
不过这只是常见的 isolated 形态之一;当前文档里也能看到 current 或 session:custom-id 这类 session 目标。核心不是记住某个前缀,而是理解:cron 可以选择复用当前会话,也可以故意隔离出去。
一句话说:
- 想“定期看看有没有新情况”,用 heartbeat。
- 想“某天某时一定做一件事”,用 cron。
第三节 实际配置时怎么用
前两节讲的是原理。这一节讲更实际的:如果你真的开始用这套系统,应该怎么配、怎么用,才不容易把自己绕进去。
3.1 先求稳,不要一上来就追求最灵活
很多人第一次碰到这些配置项,直觉都会是:“既然系统这么强,是不是应该把最灵活的模式都打开?”
通常不是。对刚开始使用的人来说,最重要的不是“它能不能立刻聪明地改路”,而是:它的行为是不是稳定、好猜、可复盘。
所以更稳的起步方式通常是:
| 项目 | 起步建议 |
|---|---|
| 默认 queue mode | 先用 collect |
| 并发 | 先保守,不要把全局并发开太高 |
| heartbeat | 先写很短的检查清单 |
| cron | 只给真正需要准点执行的任务 |
为什么先用 collect?因为它最符合大多数人的直觉:当前任务先做完,新的补充先记着,做完以后再统一接上。这样更稳定,也更容易观察。等你熟悉以后,再根据具体场景引入 steer 或 interrupt。
3.2 四个常用模式怎么选
如果你只想记一个最实用的判断法,可以记下面这张表:
| 你的真实意图 | 更适合的模式 |
|---|---|
| “我只是补充一点信息,你先做完手上的” | collect |
| “顺序别错,这件做完再做下一件” | followup |
| “别停,但请马上按我说的新方向调整” | steer |
| “先停,现在这条路已经不该走了” | interrupt |
这里面最容易混的是 steer 和 interrupt。区别其实很朴素:steer 是“边走边拐弯”,interrupt 是“先刹车,再重来”。
如果当前运行还有保留价值,用 steer。如果当前运行已经明显错了,继续跑只会浪费时间,用 interrupt。
而 followup 的价值在于“把顺序说死”。它适合那种非常明确的任务链,比如:
先跑测试
→ 测试跑完以后再整理失败用例
→ 整理完以后再写修复建议这种时候,followup 往往比 collect 更明确,因为它不只是“等会儿再说”,而是“这件事一定排在后一步”。
3.3 让 heartbeat 和 cron 分工
一个很实用的组合方式是这样的:
heartbeat:
负责轻量巡检、跟进、提醒
cron:
负责准点执行、报表、例行任务比如一个学生项目场景:heartbeat 每隔一段时间看一下,有没有实验跑完、有没有同学发来新消息、有没有待跟进事项;cron 每天晚上固定整理一次实验结果摘要。
这时两者就各干各的:heartbeat 保持“活着”,cron 保持“准时”。
反过来,如果你拿 cron 去做持续巡检,通常会很僵,因为 cron 更像一个固定时刻的锤子。它擅长“到点就做”,但不擅长“围着当前会话持续观察有没有新变化”。
同样,如果你拿 heartbeat 去承担所有定时任务,也会变得混乱,因为 heartbeat 的本意是巡检,不是时间表。所以更好的思路不是二选一,而是:该巡的交给 heartbeat,该准点的交给 cron。
3.4 排障时先看调度,再看模型
很多人遇到问题时,第一反应是:“是不是模型理解错了?”
有时候确实是,但在消息系统这一层,更常见的原因其实是前面几步就已经乱了。所以更稳的排障顺序通常是:
- 先看这条消息是不是进了正确的 session;
- 再看它是不是进了正确的 lane;
- 再看当前 queue mode 是否符合你的真实意图;
- 然后看 heartbeat 或 cron 有没有被跳过、延后或静默处理;
- 最后再去怀疑模型本身的判断。
你可以把它记成一句顺口的话:先看队排没排对,再看人做没做好。
比如:
- 你以为系统“没回你”,其实它还在前一个长任务后面排队;
- 你以为 heartbeat “失效了”,其实它可能只是正常返回了
HEARTBEAT_OK,或者这一轮只做了内部巡检、没有对外发送; - 你以为 cron “没执行”,其实它跑在独立会话里,结果不在你当前聊天窗口里。
这些都不是模型理解问题,而是运行机制问题。一旦把这个排障顺序建立起来,你会发现很多问题都能更快定位。
如果把第三节收成一句话,就是:对于大多数人来说,最好的用法不是一开始把系统调到最灵活,而是先让它稳定、安静、可预期,再一点点放开更主动的能力。
本章小结
这一章的重点只有两个。
第一,OpenClaw 不能再按“收到一句就回一句”的方式工作,因为持续协作一定会遇到并发、顺序、上下文和时间问题。
第二,OpenClaw 处理这些问题,不靠一个神奇提示词,而靠一套运行机制:
| 机制 | 作用 |
|---|---|
| command queue | 先整理消息,再执行 |
| session | 把不同现场分开 |
| lane | 守住同一现场里的顺序 |
| queue mode | 决定忙时怎么接新消息(collect / steer / followup / interrupt / steer-backlog) |
| heartbeat | 定期巡检 |
| cron | 准点执行 |
所以消息循环和事件驱动的意义,不只是“更工程化”,而是让 Agent 真能长期稳定地协作。
下一章: chapter6/index.md