# Hermes Agent 架构详细拆解-一个工业级的 Agent 框架底层运行时揭秘
**作者**: MateMatt
**日期**: 2026-07-05T09:22:35.000Z
**来源**: [https://x.com/mate_mattt/status/2074313623523271010](https://x.com/mate_mattt/status/2074313623523271010)
---

> AI Agent 是具有状态的(Stateful)、事件驱动的、甚至需要长期存活在后台不断自我循环的。
> 这恰恰是前端和客户端写烂了的模式(管理复杂的 UI 状态机、处理高频的用户交互事件、RunLoop 守护进程)。
如果用过去传统编程领域的知识看待 Agent 架构,会发现它并没有发明任何新的技术框架,仅仅是在某些事件节点中加入了 LLM 能力。
这也就是就是那个经典公式的由来:
> Agent = Harness + Model
读完全文,你会发现 Harness 是个整合了各种已经被广泛使用的技术架构的综合体,并且使用的全是大家熟悉的框架思想。
这篇是 Agent 架构系列第三篇文章,上一篇分析了 Agent 架构发展,并重点分析了 LangGraph、Pi-Agent (读完本篇,可以补充阅读,加深印象):
> **MateMatt@mate_mattt**: [原文链接](https://x.com/mate_mattt/status/2073699050671734919)
>
如果感兴趣,记得关注我,我会持续输出该系列深度解析文章。
## Hermes Agent 总览
Hermes 并不是"把一个 LLM 扔进 ReAct 循环里跑"那么简单。它是一个正经的运行时——接收外部事件、通过生命周期脚手架路由、安全地调度 agent 工作、在可能的情况下复用本地 agent 控制器、然后用重建的上下文去调用一个无状态的远端 LLM。
整体架构全景图:

架构全景图从左往右看,分别是:
1. 用户/平台入口 - 飞书、tg 等入口
2. 适配器层 - 将不同入口的信息统一为内部的 MessageEvent 结构
3. 事件总线层 Event Bus - 管理完整的生命周期监听、事件派发
4. GatewayRunner 核心调度层 - 并发控制、线程管理调度 (这一层是核心层)
5. AI Agent - 大家熟悉的 ReAct 的实现运行层
6. 无状态 LLM 接口层 - 调用各个 LLM 厂商接口
接下来,根据这个架构全景图进行层层拆解。
## Hermes Agent 第一层:用户 / 平台入口
最外层就是真实世界入口:用户在某个平台上发了条消息——可能是 Telegram、Discord、Slack、CLI,或者其他接入方式。

这时候消息还是平台相关的"半成品":
```
Telegram update
Discord message
CLI input
Webhook payload
```
这些原始的平台 payload 还不是 agent 任务。它们得先被"洗一遍"(标准化),核心运行时才能处理。
## 第二层:适配器层
适配器层负责把平台特定的输入转成 Hermes 内部事件。

它要回答的问题包括:
- 谁发的?
- 属于哪个会话 (Session)?
- 消息文本是什么?
- 有没有图片、文件、音频附件?
- session_key 是啥?
- 这个 Session 会话是不是已经在忙了?
概念上,适配器产出的东西长这样:
> MessageEvent + session_key + 来源元数据
session_key 极其重要。它是本地会话的身份标识,用来把工作路由到正确的会话通道里。
> session_key = 会话身份证
适配器还参与了会话级别的"门禁":
> active_sessions[session_key] = 忙状态守卫
> pending_messages[session_key] = 下一轮 / 中断
这意味着 Hermes 会尽量避免同一个会话同时跑两个独立的 agent 循环——那不乱套了嘛。
## 第三层:事件总线 / 脚手架层
事件总线/脚手架层是整个架构里最核心的概念层。
这层很容易被误解。事件总线不跑 ReAct 循环,它不"思考",也不以 agent 的身份调用工具。它的活儿是协调事件在整个运行时中的生命周期流转。

你可以把它理解成以下几样东西的缝合体:
- 一个事件发射器。
- 一条中间件管道。
- 一套生命周期钩子系统。
- 一个异步协调层。
- 一个在主 agent 运行前后触发副作用的地方。
## Event Bus 事件总线干了啥

事件总线/脚手架层负责路由和生命周期控制。它处理的概念包括:
- publish(event) —— 发布事件
- subscribe(handler) —— 订阅处理器
- lifecycle hooks —— 生命周期钩子
- async dispatch —— 异步派发
- side effects —— 副作用
- interrupt signals —— 中断信号
- pending-message triggers —— 挂起消息触发
简化版的生命周期大概长这样:
```
on_message_received —— 收到消息
-> on_processing_start —— 开始处理
-> on_agent_run —— agent 运行
-> on_tool_event —— 工具事件
-> on_response_sent —— 回复已发送
-> on_processing_complete —— 处理完成
```
在源码中,这些"总线"职责可能分散在适配器、钩子、后台任务和 GatewayRunner 里,而不是实现成一个叫 `EventBus` 的传统单一类。
## 事件总线不管啥
事件总线不拥有这些东西:
- 工作线程池。
- AIAgent 缓存。
- ReAct 循环。
- 远端 LLM 会话。
- 本地工具执行的语义。
它的角色更像是:
```
"有个事件发生了。该通知谁?该跑哪些生命周期钩子?
要不要触发中断?要不要开始处理?要不要发 typing 指示器、
打日志、上报指标、或者做清理?"
```
## 这里使用 Node.js 事件循环做类比
如果你熟悉 Node.js,你肯定对这个事件总线不陌生:
> Node.js EventEmitter ~= 发布 / 订阅
> Express 中间件 ~= 生命周期拦截
> Node 事件循环 Event Loop ~= 异步调度面
> libuv 工作线程池 ~= 共享执行器(跑阻塞任务)
> 应用处理器 ~= GatewayRunner + AIAgent

事件总线跟操作系统的事件循环不是一回事。它是在异步运行时行为之上构建的应用层事件协调模式。
## 为啥 Harness 要有这一层
没有这一层的话,每个平台适配器(tg feishu 等平台入口)都得自己撸一遍相同的逻辑:
- 开关 typing 指示器。
- 记录消息生命周期事件。
- 触发进度更新。
- 处理中断。
- 处理挂起消息。
- 跑清理逻辑。
- 通知可观测性或指标处理器。
- 把平台相关代码和 agent 逻辑隔离开。
事件总线/脚手架层把这些生命周期关注点挡在核心 ReAct 循环外面。
这个分离很重要:
> 事件总线/脚手架 = 事件生命周期 + 副作用
> AIAgent ReAct 循环 = 推理 + 工具调用 + 观测 + 最终回答
## 事件总线中的顺序与并发

对单条消息来说,生命周期阶段大体是有序的:
> 收到 -> 开始处理 -> agent 运行 -> 回复 -> 完成
但跨不同会话时,事件可以并发推进:
> 会话 A 的事件正在处理中
> 会话 B 的事件可以同时开始处理
> 会话 C 的事件在排队等工作线程
对同一个会话,Hermes 尽量保持同一时间只有一个活跃运行:
> 同一个 session_key -> 一个活跃运行
> 活跃期间来新消息 -> 挂起 / 中断 / 排队行为
这是一个关键的设计决策。系统允许跨会话并发,但保护单个会话不会同时跑多个互相打架的 agent 循环。
事件总线伪代码:
```
async def handle_raw_platform_message(raw):
event = adapter.to_message_event(raw)
session_key = event.session_key
await publish("message_received", event)
if adapter.is_session_active(session_key):
adapter.store_pending_or_interrupt(session_key, event)
await publish("message_pending", event)
return
await publish("processing_start", event)
try:
response = await gateway_runner.handle_message(event)
await publish("response_sent", response)
finally:
await publish("processing_complete", event)
```
## 第四层:GatewayRunner
GatewayRunner 是运行时的全局调度器 (Worker)。
如果说事件总线是生命周期协调层,那 GatewayRunner 就是决定 agent 工作怎么准备、怎么执行的那一层。

## GatewayRunner 管什么
GatewayRunner 拥有或协调这些主要的运行时资源:
> ThreadPoolExecutor —— 线程池
> running_agents —— 活跃运行追踪
> agent_cache —— 按 session_key 缓存的 AIAgent 对象
> session store —— 会话存储
> queued events —— 排队事件
> model / provider config —— 模型/提供商配置
> progress and interrupt monitoring —— 进度和中断监控
> adapters —— 适配器
当任务到来时,GatewayRunner 并不会为 Session 会话创建一个永久的工作线程。它把阻塞的 agent 工作提交到共享执行器中。
> 会话 A 借用工作线程 1
> 会话 B 借用工作线程 2
> 会话 C 如果池满了就等着
当 agent 跑完,工作线程还回池里。
> 工作线程不归会话所有
## 共享线程池
Hermes 用一个共享线程池来处理阻塞的 agent 工作。
原因很实际:agent.run_conversation() 可能会调远端 API、跑工具、跟文件交互、等子进程、流式输出——这些活儿不应该阻塞异步消息处理循环。
所以 GatewayRunner 概念上是这么干的:
```
await loop.run_in_executor(
shared_thread_pool,
run_sync,
)
```
在 `run_sync` 里面,Hermes 最终调用:
```
agent.run_conversation(message, conversation_history=history)
```
关键思想:
> 异步事件处理保持响应灵敏
> 阻塞的 agent 运行扔到工作线程里搞
目前调研下来,GatewayRunner 里有用协程,_handle_message、_run_agent_inner 都是 async def,周边也会用 asyncio.create_task() 跑 progress、stream、interrupt monitor 等任务。但核心 AIAgent loop 不是协程,是同步函数,放进线程池跑。
参考源代码:gateway/run.py
所以调用流程大致如下:
> 某个 worker thread 被 Session A 的 agent run 占用
> -> agent 正在等 LLM / 读文件 / 跑工具
> -> 这个 worker thread 不会让出来给 Session B
> -> 只有等这个 run 返回,线程才归还线程池
可以看到,agent 读取文件的时候,明显是个 IO 阻塞。但是线程依然等待着,不会让出;这并不符合现代协程的高效特点。我猜是为了降低代码复杂度,毕竟这是一个跑在用户机器上的 Agent,并发要求很低。(这里比较复杂,有不对的地方,还请指正)
## 线程池满了咋办
如果所有工作线程都在忙,新的执行器任务就在执行器队列里排队等着。
这就是正常的背压:
> 池里有空闲线程 -> 立刻开跑
> 池满了 -> 等着
这不代表会话拥有某个工作线程。只代表任务在等共享资源空出来。
## 每个会话一个活跃运行
GatewayRunner 和适配器层一起协作,保护会话不会被重复运行。
对单个 session_key:
> 同一时间只有一个活跃的 agent 运行
如果会话活跃期间又来了新消息,Hermes 可能会:
- 把它存为挂起的下一轮。
- 把它当作中断信号处理。
- 把它当命令路由。
- 如果用户用了队列语义,就放进显式的 FIFO 队列里。
重要的区别:
> 默认挂起消息 = 通常是一个下一轮/中断槽位
> 显式队列 = FIFO 语义
所以别想当然地以为每条普通消息都会进一个大 FIFO 队列。默认行为更像是"保护这个会话,记下接下来来了啥,可能的话打断它。
GatewayRunner 伪代码:
```
async def gateway_handle_message(event):
session_key = event.session_key
mark_running_or_pending(session_key)
def run_sync():
history = load_history(session_key)
config = resolve_model_and_tools(event)
agent = agent_cache.get(session_key, config)
if agent is None:
agent = AIAgent(config=config, session_key=session_key)
agent_cache[session_key] = agent
return agent.run_conversation(
event.text,
conversation_history=history,
)
result = await run_in_executor(shared_thread_pool, run_sync)
persist_result(session_key, result)
clear_running(session_key)
drain_pending_if_any(session_key)
return result.final_response
```
这里展示了核心的调度关系:
> GatewayRunner 负责调度。
> ThreadPoolExecutor 提供临时执行线程。
> AIAgent 执行 ReAct 循环。
## 第五层:AIAgent

`AIAgent` 是本地 agent 控制器。它是一个本地运行时对象:
- - 用哪个模型/提供商。
- - 有哪些工具可用。
- - 怎么构建消息。
- - 怎么管理会话历史。
- - 怎么跑 ReAct 循环。
- - 怎么处理中断。
- - 怎么执行工具调用。
- - 怎么产出最终回复。
内部循环:
```
调 LLM
-> 模型返回最终回答
-> 停
调 LLM
-> 模型返回 tool_calls
-> 执行工具
-> 追加观测结果
-> 再调 LLM
```
这就是 ReAct 模式:
> 推理(Reason) -> 行动(Action) -> 观测(Observation) -> 重复
用代码表示:
```
while not done and iteration_count < max_iterations:
assistant_message = call_llm(messages, tools=tool_schemas)
if assistant_message.tool_calls:
tool_results = execute_tools(assistant_message.tool_calls)
messages.append(tool_results)
continue
return assistant_message.final_text
```
循环是本地的。智能来自远端模型,但控制结构在本地。
## 第六层:远端 LLM API
远端 LLM API 是无状态的,远端模型不记得你的会话。
每次 Hermes 调模型时,都会把这次调用需要的上下文一股脑发过去:
```
system prompt —— 系统提示词
conversation history —— 对话历史
latest user message —— 最新用户消息
tool schemas —— 工具 schema
tool observations —— 工具观测结果
runtime instructions —— 运行时指令
```
所以图上写的是:
> messages/history 重建上下文
远端 LLM 看起来好像"记得"——那是因为 Hermes 把会话状态重新拼好又发了一遍。
## 第七层:本地工具
工具是暴露给模型的本地能力,通过结构化的工具 schema 来描述。例如:
```
读文件
写文件
跑终端命令
查记忆
搜上下文
调 API
生成媒体
```
LLM 不直接执行这些工具。它只是发出结构化的工具调用。本地运行时负责验证和执行,然后把观测结果塞回消息列表里。
```
LLM 发出工具调用
-> AIAgent 验证它
-> 本地运行时执行它
-> 结果变成观测
-> 观测送回 LLM
```
## 完整消息流转
端到端的全路径:
```
1. 用户发消息。
2. 平台适配器收到原始平台载荷。
3. 适配器把它标准化成 MessageEvent + session_key。
4. 事件总线/脚手架发布生命周期事件。
5. 适配器/运行时检查该会话是否已在活跃中。
6. GatewayRunner 准备会话历史、配置、工具和 agent 对象。
7. GatewayRunner 把阻塞的 agent 工作提交到共享 ThreadPoolExecutor。
8. 某个工作线程跑 agent.run_conversation()。
9. AIAgent 进入 ReAct 循环。
10. 远端 LLM 被调用,传入消息/历史/工具 schema。
11. 如果返回了工具调用,本地工具执行,观测结果被追加。
12. 循环重复,直到最终回复或停止条件触发。
13. GatewayRunner 持久化并清理运行状态。
14. 事件总线/脚手架触发回复和完成的钩子。
15. 适配器把最终回复发回给用户。
```
## 所有权与共享关系表
## 总结
一个简化版的处理流程:
```
事件总线 / 脚手架:
处理生命周期、路由、钩子、副作用、中断
GatewayRunner:
调度工作、拥有共享执行器、追踪活跃 agent、管理缓存
ThreadPoolExecutor:
共享的工作线程池;会话临时借用线程
session_key:
会话身份证;路由状态、挂起消息、缓存的 agent
AIAgent:
本地 ReAct 控制器;有效时按 session_key 缓存
远端 LLM:
无状态 API;上下文通过消息/历史重建
```
这篇文章整体较长,原因是完整的 Hermes 运行时框架本身复杂度较高。
如果觉得有帮助,记得帮忙点赞,关注。后续我会持续更新 Agent 架构系列文章。
## 相关链接
- [MateMatt](https://x.com/mate_mattt)
- [@mate_mattt](https://x.com/mate_mattt)
- [2.5K](https://x.com/mate_mattt/status/2074313623523271010/analytics)
- [Jul 5](https://x.com/mate_mattt/status/2073699050671734919)
- [18K](https://x.com/mate_mattt/status/2073699050671734919/analytics)
- [agent.run](https://agent.run/)
- [gateway/run.py](https://github.com/NousResearch/hermes-agent/blob/main/gateway/run.py)
- [Upgrade to Premium](https://x.com/i/premium_sign_up)
- [10:04 AM · Jul 7, 2026](https://x.com/mate_mattt/status/2074313623523271010)
- [2,574 Views](https://x.com/mate_mattt/status/2074313623523271010/analytics)
---
*导出时间: 2026/7/7 17:05:25*