本文对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": "帮我分析一下这个文件"
}
如果让 AIAgent 或 GatewayRunner 直接理解所有平台协议,代码最终很容易变成:
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,恰好就是连接这几个世界的那座桥。