4.1 协议设计哲学
生成模型:Claude Opus 4.6 (anthropic/claude-opus-4-6) Token 消耗:输入 ~200,000 tokens,输出 ~15,000 tokens(本章合计)
在第 3 章中我们看到了 Gateway 如何启动 WebSocket 和 HTTP 服务器。但是,"有一条 WebSocket 连接"和"客户端与服务器能有效通信"之间还有巨大的鸿沟。本节将深入分析 OpenClaw 如何在 WebSocket 之上设计了一套完整的应用层协议,以及这些设计决策背后的权衡。
4.1.1 为什么选择 WebSocket 而非 REST
在讨论 OpenClaw 的协议设计之前,我们先回顾一个基础问题:为什么要用 WebSocket?
REST 的局限性
REST(Representational State Transfer)是 Web 应用中最常见的 API 设计风格。在 REST 架构中,客户端发起 HTTP 请求,服务器返回响应,然后连接关闭。这种"请求-响应"模型简单、无状态、易于缓存和扩展,非常适合 CRUD(增删改查)操作。
但对于 AI 助手这类应用,REST 有几个根本性的局限:
1. 无法实现服务器主动推送
当 Agent 正在运行时,会产生一系列中间事件——工具调用开始了、模型返回了一个文本块、工具执行完毕了。这些事件需要实时推送给客户端。在 REST 模型中,客户端只能通过轮询(Polling)来获取这些事件,效率低下且延迟高。
衍生解释:轮询(Polling)是客户端定期向服务器发送请求来检查是否有新数据的方式。虽然 HTTP 长轮询(Long Polling)和 Server-Sent Events(SSE)可以部分解决这个问题,但前者仍然需要频繁建立连接,后者只支持服务器到客户端的单向通信。
2. 请求-响应模型不匹配 AI 交互模式
一次 AI 对话可能持续数分钟(Agent 可能要执行多个工具调用、等待审批等)。在 REST 模型中,这意味着要么维持一个超长的 HTTP 连接等待响应,要么拆分成"启动任务"和"查询结果"两个接口。
3. 多路复用需求
OpenClaw 需要在同一个连接上同时进行:发起 Agent 对话、查询会话列表、管理配置、接收状态事件……如果用 REST,每个操作都需要独立的 HTTP 请求。
WebSocket 的优势
WebSocket 是 HTML5 标准定义的全双工通信协议。它在一次 HTTP 升级握手之后,建立持久的 TCP 连接,双方可以随时主动发送消息。
对 OpenClaw 来说,WebSocket 的核心优势:
连接模式
每次请求新建连接
持久长连接
通信方向
客户端→服务器
双向
服务器推送
需要轮询或 SSE
原生支持
多路复用
不支持(需多次请求)
单连接多路复用
连接开销
每次都有 HTTP 头
一次握手后极低
OpenClaw 的选择:WebSocket + HTTP 互补
OpenClaw 并没有完全放弃 HTTP。如第 3.4 节所述,Gateway 同时提供 HTTP 端点用于:
OpenAI 兼容 API(
/v1/chat/completions):为了与现有工具链兼容Webhook 回调(
/hook/):某些通道(如 Slack)需要 HTTP 回调健康检查(
/health):标准运维需求
这是一种务实的设计——用 WebSocket 处理有状态的、实时的交互,用 HTTP 处理无状态的、兼容性的需求。
4.1.2 请求-响应 + 服务器推送事件的混合模式
WebSocket 协议本身只定义了"发送消息"的能力,并没有内置"请求-响应"的概念。OpenClaw 在 WebSocket 之上设计了一套应用层帧(Frame)协议来解决这个问题。
三种帧类型
OpenClaw 协议定义了三种顶层帧类型,通过 type 字段区分:
衍生解释:帧(Frame)在通信协议中指的是一个完整的数据单元。WebSocket 本身有帧的概念(文本帧、二进制帧等),但 OpenClaw 在此之上又定义了应用层帧。这种分层设计在网络协议中很常见——TCP 有自己的帧,HTTP 在 TCP 之上有自己的消息格式,OpenClaw 又在 WebSocket 之上定义了自己的消息格式。
这三种帧构成了一个混合通信模式:
请求-响应关联
请求和响应通过 id 字段关联。客户端发送请求时指定一个唯一 ID,服务器在响应中回传相同的 ID。这使得客户端可以在同一个连接上同时发起多个请求,并正确地将响应与请求对应起来。
衍生解释:这种模式在协议设计中称为多路复用(Multiplexing)。HTTP/2 也使用了类似的设计(Stream ID)来在一个 TCP 连接上并发处理多个请求。与之对比,HTTP/1.1 需要按顺序处理请求(Head-of-line Blocking),或者通过打开多个连接来实现并发。
联合帧类型与判别器
OpenClaw 将三种帧合并为一个联合类型(Union Type),并使用 type 字段作为判别器(Discriminator):
判别器的作用是让下游代码生成工具(如 quicktype、Swift codegen)能生成精确的类型,而不是生成一个"所有字段都可选"的大型结构体。这一点在第 4.2.3 节的 Swift 代码生成中会详细讨论。
错误格式
当请求失败时,响应帧的 error 字段包含标准化的错误信息:
错误码定义在 src/gateway/protocol/schema/error-codes.ts 中:
注意 retryable 和 retryAfterMs 字段——这是一种协议级别的退避指导(Backoff Guidance)。服务器可以告诉客户端:"这个错误是暂时性的,你可以在 N 毫秒后重试"。这比让客户端自行猜测重试策略更加精确。
4.1.3 幂等键(Idempotency Key)机制防止重复操作
分布式系统中一个经典问题是重复投递:网络抖动导致客户端不确定请求是否已被服务器处理,于是重发请求,结果操作被执行了两次。对于 AI 助手来说,这意味着可能发出两条相同的回复消息,非常不理想。
衍生解释:幂等性(Idempotency)是指一个操作执行一次和执行多次效果相同的性质。例如,HTTP 的 GET 请求天然是幂等的(获取同一个资源多少次结果都一样),而 POST 请求通常不是幂等的(每次 POST 都可能创建一个新资源)。幂等键是一种让非幂等操作变得幂等的技术手段。
OpenClaw 的幂等键设计
在 OpenClaw 协议中,涉及副作用的操作(发送消息、发起 Agent 对话、创建投票等)都要求客户端提供一个 idempotencyKey 字段:
幂等键的工作流程如下:
为什么不用请求 ID 来去重?
你可能会问:请求帧已经有 id 字段了,为什么还需要单独的幂等键?
原因在于请求 ID 的生命周期与幂等键不同。请求 ID 是一次 WebSocket 会话内的标识符——当 WebSocket 断开重连后,客户端会生成新的请求 ID。但幂等键由客户端预先生成并持久化,即使重连后也能使用相同的幂等键来标识"这是同一个业务操作"。
这种设计在 Stripe 等支付 API 中非常常见——它们也要求在创建支付请求时提供 Idempotency-Key HTTP 头,以防止因网络问题导致重复扣款。
幂等键的生成策略
OpenClaw 对幂等键的格式没有强制要求,只需要是非空字符串。在实际使用中,客户端通常使用 UUID v4 或类似的随机唯一标识符。关键点在于:
同一个业务操作在重试时使用相同的幂等键
不同的业务操作使用不同的幂等键
幂等键在合理的时间窗口内不应重复
本节小结
OpenClaw 的协议设计体现了几个核心原则:
选择合适的传输层:WebSocket 的全双工能力天然适合 AI 助手的实时交互需求,同时保留 HTTP 端点满足兼容性。
在传输层之上构建应用协议:三种帧类型(请求、响应、事件)构成了一个灵活的混合通信模式,既支持经典的请求-响应,也支持服务器主动推送。
面向可靠性设计:幂等键机制确保了网络不稳定时的操作安全性,错误格式中的重试指导让客户端能做出更智能的决策。
在下一节中,我们将看到这些帧类型是如何使用 TypeBox 这一 Schema 系统来定义的,以及 OpenClaw 如何从单一定义中同时获得 TypeScript 类型安全和运行时校验。
Last updated