2.1. 三层骨架解决了调用方式与执行逻辑的耦合
三层骨架解决了调用方式与执行逻辑的耦合
2026年4月,Hermes Agent 在 v0.16 版本中同时发布了命令行工具 hermes、桌面应用 Gateway 和 Telegram/Discord/Slack 等多平台接入能力。翻开源码你会发现,这些完全不同的交互方式最终都指向同一个对象——agent/run_agent.py 中的 AIAgent。这意味着你在终端里敲下的每一行字,和老板在 WhatsApp 里发的每一条语音,走的是同一套执行逻辑。Hermes 的做法很直接:把“谁来调用”和“如何执行”做成两层独立的骨架,中间用一层动态模型路由隔开。这就是被社区广泛讨论的三层骨架,也恰好是本章要拆解的 Entry Points、Agent Core、Provider Resolution 三条分界线。
现象:一个 Agent,四种入口,同一套大脑
这种设计一开始并不显眼。在 Hermes 的早期中,CLI 是唯一入口,所有对话逻辑堆在一个文件里。发展到 v0.16 时,仓库里已清晰分出三条调用路径:
| 入口 | 项目文件 | 典型使用场景 | 消息封装 | 启动方式 |
|---|---|---|---|---|
| CLI | cli.py |
开发者本地调试 | 终端文本流 | hermes chat 或 hermes TUI |
| Gateway | gateway/run.py |
多平台聊天 | 平台事件 → 统一事件格式 | 长期运行后台进程 |
| ACP 适配器 | acp/ 模块 |
企业内集成 | Agent Communication Protocol | 可选的服务模块 |
| Python 库 | agent/run_agent.py 直接调用 |
其他工具链嵌入 | 原生 Python 对象 | AIAgent().run() |
这些入口的启动方式、协议、消息格式完全不同,但它们复用的是同一个 AIAgent 实例的 run() 方法。以 Gateway 为例:当你从 Telegram 发一条消息,Gateway 会把它打包成标准文本事件,推送给 Agent Core;Agent Core 完全不感知 Telegram 的 bot token 或聊天 ID,它只看到一个“用户输入字符串”。同样,ACP 适配器通过 gRPC 进来的一段结构化指令,在进入核心循环前也会被统一序列化为内部消息体。
这种“入口归一”的好处是显而易见的:新增一个平台不会碰核心执行逻辑。2026 年 5 月社区贡献者为 Hermes 增加了 Microsoft Teams 接入,据当时的 Pull Request 信息,仅需在 Gateway 中新增不到 200 行适配代码,就完成了全部接入——整个 agent/ 目录一个字节都没改。
Entry Points 统一接入层:将协议差异隔离在外
三类入口的核心工作都落在三件事上:建立连接、序列化消息、消费响应。Hermes 对此有一套清晰的分工。
协议适配与消息标准化
- CLI 最简单:
input()读取字符串,print()输出响应,连会话状态都直接存在内存里。它是单用户的,没有并发问题。 - Gateway 要处理多平台 Webhook:Telegram 通过 HTTP POST,Discord 走 WebSocket,Slack 用 RTM。Gateway 为每个平台维护一个
PlatformAdapter,核心方法就是normalize_message(),把不同平台的事件转成统一的IncomingMessage对象。 - ACP 适配器 面向企业级的标准化通信,它同时支持 HTTP 和 gRPC,消息体用 protobuf 定义。但同样,在打入 Agent 之前,protobuf 会被拆解成纯文本或命令参数。
Gateway 和多用户场景还必须解决一个问题:如何把返回响应正确路由回对应聊天窗口。为此,Gateway 在调用 AIAgent.run() 时携带一个 session_id,Agent 执行完毕后,响应会携带同样的 session_id,再由 Gateway 的 Router 根据之前的映射将响应投递到正确的平台和对话中。这个机制让 Agent 内核依然保持“单会话循环”的认知,完全不需要理解多租户概念。
可操作总结
| 入口 | 消息进入方式 | 内部统一格式 | 对 Agent 的调用 | 结论与最佳实践 |
|---|---|---|---|---|
| CLI | 标准输入 | str |
agent.run(user_input) 同步调用 |
适合快速测试和单会话 |
| Gateway | Webhook/WS | IncomingMessage 对象 |
dispatch_to_agent() 通过事件循环异步调用 |
生产多平台推荐,网关单独进程 |
| ACP | gRPC/HTTP | protobuf → str/dict |
异步 agent.run() 包装成服务 |
需要与内部系统集成时使用 |
| Library | 函数调用 | Python 原生对象 | agent.run() 直接调用 |
嵌入其他脚本或 Agent 工作流 |
记住一条规律:入口再怎么变,Agent Core 的输入永远是一个字符串和一组元数据。 这是 Hermes 入口层设计的核心约束。
Agent Core 轻量级运行时:单一对话循环的状态机
在所有入口之下,实际的“思考”发生在 agent/run_agent.py 的 AIAgent 类中。尽管这个文件在 v0.16 时仍被注释为“大文件”,但其内部逻辑已经被清晰地拆解成一个单线程异步事件循环 + 状态机。
对话循环的七个步骤
当 agent.run(user_input) 被调用时,Hermes 会启动一个 async 循环,依次走过七个状态:
[WAIT_INPUT] → [BUILD_PROMPT] → [RESOLVE_PROVIDER] → [EXECUTE] → [DISPATCH_TOOLS] → [COMPRESS] → [WRITE_RESPONSE]
- WAIT_INPUT:入口输入已就绪,触发循环。
- BUILD_PROMPT:
PromptBuilder从 SQLite 读取对话历史、系统提示(SOUL.md)、外部知识片段,拼接成完整的 prompt。 - RESOLVE_PROVIDER:根据用户配置、历史调用情况和当前可用性,动态选择模型提供商(见下一节)。
- EXECUTE:调用提供商的
chat_completionAPI,流式获取推理结果。 - DISPATCH_TOOLS:如果模型输出决定调用工具,
ToolDispatch会匹配注册表中的工具,触发执行并收集结果。 - COMPRESS:当对话上下文接近 token 上限,
Compression模块自动汇总早期对话,防止溢出。 - WRITE_RESPONSE:将最终文本或工具结果写回给入口层。
这个循环是 Hermes Agent 的灵魂。入口层无论来自 CLI 还是 Gateway,触发的都是这套完全一致的序列。为了确保这个循环的轻量,Hermes 刻意保持其为单线程协程模型——所有外部调用(API 请求、工具执行)都是异步 I/O,不会阻塞事件循环。这让一个 Agent 实例可以同时处理多个会话(Gateway 场景),每个会话在同一个事件循环中以协程形式交替执行。
协程模型与并发
调研资料显示,Hermes 使用 Python 的 asyncio 作为并发框架,但没有引入多线程或多进程。它的并发策略是将每个对话 session 映射为一个 asyncio.Task,所有 task 共享同一个 AIAgent 实例,但各自持有独立的上下文栈。SQLite 写入通过 aiosqlite 实现非阻塞,避免会话间互相干扰。
这种设计使得 Hermes 可以被部署在一个很小的资源里(文档提到在树莓派上运行 Gateway 的案例),同时保持高吞吐。缺点也明显:一个 task 内的 CPU 密集操作(如 prompt 本地处理)仍会短暂影响事件循环,不过这在实践中可以通过将重型任务委托给工具后端来规避。
关键对比:传统循环 vs Hermes 事件循环
| 特征 | 传统顺序脚本 | Hermes Agent Core |
|---|---|---|
| 调用方式 | 入口代码中直接写逻辑 | 入口只负责触发状态机 |
| 多平台支持 | 每个平台一套复制粘贴的逻辑 | 一个循环,多种入口复用 |
| 模型切换 | 硬编码 API key 和模型名 | 动态解析,运行时切换 |
| 工具调用 | 函数调用散落各处 | 统一 ToolDispatch 分发 |
| 上下文管理 | 手动拼接字符串 | PromptBuilder 自动拉取 |
可以看到,Agent Core 做了两件关键的事:把执行逻辑收敛到一个可靠的状态机里,把所有外部不确定性(模型、工具、上下文)都变成可替换的模块。这就是轻量级运行时的真正含义——不是功能少,而是高层控制逻辑足够薄。
Provider Resolution 动态模型路由:解耦模型与业务
三层骨架的第三层位于 Agent Core 内部,但设计上足够独立,值得我们单独拆解。它就是 Provider Resolution——Hermes 在运行时决定“用哪个模型”的机制。
为什么需要一个解析层
Hermes 支持至少 11 种模型家族,而不同提供者的 API 风格差异很大:有的用 OpenAI 兼容的 /v1/chat/completions,有的走 Anthropic 的 Messages API,还有的用自定义端点。如果让 Agent Core 直接记住这些差异,代码会迅速腐化。Provider Resolution 层的作用就是把这些差异收敛到一个接口后面:chat_completion(prompt, model, **params)。
选择流程与策略
当 Agent 进入 RESOLVE_PROVIDER 状态时,解析器会按以下顺序查找可用的提供者:
- 检查用户覆盖:是否通过
hermes model use临时指定了模型?是 → 验证其凭据(auth.json中有无对应 key 或 OAuth token)和可用性。 - 读取配置优先级:从
config.yaml中读取model_priority列表,依次检查每个提供者的状态。 - 故障转移:如果首选提供者不可用(网络超时、额度耗尽等),自动尝试下一个。重试策略为指数退避,最多 3 次。
- 选用默认提供者:如果以上均失败,使用随 Hermes 分发的默认提供者(如本地 Ollama 实例,如果已启动)。
所有解析结果会缓存在内存中一个会话的生命周期内,因此同一轮对话不会反复 ping 提供者健康检查。
三种认证路径的解耦
| 认证方式 | 配置位置 | 典型提供者 | 适用场景 | 注意点 |
|---|---|---|---|---|
| API Key | auth.json |
OpenAI, Groq | 个人开发,直接调用 | 不要提交到 Git |
| OAuth 2.0 | auth.json + 系统浏览器认证 |
Google GenAI, Azure | 企业 SSO 集成 | 需在 Gateway 环境配置回调 |
| 自定义端点 | config.yaml |
内部部署的 vLLM, Ollama | 私有机房,代理 | 必须指定 base_url |
三层隔离后,Agent 核心只知道“我有一个 chat completion 方法”,而不知道背后是调用 OpenAI 还是 Anthropic。同样,入口层 CLI 或 Gateway 完全不用关心模型选择逻辑,它们只把用户输入丢给 Agent Core;Provider 层的变更(比如添加一个新模型)对它们透明。
社区踩坑:解析失败的头号原因
根据 Blake Crosley 在 v0.16 Reference 中的提醒,新手第一次运行 hermes chat 时常遇到模型不可用的报错,原因有三:
- 未运行
hermes model交互配置,使用了不存在的默认模型; auth.json格式错误或未放置在~/.hermes/;- 公司网络代理未在环境变量中设置,导致自定义端点不可达。
建议:读完本章后,不妨先跑一次 hermes model 完成提供者初始化,再继续探索 Gateway 部署。
先给结论:三层如何在工程上解耦
如果我们把 Hermes 看作一个三明治:
- 上层 Entry Points 负责“谁来吃”—— CLI、Gateway、ACP,分别对应本地用户、消息平台、企业系统。
- 中层 Agent Core 负责“怎么吃”——统一的状态机,保持对话节奏,决定何时调用工具、何时压缩上下文。
- 底层 Provider Resolution 负责“吃什么”——决定用哪个模型的大脑,处理所有 API 细节和故障转移。
三层的耦合点被压缩成两个标准接口:消息输入接口(入口 → 核心)、chat completion 接口(核心 → 提供者)。任何一层的替换或扩展都不跨层影响。
传统 Monolithic Agent 与 Hermes 三层骨架对比
| 维度 | 单体 Agent | Hermes 三层骨架 | 解耦价值(作者结论) |
|---|---|---|---|
| 新增一个聊天平台 | 整个重新实现一遍对话循环 | 增加 Gateway 适配器,约 200 行代码 | 开发成本降低 90%(基于社区 PR 统计) |
| 切换模型提供商 | 需要修改核心代码里的 API 调用 | 运行 hermes model 或修改 config.yaml |
风险极低,可在线热切换 |
| 逻辑 Bug 修复 | 影响所有入口 | 只需改 agent/run_agent.py,一次修复全平台受益 |
回归测试集大大缩小 |
| 部署形态 | 通常是重量级服务 | 支持本地 CLI、轻量级 Gateway、企业 ACP 三种形态 | 符合从开发到生产的演进路径 |
这个结论的可操作性很强:如果你现在的 Agent 还在入口代码里 hard-code 模型调用逻辑,那么按照三层骨架重构是性价比最高的架构升级路径。
按场景推荐:你该怎么用这三层
场景一:单机研究和原型开发
直接使用 hermes chat CLI。入口就是终端,Agent Core 会在你的机器上单会话运行。通过 hermes model 绑定一个或两个提供者,不需要网关。所有对话历史保存在 ~/.hermes/MEMORY.md 和 SQLite 里,随时可查阅。
场景二:为团队部署聊天机器人
启动 hermes gateway 后台进程,在 config.yaml 中启用 Telegram、Discord 等平台。Gateway 会负责从多个平台接收消息,路由给同一个 Agent 实例。必须提前在 auth.json 中配置好平台 Bot token。这个模式下,你只需要管理一个 Gateway 进程,所有平台享受一致的 Agent 行为。
场景三:将 Hermes 嵌入产品后端
如果你已有自己的产品系统和用户界面,可以使用 Python 库方式直接 import agent,或者启动 ACP 适配器,通过 HTTP/gRPC 暴露 Agent 能力。这样,你的产品后端只需发送一条标准化消息,就能获得推理结果,而 Hermes 的状态管理和模型切换完全由它自己接管。
无论哪种场景,记得将
auth.json和.env排除在版本控制之外,这是 Hermes 社区贡献者中最常见的隐私事故。
从三层骨架到 Agent 操作系统内核
Entry Points、Agent Core、Provider Resolution 构成了 Hermes 的“外层骨骼”。它们解决的是宏观层面的调用与执行解耦。但一个能自主复盘、不断进化的 Agent,显然不能只靠单次对话循环。在下一章《六大子系统构成 Agent 操作系统的内核》中,我们会走进 Agent Core 的内部,去看消息总线、提示构建器、工具调度、压缩模块等六个子系统是如何像操作系统的内核服务一样,支撑起这个会成长的自主推理系统。你刚刚在 CLI 里看到的每一次上下文自压缩、每一次工具调度,都源于这些子系统之间的精密协作。
Hermes Agent 系统设计与工程落地
关于 LearnKu