从整体架构看 Hermes Agent:一个会自我沉淀经验的终端智能体
第一次打开 Hermes Agent,很多人会把它归类成“带工具调用的命令行 AI 助手”。这个判断并不算错,但它只描述了最外层的使用体验。真正深入源码后会发现,Hermes Agent 更像一个面向长期运行的 Agent 运行时:模型只是其中一个组件,工具、会话、记忆、技能、插件、安全策略、多平台入口和状态恢复共同组成了系统。 
如果把 Hermes 当成一个普通 CLI 项目阅读,很容易迷路。因为它既有 cli.py 这样上万行的经典终端界面,也有 ui-tui/ 里的 Ink TUI,还有 gateway/ 里的 Telegram、Discord、Slack、WhatsApp 等平台适配;既有 tools/registry.py 负责工具发现,也有 model_tools.py 负责工具 schema 和执行调度;既有 hermes_state.py 维护 SQLite 会话数据库,也有 Memory、Skills 和 Session Search 做长期经验沉淀;同时还提供插件、middleware、observer hooks、model-provider plugins 和 MCP。
本篇先从整体架构出发,建立一张“源码地图”。后续系列文章会沿着这张地图逐层展开,欢迎订阅本主题系列。
文章目录
- 从整体架构看 Hermes Agent:一个会自我沉淀经验的终端智能体
-
- 源码地图
- 一张图看整体架构
- 入口层:不是写了多个聊天程序
- 运行时层:AIAgent 是门面,conversation_loop 是主轴
- 能力层:工具注册和 Toolset 共同定义模型能做什么
- 状态层:SessionDB 让 Agent 不是一次性问答
- 经验层:Memory 与 Skills 的分工
- 扩展层:插件、Hooks、Middleware 和 Provider Profile
- 安全层:能力越强,边界越要清楚
- 常见误区
-
- 误区一:把 Hermes 当成一个 CLI 项目
- 误区二:工具越多越强
- 误区三:Dashboard 应该重写聊天界面
- 误区四:插件需要能力就改核心
- 二次开发检查清单
- 总结
- 源码走读建议:先画边界,再看函数
- 一个真实改造案例:新增内部工单查询能力
源码地图
下面这些文件和目录,是理解 Hermes Agent 的第一批入口:
| run_agent.py | AIAgent 门面 | 代理对象的公共入口,保留大量测试和外部 patch 点 |
| agent/conversation_loop.py | 会话循环 | 真正执行模型请求、工具调用、重试、收尾 |
| model_tools.py | 工具编排层 | 把 registry 中的工具转成模型 schema,并执行 tool call |
| tools/registry.py | 工具注册中心 | 负责工具自注册、AST 发现、可用性缓存 |
| toolsets.py | 工具集合定义 | 决定哪些工具在什么场景暴露给模型 |
| cli.py | 经典 CLI | prompt_toolkit 终端交互、命令处理、审批、状态栏 |
| ui-tui/ | Ink TUI 前端 | TypeScript/React 终端 UI |
| tui_gateway/server.py | TUI Python 后端 | JSON-RPC 方法、session、prompt、slash、审批 |
| gateway/ | 消息平台入口 | Telegram、Discord、Slack 等平台适配和授权 |
| hermes_state.py | SessionDB | SQLite + FTS5 会话存储、恢复和搜索 |
| hermes_cli/plugins.py | 插件系统 | hooks、middleware、插件工具、CLI 子命令扩展 |
一张图看整体架构
#mermaid-svg-pH1OTCJ9pkXiNrGK{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-pH1OTCJ9pkXiNrGK .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-pH1OTCJ9pkXiNrGK .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-pH1OTCJ9pkXiNrGK .error-icon{fill:#552222;}#mermaid-svg-pH1OTCJ9pkXiNrGK .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-pH1OTCJ9pkXiNrGK .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-pH1OTCJ9pkXiNrGK .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-pH1OTCJ9pkXiNrGK .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-pH1OTCJ9pkXiNrGK .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-pH1OTCJ9pkXiNrGK .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-pH1OTCJ9pkXiNrGK .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-pH1OTCJ9pkXiNrGK .marker{fill:#333333;stroke:#333333;}#mermaid-svg-pH1OTCJ9pkXiNrGK .marker.cross{stroke:#333333;}#mermaid-svg-pH1OTCJ9pkXiNrGK svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-pH1OTCJ9pkXiNrGK p{margin:0;}#mermaid-svg-pH1OTCJ9pkXiNrGK .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-pH1OTCJ9pkXiNrGK .cluster-label text{fill:#333;}#mermaid-svg-pH1OTCJ9pkXiNrGK .cluster-label span{color:#333;}#mermaid-svg-pH1OTCJ9pkXiNrGK .cluster-label span p{background-color:transparent;}#mermaid-svg-pH1OTCJ9pkXiNrGK .label text,#mermaid-svg-pH1OTCJ9pkXiNrGK span{fill:#333;color:#333;}#mermaid-svg-pH1OTCJ9pkXiNrGK .node rect,#mermaid-svg-pH1OTCJ9pkXiNrGK .node circle,#mermaid-svg-pH1OTCJ9pkXiNrGK .node ellipse,#mermaid-svg-pH1OTCJ9pkXiNrGK .node polygon,#mermaid-svg-pH1OTCJ9pkXiNrGK .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-pH1OTCJ9pkXiNrGK .rough-node .label text,#mermaid-svg-pH1OTCJ9pkXiNrGK .node .label text,#mermaid-svg-pH1OTCJ9pkXiNrGK .image-shape .label,#mermaid-svg-pH1OTCJ9pkXiNrGK .icon-shape .label{text-anchor:middle;}#mermaid-svg-pH1OTCJ9pkXiNrGK .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-pH1OTCJ9pkXiNrGK .rough-node .label,#mermaid-svg-pH1OTCJ9pkXiNrGK .node .label,#mermaid-svg-pH1OTCJ9pkXiNrGK .image-shape .label,#mermaid-svg-pH1OTCJ9pkXiNrGK .icon-shape .label{text-align:center;}#mermaid-svg-pH1OTCJ9pkXiNrGK .node.clickable{cursor:pointer;}#mermaid-svg-pH1OTCJ9pkXiNrGK .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-pH1OTCJ9pkXiNrGK .arrowheadPath{fill:#333333;}#mermaid-svg-pH1OTCJ9pkXiNrGK .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-pH1OTCJ9pkXiNrGK .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-pH1OTCJ9pkXiNrGK .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-pH1OTCJ9pkXiNrGK .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-pH1OTCJ9pkXiNrGK .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-pH1OTCJ9pkXiNrGK .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-pH1OTCJ9pkXiNrGK .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-pH1OTCJ9pkXiNrGK .cluster text{fill:#333;}#mermaid-svg-pH1OTCJ9pkXiNrGK .cluster span{color:#333;}#mermaid-svg-pH1OTCJ9pkXiNrGK div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-pH1OTCJ9pkXiNrGK .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-pH1OTCJ9pkXiNrGK rect.text{fill:none;stroke-width:0;}#mermaid-svg-pH1OTCJ9pkXiNrGK .icon-shape,#mermaid-svg-pH1OTCJ9pkXiNrGK .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-pH1OTCJ9pkXiNrGK .icon-shape p,#mermaid-svg-pH1OTCJ9pkXiNrGK .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-pH1OTCJ9pkXiNrGK .icon-shape .label rect,#mermaid-svg-pH1OTCJ9pkXiNrGK .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-pH1OTCJ9pkXiNrGK .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-pH1OTCJ9pkXiNrGK .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-pH1OTCJ9pkXiNrGK :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
用户
经典 CLI / cli.py
Ink TUI / ui-tui
Dashboard / xterm + PTY
消息平台 Gateway
AIAgent / run_agent.py
tui_gateway/server.py
PTY bridge
agent/conversation_loop.py
模型提供商
工具系统
SessionDB / hermes_state.py
Memory / Skills / Session Search
tools/registry.py
toolsets.py
hooks / middleware / plugins
图里最重要的不是节点数量,而是方向。CLI、TUI、Dashboard、Gateway 都只是入口,最终尽量回到同一个 AIAgent 和同一套会话循环。工具不直接散落在各入口中,而是通过 registry 和 toolset 暴露。状态不由各界面私有维护,而是写入 SessionDB。扩展能力不鼓励修改核心文件,而是通过插件、hooks、middleware 和 provider profile 接入。
这就是 Hermes 的主线:多入口、单运行时;多工具、单注册体系;多平台、单会话状态;多扩展、明确边界。
入口层:不是写了多个聊天程序
Hermes 有多个用户入口:
- 经典 CLI:cli.py,基于 prompt_toolkit,负责传统终端交互。
- Ink TUI:ui-tui/,基于 React/Ink,负责更结构化的终端界面。
- Dashboard:通过 PTY 嵌入真实 hermes –tui,而不是重写聊天界面。
- Gateway:gateway/,接入 Telegram、Discord、Slack、WhatsApp、Signal 等消息平台。
- Desktop:独立 Electron + React 界面,使用 tui_gateway 后端协议,但不是嵌入 TUI。
这个设计的核心原则是:入口可以多,但 Agent 不应该复制多份。比如 Dashboard 的 /chat 页面并没有重新实现 transcript 和 composer,而是嵌入真实 TUI。这一点非常关键。因为主聊天体验如果被 React Dashboard、Ink TUI、经典 CLI 各自实现一套,slash command、审批、输入中断、流式输出、工具进度和历史恢复就会不断分叉。
Hermes 选择让入口层做自己最擅长的事:CLI 处理终端输入和 prompt_toolkit 兼容,TUI 处理 Ink UI 状态和键盘交互,Gateway 处理平台消息、授权和投递,Dashboard 处理浏览器容器和管理侧栏。真正的对话运行时尽量回到 AIAgent。
这种架构对二次开发很有启发。如果你要新增一个用户可触发动作,不应该先问“在哪个界面加按钮”,而应该先问“这个动作的语义属于命令、工具、会话状态还是配置”。语义落好以后,再让 CLI、TUI、Gateway 或 Dashboard 派生入口。
运行时层:AIAgent 是门面,conversation_loop 是主轴
run_agent.py 定义了 AIAgent。从外部看,它提供 chat() 和 run_conversation()。但如果只读 run_agent.py,会发现很多方法已经转发到 agent/ 包内。例如 run_conversation() 实际调用 agent.conversation_loop.run_conversation()。
这是一种很务实的重构方式。run_agent.py 仍然保留公共 API 和测试 patch 点,避免大规模破坏外部依赖;具体实现逐步拆到 agent/ 包里,降低单文件复杂度。
核心会话循环可以简化成:
准备系统提示、用户消息、历史、记忆、工具 schema
while 没超过 max_iterations 且还有 iteration budget:
调用模型
如果模型返回 tool_calls:
执行工具
把工具结果追加为 role=tool
继续下一轮
否则:
得到最终 assistant 文本
持久化并返回
真实实现当然比这复杂得多。Hermes 要处理:
- provider rate limit 和 fallback model。
- 空响应、截断响应、invalid JSON arguments。
- reasoning-only assistant message。
- Codex Responses API 和严格 Chat Completions provider 的字段差异。
- 工具调用去重、并发判断、delegate_task 上限。
- 中断、预算、checkpoint、记忆同步、hooks 和 middleware。
这些复杂度都围绕一个事实:Agent 循环不是一次 HTTP 请求,而是一个可能持续很多轮、不断改变外部世界、需要可恢复和可审计的执行过程。
能力层:工具注册和 Toolset 共同定义模型能做什么
Hermes 的工具系统不是在一个文件里写死所有工具。每个工具模块在顶层调用 registry.register(),tools/registry.py 用 AST 扫描找出真正注册工具的模块,然后导入它们,让注册动作发生。model_tools.py 再从 registry 中取出工具定义,过滤成模型 API 可以接收的 schema。
这条链路大致是:
#mermaid-svg-zF3kQrwP48HxGOV7{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-zF3kQrwP48HxGOV7 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-zF3kQrwP48HxGOV7 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-zF3kQrwP48HxGOV7 .error-icon{fill:#552222;}#mermaid-svg-zF3kQrwP48HxGOV7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-zF3kQrwP48HxGOV7 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-zF3kQrwP48HxGOV7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-zF3kQrwP48HxGOV7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-zF3kQrwP48HxGOV7 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-zF3kQrwP48HxGOV7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-zF3kQrwP48HxGOV7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-zF3kQrwP48HxGOV7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-zF3kQrwP48HxGOV7 .marker.cross{stroke:#333333;}#mermaid-svg-zF3kQrwP48HxGOV7 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-zF3kQrwP48HxGOV7 p{margin:0;}#mermaid-svg-zF3kQrwP48HxGOV7 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-zF3kQrwP48HxGOV7 .cluster-label text{fill:#333;}#mermaid-svg-zF3kQrwP48HxGOV7 .cluster-label span{color:#333;}#mermaid-svg-zF3kQrwP48HxGOV7 .cluster-label span p{background-color:transparent;}#mermaid-svg-zF3kQrwP48HxGOV7 .label text,#mermaid-svg-zF3kQrwP48HxGOV7 span{fill:#333;color:#333;}#mermaid-svg-zF3kQrwP48HxGOV7 .node rect,#mermaid-svg-zF3kQrwP48HxGOV7 .node circle,#mermaid-svg-zF3kQrwP48HxGOV7 .node ellipse,#mermaid-svg-zF3kQrwP48HxGOV7 .node polygon,#mermaid-svg-zF3kQrwP48HxGOV7 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-zF3kQrwP48HxGOV7 .rough-node .label text,#mermaid-svg-zF3kQrwP48HxGOV7 .node .label text,#mermaid-svg-zF3kQrwP48HxGOV7 .image-shape .label,#mermaid-svg-zF3kQrwP48HxGOV7 .icon-shape .label{text-anchor:middle;}#mermaid-svg-zF3kQrwP48HxGOV7 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-zF3kQrwP48HxGOV7 .rough-node .label,#mermaid-svg-zF3kQrwP48HxGOV7 .node .label,#mermaid-svg-zF3kQrwP48HxGOV7 .image-shape .label,#mermaid-svg-zF3kQrwP48HxGOV7 .icon-shape .label{text-align:center;}#mermaid-svg-zF3kQrwP48HxGOV7 .node.clickable{cursor:pointer;}#mermaid-svg-zF3kQrwP48HxGOV7 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-zF3kQrwP48HxGOV7 .arrowheadPath{fill:#333333;}#mermaid-svg-zF3kQrwP48HxGOV7 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-zF3kQrwP48HxGOV7 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-zF3kQrwP48HxGOV7 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zF3kQrwP48HxGOV7 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-zF3kQrwP48HxGOV7 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zF3kQrwP48HxGOV7 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-zF3kQrwP48HxGOV7 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-zF3kQrwP48HxGOV7 .cluster text{fill:#333;}#mermaid-svg-zF3kQrwP48HxGOV7 .cluster span{color:#333;}#mermaid-svg-zF3kQrwP48HxGOV7 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-zF3kQrwP48HxGOV7 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-zF3kQrwP48HxGOV7 rect.text{fill:none;stroke-width:0;}#mermaid-svg-zF3kQrwP48HxGOV7 .icon-shape,#mermaid-svg-zF3kQrwP48HxGOV7 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zF3kQrwP48HxGOV7 .icon-shape p,#mermaid-svg-zF3kQrwP48HxGOV7 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-zF3kQrwP48HxGOV7 .icon-shape .label rect,#mermaid-svg-zF3kQrwP48HxGOV7 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zF3kQrwP48HxGOV7 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-zF3kQrwP48HxGOV7 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-zF3kQrwP48HxGOV7 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
tools/*.py 顶层 registry.register
ToolRegistry
discover_builtin_tools AST 扫描
model_tools.py
模型 tools schema
模型选择 tool_calls
handle_function_call
工具 handler
但工具被 registry 发现,还不代表模型能看到它。toolsets.py 决定了工具暴露边界。比如 _HERMES_CORE_TOOLS 是 CLI 和主要消息平台默认继承的核心工具集合;_HERMES_WEBHOOK_SAFE_TOOLS 则是 webhook 这类不可信入口的保守工具集,只暴露 Web、vision、clarify 等相对安全的能力。
这说明 Toolset 不只是分类,也是一种安全和上下文控制。模型工具越多,提示越长,选择越难,误调用概率越高。尤其在 webhook 场景,如果默认给外部 payload 暴露终端和文件工具,提示注入的后果会被放大成本地执行风险。
新增工具时最常见的错误是只做了 registry.register(),忘了把工具加入 toolsets.py。结果工具在 registry 里存在,但模型永远看不到。正确理解应该是:
- registry 解决“系统知道这个工具存在”。
- toolset 解决“某个会话允许模型看到这个工具”。
状态层:SessionDB 让 Agent 不是一次性问答
Hermes 的会话状态由 hermes_state.py 的 SessionDB 承担。它使用 SQLite,并启用 FTS5 做全文搜索。它保存的不只是最终答案,而是 session metadata、完整消息历史、模型配置、来源信息、tool calls、reasoning 等可以支持恢复和检索的状态。
为什么不直接用 JSONL?因为 Hermes 要支持:
- /resume 恢复旧会话。
- /history 查看历史。
- /title 设置标题。
- /branch 从当前会话分支。
- session_search 搜索过去对话。
- Gateway 多平台共享同一套会话记录。
- Dashboard 和 TUI 列出最近会话。
这些需求需要索引、并发、迁移和故障恢复。hermes_state.py 中甚至专门处理 WAL 在 NFS/SMB/FUSE 上不可用的问题,必要时回退到 DELETE journal mode。它还处理 malformed schema 的恢复,尽量保护 canonical 的 sessions/messages 数据,只重建派生的 FTS 层。
这说明 SessionDB 在 Hermes 里不是缓存,而是用户长期资产。改数据库结构时必须格外保守。
经验层:Memory 与 Skills 的分工
Hermes 的长期经验不是单一“记忆”概念。它至少拆成三类:
| Memory | 用户偏好、事实、画像、项目常识 | agent/memory_manager.py、plugins/memory/ |
| Skills | 可复用流程、领域方法、工具使用规程 | skills/、optional-skills/、tools/skills_tool.py |
| Session Search | 过去对话和执行轨迹 | tools/session_search_tool.py、hermes_state.py |
Memory 适合存“用户通常用 pnpm”“这个项目的主分支叫 main”“用户偏好中文回答”。Skills 适合存“做代码评审时按什么步骤”“发布前要检查哪些文件”“某个 API 的调用流程”。Session Search 适合回答“上次我们怎么处理这个 bug”。
把这三者混在一起,会产生噪声。所有事实都写成技能,技能会膨胀;所有流程都塞进 memory,检索会失真;所有历史都靠模型上下文硬塞,prompt 会爆炸。Hermes 的设计倾向是把长期经验按类型分开,再在需要时注入。
扩展层:插件、Hooks、Middleware 和 Provider Profile
Hermes 的扩展面很大,但不是无限制地让用户改核心文件。项目提供了几条明确路径:
- 普通插件:hermes_cli/plugins.py,可以注册工具、hooks、middleware、CLI 子命令。
- Memory provider 插件:plugins/memory/,接入不同长期记忆后端。
- Model provider 插件:plugins/model-providers/,注册 ProviderProfile。
- Observer hooks:只读观测运行时事件。
- Middleware:改写 LLM 请求、工具请求或包裹执行。
- MCP:把外部进程和服务作为工具接入。
这套扩展边界的价值在于避免核心文件被插件名字污染。如果某个插件需要新能力,正确做法是扩展通用 plugin surface,而不是在 run_agent.py 或 cli.py 中写 if plugin_name == …。
安全层:能力越强,边界越要清楚
Hermes 能执行终端、读写文件、控制浏览器、发送消息、接收 webhook、运行后台任务。这些能力如果没有边界,就不是生产力工具,而是风险入口。
安全层至少包括:
- 危险命令审批:工具执行前触发 CLI/TUI/Gateway 审批。
- 工作目录控制:CLI 用进程 cwd,Gateway 用 terminal.cwd。
- 平台授权:gateway/authz_mixin.py 过滤 allowed users/chats/groups。
- Toolset 降权:Webhook 默认只给安全工具集。
- 依赖 pinning:pyproject.toml 中核心依赖精确锁定或设置上界。
- Secret 管理:API key 等放 .env,普通设置放 config.yaml。
这些不是互相替代的关系。审批不能代替平台授权,依赖锁定不能代替工具审批,工作目录也不能代替入口降权。真正可靠的 Agent 安全,需要多层防线。
常见误区
误区一:把 Hermes 当成一个 CLI 项目
如果只从 cli.py 看 Hermes,会误以为所有逻辑都该放进 CLI。实际上 CLI 只是入口之一。一个新能力如果需要 TUI、Gateway、Dashboard 复用,应该落在命令 registry、工具 registry、AIAgent、tui_gateway RPC 或插件扩展点,而不是只写在 HermesCLI.process_command() 里。
误区二:工具越多越强
工具越多,模型越容易误选,schema 越长,安全面越大。Toolset 存在的意义就是把能力和场景匹配起来。Webhook 和本地 CLI 不应该看到同样的工具集合。
误区三:Dashboard 应该重写聊天界面
Dashboard 的主聊天体验嵌入真实 TUI,是为了避免第二套 transcript/composer。React 可以做侧栏、模型选择、状态面板,但主聊天路径应该跟 TUI 保持一致。
误区四:插件需要能力就改核心
插件系统的规则恰恰相反。插件需要能力时,应扩展通用 hook、middleware 或 ctx method,而不是把插件名写进核心逻辑。
二次开发检查清单
准备改 Hermes 前,可以先问自己:
- 这是入口能力、模型工具、状态能力、运行时扩展,还是安全策略?
- 有没有现成的单一事实来源?例如 COMMAND_REGISTRY、ToolRegistry、toolsets.py、SessionDB、provider profile。
- 这个改动是否需要同时影响 CLI、TUI、Gateway、Dashboard?
- 失败结果会不会写回用户可理解的错误?
- 长进程场景下是否会阻塞 event loop、泄漏线程或污染缓存?
- 是否需要测试 alias、schema、session 恢复、审批或插件契约?
总结
Hermes Agent 的架构可以概括为:多入口、单运行时;多工具、单注册体系;多状态、统一会话数据库;多扩展、明确边界;高能力、多层安全。
理解它时,不要先被文件数量吓住。先抓六条主线:
后面的文章会分别展开这些层。读完整个系列后,再回头看仓库,就不会只是看到一个复杂项目,而是能看出它为什么这样复杂,以及每种复杂度分别服务什么目标。
源码走读建议:先画边界,再看函数
读 Hermes 这种项目,最容易犯的错误是直接打开最大的文件,从第一行开始往下看。这样会很快陷入细节:某个 callback 为什么存在,某个字段为什么要清理,某个平台为什么有特殊分支。更有效的方法是先画边界。
第一条边界是入口边界。cli.py、ui-tui/、gateway/、Dashboard 和 Desktop 都是入口,但它们不应该各自拥有一套 Agent。判断一个改动是否合理,先看它是不是只属于入口体验。如果只是按钮、快捷键、布局、颜色、前端状态,那放在入口层;如果涉及会话、工具、模型请求、记忆、审批,就应该回到共享运行时。
第二条边界是能力边界。模型能做什么,不是由 prompt 里一句“你可以使用工具”决定的,而是由 tools/registry.py、model_tools.py 和 toolsets.py 共同决定。新增能力时,不要只问“函数写在哪里”,还要问“什么入口能看到它,什么配置能禁用它,什么依赖检查能隐藏它,什么安全策略能拦截它”。
第三条边界是状态边界。会话、记忆、技能、历史搜索都是状态,但粒度不同。SessionDB 保存执行轨迹,Memory 保存长期事实,Skills 保存复用流程,Context Files 保存项目静态约定。把这些状态混在一起,短期可能省事,长期会让召回和维护失控。
第四条边界是扩展边界。插件工具、observer hook、middleware、model-provider plugin、MCP、Gateway platform adapter 都是扩展,但能力不同。只是记录运行时事件,用 observer;要改写请求或执行,用 middleware;要新增模型后端,用 provider plugin;要接消息平台,用 Gateway adapter。选错扩展点,代码就会往核心文件里渗透。
如果把这些边界先画出来,后面读函数会轻松很多。看到 run_agent.py 中的转发方法,不会觉得绕;看到 model_tools.py 中的缓存,不会觉得过度优化;看到 Dashboard 嵌入 TUI,不会误以为少做了 React 聊天页面;看到 webhook safe tools,也能明白它不是功能缺失,而是入口降权。
一个真实改造案例:新增内部工单查询能力
假设团队想让 Hermes 查询内部工单系统。最粗暴的做法是在 run_agent.py 中加一个特殊判断:如果用户问工单,就调用内部 API。这个做法很快,但架构上是错的。它绕过了工具 schema,模型不知道能力边界;TUI 和 Gateway 可能无法展示工具调用;权限和错误也不统一。
更合理的路线是:
这个案例体现了 Hermes 架构的好处:能力进入工具层后,各入口自然受益;状态写入 SessionDB;插件可以启用禁用;工具调用能被 observer 记录;middleware 也可以统一加策略。



