Hermes Agent深度探索:一个会自我沉淀经验的终端智能体

从整体架构看 Hermes Agent:一个会自我沉淀经验的终端智能体

第一次打开 Hermes Agent,很多人会把它归类成"带工具调用的命令行 AI 助手"。这个判断并不算错,但它只描述了最外层的使用体验。真正深入源码之后,你会发现它更像一个面向长期运行的 Agent 运行时------模型只是其中一个组件,工具、会话、记忆、技能、插件、安全策略、多平台入口和状态恢复共同构成了完整系统。

这篇文章不会立刻深入某个具体模块的实现,而是先画出一张"源码地图",让你在阅读整个项目时,知道每一块代码为什么存在,以及它们之间怎么配合。后续系列文章会沿着这张地图逐层展开,建议先订阅收藏,再慢慢消化。

文章目录


一、源码地图:读懂 Hermes 的第一份指南

面对一个庞大的开源项目,最忌讳的就是随便点开一个文件从头读到尾。Hermes 既有一个上万行的 cli.py,也有 TypeScript 前端、消息平台适配、SQLite 会话管理......如果找不到主线,很容易在细节中迷失。

下面的表格列出了理解 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 子命令扩展

这 11 个入口覆盖了交互、运行、能力、状态、扩展五大块。把它们串起来,就得到了 Hermes 的主干架构。


二、一张图看懂整体架构:多入口、单运行时

下面这张图是我从源码抽象出来的整体架构,它比文件列表更直观:
#mermaid-svg-5Y5euR8ElmfmMCN8{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-5Y5euR8ElmfmMCN8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-5Y5euR8ElmfmMCN8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-5Y5euR8ElmfmMCN8 .error-icon{fill:#552222;}#mermaid-svg-5Y5euR8ElmfmMCN8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-5Y5euR8ElmfmMCN8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-5Y5euR8ElmfmMCN8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-5Y5euR8ElmfmMCN8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-5Y5euR8ElmfmMCN8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-5Y5euR8ElmfmMCN8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-5Y5euR8ElmfmMCN8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-5Y5euR8ElmfmMCN8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-5Y5euR8ElmfmMCN8 .marker.cross{stroke:#333333;}#mermaid-svg-5Y5euR8ElmfmMCN8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-5Y5euR8ElmfmMCN8 p{margin:0;}#mermaid-svg-5Y5euR8ElmfmMCN8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-5Y5euR8ElmfmMCN8 .cluster-label text{fill:#333;}#mermaid-svg-5Y5euR8ElmfmMCN8 .cluster-label span{color:#333;}#mermaid-svg-5Y5euR8ElmfmMCN8 .cluster-label span p{background-color:transparent;}#mermaid-svg-5Y5euR8ElmfmMCN8 .label text,#mermaid-svg-5Y5euR8ElmfmMCN8 span{fill:#333;color:#333;}#mermaid-svg-5Y5euR8ElmfmMCN8 .node rect,#mermaid-svg-5Y5euR8ElmfmMCN8 .node circle,#mermaid-svg-5Y5euR8ElmfmMCN8 .node ellipse,#mermaid-svg-5Y5euR8ElmfmMCN8 .node polygon,#mermaid-svg-5Y5euR8ElmfmMCN8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-5Y5euR8ElmfmMCN8 .rough-node .label text,#mermaid-svg-5Y5euR8ElmfmMCN8 .node .label text,#mermaid-svg-5Y5euR8ElmfmMCN8 .image-shape .label,#mermaid-svg-5Y5euR8ElmfmMCN8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-5Y5euR8ElmfmMCN8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-5Y5euR8ElmfmMCN8 .rough-node .label,#mermaid-svg-5Y5euR8ElmfmMCN8 .node .label,#mermaid-svg-5Y5euR8ElmfmMCN8 .image-shape .label,#mermaid-svg-5Y5euR8ElmfmMCN8 .icon-shape .label{text-align:center;}#mermaid-svg-5Y5euR8ElmfmMCN8 .node.clickable{cursor:pointer;}#mermaid-svg-5Y5euR8ElmfmMCN8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-5Y5euR8ElmfmMCN8 .arrowheadPath{fill:#333333;}#mermaid-svg-5Y5euR8ElmfmMCN8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-5Y5euR8ElmfmMCN8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-5Y5euR8ElmfmMCN8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5Y5euR8ElmfmMCN8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-5Y5euR8ElmfmMCN8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5Y5euR8ElmfmMCN8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-5Y5euR8ElmfmMCN8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-5Y5euR8ElmfmMCN8 .cluster text{fill:#333;}#mermaid-svg-5Y5euR8ElmfmMCN8 .cluster span{color:#333;}#mermaid-svg-5Y5euR8ElmfmMCN8 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-5Y5euR8ElmfmMCN8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-5Y5euR8ElmfmMCN8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-5Y5euR8ElmfmMCN8 .icon-shape,#mermaid-svg-5Y5euR8ElmfmMCN8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5Y5euR8ElmfmMCN8 .icon-shape p,#mermaid-svg-5Y5euR8ElmfmMCN8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-5Y5euR8ElmfmMCN8 .icon-shape .label rect,#mermaid-svg-5Y5euR8ElmfmMCN8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5Y5euR8ElmfmMCN8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-5Y5euR8ElmfmMCN8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-5Y5euR8ElmfmMCN8 :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 还是 Telegram 消息,所有的对话最终都流向同一个 AIAgent 和同一套 conversation_loop.py。工具不是散落在各个入口里的,而是通过统一的 registry 和 toolset 暴露;会话状态不是由各个界面私有的 SQLite 维护,而是全部写入 SessionDB;扩展能力也不是让你去改核心代码,而是通过插件、hooks、middleware 和 provider profile 接入。

这就是 Hermes 最核心的设计思想:

  • 多入口,单运行时
  • 多工具,单注册体系
  • 多平台,单会话状态
  • 多扩展,明确边界

理解这一点之后,再去看那些动辄数千行的文件,就不会觉得它们是在"重复造轮子",而是不同入口在同一个底座上的具体表达。


三、逐层拆解:六条主线的分工与配合

架构图把复杂的系统压缩成了几个大方块,但每个方块内部都有自己的设计考量。下面我们从六个维度逐层拆解,看看每一层的核心问题和解决方案。

1. 入口层:不是为了多写几个聊天程序

Hermes 的入口种类之多,可能让第一次看项目的人犯迷糊:为什么既有 cli.py,又有 Ink TUI,还有 Dashboard、Desktop,甚至 Gateway 消息平台?

  • 经典 CLI :基于 prompt_toolkit,面向习惯终端操作的用户,处理键盘输入、命令解析、审批流程。
  • Ink TUI:基于 React/Ink,在终端内提供更结构化的面板布局,支持多栏显示。
  • Dashboard :通过 xterm.js + PTY 直接嵌入真实 hermes --tui 进程,而不是用 React 重新写一个聊天界面。
  • Gateway:对接 Telegram、Discord、Slack、WhatsApp 等消息平台,把对话请求转发给 Agent。
  • Desktop :独立 Electron + React 桌面应用,使用 tui_gateway 提供的后端协议,但本身并不内嵌 TUI 终端。

这里面最关键的设计决策是:Dashboard 的主聊天界面没有重新实现 transcript 和 composer,而是嵌入了真实的 TUI。为什么?因为对话体验涉及 slash command、审批、输入中断、流式输出、工具进度和历史恢复等大量细节,如果每个入口都自己实现一遍,这些逻辑很快就会分叉、互不一致。让聊天体验的"单一事实来源"保持在 Ink TUI 中,其他入口负责做好各自擅长的包装和上下文,才是长期可维护的做法。

启发:如果你要新增一个用户可触发的动作,不要第一反应是"在哪个界面加按钮"。先问清楚:这个动作的语义属于命令、工具、会话状态还是配置?语义落好之后,再让 CLI、TUI、Gateway 或 Dashboard 各自派生入口。这样动作就不会成为某个界面的特例,而是整个系统的通用能力。

2. 运行时层:AIAgent 是门面,conversation_loop 是主轴

run_agent.py 里的 AIAgent 类,从外面看提供了 chat()run_conversation() 两个入口。但如果只读这个文件,你会发现很多方法其实只是做了转发,实际逻辑已经拆到了 agent/ 包中。例如 run_conversation() 最终调用的是 agent.conversation_loop.run_conversation()

这是一种务实的重构策略:保留 run_agent.py 作为公共 API 和测试 patch 点,避免大规模破坏已有依赖,同时把具体实现逐步拆解到独立模块,降低单文件复杂度。

真正的核心会话循环可以简化成以下伪代码:

text 复制代码
准备系统提示、用户消息、历史、记忆、工具 schema
while 没超过 max_iterations 且还有 iteration budget:
    调用模型
    如果模型返回 tool_calls:
        执行工具
        把工具结果追加为 role=tool
        继续下一轮
    否则:
        得到最终 assistant 文本
        持久化并返回

当然,生产环境的实现远比这复杂。Hermes 还需要处理:

  • provider 的速率限制和 fallback model 切换
  • 空响应、截断响应、invalid JSON arguments 的容错
  • reasoning-only assistant message(模型只输出思考过程,不返回最终文本)
  • Codex Responses API 和严格 Chat Completions provider 之间的字段差异
  • 工具调用去重、并发判断、delegate_task 上限控制
  • 中断、预算管理、checkpoint、记忆同步以及 hooks 和 middleware 的触发

这些复杂度都围绕一个事实:Agent 循环不是一次 HTTP 请求,而是一个可能持续很多轮、不断改变外部世界、需要可恢复和可审计的执行过程。 所以会话循环的设计,必须把"可靠执行"放在第一位,而不是简单拼几次 API 调用。

3. 能力层:工具注册 + Toolset 共同定义"模型能做什么"

Hermes 的工具系统并不在一个文件里罗列所有工具。每个工具模块(比如 tools/file_tools.py)在顶层调用 registry.register() 把自己注册进去;tools/registry.py 用 AST 扫描找出哪些模块真正注册了工具,然后导入它们,让注册动作自动发生。model_tools.py 再从 registry 取出工具定义,过滤成模型 API 能接受的 JSON Schema。

整个链路如下:
#mermaid-svg-ItHA2ijAUoHyk2AV{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-ItHA2ijAUoHyk2AV .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ItHA2ijAUoHyk2AV .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ItHA2ijAUoHyk2AV .error-icon{fill:#552222;}#mermaid-svg-ItHA2ijAUoHyk2AV .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ItHA2ijAUoHyk2AV .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ItHA2ijAUoHyk2AV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ItHA2ijAUoHyk2AV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ItHA2ijAUoHyk2AV .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ItHA2ijAUoHyk2AV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ItHA2ijAUoHyk2AV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ItHA2ijAUoHyk2AV .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ItHA2ijAUoHyk2AV .marker.cross{stroke:#333333;}#mermaid-svg-ItHA2ijAUoHyk2AV svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ItHA2ijAUoHyk2AV p{margin:0;}#mermaid-svg-ItHA2ijAUoHyk2AV .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ItHA2ijAUoHyk2AV .cluster-label text{fill:#333;}#mermaid-svg-ItHA2ijAUoHyk2AV .cluster-label span{color:#333;}#mermaid-svg-ItHA2ijAUoHyk2AV .cluster-label span p{background-color:transparent;}#mermaid-svg-ItHA2ijAUoHyk2AV .label text,#mermaid-svg-ItHA2ijAUoHyk2AV span{fill:#333;color:#333;}#mermaid-svg-ItHA2ijAUoHyk2AV .node rect,#mermaid-svg-ItHA2ijAUoHyk2AV .node circle,#mermaid-svg-ItHA2ijAUoHyk2AV .node ellipse,#mermaid-svg-ItHA2ijAUoHyk2AV .node polygon,#mermaid-svg-ItHA2ijAUoHyk2AV .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ItHA2ijAUoHyk2AV .rough-node .label text,#mermaid-svg-ItHA2ijAUoHyk2AV .node .label text,#mermaid-svg-ItHA2ijAUoHyk2AV .image-shape .label,#mermaid-svg-ItHA2ijAUoHyk2AV .icon-shape .label{text-anchor:middle;}#mermaid-svg-ItHA2ijAUoHyk2AV .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ItHA2ijAUoHyk2AV .rough-node .label,#mermaid-svg-ItHA2ijAUoHyk2AV .node .label,#mermaid-svg-ItHA2ijAUoHyk2AV .image-shape .label,#mermaid-svg-ItHA2ijAUoHyk2AV .icon-shape .label{text-align:center;}#mermaid-svg-ItHA2ijAUoHyk2AV .node.clickable{cursor:pointer;}#mermaid-svg-ItHA2ijAUoHyk2AV .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ItHA2ijAUoHyk2AV .arrowheadPath{fill:#333333;}#mermaid-svg-ItHA2ijAUoHyk2AV .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ItHA2ijAUoHyk2AV .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ItHA2ijAUoHyk2AV .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ItHA2ijAUoHyk2AV .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ItHA2ijAUoHyk2AV .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ItHA2ijAUoHyk2AV .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ItHA2ijAUoHyk2AV .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ItHA2ijAUoHyk2AV .cluster text{fill:#333;}#mermaid-svg-ItHA2ijAUoHyk2AV .cluster span{color:#333;}#mermaid-svg-ItHA2ijAUoHyk2AV 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-ItHA2ijAUoHyk2AV .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ItHA2ijAUoHyk2AV rect.text{fill:none;stroke-width:0;}#mermaid-svg-ItHA2ijAUoHyk2AV .icon-shape,#mermaid-svg-ItHA2ijAUoHyk2AV .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ItHA2ijAUoHyk2AV .icon-shape p,#mermaid-svg-ItHA2ijAUoHyk2AV .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ItHA2ijAUoHyk2AV .icon-shape .label rect,#mermaid-svg-ItHA2ijAUoHyk2AV .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ItHA2ijAUoHyk2AV .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ItHA2ijAUoHyk2AV .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ItHA2ijAUoHyk2AV :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 解决的是"当前会话允许模型看到这个工具"。

4. 状态层: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 用户偏好、事实、画像、项目常识 agent/memory_manager.pyplugins/memory/
Skills 可复用流程、领域方法、工具使用规程 skills/optional-skills/tools/skills_tool.py
Session Search 过去对话和执行轨迹 tools/session_search_tool.pyhermes_state.py
  • Memory 适合存: "用户通常用 pnpm""这个项目的主分支叫 main""用户偏好中文回答"。
  • Skills 适合存: "做代码评审时按什么步骤""发布前要检查哪些文件""某个 API 的调用流程"。
  • Session Search 适合回答:"上次我们怎么处理那个内存泄漏的 bug?"

把这三者混在一起,会产生大量噪声。所有事实都写成技能,技能文件会无限膨胀;所有流程都塞进 memory,检索会严重失真;所有历史都靠模型上下文硬塞,prompt 会爆炸。Hermes 的设计倾向是把长期经验按类型分开存储,在需要时再按需注入到提示或工具调用中。

6. 扩展层:插件、Hooks、Middleware、Provider Profile

Hermes 的扩展面很大,但它并不是允许用户随意修改核心文件。项目提供了几条明确的扩展路径:

  • 普通插件hermes_cli/plugins.py):可以注册工具、hooks、middleware、CLI 子命令。
  • Memory provider 插件plugins/memory/):接入不同的长期记忆后端(如 Chroma、Weaviate 等)。
  • Model provider 插件plugins/model-providers/):注册自定义的 ProviderProfile,接入新模型。
  • Observer hooks:只读观测运行时事件,用于日志、监控、审计。
  • Middleware:改写 LLM 请求、工具请求或包裹执行过程,实现策略控制。
  • MCP:把外部进程和服务作为标准化的工具接入。

这套扩展边界的价值在于 避免核心文件被插件名字污染 。如果某个插件需要新能力,正确做法是扩展通用的 plugin surface(比如增加一个新的 hook 点),而不是在 run_agent.pycli.py 里写 if plugin_name == 'xxx'


四、安全层:能力越强,边界就越要清晰

Hermes 能执行终端命令、读写文件、控制浏览器、发送消息、接收 webhook、运行后台任务。这些能力如果没有边界,就不是生产力工具,而是安全灾难。

当前版本的安全体系至少包含这几道防线:

  1. 危险命令审批:在执行某些高风险命令之前,CLI/TUI/Gateway 会触发审批流程,需要用户确认。
  2. 工作目录控制 :CLI 使用进程的当前工作目录,Gateway 通过 terminal.cwd 限制操作范围。
  3. 平台授权gateway/authz_mixin.py 可以过滤允许交互的用户、聊天或群组。
  4. Toolset 降权:Webhook 等不可信入口默认只获得安全工具集,不直接暴露终端和文件能力。
  5. 依赖锁定pyproject.toml 中核心依赖精确锁定或设置上界,避免供应链攻击。
  6. Secret 管理 :API key 等敏感信息放在 .env 中,普通配置放在 config.yaml 里,避免误提交。

这些防线不是互相替代的关系。审批不能替代平台授权,依赖锁定不能替代工具审批,工作目录限制也不能替代入口降权。一个真正可靠的 Agent 安全体系,必须是多层防线的叠加。


五、阅读源码的正确姿势:先画边界,再看函数

读 Hermes 这种大型项目,最容易犯的错误就是直接打开最大的文件从头看起。这样你很快会陷入细节,纠结某个 callback 为什么存在、某个字段为什么要清理、某个平台为什么有特殊分支。

我建议先画四条边界,建立坐标系:

第一条边界:入口边界

cli.pyui-tui/gateway/、Dashboard 和 Desktop 都是入口,但它们不应该各自拥有一套 Agent 逻辑。判断一个改动是否合理,先看它是不是只属于"入口体验"。如果只是按钮、快捷键、布局、颜色、前端状态,就放在入口层;如果涉及会话、工具、模型请求、记忆、审批,就应该回到共享运行时。

第二条边界:能力边界

模型能做什么,不是由 prompt 里一句"你可以使用工具"决定的,而是由 tools/registry.pymodel_tools.pytoolsets.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 中的那些转发方法并不是多余的间接层;你会明白 Dashboard 嵌入 TUI 不是在偷懒,而是在刻意避免第二套聊天实现;你也会看懂 webhook safe tools 不是功能缺失,而是入口降权的安全设计。


六、一个真实改造案例:新增内部工单查询能力

理论讲完,来看一个具体的场景。假设团队想让 Hermes 能够查询内部工单系统。最粗暴的做法是在 run_agent.py 里加一个特殊判断:如果用户问题中包含"工单"关键词,就调用内部 API。

这个方法很快,但架构上完全错误:

  • 模型不知道这个能力的存在,无法在 tool_choice 中主动选择
  • TUI 和 Gateway 无法以统一的方式展示工具调用
  • 权限控制和错误处理无法复用
  • 其他入口(比如 webhook)完全不知道这个新能力应该被排除

更合理的改造路线是:

  1. 写一个插件工具(如果能力只在团队内部使用)。
  2. 插件通过 ctx.register_tool() 注册一个 ticket_search 工具。
  3. 明确 schema 参数,例如 querystatusassignee
  4. handler 返回结构化的 JSON 字符串,失败也要返回可读的错误信息。
  5. 根据安全需要,把工具放进一个特定的 toolset,默认不加入 webhook safe tools。
  6. 如果工单系统需要 OAuth 或 token,把 secret 放在 .env 或 Hermes 的 secret 管理路径下。
  7. 给 CLI、TUI、Gateway 都跑一条端到端测试,确保工具调用能被展示、记录和恢复。

这个案例很好地体现了 Hermes 架构的价值:能力一旦进入工具层,所有入口自然受益;状态写入 SessionDB,可搜索、可恢复;插件可以独立启用或禁用;工具调用能被 observer 记录;middleware 也可以统一施加策略。 你不需要为每个界面单独写一次"工单查询"逻辑。


七、常见误区与总结

最后,把最常见的几个认知偏差收拢一下:

  • 误区一:把 Hermes 当成一个 CLI 项目

    如果只从 cli.py 看 Hermes,会误以为所有逻辑都该放进 CLI。实际上 CLI 只是入口之一。一个新能力如果需要 TUI、Gateway、Dashboard 复用,应该落在命令 registry、工具 registry、AIAgent、tui_gateway RPC 或插件扩展点,而不是只写在 HermesCLI.process_command() 里。

  • 误区二:工具越多越强

    工具越多,模型越容易误选,schema 越长,安全面越大。Toolset 存在的意义就是把能力和场景匹配起来。Webhook 和本地 CLI 不应该看到同样的工具集合。

  • 误区三:Dashboard 应该用 React 重写聊天界面

    Dashboard 的主聊天嵌入真实 TUI,是为了避免维护第二套 transcript/composer。React 可以负责侧栏、模型选择、状态面板,但主聊天路径应该与 TUI 保持一致。

  • 误区四:插件需要新能力就直接改核心

    插件系统的规则恰恰相反。插件需要能力时,应扩展通用 hook、middleware 或 ctx method,而不是把插件名写进 if 判断。

Hermes Agent 的架构可以概括为:多入口、单运行时;多工具、单注册体系;多状态、统一会话数据库;多扩展、明确边界;高能力、多层安全。

理解它时,不要先被文件数量吓住。先抓住六条主线:

  1. 入口:CLI、TUI、Dashboard、Gateway
  2. 运行时AIAgent 和 conversation loop
  3. 能力:工具 registry 和 toolset
  4. 状态:SessionDB、Memory、Skills
  5. 扩展:插件、hooks、middleware、provider、MCP
  6. 安全:审批、授权、工作目录、依赖和入口降权

后续系列文章会分别展开这些层。读完整个系列之后,你再回头看这个仓库,就不会只是看到一个复杂的项目,而是能看出它为什么这样复杂,以及每种复杂度分别服务什么目标。


如果你也在维护一个长期运行的 Agent 系统,或者正在给 Hermes 做二次开发,欢迎在评论区分享你的架构取舍和经验。下一篇我会深入 conversation_loop.py,拆解它的重试、中断和记忆同步机制,敬请期待。

相关推荐
数智顾问2 小时前
(151页PPT)XX集团信息化整体架构规划及ERP方案建议书(附下载方式)
大数据·架构
caimouse2 小时前
Reactos 第1章 概述
c语言·开发语言·架构
namexingyun2 小时前
拆解Fable 5三重安全护栏:模型路由、蒸馏防护与生物安全分类器的技术原理 - 微元算力(weytoken)
java·人工智能·python·安全·架构·ai编程
小短腿的代码世界2 小时前
行情快照与增量更新引擎:Qt在高频交易数据分发中的核心架构——你的行情推送为什么延迟了500ms?
开发语言·qt·架构
上海云盾第一敬业销售2 小时前
高效阻止网站攻击的WAF防护架构解析
web安全·架构·ddos
意图共鸣3 小时前
意图共鸣科技《AI记忆链商业化白皮书3.0》假设场景解析:从母亲到消防员,专属AI如何重塑记忆与传承
人工智能·科技·架构
FPGA小徐3 小时前
Xilinx zynq-7000系列FPGA移植Linux操作系统详细教程
fpga开发·架构
染指11103 小时前
24.RAG进阶(Advanced RAG)-摘要索引
langchain·rag
王二端茶倒水3 小时前
智慧小区宽带无线运营:从网络交付到认证、计费与运维闭环
运维·物联网·架构