技术笔记

AI Agent 基础设施与分布式系统的学习记录

关于本站

本站用于记录个人在 AI Agent 基础设施、分布式系统与跨平台工程方面的学习笔记。文章偏技术架构解读视角,关注系统是怎么构造出来的、设计上做了哪些取舍,以及工程实现中容易踩到的坑。

内容均为学习过程中的整理,不是教程,也不保证完全准确。写作的目的主要是把读过的资料、试过的配置、踩过的问题串成自己能看懂的一条线,顺便也许能对同样在看这些东西的人有点参考。

文章不定期更新。不做评论、不做订阅、不做广告。

OpenClaw 插件框架解读

分类:AI Agent 基础设施

AI Agent 最近几年从"单次补全"演化为"长期运行的助手"。一个直接的工程问题随之出现:Agent 需要同时对接多种客户端——网页、小程序、IM、命令行、第三方 API 等等。每新增一个客户端接入方式,就要在 Agent 端写一套适配,在客户端侧写一套 SDK,长期下来这部分代码会变得异常臃肿。

OpenClaw 把这类"接入不同客户端"的问题抽成了一个可插拔的插件框架。它本身是一个基于 Node.js 的运行时,Agent 的 channel 实现以独立插件的形式发布和加载,宿主不感知具体 channel 的细节。

核心抽象:Plugin、Channel、Adapter

理解 OpenClaw 的三个关键概念:

这种"一个 channel = 四个 adapter"的拆分是 OpenClaw 最值得研究的设计。它让 plugin 作者只需实现四个相对独立的小函数,而不是一个庞大的"channel class"。

Plugin 生命周期

OpenClaw 的 plugin 状态变迁和包管理器有些类似:

  1. install:通过 openclaw extensions add 指定 manifest URL,框架拉取 tgz 包、校验 SHA256,解压到 ~/.openclaw/extensions/ 下。
  2. enable / disable:运行时切换某个 channel 是否参与消息分发。disable 的插件留在磁盘但不加载。
  3. update:框架周期性拉取 manifest,对比版本号,发现新版即下载新 tgz 替换旧 artifact 并重启相关 channel。
  4. uninstall:从 ~/.openclaw/extensions/ 删除整个插件目录,清理配置。

所有状态都写在本地 ~/.openclaw/openclaw.json 中,这是 plugin 启用、配置、credential 的唯一真实源。框架对这个文件采用原子写入(临时文件 + rename),避免并发写坏配置。

Channel Adapter 四件套

config adapter 负责声明和校验插件需要的配置项。一个典型的 config adapter 会导出一个 JSON schema,框架启用时会根据 schema 校验用户在 openclaw.json 中填入的值。常见的配置项包括 server URL、rate limit 阈值、本地缓存目录等。config adapter 是静态的,不涉及运行态。

auth adapter 处理配对、OAuth、API key 等鉴权流程。它通常要支持两种模式:

设计良好的 auth adapter 会先检查环境变量,再回退到交互提示,非 TTY 且无环境变量时直接报错。鉴权成功后,adapter 负责把 credential 原子写入 ~/.openclaw/<channel-id>.json

gateway adapter 是整个 channel 的核心。它通常维护一个长连接(WebSocket、SSE、HTTP 长轮询),负责:

gateway 的难点不在"写一个 WebSocket 客户端",而在于如何把框架的生命周期事件和客户端协议的状态机对齐。例如 Agent 侧的 channel.disable 应该触发客户端那头的 "agent offline" 广播,而 Agent 重启后的 channel.enable 又要补齐这段时间错过的消息——这些都需要 gateway adapter 自行处理。

setup adapter 是引导式初始化。用户第一次安装 plugin 时,框架会调用 setup adapter 的方法:

setup: {
  resolveAccountId(): string,
  applyAccountConfig(cfg): Config,
  inspectAccount(cfg): { enabled, configured }
}

这个 adapter 让 plugin 可以在"刚装上还没配"的状态下有合理默认值,以及在 openclaw onboard 流程里可见,用户通过 CLI 交互一步步完成配置。setup 是可选 adapter,简单 channel 可以不实现。

与 MCP 的对比

MCP(Model Context Protocol)最近很热,容易让人把 OpenClaw 和 MCP 搞混。其实两者解决的是不同层面的问题:

两者完全可以叠加。一个 OpenClaw channel 里可以跑一个使用 MCP 作为 tool 发现机制的 Agent,OpenClaw 负责把这个 Agent 暴露给微信小程序的用户,MCP 负责让 Agent 调用一组外部服务。类比下来:MCP 像 USB 协议,OpenClaw 像机箱——一个管设备接入,一个管整机组装。

典型应用场景

整体看,OpenClaw 的价值在于把"AI Agent 接入各种客户端"这件事标准化了。它不做模型编排、不做工具调用,只把 channel 这一层抽出来做成插件化。对想要自建 Agent 基础设施的团队,这个设计是值得参考的。

Hermes 运行时设计

分类:AI Agent 基础设施

Hermes 是一个独立运行的 AI Agent 运行时,用 Python 实现,跨平台(macOS、Linux、Windows)。和"嵌入到某个 channel 插件里"的设计不同,Hermes 走的是"独立进程常驻"的路线:Agent 是一个长期运行的进程,通过 WebSocket 接到 Bridge 服务器上等待消息到来,然后调用本地模型推理、必要时调用本地工具。

这种形态的核心挑战是:Agent 不能崩、不能假死、升级不能中断已连接用户。下面记录一下 Hermes 为了处理这些挑战所做的几个设计选择。

分层架构:Gateway + Agent

Hermes 把一个逻辑上的"Agent 运行时"拆成两层:

分层的好处是故障隔离。Agent 进程里跑着用户代码(模型推理、工具调用),出 bug 的概率远高于 Gateway 框架代码。Agent 崩了只影响一个会话,Gateway 不用重启;反过来升级 Agent 也不需要动 Gateway,Gateway 在空闲时间内重新 spawn 新版本 Agent 即可。

这个拆分也方便做"热升级"。Gateway 监听 manifest 版本变化,发现新版时下载新 Agent artifact、替换可执行文件、下次 spawn 自然就是新版本——全程 Gateway 不重启,用户已经建立的会话不中断。

冷启动机制

Hermes 启动一次新 Agent 的典型流程分三个阶段:

  1. 健康探测(约 3 秒)——检查 Gateway 是否存活、监听端口是否可达。不直接 spawn,先做个轻量握手。
  2. 配置初始化(约 10 秒)——拉取 channel 配置、模型配置、用户 profile 等。写入到 Agent 环境。
  3. 连接建立(最多 10 次重试,每次 1 秒,最长 10 秒)——向 Bridge 发起 WebSocket 连接,失败则重试。

整个冷启动最长约 23 秒。这个长度是被故意控制的:过短会让"Gateway 刚启动还没 ready"的瞬态误判为真实故障;过长用户等不及。分阶段而不是一把梭的原因是每阶段失败语义不同,健康探测失败可以明确告诉用户"Gateway 有问题",而连接建立失败则更可能是网络抖动。

WebSocket 重连策略

连上 Bridge 之后,Agent 需要 24/7 在线。长连接不可避免会遇到断网、服务端重启、NAT 超时等情况。Hermes 用标准的 exponential backoff:

MAX_RECONNECT_ATTEMPTS = 10
RECONNECT_BASE_MS      = 1000
RECONNECT_MAX_MS       = 30000

每次重连的延迟是 min(BASE * 2^attempt, MAX) + jitter。attempt 从 0 开始,最长延迟封顶在 30 秒。10 次尝试全部失败后 Agent 主动退出,把决策权交还给上层。

10 次上限是一个经验值。支持的场景是"常见网络抖动",比如切 WiFi、电脑休眠唤醒、CDN 短时故障——这些都会在几分钟内自行恢复,10 次重试(累计最多约 4 分钟)足够覆盖。对于更长时间的故障(数据中心宕机、账号被封禁),继续重连没有意义,应该让 Agent 退出、让用户或运维感知。

端到端加密

Hermes 和客户端之间经过 Bridge 中继,而 Bridge 本身被设计成"永远不接触消息明文"。这是通过经典的 X25519 + AES-GCM 组合实现的:

  1. 客户端(比如微信小程序)首次发起配对,生成一个 X25519 ephemeral keypair,把公钥发给 Bridge。
  2. Bridge 生成一个 6 字符配对码,绑定到这个公钥,TTL 15 分钟。
  3. 用户把配对码告诉 Agent 侧(通过命令行参数、skill prompt 等)。
  4. Agent 拿配对码去 Bridge 换到客户端公钥,同时生成自己的 ephemeral keypair,把 Agent 公钥回传给 Bridge。
  5. 双方都用 ECDH 从对端公钥 + 自己私钥派生出对称密钥。
  6. 后续消息以 nonce + AES-GCM(key, nonce, plaintext) + auth_tag 的格式发送,Bridge 只转发密文。

这个方案的几个关键性质:

这里有个容易忽略的细节:配对码只是"换密钥的一次性凭证",它本身没有加密意义。但配对码的 TTL 要足够短(15 分钟),防止泄漏后被穷举复用。

跨平台安装

Hermes 的安装脚本由 Bridge 按 User-Agent 动态分发:

Bridge 根据请求头判断 OS,返回对应的 shell 脚本。脚本负责检查 Python 版本、创建 venv、pip install Hermes 包、写启动脚本到用户家目录。

一个工程细节是 PID 文件管理。Hermes 在 ~/.hermes-agent/agent.pid 里记录自己的 PID,方便升级时 kill 旧进程。但裸 PID 不够——Unix 的 PID 会被内核复用,旧 Agent 崩溃后这个 PID 可能被分配给系统里别的 Python 进程,升级脚本如果只凭 PID 去 kill,就会误杀无关进程。

标准解法是给 PID 加上进程启动时间(/proc/<pid>/stat 的第 22 个字段)作为联合身份:PID 相同且 starttime 相同才认为是同一个进程。升级脚本在发 SIGTERM 前后各 sample 一次 starttime,不匹配就放弃 kill。这种做法和 systemd 的 pid_is_alive() 实现完全一致,是 Linux 生态里的既定模式。

Hermes 还要处理 Unix 特有的 zombie 进程——如果父进程不 wait() 子进程,子进程退出后会变成僵尸(State: Z),此时 kill -0 仍然返回 0(PID 有效),但 cmdline 是空的,任何信号都没有效果。升级脚本如果不判断 zombie 状态,就会在 SIGTERM + SIGKILL 两次失败后陷入死循环。正确做法是读 /proc/<pid>/statusState 字段,识别到 ZX 就认为进程已死,直接清理 PID 文件继续。

整体来看,Hermes 的设计思路是"尽量让失败被可靠地感知和恢复"——分层让故障有边界,重试让抖动能自愈,加密让中继节点不可信也能用,PID 联合身份让升级不会误伤。这类长期运行的基础设施软件,重点从来不是算法多漂亮,而是这些细节能不能经得起生产环境里的各种意外。