Hermes架构拆解之Gateway

本文对Hermes的架构拆解是基于v2026.7.30 版本的源码解析

如果把 Hermes 看成一个完整的 Agent Runtime,那么 Gateway 就是整个系统面向外部世界的接入和调度层

它要解决的不只是"接收一条消息"这么简单,还需要处理多平台协议差异、消息格式统一、Session 路由、同会话并发、特殊控制命令以及 Agent 调度等问题。

整条链路可以先记成:

复制代码
外部平台
   ↓
Platform Adapter
   ↓
MessageEvent
   ↓
adapter.handle_message(event)
   ↓
GatewayRunner._handle_message(event)
   ↓
Session / Context
   ↓
AIAgent

理解这条链路,基本就理解了 Hermes Gateway 的核心设计。


一、为什么 Hermes 需要 Gateway?

假设我们现在只接 Telegram,那么架构似乎可以非常简单:

复制代码
Telegram
   ↓
Telegram SDK
   ↓
AIAgent

但实际的 Agent 产品显然不会只有一个入口。

Hermes 可能同时需要面对:

复制代码
Telegram
Discord
Slack
WhatsApp
Web API
其他 IM 平台

问题在于,这些平台的数据格式完全不同。

Telegram 可能传过来一个:

复制代码
Update(...)

Discord 可能是:

复制代码
Message(...)

而 Web 前端发送的可能只是:

复制代码
POST /api/xxx

{
    "message": "帮我分析一下这个文件"
}

如果让 AIAgentGatewayRunner 直接理解所有平台协议,代码最终很容易变成:

复制代码
if platform == "telegram":
    ...
elif platform == "discord":
    ...
elif platform == "slack":
    ...
elif platform == "whatsapp":
    ...

平台越多,核心逻辑越混乱。

所以 Hermes 在外部平台和核心 Gateway 之间加入了一层:

复制代码
Platform Adapter

它的目的就是:

把不同平台的协议差异隔离在 Adapter 层,让 Gateway 后面的核心逻辑只处理一种统一的数据结构。

Hermes 中的 Telegram、Discord、WhatsApp 等 Adapter 都基于统一的平台适配器抽象。


二、Platform Adapter:平台协议翻译器

可以把 Platform Adapter 理解成:

外部平台和 Hermes 内部世界之间的协议翻译器。

例如:

复制代码
Telegram Update
       ↓
TelegramAdapter
       ↓
MessageEvent

或者:

复制代码
Discord Message
       ↓
DiscordAdapter
       ↓
MessageEvent

最终不同渠道都会变成 Hermes 能理解的统一消息对象。

这就是:

复制代码
MessageEvent

三、MessageEvent:Hermes 内部统一消息格式

这里有一个非常重要的概念:

MessageEvent 并不是一个处理模块。

它只是一个统一的数据结构。

真正负责转换的是:

复制代码
Platform Adapter

也就是说:

复制代码
外部平台消息
     ↓
Platform Adapter
     │
     │ 构造
     ▼
MessageEvent

Hermes 源码对 MessageEvent 的定义非常直接:

Incoming message from a platform.

Normalized representation that all adapters produce.

也就是说,它就是所有 Adapter 最终产生的标准化消息对象。

一个简化后的 MessageEvent 可以理解成:

复制代码
MessageEvent(
    text="帮我查看一下项目目录",
    message_type=MessageType.TEXT,

    source=...,

    message_id="xxx",

    media_urls=[],

    reply_to_message_id=None,

    timestamp=...
)

它除了文本之外,还可以携带:

复制代码
消息类型
来源平台
chat_id
user_id
thread_id
附件
图片
语音
回复上下文
平台原始消息

所以从 MessageEvent 开始,Hermes 就基本进入了自己的"统一世界"。

复制代码
Telegram 世界 ──┐
Discord 世界  ──┤
Slack 世界    ──┤
Web 世界      ──┘
        │
        ▼
 Platform Adapter
        │
        ▼
   MessageEvent
        │
        ▼
    Hermes 世界

后面的 Gateway 不再需要反复关心:

这到底是 Telegram 的 Update,还是 Discord 的 Message?


四、消息进入 adapter.handle_message(event)

Adapter 将原始消息转换成 MessageEvent 后,会进入一个非常关键的方法:

复制代码
adapter.handle_message(event)

这里开始进入 Hermes 的会话并发控制阶段

它第一件非常重要的事情,就是根据消息来源生成:

复制代码
session_key

实际源码中会执行类似:

复制代码
session_key = build_session_key(
    event.source,
    ...
)

然后判断:

复制代码
session_key in self._active_sessions

也就是:

当前这个 Session,是不是已经有一条消息正在处理?

源码在进入 GatewayRunner 之前就会生成 Session Key,并检查 _active_sessions

这一点非常关键,因为它决定了接下来消息走哪条路径。


五、Session 空闲时:直接开始处理

假设用户当前没有正在运行的 Agent。

例如:

复制代码
用户:
帮我总结一下这个项目

此时:

复制代码
session_key
    ↓
_active_sessions
    ↓
不存在

那么 Hermes 不需要排队,而是直接开始处理。

整体逻辑可以理解成:

复制代码
MessageEvent
     ↓
handle_message()
     ↓
生成 session_key
     ↓
Session 是否 Active?
     ↓
     否
     ↓
标记 Session Active
     ↓
创建后台 Task
     ↓
GatewayRunner._handle_message()

这里还有一个很有意思的并发设计。

Hermes 会先把 Session 标记成 Active,再启动后台任务

原因是为了避免这种情况:

复制代码
Message A 到达
    ↓
检查:Session 空闲

Message B 同时到达
    ↓
检查:Session 也空闲

结果:
同时启动两个 Agent

所以 Hermes 会提前建立 Session Guard,堵住这个并发窗口。源码中也专门解释了这一点。


六、Session 正在运行时:开始出现分流

现在假设:

复制代码
用户:
帮我分析这个大型项目

Agent 已经开始运行,而且可能需要几分钟。

此时 Session 已经存在于:

复制代码
_active_sessions

如果用户又发送了一条消息:

复制代码
顺便再看看 README

那么:

复制代码
session_key
   ↓
已经 Active

这时候 Hermes 就不会直接再启动第二个 Agent。

而是进入消息分流逻辑。

完整流程可以画成:

注意:

这里更准确的说法是:

复制代码
Pending Message

而不一定是传统意义上的严格 FIFO 队列。

Hermes 对连续文本消息还存在 merge / debounce 等策略,所以多条快速到达的 follow-up message 可能会进行合并。

源码中 _pending_messages 就承担了这一类"当前 Session 忙时暂存 follow-up"的职责。


七、为什么 /stop 不能进入普通等待流程?

这里是 Gateway 设计里我认为非常值得学习的一个点。

假设 Agent 正在执行一个五分钟的任务:

复制代码
Agent
 │
 ├── 查文件
 ├── 调接口
 ├── 执行 Terminal
 └── ...

用户突然发送:

复制代码
/stop

如果按照普通消息处理:

复制代码
/stop
 ↓
Pending
 ↓
等待 Agent 执行五分钟
 ↓
Agent 已经执行完
 ↓
再处理 /stop

这个 /stop 就完全没有意义了。

所以 Hermes 为这类命令设计了一条特殊路径:

复制代码
/stop
   ↓
绕过 Active Session 的普通 Pending 逻辑
   ↓
直接进入 GatewayRunner
   ↓
中断当前 Agent

Hermes v2026.7.30 中,/stop/new/reset 属于类似:

复制代码
interrupt_then_dispatch

的处理策略。

也就是说:

复制代码
先处理中断
+
再分发命令

而另外一些控制命令也可以绕过 Active Session Guard,但不一定需要终止当前 Agent。

从架构角度看,这其实已经有一点类似:

复制代码
普通用户消息
    =
Data Plane

/stop / status / approve 等
    =
Control Plane

普通消息关心的是:

Agent 接下来要做什么?

而控制消息关心的是:

当前 Agent 应该继续、暂停、终止还是改变状态?

因此它们不能简单共用同一套排队逻辑。


八、为什么 handle_message() 要使用后台 Task?

源码里 handle_message() 有一个很重要的设计:

复制代码
收到消息
   ↓
快速返回
   ↓
真正 Agent 工作放到后台 Task

而不是:

复制代码
收到消息
   ↓
await Agent 执行 5 分钟
   ↓
5 分钟后才能继续接收消息

源码对此的解释也很明确:handle_message() 会通过后台 Task 执行处理,从而保证 Agent 正在运行时仍然可以接收新的消息,这也是 interrupt 能力成立的重要基础。

否则就会出现:

复制代码
用户:帮我分析项目
        ↓
Adapter 被阻塞 5 分钟
        ↓
用户:/stop
        ↓
根本没有机会被及时处理

正确的设计应该是:

复制代码
                 ┌── Agent Task A 持续运行
                 │
Adapter ─────────┤
                 │
                 ├── 接收新消息
                 │
                 └── 接收 /stop

也就是说:

消息接收生命周期和 Agent 执行生命周期必须解耦。

这一点对于做企业级 Agent 的并发控制非常重要。


九、GatewayRunner:真正的 Gateway 调度中心

经过 Adapter 层以后,消息最终会进入:

复制代码
GatewayRunner._handle_message(event)

如果说:

复制代码
Platform Adapter

解决的是:

消息从哪里来?

那么:

复制代码
GatewayRunner

解决的就是:

这条消息接下来应该怎么办?

Hermes 源码甚至直接在 _handle_message() 的注释中给出了主流程:

复制代码
Authorization

→ Command

→ Running Agent

→ Get / Create Session

→ Build Context

→ Run Agent Conversation

→ Return Response

因此可以把 GatewayRunner 理解成整个 Gateway 层真正的:

复制代码
Dispatcher
+
Session Router
+
Agent Coordinator

十、Session Key 到底解决什么问题?

现在假设:

复制代码
用户 A:
我的名字叫小明

过了一会:

复制代码
用户 A:
我刚才告诉你我叫什么?

Hermes 必须知道:

复制代码
第二条消息
和
第一条消息

属于同一个 Session

这就是 session_key 的作用。

它本质上是在回答:

这条消息属于哪一条对话车道?

可以简单理解成:

复制代码
platform
+
chat
+
thread
+
user
+
其他隔离信息
        ↓
   session_key

例如概念上可能形成:

复制代码
agent:main:telegram:dm:123456

实际 Hermes 的 build_session_key() 会根据 DM、群聊、Thread、用户隔离策略等生成更完整的 Key。

尤其是在多用户场景中,Session Key 的设计非常重要。

Hermes 代码甚至专门处理了 participant identity,避免不同用户因为 Session Key 冲突而出现历史消息串线。


十一、SessionStore:根据 Key 找到真正的会话

有了:

复制代码
session_key

还只是知道:

我要找哪一条 Session。

真正的 Session 数据还需要:

复制代码
SessionStore

管理。

可以类比 Java:

复制代码
session_key
    ≈
业务唯一 Key

SessionStore
    ≈
Session Manager + Repository

Hermes v2026.7.30 中,SessionStore 主要通过 SQLite 保存 Session metadata 和消息 transcript,同时还保留 legacy JSONL fallback。

所以:

复制代码
MessageEvent
     ↓
session_key
     ↓
SessionStore
     ↓
SessionEntry / Transcript
     ↓
Agent Context

最终才具备一次完整 Agent Turn 所需要的上下文。


十二、最后才真正进入 AIAgent

到这里 Gateway 已经完成了大量工作:

复制代码
平台协议转换
        ↓
MessageEvent 标准化
        ↓
Session 并发判断
        ↓
特殊控制命令处理
        ↓
用户鉴权
        ↓
Session 路由
        ↓
Context 构建

最后才来到:

复制代码
AIAgent

所以完整架构应该理解成:

AIAgent 为止,可以认为:

Gateway 的主要工作基本完成。

接下来才正式进入我们熟悉的:

复制代码
Prompt
   ↓
LLM
   ↓
Tool Call
   ↓
Tool Result
   ↓
LLM
   ↓
Final Answer

也就是 Agent Loop。


十三、Gateway 和 API Server 在这里是什么关系?

理解上面的流程之后,Gateway 和 API Server 的关系也非常容易理解。

不要把它们理解成:

复制代码
Gateway
API Server

两套完全独立的 Agent 系统

更适合从实现角度理解成:

复制代码
                Gateway
                   │
       ┌───────────┼────────────┐
       │           │            │
 Telegram Adapter │     Discord Adapter
                   │
            API Server Adapter

Telegram 负责把 Telegram 世界翻译成 Hermes 世界。

Discord Adapter 负责 Discord。

API Server Adapter 则负责:

复制代码
HTTP / SSE / Web

所以对于 Web UI:

复制代码
Web Frontend
     ↓
HTTP / SSE
     ↓
API Server Adapter
     ↓
Gateway
     ↓
Session
     ↓
AIAgent

本质上和 Telegram 并没有完全不同。

真正不同的主要是:

复制代码
最前面的接入协议

进入 Hermes 统一消息体系以后,后面的很多逻辑都可以复用。


十四、最后形成一个 Gateway 心智模型

如果让我用最简单的话总结 Hermes Gateway,我会把它拆成五个角色:

模块 核心职责
Platform Adapter 将不同平台协议接入 Hermes
MessageEvent Hermes 内部统一消息格式
handle_message() Session Guard、并发与消息分流
GatewayRunner 鉴权、命令、Session、Agent 调度
SessionStore 会话路由、历史消息与 Session 状态

最后才是:

复制代码
AIAgent

因此整条链路可以压缩成一句话:

复制代码
外部平台
   ↓
协议适配
   ↓
统一消息
   ↓
会话并发控制
   ↓
Gateway 调度
   ↓
Session 上下文
   ↓
AIAgent

结语

Hermes Gateway 看起来代码很多,但如果先把各种异常处理、媒体处理、插件、命令细节暂时拿掉,它最核心解决的其实就是三个问题:

第一,消息从哪里来?

交给 Platform Adapter。

第二,这条消息属于谁、属于哪个会话、现在能不能执行?

交给 Session Key、Active Session Guard 和 SessionStore。

第三,这条消息最终应该交给谁处理?

交给 GatewayRunner 进行调度,最后进入 AIAgent。

一旦这个心智模型建立起来,我们再来看 Hermes 的 Gateway 源码,就不会觉得它只是几万行复杂的异步代码。

就会开始看到很清晰的边界:

复制代码
平台世界
    ↓
Adapter
    ↓
Hermes 消息世界
    ↓
Gateway
    ↓
Agent 世界

而 Gateway,恰好就是连接这几个世界的那座桥。

相关推荐
代码方舟1 小时前
零信任架构实战:基于天远企业四要素验证构建自动化B2B供应链金融网关
人工智能·金融·架构·自动化
天远API2 小时前
零信任架构实战:基于天远企业四要素验证构建自动化供应商准入网关
运维·人工智能·架构·自动化
Seoyoneh11 小时前
Agentic Workflow编排架构:云客服从“被动响应”迈向“主动执行”的技术实现
java·开发语言·架构
AI_Auto13 小时前
架构视角看数字化转型|核心架构:大共享平台+小应用,从按需走向适变
大数据·人工智能·架构·制造
科芯创展13 小时前
XU9238 外置NMOS架构宽压Boost升压恒压驱动器方案设计与工程落地指南
架构
Dawson Zhu13 小时前
AI 辅助调试陷入「反复修改」循环?用证据驱动的四步法破局
人工智能·语言模型·架构·aigc·agi
阿祖zu14 小时前
训练师 Agent 小程序产品上线啦
微信小程序·llm·agent
slacker-kian14 小时前
[实践]-本地大模型Agent使用自定义MCP服务实现与SAP系统集成(二)
ai·llm·sap·agent·mcp·odata·qwen-agent
她的男孩15 小时前
多租户和数据权限怎么共存?扒完拦截器注册链路,我找到 4 个隐蔽的坑
java·后端·架构