摘要:一个 Agent 要同时接住 Telegram、Discord、Slack、WhatsApp、Signal、Matrix、CLI 十几个入口,最省事的做法是上消息队列,把每个平台当成独立服务。Hermes Agent 反其道而行:所有平台连接塞进同一个进程。本文拆它为什么敢这么做、八段消息管道怎么走、会话路由的 30 分钟窗口是什么、能力协商如何把"说什么"和"怎么说"拆开,以及这条路和消息总线方案的真实差距。
这是「Hermes Agent 深度剖析」系列的第 4 篇,全系列 7 篇:
01 总览:25 万 Star 的自改进 Agent 到底强在哪
02 三层记忆架构:会话记忆、持久记忆与用户模型怎么分工
03 技能学习闭环:Agent 怎么自己写技能文档
04 多平台网关:一个进程接住 16+ 个消息平台(本文)
05 安全架构:容器沙箱、技能扫描与人在回路
06 从零到生产:四种部署方式与成本账
07 vs OpenClaw:十二个维度的选型对比
一、起点不是"多平台",是"协议异构"
很多人以为多平台接入的难点是"平台数量多",其实真正的难点是每个平台的连接语义完全不同。同样是"收一条消息",六个平台的实现路径几乎没有交集:
| 平台 | 消息入站方式 | 保活机制 | 特殊约束 |
|---|---|---|---|
| Telegram | Long Polling 或 Webhook | 无心跳,靠长轮询超时重连 | Webhook 需要公网 HTTPS 端点 |
| Discord | WebSocket Gateway | HELLO 下发心跳间隔,实务上约 41 秒一次 | 需要 Gateway Intents 声明订阅范围 |
| Slack | Socket Mode(WebSocket) | 服务端定时发 ping,需快速回 pong | 消息事件有 3 秒 ACK 窗口 |
| Cloud API Webhook | 由 Meta 侧回调,无长连接 | 模板消息审核、24 小时会话窗口 | |
| Signal | 本地 signal-cli 守护进程 | 守护进程自身维护 | 需要设备关联,媒体走加密链路 |
| Matrix | /sync 长轮询 |
since token 增量同步 | 房间状态需自行维护,E2EE 可选 |
| CLI | 标准输入 | 无 | 无富媒体、无按钮 |
一张表看完就明白:这不像是"接几个 API",更像是把七种通信模型塞进同一个事件循环。Discord 的心跳、Slack 的 ACK、Matrix 的增量同步、Telegram 的 webhook 回调——它们对"连接还活着吗"的定义都不一样。
如果按微服务思路做,你会得到七个服务、七套部署、七个健康检查、七份日志。Hermes 的选择是把这七件事压进一个进程。
二、单进程架构:一个 Gateway 装下所有连接
整体链路是一条自左向右的箭头链:
平台 Adapter(每平台一个)
→ Normalizer(消息标准化)
→ Identity Resolver(身份解析,跨平台用户对齐)
→ Session Router(会话路由)
→ Context Builder(记忆 + 技能 + 历史注入)
→ Agent Engine(LLM 推理 + 工具调用)
→ Capability-aware Dispatcher(能力感知分发)
→ 目标平台 Adapter(渲染 + 发送)
所有 Adapter 共享同一个事件循环、同一份会话表、同一套记忆索引。
2.1 单进程换来的三个确定性
第一,零延迟的上下文同步。 用户在 Telegram 说了一半,切到 Discord 继续说,Agent 需要立刻知道"这是同一个人的同一个话题"。如果是多进程 + 外部存储,中间至少隔着一次序列化、一次网络往返、一次反序列化。单进程里,这就是一次内存对象读取。
第二,会话操作是原子的。 跨平台继续对话时,要做"关掉旧会话的活跃标记 + 挂到新会话 + 迁移上下文"三件事。在单进程里这是一个临界区,加把锁就完事;在多进程里就是分布式事务,要么上两阶段提交,要么接受中间态不一致。
第三,运维面被压到最小。 一个二进制、一个数据目录、一份日志。不需要 Redis 存会话、不需要消息队列做缓冲、不需要服务发现。对个人 Agent 这个定位来说,这一点比性能更重要——用户不会为了一个私人助理去维护一套中间件。
2.2 三个必须承认的代价
| 代价 | 具体表现 | Hermes 的缓解手段 |
|---|---|---|
| 单点故障 | 进程挂了,所有平台一起断 | 适配器级错误边界:单个 Adapter 崩溃只影响该平台,并按指数退避自动重启 |
| 资源竞争 | 一个平台的大文件上传可能拖慢其他平台 | 每个 Adapter 有独立队列与并发上限,出站发送走异步池 |
| 规模上限 | 单进程的并发会话数有天花板 | 官方定位是个人/小团队场景,数百并发会话量级够用 |
这里要说清楚:单进程不是"更先进",而是"在这个场景下更划算"。如果你要做的是面向百万用户的多租户 Agent 平台,单进程会立刻成为瓶颈,消息队列和水平扩展是必需的。但个人 Agent 的典型负载是"一个人 + 几个群",单进程的性能余量大到用不完。
三、八段消息管道:每一段在防什么
管道化的价值不在于"分了几段",而在于每一段都能独立测试、独立替换:
| 阶段 | 输入 → 输出 | 不这么做会怎样 |
|---|---|---|
| 1. 反序列化 | 平台原始事件 → 平台消息对象 | 各平台细节散落到业务逻辑里 |
| 2. 标准化 | 平台消息 → UnifiedMessage | 下游每处都要写 if platform == ... |
| 3. 身份解析 | 平台用户 ID → 统一身份 | 同一个人在 Telegram 和 Discord 被当成两个人 |
| 4. 会话路由 | 统一身份 + 消息 → 会话 ID | 每次说话都开新会话,上下文永远接不上 |
| 5. 上下文构建 | 会话 ID → 完整上下文 | Agent 每次都要自己"找之前聊了什么" |
| 6. 推理执行 | 上下文 → 回复内容 + 工具调用 | — |
| 7. 能力查询 | 回复内容 + 目标平台 → 渲染方案 | Agent 被迫关心"我在哪个平台" |
| 8. 出站发送 | 渲染结果 → 平台 API 调用 | 平台限流直接打到 Agent 引擎上 |
第 2 段和第 7 段是最容易被低估的。前者决定了系统的可扩展性(新增平台只需实现 Adapter → UnifiedMessage 的映射),后者决定了系统的体验上限(同一个语义回复在不同平台呈现成不同形态)。
四、统一消息模型:最大公约数,不是最小公分母
统一消息模型常见的两种做法:
| 做法 | 结果 | 问题 |
|---|---|---|
| 最小公分母 | 只保留所有平台都支持的字段(纯文本 + 发送者) | 丢掉按钮、引用、附件、表情回应,体验降级到短信水平 |
| 最大公约数 + 扩展位 | 通用字段统一建模,平台特有信息放扩展字段 | 需要下游知道"什么时候该看扩展字段" |
Hermes 走的是第二条路。核心字段大致是:
| 字段 | 含义 | 备注 |
|---|---|---|
platform |
来源平台标识 | telegram / discord / slack / ... |
chat_id |
会话(房间/频道/私聊)ID | 平台原生 ID |
user_id |
平台内用户 ID | 与统一身份做映射 |
text |
文本内容 | 标准化后的纯文本 |
attachments |
附件列表 | 统一为 url + mime + size |
reply_to |
引用消息 ID | 用于"回复某条"的语义 |
thread_id |
话题/线程 ID | Discord thread、Slack thread |
raw |
平台原始事件 | 兜底,处理特殊场景 |
capabilities |
目标平台能力快照 | 供第 7 段能力协商使用 |
raw 这个字段很关键。标准化不可能穷尽所有平台特性,留一个原始事件的逃生舱,比事后改模型安全得多。
五、会话路由:平台优先,再退到 30 分钟窗口
路由逻辑本身很短,但它是整个"跨平台连续对话"体验的开关:
def resolve_session(msg: UnifiedMessage, registry: SessionRegistry) -> Session:
# 1. 优先:同平台同会话的活跃 session
s = registry.find_active(platform=msg.platform, chat_id=msg.chat_id)
if s:
return s
# 2. 退一步:同平台内该用户的最近活跃 session
s = registry.find_recent_by_user(msg.platform, msg.user_id)
if s:
return s
# 3. 再退一步:跨平台的活跃 session(30 分钟窗口)
s = registry.find_cross_platform_active(msg.user_id, window_minutes=30)
if s:
registry.attach(msg.platform, msg.chat_id, s)
return s
# 4. 都没有:新建
return registry.create(msg)
为什么是 30 分钟,而不是"用 LLM 判断这句话是不是延续上文"?
| 方案 | 成本 | 可靠性 | 失败模式 |
|---|---|---|---|
| LLM 意图判断 | 每条消息一次额外推理 | 中(模型判断有波动) | "继续"两个字被判定为新话题 |
| 时间窗口启发式 | 零额外推理 | 高(规则确定) | 超过 30 分钟的延续会被当成新会话 |
第 3 步是精髓:用户从 Telegram 切到 Discord,只要在 30 分钟内,说的第一句话就会被自动挂到原会话上。这覆盖了绝大多数"换设备继续聊"的场景,而代价只是几行注册表查询。
六、能力协商:Agent 说"说什么",Gateway 管"怎么说"
这是这套设计里我认为最值得抄走的部分。
每个 Adapter 除了协议转换,还要声明自己的能力矩阵:
enum Capability {
TEXT = "text",
MARKDOWN = "markdown",
IMAGE = "image",
FILE = "file",
REACTION = "reaction",
BUTTONS = "buttons",
THREADS = "threads",
EDIT = "edit", // 可编辑已发送消息
READ_RECEIPT = "read_receipt",
}
interface PlatformAdapter {
readonly platform: string;
readonly capabilities: Set<Capability>;
start(): Promise<void>;
stop(): Promise<void>;
send(chatId: string, payload: RenderedMessage): Promise<void>;
}
能力矩阵大致长这样:
| 能力 | Discord | Telegram | Slack | Matrix | CLI |
|---|---|---|---|---|---|
| 富文本 Markdown | ✅ | ✅ | ✅(mrkdwn) | ✅ | ❌ |
| 内联按钮 | ✅ Components | ✅ Inline Keyboard | ✅ Block Kit | ⚠️ 需 Widget | ❌ |
| 表情回应 | ✅ | ✅ | ✅ | ✅ | ❌ |
| 消息编辑 | ✅ | ✅ | ✅ | ✅ | ❌ |
| 已读回执 | ⚠️ 有限 | ❌ | ✅ | ✅ | ❌ |
| 文件附件 | ✅ | ✅ | ✅ | ✅ | ⚠️ 仅路径 |
于是,Agent 侧只需要生成语义回复,不需要知道目的地:
Agent 生成:"问题已定位,是否执行修复?[确认] [取消]"
→ Gateway 查目标平台能力
→ Discord:支持按钮 → 渲染为 Message Components
→ Telegram:支持按钮 → 渲染为 Inline Keyboard
→ Slack:支持按钮 → 渲染为 Block Kit Actions
→ CLI:不支持按钮 → 降级为 "回复 1 确认 / 2 取消"
这个降级链是必须的。没有能力协商,你要么让 Agent 自己判断平台(污染推理),要么在每个平台分支里写渲染逻辑(污染适配器)。
6.1 新增一个平台要写多少代码
| 平台复杂度 | 适配器规模 | 主要工作量 | 代表平台 |
|---|---|---|---|
| 简单 | ~200 行 | 文本收发 + 会话映射 | CLI |
| 中等 | ~500 行 | WebSocket 连接 + 消息模型转换 | Slack、Matrix |
| 复杂 | ~1000 行 | Gateway Intents + 交互组件 + 频道管理 | Discord |
| 极复杂 | ~1500 行 | 端到端加密 + 媒体交付 + 回执 | Signal、WhatsApp |
新增平台的流程只有三步:实现 PlatformAdapter 接口、注册到 AdapterRegistry、配置平台凭证。
但我想强调:插件化的真正价值不是"易扩展"。 大多数讲插件化的文章都会说"新增平台只需写一个适配器",这当然对,但真正的价值在能力协商——它把 Agent 引擎和平台适配器彻底解耦,两者可以独立演进。新增平台不需要动 Agent 一行代码,Agent 增加新回复形态(比如"卡片")也不需要动所有适配器,只要适配器声明"我不支持",网关自动降级。
七、和消息总线方案的真实差距
另一条主流路线是消息总线:每个平台一个服务,消息统一进队列,再由调度器分发。
| 维度 | 消息总线方案 | 单进程 Gateway |
|---|---|---|
| 架构 | 中心化总线 + 意图路由 | 统一入口 + 管道处理 |
| 上下文 | 外部存储(缓存/数据库),按需加载 | 进程内存 + 全文索引,始终在线 |
| 跨平台会话 | 用户 ID 映射 + 外部关联表 | 进程内注册表 + 30 分钟窗口 |
| 平台扩展 | 新增服务 + 注册到总线 | 实现接口 + 自动能力协商 |
| 部署依赖 | 队列 + 缓存 + 数据库 | 单二进制,零外部依赖 |
| 故障隔离 | 队列天然解耦 | 适配器级错误边界 |
| 流量削峰 | 队列缓冲,天然支持 | 每适配器独立队列 |
消息总线的优势是真实的:解耦彻底、能削峰、便于水平扩展、天然适合链路追踪。但它在一个具体场景上会翻车。
7.1 一个"继续"引发的故障
场景:用户在 Telegram 发起一个复杂的数据分析任务,中途切到 Discord 发一句"继续"。
消息总线方案:
1. Telegram 消息进总线 → 意图识别判定"复杂任务" → 路由到多 Agent 流程
2. 用户切到 Discord,发"继续"
3. Discord 消息进总线 → 意图识别看到只有两个字 → 判定"简单消息"
→ 路由到直接 LLM 通道
4. 直接 LLM 不知道"继续"指什么 → 只能回头查外部存储找上下文 → 拼回去重推理
单进程 Gateway:
1. Telegram 消息 → 标准化 → 建会话 session_001 → 推理
2. Discord 的"继续" → 标准化 → 查注册表,命中 session_001(30 分钟窗口内)
→ 加载完整上下文 → 直接接着推理
根因很清楚:意图识别和上下文管理在两个地方,而意图识别只看当前这条消息。用 AI 判断"该用哪个 AI 来处理",本身就引入了额外的不确定性和延迟。
7.2 我的看法:消息总线在 Agent 场景下是"过度企业化"
消息总线是 ESB(企业服务总线)思路的延续。它在企业系统集成里非常正确——那边要解决的是异构协议、消息路由、流量控制。但 Agent 的多平台接入和企业集成有个根本差别:
| 维度 | 企业系统集成 | Agent 多平台接入 |
|---|---|---|
| 核心目标 | 异构系统互通 | 统一用户体验 |
| 关键指标 | 吞吐量、可靠性 | 上下文连续性、回复质量 |
| 消息特征 | 结构化、可预测 | 非结构化、对话式 |
| 路由依据 | 规则(内容/Header) | 上下文(历史/意图) |
| 扩展方式 | 水平扩容 | 垂直加能力 |
消息总线把"消息"当成需要路由的数据包,而 Agent 场景下"消息"是需要理解的对话。 一段对话被拆成数据包在总线上流转时,连续性就丢了。
所以我更认同 Hermes 的取向:多平台 Agent 的核心不是"消息路由",而是"上下文管理"。上下文只要完整、一致、即时可用,消息从哪来、回到哪去,只是最后的呈现问题。
八、平台深度 vs 平台数量:版本演进看什么
一个框架宣称"支持 20+ 平台"时,值得追问一句:支持的到底是能收发文本,还是能原生交互?
看 Hermes 近几个版本的更新方向,能看出它选的是后者(以下能力在各版本中陆续加入,写作本文时最新版本为 v0.22.5):
| 平台 | 新增能力 | 解决的问题 |
|---|---|---|
| Matrix | 表情回应 + 已读回执 | "Agent 到底看没看到我的消息" |
| Discord | 频道创建/慢速模式/归档 | Agent 能否按任务组织自己的环境 |
| Signal | 全类型媒体加密交付 | E2EE 下的图片/视频/文档收发 |
| Mattermost | 文件附件 | 企业场景的代码审查、文档协作 |
其中已读回执这个功能特别值得说。在没有它之前,用户发完消息只能干等——不知道 Agent 是在处理、消息丢了、还是进程崩了。一个"已读"标记,把这种不确定感直接消掉了。
我的判断是:个人 Agent 的用户通常只活跃在 2-3 个平台上,所以"覆盖 20 个平台但每个都只能发纯文本",价值远低于"覆盖 6 个平台但每个都能用原生交互"。平台的深度比数量更能决定体验。
九、四个可以抄走的模式
从这套网关设计里,我提炼出四个与平台无关、可以直接用在别处的模式:
| 模式 | 结构 | 适用场景 |
|---|---|---|
| 管道化处理 | 反序列化 → 标准化 → 身份 → 路由 → 上下文注入 | 任何异构输入的统一接入 |
| 能力协商 | 业务侧只产出语义,呈现侧按目标能力渲染 | 一端内容、多端展示 |
| 时间窗口会话复用 | 先同平台精确匹配,再退到跨平台时间窗 | 低成本实现"接着上次聊" |
| 适配器错误边界 | 每适配器独立队列 + 指数退避重启 | 单进程内的软隔离 |
第四个模式最容易被忽略。单进程架构最怕的就是"一个坏适配器拖死所有人",而错误边界是唯一不需要引入额外进程就能做到的软隔离。
十、总结
Hermes 的多平台网关代表了一次方向选择:从"消息路由"转向"上下文管理"。
关键要点:
- 单进程是刻意的减法,用零延迟上下文同步、原子会话操作、最小运维面,换取规模上限的让渡。个人 Agent 的负载撑得起这个取舍。
- 八段管道把复杂度关进了盒子,每段职责单一、可独立替换,标准化和能力协商是其中最关键的两段。
- 30 分钟窗口是性价比极高的启发式,用几行注册表查询,覆盖了绝大多数跨平台续聊场景,避免了每条消息一次意图推理。
- 能力协商解耦了"说什么"和"怎么说",Agent 不需要知道自己在哪个平台,网关自动按目标能力渲染或降级。
- 平台深度优于平台数量,已读回执、表情回应、媒体交付这些"小功能"才是浅层集成和深度集成的分水岭。
给正在设计多平台 Agent 的同行一个建议:把 80% 的精力放在上下文管理上,而不是消息路由上。路由是术,上下文是道。上下文做对了,路由自然简单;上下文做错了,再漂亮的总线也只是把问题分散到更多服务里。
下一篇:《安全架构:容器沙箱、技能扫描与人在回路》——当 Agent 能执行代码、能装技能、能读写文件时,怎么保证它不越界。
延伸阅读
这套「拆开源 Agent 源码」的连载还在继续,下一篇讲安全架构。也想听听你的场景:你的 Agent 现在接了几个平台?跨平台续聊这件事,是靠人肉复制上下文,还是框架自己记得住?