OpenClaw 的队列、插话(steering)与重试:消息挤在一起时它到底怎么排
把 OpenClaw 接进群里之后很快会撞上一个场景:Agent 正在执行一串工具调用,用户又连着补了三条消息,最后一条是”等等,先别改那个文件”。这时候系统该怎么办——丢掉?排队等它跑完?还是塞进正在跑的这一轮?
官方文档把这类问题拆成两块:**队列(queue)**管”新消息什么时候变成一次 Agent 运行”,插话(steering)管”消息能不能进入已经在跑的那一轮、在哪个时刻进入”。另有一层容易和它们混淆的重试(retry),管的是发往模型供应商和渠道 API 的单个 HTTP 请求失败后怎么退避。三条机制独立,配置项也不在一处,排查时先分清是哪一层能省掉大半弯路。
下面按官方文档口径把这三块串一遍,只讲文档写明的机制与默认值。
一次会话同时只有一个运行:队列是怎么分道的
OpenClaw 把所有渠道的自动回复运行都串到一个进程内的小队列里,避免多次 Agent 运行互相撞车,同时允许不同会话安全并行。理由很直白:自动回复运行开销不小(涉及 LLM 调用),几条入站消息前后脚到达时容易抢同一批共享资源——会话状态、日志、CLI 的 stdin;串行化还能降低触发上游限流的概率。
具体实现是一个按泳道(lane)划分的 FIFO 队列,每条泳道各自有并发上限:
| 泳道 | 默认并发 | 说明 |
|---|---|---|
| 未配置的泳道 | 1 | 缺省保守值 |
main | min(16, max(8, 可用 CPU 并行度)) | 入站消息与 main 心跳共用,进程级 |
subagent | 8 | 子 Agent |
session:<key> | 每会话一个 | 保证同一会话同时只有一次运行 |
runEmbeddedAgent 先按会话键入队(泳道 session:<key>),这一层保证”一个会话同一时刻只有一个 Agent 运行在动它”;通过之后再塞进一条全局泳道(默认 main),整体并行度由 agents.defaults.maxConcurrent 兜底。想让多个会话真正并行,改的是这个值。
此外还有 cron、cron-nested、nested、subagent 等泳道,好处是后台作业不会把入站回复堵住。文档提了一句:隔离的 cron Agent 轮次占 cron 槽位,而它内部真正的 Agent 执行走 cron-nested。这些脱离主线的运行按后台任务跟踪。
排队期间用户不是完全无感——渠道支持的话,“正在输入”指示器在入队那一刻就触发。开了 verbose 日志时,等待超过约 2 秒的排队运行会打一条简短提示,排障很依赖它。队列这一层没有外部依赖也没有后台工作线程,文档写明是纯 TypeScript + promise。它在整个系统里的位置可对照整体架构:网关、Agent、节点、渠道四层。
四种队列模式:steer / followup / collect / interrupt
/queue 控制的是”会话已经有活动运行时,普通入站消息该怎么办”。四种模式:
| 模式 | 对活动运行的处理 | 之后的行为 |
|---|---|---|
steer | 能插就插进活动运行时 | 插不进去时,等活动运行结束再开始 |
followup | 不插话 | 活动运行结束后,作为后续轮次逐条跑 |
collect | 不插话 | 静默窗口过后,把兼容的排队消息合并成一个后续轮次 |
interrupt | 直接中止该会话的活动运行 | 中止后,跑最新那条消息 |
默认是 steer,即同轮插话,不需要任何配置:运行中途到达的提示词会被注入活动运行时,不新起第二次会话运行;只有活动运行接不了插话时,OpenClaw 才等它跑完再开始。
collect 有个细节:排队消息若指向不同渠道或话题,会被逐条排出而非合并,为的是保住路由信息,所以它不是无条件揉成一条。followup 和 collect 都不碰活动运行,区别只在合并与否。interrupt 则是另一种意图——文档说得很清楚:停掉已经在跑的活儿,和把未来的活儿掰个方向,是两回事。想要”最新这条顶掉当前运行”就用 /queue interrupt(或 /stop),别指望 steer。
steer 为什么不打断正在跑的工具
最容易误解的一点:插话不会中断已经开始执行的工具调用。OpenClaw 运行时的检查点同时落在工具启动边界和模型边界上:
- 助手请求一批工具调用;
- 顺序模式下每次调用启动前立刻检查一次,包括异步解析、校验和预执行 hook 之后;
- 正在跑的调用结束时,若后面有插话在等,尚未启动的顺序调用尾巴会被跳过;
- 并行模式下先准备好调用,然后在启动这批调用之前只检查一次,已越过检查点的调用继续跑完;
- 每个被跳过的调用都会收到成对的工具 start/end 事件和一个合成错误结果
Skipped due to queued user message.,顺序按助手原始请求排列; - OpenClaw 在下一次 LLM 调用之前原样追加排出的插话消息。
这样设计是为了让两个约束同时成立:每个被请求的工具调用都有配对结果(transcript 结构不破),且被接受的插话在任何后续工具启动之前对模型可见。并行批次的关键词是”原子启动检查点”——插话出现在检查点之前,整批准备好的调用会被压掉;出现在之后,一个都召不回来。检查点之前已定论的校验或策略结果保持真实,只有没启动的可执行调用才拿到跳过结果。
原生 Codex app-server 走的是另一套:它暴露 turn/steer,而不是 OpenClaw 运行时内部的插话队列。OpenClaw 在配置的静默窗口内把排队提示词攒起来,按到达顺序打成一个 turn/steer 发过去;Codex 上游的轮次调度器自己管工具调度,在下一个模型边界消费插话,OpenClaw 不给这个运行时加逐工具抢占。另外,Codex 的 review 轮次和手动压缩轮次拒绝同轮插话,这两种情况下 steer 会退化成”等活动运行结束再开始”。
流式输出还会影响观感:partial 下预览可能提前定稿、插话被接受后重开一段新预览,block 下草稿大小的块也会造成同样的顺序感,两者都可能让一次插话看起来像好几条短回复。这套边界逻辑和 Agent 主循环的执行节奏咬合得很紧,可参考 Agent 主循环怎么转。
debounce、cap、drop:队列满了丢谁
未设置时,所有入站渠道面用的默认值是 mode: "steer"、内置 500ms debounce(用于 steer、followup 和 collect 的批处理)、cap: 20、drop: "summarize"。每会话的 /queue 选项作用在排队投递上:
debounce:排出排队 followup 或 collect 批次前的静默窗口;Codex 的steer模式下它同时是发送批量turn/steer前的静默窗口。裸数字按毫秒算,也接受ms、s、m、h、d。cap:每会话最多排多少条,小于 1 的值忽略。drop: "summarize"(默认):按需丢最旧的排队条目但保留紧凑摘要,作为一条合成 followup 提示词注入。drop: "old":按需丢最旧的,不保留摘要。drop: "new":队列已满时拒绝最新那条。
两个反直觉的点。一是默认的 summarize 丢的是老消息而非新消息,丢掉的内容会以摘要形态回到对话里;若”晚到的消息价值更低”,得显式改成 drop: "new"。二是 OpenClaw 的活动插话不使用 debounce 计时器,它在工具启动边界和模型边界上按运行时配置的排出模式做 FIFO 排出;debounce 真正作用的是排队投递(followup/collect)以及原生 Codex harness 在 steer 模式下的批量窗口——“调小 debounce 插话没变快”的疑问就出在这。
配置可全局写也可按渠道写,走 messages.queue:
{
messages: {
queue: {
mode: "steer",
cap: 20,
drop: "summarize",
byChannel: { discord: "collect" },
debounceMsByChannel: { discord: 1000 },
},
},
}
模式解析优先级四层:内联或已存储的每会话 /queue 覆盖 → messages.queue.byChannel.<channel> → messages.queue.mode → 默认 steer。选项则是 /queue 压过配置文件,然后依次应用渠道级 debounce(messages.queue.debounceMsByChannel)、插件 debounce 默认值、内置默认值。注意**cap 和 drop 是全局/会话级选项,不是按渠道的配置键**。
每会话覆盖的用法:把 /queue <steer|followup|collect|interrupt> 作为独立命令发出去,会为当前会话存下队列模式;选项可组合,例如 /queue collect debounce:0.5s cap:25 drop:summarize;/queue default 或 /queue reset 清除覆盖。
还在队列里的那一轮,怎么取消
一条提示词还在 followup/collect 队列里坐着时,网关会为那个客户端 runId 保留一个网关持有的取消身份,直到排队内容真的跑了或被丢弃;内容被折进溢出摘要时身份也跟着走。由此派生:
chat.abort带具体runId,在该轮次还在排队时就能取消它,前提是请求方被授权(和活动运行同一套归属规则)。chat.abort不带runId只给会话时,先取消已授权的排队轮次,再中止已授权的活动运行。这个顺序防的是队列排出把新活儿推进一个半停状态的会话。- 对多归属会话,不做逐请求方检查直接清空整个会话队列,不是官方认可的停止路径。
- 排队等待不会在
sessions.list里被当作活跃 Agent 运行投影,也不承担活动运行的超时语义。
网关支撑的客户端(含 openclaw tui)会把运行中途的提示词转发给网关,由网关套用队列模式;Esc 或 /stop 用会话范围的中止,本地句柄丢了也不会留下仍在排队的提示词继续跑。openclaw chat 和 openclaw tui --local 在嵌入式运行时里同样支持这四种模式,但显式的 /steer <message> 命令不是本地模式的命令。
重试是另外一层:对外请求失败怎么退避
队列管的是”什么时候跑”,重试管的是”发出去的请求失败了怎么办”。文档给的三条目标:按每个 HTTP 请求重试而不是按多步流程重试、只重试当前这一步以保住顺序、避免重复执行非幂等操作。默认值:
| 设置 | 默认 |
|---|---|
| 尝试次数 | 3 |
| 最大延迟上限 | 30000 ms |
| 抖动 | 0.1(10%) |
| Telegram 最小延迟 | 400 ms |
| Discord 最小延迟 | 500 ms |
模型供应商这一侧,OpenClaw 让供应商 SDK 自己处理常规的短重试。对 Anthropic、OpenAI 这类基于 Stainless 的 SDK,可重试的响应(408、409、429 和 5xx)可能带 retry-after-ms 或 retry-after;当这个等待超过 60 秒时,OpenClaw 会注入 x-should-retry: false 让 SDK 立刻抛错,好让模型故障转移轮换到另一个认证配置或备用模型。上限可用环境变量 OPENCLAW_SDK_RETRY_MAX_WAIT_SECONDS=<秒数> 覆盖,设成 0、false、off、none 或 disabled 就让 SDK 内部老实睡完长 Retry-After。细节见模型供应商接入与故障转移。
渠道侧:Discord 在限流(HTTP 429)、请求超时、5xx 以及 DNS 查询失败、连接重置、socket 关闭、fetch 失败这类瞬时传输故障上重试;Telegram 在 429、超时、连接/重置/关闭、临时不可用上重试。两者都优先用 retry_after,没有就指数退避。但 Telegram 的 HTML/Markdown 解析错误不重试,第一次尝试就回退成纯文本。
一个硬约束:Discord 和 Telegram 的渠道重试时序是内置的,openclaw.json 里不可配置。重试按每个请求算(消息发送、媒体上传、表情回应、投票、贴纸各算各的),复合流程不会重跑已完成的步骤。
看起来卡住了:先看是哪一类
文档给的起手式很实用:命令看起来卡住时打开 verbose 日志找 “queued for …ms” 那几行,先确认队列到底有没有在排出——排不出去和跑得慢是两个方向的问题。
开了诊断之后,处于 processing 状态超过内置警告阈值、且没有观察到回复、工具、状态、块或 ACP 进度的会话,会按当前活动分类:
session.long_running:有活动且有近期进度日志。有归属的静默模型调用也保持这一分类直到内置中止阈值,为的是不把慢的或非流式供应商过早报成卡死。session.stalled:有活动但无近期进度日志。有归属的模型调用、被阻塞的工具调用、卡住的嵌入式运行,到达或超过中止阈值时切到这一类。session.stuck:留给可恢复的陈旧会话簿记,包括带陈旧无归属模型/工具活动的空闲排队会话。
session.stuck 总会触发能释放会话泳道的恢复,超过中止阈值的 session.stalled 也能触发主动中止恢复——两种分类都可能把队列解开,不只是 stuck。重复的警告日志行会在会话不变期间指数退避,但恢复尝试仍在每个心跳节拍上跑。另外,接了轮次却停止发进度的 Codex app-server 运行会被适配器中断,好让活动会话泳道释放。
会话本身的状态模型和队列泳道不是一回事,容易混,可以对照会话模型:主会话、附着、状态看。
什么时候不适用,以及文档没覆盖的部分
队列的适用范围很明确:走网关回复管线的入站渠道的自动回复 Agent 运行(文档举例列了 WhatsApp web、Telegram、Slack、Discord、Signal、iMessage、webchat 等),不走这条管线的不受这套模式约束。几个实际边界:
- 运行时接不了同轮插话时(比如 Codex 的 review 和手动压缩轮次),配
steer等于配了”等它跑完”,不如按业务直接选followup或interrupt,语义更确定。 - 插话不改变活动运行的工具策略、不新建会话、不按发送者拆分消息。多用户渠道里入站提示词自带发送者和路由上下文,下一次模型调用能看出每条是谁发的——但”谁能插谁的话”靠渠道侧准入,不是队列。
- 按渠道分别限队列长度做不到(
cap/drop只有全局和会话两级);Discord/Telegram 重试时序也不可配,只能从发送频率、消息拆分等上层手段想办法。
最后提一句排查次序:消息没反应时先判断是没入队(渠道侧没收到)、入队没排出(看 “queued for …ms” 和会话诊断分类),还是排出了但请求失败(看重试和模型故障转移)。这三层的日志和配置项各在各处,混着看容易误判。
延伸阅读
- 从头读起:OpenClaw 架构总览:网关、Agent、渠道、节点、插件五层各管什么,出问题该往哪查
- 本专题共 40 篇,完整分组目录见专题页
- OpenClaw 里工具被拦住了:沙箱、工具策略、elevated 三者的边界怎么分
- OpenClaw 插件体系拆解:能力注册、加载四层与所有权边界,附最小插件从写到装的完整路径
本文依据 OpenClaw 官方仓库(github.com/openclaw/openclaw)docs/ 下的官方文档整理,核对日 2026-08-17。
我们没有安装或运行过 OpenClaw,因此不涉及界面外观、操作手感与实测耗时的任何描述;
文中的默认值、命令与配置项均为文档口径,不构成对实际运行结果的保证。
该项目迭代很快,请以仓库最新内容为准。接入即时通讯平台前,请自行确认所在平台的规则与合规要求。