OpenClaw 架构详解 · 第二部分:并发、隔离以及确保智能体正常运行的不变量(Invariants)

0 阅读13分钟

编者按: 为什么你的 AI 智能体总是在生产环境中悄无声息地出错,甚至在你睡觉时做出不可预测的行为?

我们今天为大家带来的文章,作者的观点是:构建可靠 AI 智能体系统的关键,不在于精巧的提示词,而在于通过强制执行少量核心不变量(Invariants)来管理并发和状态,将自主性约束在可控范围内。

文章重点介绍了 OpenClaw 架构如何通过队列模型实现会话串行化与全局限流,确立了“单写入者”等核心不变量以防止状态竞争;详细阐释了包括“steer 模式”在内的多种队列策略,以解决任务执行中途新消息介入的并发难题;同时强调了去重、防抖及会话隔离等底层传输语义对于保障系统稳定性与隐私安全的重要性。

作者 | Vinoth Govindarajan

编译 | 岳扬

在第一部分,有一个令人背脊发凉的“凌晨 3 点的电话”之问:

为什么我的 AI 助手会在我睡觉时做某些事?

第二部分则是更可怕的后续问题:

是什么阻止了它“同时执行两项任务并破坏其自身状态”?

因为一旦你拥有了:

  • 多个聊天界面(WhatsApp + Slack + WebChat),
  • 基于时间的触发器(heartbeats, cron),
  • 外部触发器(webhooks),
  • 以及长时间运行的工具链(browser + shell + 文件写入),

……无论你是否能够意识到,你都已经构建了一个并发系统。

当我看到 session-lane(会话通道)设计的那一刻,我就不再去想什么“智能体魔法”,而是开始基于状态机的确定性保证(state machine guarantees)来思考问题了。

我喜欢 OpenClaw 的其中一个原因是,它没有试图用巧妙的提示词来“解决自主性问题”。

它通过强制执行少量的 invariants(译者注:指的是在整个系统运行过程中,必须始终成立、永远不能被打破的规则或条件。可以把它理解成系统的“底线”或“宪法”。),并围绕这些不变量(invariants)构建队列模型(queueing model),让其自主性变得清晰可控。

其核心理念很简单:序列化每个会话(session)的写入操作,对全局工作(global work)进行限流,并明确规定当任务正在执行的过程中,如果突然收到了新的输入,系统具体该如何应对。

01 为什么智能体系统需要不变量(否则就会显得“像是在闹鬼”)

如果我在审查一个智能体运行时(agent runtime),这部分永远是我最先关注的:

invariants 有哪些?以及它们又是在哪里被强制执行的?

因为如果没有不变量,智能体不会“大张旗鼓地报错”。

它们会悄无声息地出错。

比如:文件只写入了一半。Webhook 在网络重连后重复触发。对用户的一个请求,生成并发送了两条回复。

突然间,整个系统感觉就像闹鬼了一样。

不是因为它看起来有多么智能。

而是因为你违反了某个 invariant。

02 最关键的那个 invariant:单写入者规则

下面这个 invariant,能解释系统的很多稳定性问题:

在同一时间,只应该有一个 Agent 运行实例(agent run)去操作某个特定会话。

OpenClaw 通过一个感知通道(lane-aware)的 FIFO 队列来强制执行这一点:

  • 每个会话都有自己专属的 lane:session:
  • 这个 lane 保证每个会话同一时间只有一个活跃的运行实例
  • 而每个运行实例同时还会经过一个全局 lane(稍后详述)

正是这部分机制,阻止了那些经典的“同时产生两个想法”类的故障模式:

  • 工具调用以错误的顺序交错执行
  • 两个运行实例同时修改同一份会话记录
  • 导致重复发送或前后相互矛盾的操作
  • Agent 继续执行一个已经过时的计划

一个不那么光鲜但真实的事实是:一旦涉及工具和状态,协调机制比巧妙的提示词更重要。

03 队列(queue)不是细枝末节 —— 它是一种控制机制

如果你在文档/代码库中略读某个章节,请不要略读这一节。

OpenClaw 这个系统在设计时,追求的不是那种让人惊叹的“智能体魔法感”,而是踏踏实实地解决那些“无聊但重要”的基础架构问题:

  • LLM 运行成本高昂
  • 输入(inputs)可能接连到达(消息(message) + webhook + 心跳(heartbeat))
  • 共享资源不应该被争抢(session 文件、日志、CLI stdin)
  • 不要让你自己的 Agent 因为并发请求太多,意外地 DDoS 了上游服务

3.1 两阶段模型:将每个会话的任务执行强制变为串行 + 全局限流

OpenClaw 的队列做两件事:

1)将每个会话(session)的任务执行强制变为串行(Per-session serialization)

运行时(runtime)将工作按 session key 排入通道(lane) session:,这样保证每个 session 同时只有一个任务活跃运行。

2)全局限流

然后这些 session 范围的工作会经过一个全局 lane(默认是 main),所以整体并行度被 agents.defaults.maxConcurrent 限制。

两个容易被忽略但比看起来更重要的细节:

  • 即使用户的消息因为队列机制需要等待执行,系统也可以立即显示“正在输入…”的状态,让用户感觉界面是响应的,没有“卡住”。
  • 这是一个小的进程内队列(in-process queue):没有外部 worker,没有后台线程 —— 常被描述为“纯 TypeScript + promises”(译者注:完全使用 TypeScript 编程语言本身的异步特性(Promise/async-await)在进程内部实现队列逻辑,无需依赖外部基础设施或多线程模型。)

如果你要从本篇文章中记住一张图,就是这张:

image.png

04 队列模式:在“确定性”和“快速响应”之间做权衡(以及为什么需要“steer”这个模式)

现在真正的并发问题是:

当 Agent 正在执行一个任务(还没跑完)的时候,如果突然来了一个新消息,系统该怎么办?

OpenClaw 没有把这个复杂问题,藏在模糊的“让 Agent 自己决定”背后。

而是把它做成明确的、可配置的策略选项(队列模式),让开发者主动选择。

队列模式(queue modes)包括以下几种:

  • collect(默认模式) :将队列中的消息合并为一个后续对话轮次。
  • followup:始终等待直到当前任务运行结束。
  • steer:不等任务全做完,只要当前正在执行的“工具调用”(如 API 调用、文件写入)完成了,就在间隙插入新消息。
  • steer-backlog:既立即注入新消息(执行 steer),又把这条消息存起来,等当前任务彻底结束后再作为后续输入处理一次(执行 followup)。
  • interrupt(遗留模式) :直接杀掉当前任务,不管做到哪了,立刻开始处理新消息。

如果你见过 Agent “疯狂刷屏回复”,或者在执行中途“无视用户的纠正”,那么上文提到的队列模式(queue modes)就是控制这些行为的“调节旋钮”。

4.1 “steer”模式到底具体指什么?

这就是一种契约:

  • 每次工具调用结束后检查队列。
  • 如果在队列中发现了新消息,那么 Agent 当前计划中尚未执行的工具调用将被取消,不再执行。
  • 然后,将排队的用户消息插入到 Agent 生成下一条回复之前,作为新的上下文输入。

一个我无法忽视的简单故障案例:

当智能体正在执行一个耗时的多步骤任务时,用户在 Slack 上发来了一条修正指令。

如果没有明确的策略(policy),系统只有两个糟糕的选择。要么忽略用户的纠正(糟糕的用户体验),要么立刻停止当前任务,处理新消息(不安全)。

steer 模式选择了第三种方案:不中断正在运行的工具(保证安全),但一旦当前工具执行完毕,立刻停止后续计划,转而处理用户的纠正消息(保证响应性)。

这是我阅读 steering 行为时使用的思维模型:

image.png

(有一个值得记住的细微差别:根据流式输出/聊天界面的行为差异,steer 模式在某些界面上的实际表现,可能会更接近 followup 模式)

05 入站消息治理:去重和防抖——这些底层基础设施你根本绕不开

这一节属于那种“考验工程师直觉/经验”的内容 —— 有经验的开发者会本能地重视它,而新手可能会忽略,然后踩坑。

如果你不处理好入站消息治理(去重、防抖等),最后你会把"自己在底层基础设施(plumbing)里引入的 bug",错误地归咎于"模型不聪明"。

5.1 入向消息去重(Inbound dedupe)

真实的通信渠道会重复投递消息。重连(Reconnects)会发生。重试(Retries)会发生。

OpenClaw 通过一个短生命周期的去重缓存来应对这些情况,缓存的键由渠道/账号/发送方/会话/消息 ID 等字段组合而成 —— 这样重复投递的消息就不会触发新的任务执行。

5.2 入向消息防抖(Inbound debouncing)

人类打字是一阵一阵的。

OpenClaw 能够通过配置项 messages.inbound.debounceMs,将用户快速连续发送的多条文本消息合并为一个 agent 执行轮次(同时支持按不同渠道单独覆盖配置)。

有两个让系统行为符合人类直觉的运营细节:

  • 防抖仅适用于纯文本消息,附件会立即触发 Agent 执行;
  • 控制命令会跳过防抖等待逻辑,直接注入队列。保持独立、即时的执行,不被其他消息“拖累”。

06 隔离机制:会话键(session keys)是上下文隔离的边界(而 dmScope 是一个防止隐私泄露的安全开关)

队列(queue)防止“同时多个写入者”。

隔离(Isolation)防止“错误的上下文”。

OpenClaw 中会话的划分规则是明确定义的,不是靠运行时动态猜测或隐式推断:

  • 当用户与 Agent 进行一对一私聊(direct-chat)时,系统会标记其中一个会话为主要上下文(primary)。
  • 所有私聊会话(direct chats)默认会被合并/映射到同一个标准格式的会话键 agent:<agentId>:<mainKey>(其中 <mainKey> 默认为 main)。
  • 群聊/频道会话使用各自独立的会话键。

这个问题很快就会涉及安全层面。如果你的 Agent 能接收来自多个人的私信(DM),默认的私信行为可能会导致私有上下文泄露。解决方案是通过 dmScope 配置启用“安全私信模式”。

// ~/.openclaw/openclaw.json
{
session:{
dmScope:"per-channel-peer"
}
}

而且重要的是:Gateway(网关)才是会话状态的唯一权威数据源。

客户端不应该通过自行解析聊天记录来“重建”会话状态 —— 而应该直接向 Gateway 查询会话列表和用量统计。这是一种控制平面(control-plane)层面的架构选择,也是这些 invariants(译者注:指的是在整个系统运行过程中,必须始终成立、永远不能被打破的规则或条件。可以把它理解成系统的“底线”或“宪法”。)能在多个界面和设备上保持一致的原因之一。

07 序列化是必要条件,但不是充分条件

序列化是必要条件,但不是充分条件。

序列化(串行执行)能保证你的会话历史记录不乱。

但它不会自动让你调用的外部操作(副作用)变得安全。

即使你完美实现了“每个会话只有一个写入者”(即会话内部完全串行),系统仍然可能存在以下三种安全隐患:

  • 两个不同会话同时修改同一个外部资源
  • 工具调用不是幂等的(译者注:幂等的意思是执行 1 次 和 执行 N 次,系统状态完全一样。)(调用两次会造成实际损害)
  • 耗时较长的 tool 调用执行完时,基于的假设已经过时

这就是为什么 OpenClaw 不止步于“每个会话一个队列(one queue.)”。它还:

  • 对全局并发进行限流(全局通道 + agents.defaults.maxConcurrent)
  • 系统为那些会改变外部状态的操作(比如发送邮件、调用 Agent 动作)提供或支持“幂等键”,这样当网络故障导致请求重试时,系统能识别出这是同一次操作,从而安全地忽略重复请求,避免造成重复后果。

而且在协议层,Gateway 故意让控制平面保持“简单、明确、可预测”:

  • 握手必须执行,不能跳过,第一帧必须是 connect;
  • Gateway 不会像消息队列那样,把客户端断开期间的事件自动重发;如果客户端发现自己漏了状态(比如重连后),需要主动发起查询(如拉取最新会话列表、token 用量),而不是依赖服务端“猜你需要什么”。

这正是整个架构的核心主题:把限制条件作为设计的第一原则。

不是靠玄学,不是靠魔法。只是一些约束机制,让“始终在线”的行为变得可读、可理解、可预测、可解释。

08 如果你正在构建自己的系统:复制那些 invariants,而不是炒作

如果你想要“感觉有生命”的效果,但又不想上线后一团糟,这是我会直接抄的作业:

1)将所有输入标准化为统一的事件格式

2)解析出一个 session key(你的隔离边界)

3)强制执行每个 session 只有单个写入者

4)添加一个全局并发限流(concurrency throttle)

5)决定 Agent 正在执行任务的中途,新到达的消息该如何与当前任务交互(collect / followup / steer)

6)添加入向消息去重 + 防抖(真实的通信渠道会重投消息,人类打字是一阵一阵的)

7)持久化一个仅允许追加(不能修改或删除)的执行日志,以便你能够事后调试和审计系统到底发生了什么。

这就是“invariants 优先的智能体运行时”设计模式:

image.png

一个不那么光鲜但真实的事实:决定你的 Agent 是“玩具”还是“能真正上线运行的系统”的,是这一层(基础设施/运行时层),而不是 prompt。

09 本文参考的具体文档,欢迎前往验证

如果你想验证本文这些内容的可靠性,请查阅以下文档:

第三部分预告:实践中的内存/状态所有权问题 —— 以及为什么“更好的上下文管理”本质上是个工程架构问题,而不是提示词层面的取巧。

END

本期互动内容 🍻

❓OpenClaw提供了collect/followup/steer/interrupt四种队列模式。假如你的智能体正在执行一个“浏览器自动化+写文件”的长任务,用户中途发来一条新指令 —— 你会选哪种模式?为什么?

本文经原作者授权,由 Baihai IDP 编译。如需转载译文,请联系获取授权。

原文链接:

theagentstack.substack.com/p/openclaw-…