Hermes Agent 01 | 全景图:Hermes Agent 的三层架构与核心理念
好的架构不是让你看见它,而是让你忘掉它。
你好,我是《深入 Hermes Agent:从原理到实战》专栏的作者。从这一讲开始,我们正式进入代码。开篇词聊了“为什么是 Hermes Agent”,这一讲解决一个更基础的问题:它到底是怎么搭起来的?如果你先把接入层、Agent runtime、执行层这三张图拼起来,后面再读模型无关、工具系统、记忆与技能,就不会迷路。
先纠正一个常见误解
很多人第一次看 Agent 项目,脑子里默认浮现的是这条链路:
用户输入 -> LLM 推理 -> 工具调用 -> 返回结果这条链路没错,但它只描述了一次请求,没有描述一个系统。
Hermes Agent 的源码告诉我们,它解决的问题并不是“如何把一次问答接上工具”,而是下面这三个更难的问题:
同一套能力,怎么同时服务 CLI、消息平台、IDE、Cron、Webhook?
跨会话状态,怎么持久化,同时又不把 prompt cache 打碎?
工具、执行环境、记忆、技能,怎么在一个主循环里协调起来?
所以,从源码看,Hermes 更准确的理解不是“一个永远阻塞等待输入的后台大脑”,而是:
一套可被多个入口复用的 Agent runtime,加上一套支持 profile 隔离的持久化状态目录。
这个区别很关键。
有的入口对应长驻组件,比如 Gateway 进程和其中的 Cron 定时器。
有的入口是交互式前端,比如 CLI。
有的入口是协议适配器,比如 ACP、Webhook/API。
它们共享的不是“同一个 Python 对象”,而是同一套运行时逻辑、同一套工具系统、同一份HERMES_HOME状态。这也是 Hermes 跟单终端、单进程、单任务型 Agent 的真正分野。
三层架构:先看全景,再拆细节
先把全景图摆出来:
┌──────────────────────────────────────────────────────────────┐ │ 接入层(Entry Points) │ │ CLI | Gateway(多平台) | ACP(IDE) | Cron | Webhook/API │ │ 负责把不同入口的事件、会话、上下文统一成 Agent 可消费的输入 │ └──────────────────────────────┬───────────────────────────────┘ │ ┌──────────────────────────────▼───────────────────────────────┐ │ Agent Runtime(控制面) │ │ AIAgent @ run_agent.py │ │ - system prompt 组装与缓存 │ │ - 模型调用、重试、fallback │ │ - 工具调用主循环 │ │ - 上下文压缩 │ │ - 记忆/技能 nudging 与后台 review │ │ - 回调与流式输出 │ └──────────────────────────────┬───────────────────────────────┘ │ ┌──────────────────────────────▼───────────────────────────────┐ │ 执行层(Capabilities + State) │ │ - Tool Registry + 50+ 工具 + MCP │ │ - Terminal backends: Local / Docker / SSH / Modal / │ │ Daytona / Singularity │ │ - 状态存储: state.db / memories/ / skills/ / sessions/ / │ │ checkpoints/ / cron/ │ └──────────────────────────────────────────────────────────────┘这张图最重要的,不是“它有三层”,而是这三层的边界切得很克制:
接入层负责“把不同世界的输入变成统一上下文”。
Agent runtime 负责“做决策”。
执行层负责“把决策落地,并把状态存下来”。
这也是为什么 Hermes 虽然文件很多、功能很多,但主线并不乱。你一旦理解这三个责任边界,代码就能读下去。
接入层:一个 runtime,多个入口
接入层最直观的功能,就是“多入口”。
源码里,Hermes 至少有这几类典型入口:
CLI:最传统的交互式终端入口,用户直接运行
hermes。Gateway:消息平台网关,适配 Telegram、Discord、Slack、WhatsApp、Signal、Matrix、Email、Webhook 等多个平台。
ACP:面向编辑器/IDE 的协议接入层。
Cron:计划任务入口,按调度规则定时触发 Agent。
Webhook/API:外部系统把事件投递进来,触发一次 Agent 运行。
但这里有一个很容易说错的点:
这些入口共享的不是“同一个永远活着的 AIAgent 实例”。
真实实现更细:
CLI 往往持有一个交互式
AIAgent实例,持续服务当前终端会话。Gateway 要面对并发消息,因此它一边有按消息触发的执行路径,一边又维护
_agent_cache,按 session 复用 agent,以保住 prompt cache。Cron 和 Webhook 更像“任务触发器”,负责把一段 prompt、一个调度上下文或一个事件 payload 送进 runtime。
所以,更准确的总结是:
Hermes 共享的是 runtime 和状态,不是共享一个全局单例对象。
Agent Runtime:AIAgent才是控制面
Hermes 的控制面集中在一个核心类里:AIAgent,定义在run_agent.py。
截至当前仓库,这个文件已经超过一万二千行。很多人第一次看到这种体量会本能紧张,但先别急着给它判死刑。对 Hermes 这种系统来说,真正复杂的东西恰恰不是“单个工具怎么写”,而是:
什么时候该调用工具;
什么时候该压缩上下文;
什么时候该切 fallback;
什么时候该把经验沉淀成记忆或技能;
什么时候该把结果流式发出去,什么时候该静默在后台做 review。
这些都属于控制面逻辑,而不是“功能点堆砌”。
先看一个更贴近源码的主循环
下面这段不是逐字摘抄源码的方法名,而是按真实实现整理后的结构化伪代码:
class AIAgent: def run_conversation(self, user_message, conversation_history=None): self._restore_primary_runtime() messages = list(conversation_history or [ ]) messages.append({"role": "user", "content": user_message}) if self._cached_system_prompt is None: self._cached_system_prompt = ( load_stored_prompt_or_build_once() ) while iteration_budget_not_exhausted(): api_messages = build_api_messages( system_prompt=self._cached_system_prompt, messages=messages, prefill_messages=self.prefill_messages, external_memory_prefetch=..., ) response = call_model_with_retries() if needs_context_compression(response, messages): messages, self._cached_system_prompt = self._compress_context(...) continue if response.tool_calls: messages.append(assistant_message) self._execute_tool_calls(...) maybe_warn_context_pressure() maybe_compress_context() continue final_response = strip_think_blocks(response.content) messages.append(final_message) break persist_session(messages) maybe_spawn_background_review(messages) return final_response你看,这里最重要的不是“模型调了一次 API”,而是:
system prompt 要按 session 缓存;
API 失败时可能重试,也可能 fallback;
工具调用之后要继续迭代;
上下文可能在中途被压缩;
对话结束后还可能触发后台 review。
Hermes 的复杂度,正是从这里长出来的。
关键决策一:system prompt 以 session 为单位缓存
这一点是 Hermes 架构里最容易被低估的设计。
真实方法是_build_system_prompt(),它不是每轮都重新拼,而是按 session 构造一次后缓存。如果是续聊会话,Hermes 甚至会优先从 SQLite 中取回先前存下来的 system prompt 快照,而不是重新读磁盘拼装一份新的。
为什么?因为一旦 system prompt 前缀变化,很多支持 prompt caching 的模型就会掉缓存,成本和延迟都会恶化。
但这里也要讲严谨一点:
“缓存”不等于“永远不变”。
当前实现里,system prompt 一般在 session 内保持稳定,但发生 context compression 时会重建。所以更准确的表述应该是:
Hermes 优先保持 system prompt 稳定;只有压缩等少数重建事件发生时,才显式刷新。
关键决策二:fallback 不是简单重试,而是“本轮降级、下轮恢复”
Hermes 的 fallback 逻辑,核心不在“能切备用模型”,而在“切了以后怎么收回来”。
这里对应两个真实方法:
_try_activate_fallback():本轮内切到 fallback provider / model_restore_primary_runtime():下一轮开始前恢复主运行时
这意味着 Hermes 的 fallback 是按轮次生效的。
也就是说:
本轮主模型挂了,可以临时切备用模型把任务做完;
下一轮开始时,再给主模型一次新的机会;
不会因为一次偶发故障,就把整个 session 永久钉死在备用模型上。
这个细节非常工程化,但很值钱。因为它把“高可用”和“首选模型优先”同时保住了。
关键决策三:空响应恢复不是一个独立模块,而是写进主循环
很多人喜欢把这类逻辑抽成一个_classify_empty_content_response(),听上去很优雅。
Hermes 当前实现不是这样。
它更务实:把“空响应恢复”直接写在主循环后半段,分成几类策略连续处理:
thinking-only prefill:模型只返回 reasoning,没有可见文本时,先把这轮 assistant message 作为中间态塞回历史,再继续一轮。
empty retry:真正空响应时,做有限次数重试。
fallback on empty:连续空响应后,切到 fallback provider。
context compression:如果问题本质是上下文压力过高,转到压缩路径。
这段实现不“漂亮”,但非常真实。Hermes 在这里体现出来的不是“抽象优雅”,而是对线上脏故障的耐心处理。
关键决策四:工具并行是“白名单 + 路径感知”,不是一刀切
Hermes 的工具调用并行也不是“有多个工具就并发跑”。
它先看两件事:
这批工具是不是在并行安全白名单里;
如果涉及文件路径,目标路径是否可能冲突。
比如:
clarify这类交互工具禁止并行;一部分只读工具允许并行;
read_file、write_file、patch这类 path-scoped 工具,还要做路径重叠分析;真正进入并行执行后,还有
max_workers=8的上限。
这套策略的价值在于:它没有为了“并行看起来很高级”而牺牲正确性。对 Agent 来说,写坏文件往往比慢一点更可怕。
关键决策五:上下文压缩不是只“删消息”,而是维护会话谱系
Hermes 的_compress_context()也值得特别看一眼。
它不是简单删掉旧消息,而是:
先在必要时 flush memory;
对历史做摘要压缩;
生成新的 system prompt;
在 SQLite 里把压缩前后的 session 通过
parent_session_id串起来。
这意味着压缩不是“丢历史”,而是“把历史换成更短、还能追溯的表示”。
这个设计背后其实有一句很朴素的话:
上下文窗口是有限的,但会话历史不应该凭空消失。
关键决策六:后台 review 是“尽力而为”的,而且有触发条件
Hermes 很有代表性的一个机制,是_spawn_background_review()。
它会在主响应发出去之后,起一个后台 review agent,去看这轮对话里是否值得:
写入记忆;
更新或创建技能。
但这里也要说清楚:
它不是“每轮对话无条件启动”。
当前实现里,它受 memory nudge / skill nudge 条件控制。只有达到相应阈值时,才会在 turn 结束后触发后台 review。
这比“每轮都复盘”更克制,也更符合工程现实。后台学习机制当然很酷,但它首先得不打扰主任务。
执行层:工具、终端后端、状态存储
如果说AIAgent是控制面,那么执行层就是 Hermes 真正把事情做成的地方。
它包含三部分:
工具能力层
执行环境抽象
持久化状态
工具能力层:Registry 是中心,不是散装函数集合
Hermes 的工具系统不是“写一堆函数,然后硬塞给模型”。
它有一个非常明确的中心:registry。
每个工具注册时至少要提供三样东西:
schema:给模型看的工具描述handler:真正执行逻辑check_fn:可选的可用性检查
这意味着 Hermes 的工具系统从一开始就不是“脚本拼盘”,而是一个有元数据、有可用性判断、有统一分发入口的能力层。
你会在这里看到很多熟悉的能力:
文件读写与 patch
terminal
web / browser
memory / skills / session search
cronjob
delegate
MCP 扩展
从架构上看,这一层的贡献是:把“模型能调用什么”从 Agent 主循环里剥离出来。
终端后端:六种 backend,共享一个terminal抽象
Hermes 的 terminal 能力背后,不是单一 shell,而是一层 backend abstraction。
当前支持的典型后端包括:
Local
Docker
SSH
Modal
Daytona
Singularity
但这里有一个地方也很容易被理解错:
Hermes 不是让模型在一次对话里“随意切换 backend”的。
当前实现里,模型看到的是统一的terminal工具;真正落到哪种后端,通常由terminal.backend配置或TERMINAL_ENV决定。也就是说,Hermes 的重点是:
同一个工具抽象,可以落到不同执行环境。
这跟“模型在对话里动态挑环境”不是一回事。
这种设计的好处是,控制面不需要知道 backend 的细节;它只需要把“执行命令”交给 terminal abstraction,后面由 backend adapter 去处理。
持久化状态:不是“全都文件化”,而是“按用途选载体”
Hermes 的状态设计,很值得单独夸一句:它不是把一切都扔进数据库,也不是把一切都扔进 Markdown。
它的策略是:
会话索引与搜索:SQLite
记忆与技能:文件
认证与配置:JSON / YAML /
.env检查点与计划任务:专用目录
如果你用默认 profile,典型目录大致长这样:
~/.hermes/ ├── config.yaml ├── .env ├── auth.json ├── state.db ├── memories/ │ ├── MEMORY.md │ └── USER.md ├── skills/ ├── sessions/ │ ├── session_<id>.json │ └── <session_id>.jsonl ├── checkpoints/ ├── cron/ └── logs/这里有三点特别重要:
第一,默认目录是~/.hermes/,但并不是硬编码死的。Hermes 支持 profiles,本质上通过HERMES_HOME把整套状态目录切到别的路径,比如~/.hermes/profiles/<name>。
第二,不是所有文件都会在首次启动时一次性生成。比如state.db、sessions/往往很快就会出现,但memories/MEMORY.md、USER.md通常是第一次真正写入记忆时才创建。
第三,不是“所有状态都文件化”,而是“关键状态尽量对人可见”。会话搜索显然更适合放 SQLite;记忆和技能更适合人类直接打开看、直接改。这种载体选择,比“统一存一种东西”成熟得多。
为什么是三层,而不是两层
现在我们回头看,为什么 Hermes 适合切成这三层?
答案不神秘:因为这三层的变化频率不一样。
层 | 主要职责 | 变化频率 | 典型变化 |
|---|---|---|---|
接入层 | 统一不同入口 | 高 | 新平台、新协议、新适配器 |
Agent runtime | 决策与主循环 | 低 | fallback 策略、压缩策略、回调机制 |
执行层 | 工具、环境、状态 | 中 | 新工具、新 backend、新存储策略 |
这个切法很朴素,但非常有效。
它带来的直接好处是:
新增一个消息平台,不必改
AIAgent主循环;新增一个工具,不必碰接入层;
调整 fallback 或压缩策略,不必重写 terminal backend。
这也是为什么run_agent.py虽然大,但没有糊成一锅粥。它承载的是控制面复杂度,而不是所有复杂度。
换句话说:
大文件不是优点,但“把真正该放在控制面的复杂度,老老实实放回控制面”是优点。
这句话,用来评价 Hermes 很合适。
和终端型编码代理的定位差异
如果你平时也用 Claude Code、Codex CLI 这类终端型编码代理,理解 Hermes 最好的方式,不是问“谁更强”,而是先问“它们优先优化的到底是什么”。
终端型编码代理优先优化的,通常是:
单次交互延迟
本地开发循环
终端内的可控性
编码任务的即时反馈
Hermes 优先优化的,则更像是:
多入口一致性
跨会话状态
可调度性
记忆/技能的长期积累
所以,Hermes 不是在和终端型编码代理争“谁更像一个好用的 shell 搭档”。
它更像是在回答另一个问题:
如果 Agent 不只服务一个终端窗口,而是服务你整套数字工作流,它的控制面应该怎么设计?
这也是为什么 Hermes 代码树里会同时出现gateway/、cron/、acp_adapter/、tools/environments/、hermes_state.py这些目录。它天然就是一个“面向多入口和长生命周期状态”的系统。
“The agent that grows with you” 背后的工程取舍
Hermes 的标语是 “The agent that grows with you”。这句话看起来像文案,但如果你对着源码看,会发现它背后其实是几组很明确的工程取舍。
取舍一:用 Python 做控制面,而不是把所有东西都追求成低层高性能
Hermes 的主体代码是 Python。
这件事本身已经说明了团队的优先级:他们首先优化的是控制流的可塑性,不是极限吞吐。
因为 Hermes 最难的部分不是矩阵乘法,也不是高性能网络 IO,而是这些控制面问题:
prompt 怎么拼才不破坏 cache;
fallback 怎么切才不把 session 钉死;
review 什么时候触发才不打扰主任务;
工具并发什么时候安全;
历史什么时候该压缩,压缩后怎么续上谱系。
这些问题的迭代速度,往往比运行时性能更值钱。
取舍二:SQLite,而不是单用户场景下先上 PostgreSQL
Hermes 的会话状态库是 SQLite,并且明确用了:
WAL
FTS5
应用层随机抖动重试,处理写锁竞争
这说明它不是“先用 SQLite 将就一下”,而是认真把 SQLite 当成单用户 Agent 控制面的默认数据库来调。
这背后的判断也很实际:
Hermes 的默认部署形态,不是一个要服务海量租户的 SaaS 控制平面,而是一个个人/小团队 Agent runtime。
在这个场景里,少一个数据库服务,往往比多一点理论扩展性更重要。
取舍三:记忆和技能用文件,会话检索用数据库
Hermes 没有把所有状态都结构化。
它把“需要全文搜索、排序、统计”的东西交给state.db;把“需要读、需要改、需要让人一眼看懂”的东西放到memories/和skills/。
这个选择的真正价值在于:
记忆对模型友好,Markdown 可以直接进 prompt;
对用户友好,打开文件就能看见 agent 记住了什么;
对工程也友好,不需要把“可解释的人类文本”反序列化成各种表结构。
一句话总结就是:
高频检索的数据,用数据库;长期阅读和手工编辑的数据,用文件。
取舍四:控制面偏长驻,执行面可替换
这是 Hermes 最有意思、也最容易被一句话说歪的地方。
如果你只看 Gateway 和 Cron,会觉得它强调长驻进程; 如果你再看 terminal backends,又会发现它执行面可以落到 Docker、SSH、Modal、Daytona 这些完全不同的环境。
所以更准确的表述不是“Hermes 反对 serverless”,而是:
Hermes 的控制面偏长驻,执行面则保持可替换。
这是一种很聪明的分工:
控制面长驻,才能维护 session、cache、调度、review 这些长期状态;
执行面可替换,才能把实际工作放到本地、容器、远端机器,甚至按需唤起的云环境里。
这恰恰就是“三层架构”真正发挥威力的地方。
动手:5 分钟安装并观察一次真实运行
讲到这里,最好的办法就是自己跑一遍。
目标很简单:装起来,发起第一次会话,然后看看三层架构在运行时各自留下了什么痕迹。
第一步:安装
官方安装脚本是:
curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash如果你更习惯从源码装,开发者路径通常会是:
git clone https://github.com/NousResearch/hermes-agent.git cd hermes-agent uv venv venv --python 3.11 source venv/bin/activate uv pip install -e ".[all,dev]" hermes --version第二步:配置模型提供商
最稳妥的方式是直接跑交互式模型配置:
hermes model如果你已经有 API Key,也可以先手动写入:
hermes config set OPENROUTER_API_KEY sk-or-xxxxxxxxxxxxxxxxxx hermes model这里有两个容易踩坑的地方,顺手提醒你:
统一入口是
hermes config set、hermes model、hermes auth。hermes login也已经不是推荐入口了;现在更推荐hermes model或hermes setup。
第三步:第一次对话
启动 CLI:
hermes然后给它一个很合适的首任务:
帮我分析一下当前目录下的代码结构,告诉我这是一个什么项目。这个任务通常会触发文件与搜索类工具,足够让你看到:
接入层:CLI 把输入变成一轮 agent turn;
Agent runtime:主循环不断“推理 -> 工具 -> 推理”;
执行层:文件工具和 terminal backend 真正落地执行。
第四步:看状态目录里发生了什么
另开一个终端,先看状态目录:
ls -la ~/.hermes你大概率会看到:
state.db:SQLite 会话库,负责元数据和搜索;sessions/:session log / transcript 文件;config.yaml、.env、auth.json:配置与认证状态;memories/:目录可能已经创建,但MEMORY.md、USER.md是否出现,要看本轮有没有真正写入记忆。
如果你想更系统地检查当前环境,也可以直接运行:
hermes doctor这个命令会把state.db、memories/、.env等关键状态都扫一遍。
第五步:做一个“可能触发技能沉淀”的小实验
再给它一个稍微复杂一点的任务,比如:
帮我把这个项目里所有 print() 替换成 logging.info(),并补上必要的 logger 初始化。这个实验的观察点不是“它一定会生成技能”,而是:
在复杂任务、错误恢复、复杂 workflow 出现之后,Hermes 有没有尝试把经验沉淀成 skill。
你可以做完之后看看:
find ~/.hermes/skills -name SKILL.md | head如果这轮触发了对应的 nudge 和后台 review,你可能会看到新 skill 出现;如果没有,也别惊讶。当前实现是“尽力而为”的学习闭环,不是“每做完一个复杂任务都确定性地生成一个技能文件”的编译器。
这一点,反而恰恰说明它更接近真实系统。
小结
这一讲我们做了三件事。
第一,把 Hermes 的三层架构拆清楚了。接入层负责统一多入口,AIAgent负责控制面决策,执行层负责工具、执行环境和状态存储。
第二,把几个关键实现事实说严谨了。Hermes 共享的是 runtime 和状态,不是“全局单例大脑”;system prompt 以 session 为单位缓存,但压缩时会刷新;后台 review 有触发条件,不是每轮必跑;terminal backend 是统一抽象下的可替换执行面,不是模型随口切换的六个环境按钮。
第三,把它的工程品味看明白了。Python 做控制面,SQLite 做单用户默认状态库,记忆与技能用文件,控制面偏长驻、执行面可替换。所有这些选择,组合起来才形成了 Hermes 的系统气质。
如果你现在再回头看代码树,应该已经能分辨出哪些目录属于入口,哪些目录属于 runtime,哪些目录属于执行层了。做到这一点,第一讲的任务就完成了。
下一讲,我们进入 Hermes 最有代表性的一个子系统:模型无关的实现细节。它不是一句“支持 200+ 模型”那么简单,而是一整套 provider 解析、fallback 恢复、上下文长度适配和成本控制的工程实现。那一讲会更偏源码,也更有意思。
课后思考
Hermes 通过 CLI、Gateway、ACP、Cron、Webhook 等多个入口复用同一套 runtime,但不同前端的交互能力差异极大:
CLI 可以做 Rich/TUI 渲染;
消息平台很多时候只能发文本或编辑消息;
IDE 协议又有自己的事件模型。
与此同时,AIAgent又暴露了一组生命周期回调,比如工具开始、工具完成、thinking、stream delta、status 等。
问题来了:
如果让你来设计这套回调边界,你会把它们切成“通用事件流”,还是保留现在这种更贴近产品交互语义的回调集合?
欢迎在评论区聊聊你的设计取舍。下一讲,我们继续往下拆。
这是《深入 Hermes Agent:从原理到实战》专栏的第 01 讲。下一讲:《02 | 模型无关的秘密:200+ 模型的统一接入层》。
