[Agent] Hermes Agent架构解析

Hermes(也有人戏称“爱马仕 Agent”)本质上不是又一个简单的 AI CLI,而是一套强调长期使用、持续沉淀和自我改进的 Agent runtime。它试图把工具调用、Skill 沉淀、跨会话记忆和安全边界,放进同一套可以长期演进的系统里。
很多项目在解决“怎么让 Agent 能跑起来”,Hermes 更关心的是另一层问题:怎么让 Agent 在反复使用之后,变得更稳、更熟练,也更像同一个 Agent。
把源码、官方文档和最近的 release notes 对着看完之后,我的感觉很明确:Hermes 和 OpenClaw 看上去都在做开源 Agent,但它们解决的其实不是同一层问题。
OpenClaw 更像入口层和调度层,重点是“消息怎么进来、会话怎么路由、平台怎么接”;
Hermes 更像 Agent 本体的执行与学习引擎,重点是“工具怎么用、经验怎么沉淀、下次怎么变强”。
也正因为这个差异,Hermes 真正值得拆的,不是它支持多少 provider、多少命令,而是它把下面这几件事串成了一条闭环:

  • 前台执行循环怎么跑
  • 复杂任务怎么复盘成 Skill
  • 长期记忆怎么分层存储与按需召回
  • 安全边界怎么放在框架层,而不是全靠模型自觉

下面我从架构解析开始讲起,逐步追踪这条闭环

Hermes Agent架构解析

系统概览

┌─────────────────────────────────────────────────────────────────────┐
│                        Entry Points                                  │
│                                                                      │
│  CLI (cli.py)    Gateway (gateway/run.py)    ACP (acp_adapter/)     │
│  Batch Runner    API Server                  Python Library          │
└──────────┬──────────────┬───────────────────────┬───────────────────┘
           │              │                       │
           ▼              ▼                       ▼
┌─────────────────────────────────────────────────────────────────────┐
│                     AIAgent (run_agent.py)                          │
│                                                                     │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐               │
│  │ Prompt       │  │ Provider     │  │ Tool         │               │
│  │ Builder      │  │ Resolution   │  │ Dispatch     │               │
│  │ (prompt_     │  │ (runtime_    │  │ (model_      │               │
│  │  builder.py) │  │  provider.py)│  │  tools.py)   │               │
│  └──────┬───────┘  └──────┬───────┘  └──────┬───────┘               │
│         │                 │                 │                       │
│  ┌──────┴───────┐  ┌──────┴───────┐  ┌──────┴───────┐               │
│  │ Compression  │  │ 3 API Modes  │  │ Tool Registry│               │
│  │ & Caching    │  │ chat_compl.  │  │ (registry.py)│               │
│  │              │  │ codex_resp.  │  │ 70+ tools    │               │
│  │              │  │ anthropic    │  │ 28 toolsets  │               │
│  └──────────────┘  └──────────────┘  └──────────────┘               │
└─────────┴─────────────────┴─────────────────┴───────────────────────┘
           │                                    │
           ▼                                    ▼
┌───────────────────┐              ┌──────────────────────┐
│ Session Storage   │              │ Tool Backends         │
│ (SQLite + FTS5)   │              │ Terminal (7 backends) │
│ hermes_state.py   │              │ Browser (5 backends)  │
│ gateway/session.py│              │ Web (4 backends)      │
└───────────────────┘              │ MCP (dynamic)         │
                                   │ File, Vision, etc.    │
                                   └──────────────────────┘

目录结构

hermes-agent/
├── run_agent.py              # AIAgent — 核心对话循环(大文件)
├── cli.py                    # HermesCLI — 交互式终端 UI(大文件)
├── model_tools.py            # 工具发现、schema 收集、分发
├── toolsets.py               # 工具分组与平台预设
├── hermes_state.py           # 带 FTS5 的 SQLite 会话/状态数据库
├── hermes_constants.py       # HERMES_HOME、感知 profile 的路径
├── batch_runner.py           # 批量轨迹生成
│
├── agent/                    # Agent 内部模块
│   ├── prompt_builder.py     # 系统 prompt 组装
│   ├── context_engine.py     # ContextEngine ABC(可插拔)
│   ├── context_compressor.py # 默认引擎——有损摘要压缩
│   ├── prompt_caching.py     # Anthropic prompt 缓存
│   ├── auxiliary_client.py   # 辅助 LLM,用于旁路任务(视觉、摘要)
│   ├── model_metadata.py     # 模型上下文长度、token 估算
│   ├── models_dev.py         # models.dev 注册表集成
│   ├── anthropic_adapter.py  # Anthropic Messages API 格式转换
│   ├── display.py            # KawaiiSpinner、工具预览格式化
│   ├── skill_commands.py     # Skill 斜杠命令
│   ├── memory_manager.py    # 记忆管理器编排
│   ├── memory_provider.py   # 记忆提供者 ABC
│   └── trajectory.py         # 轨迹保存辅助函数
│
├── hermes_cli/               # CLI 子命令与设置
│   ├── main.py               # 入口点——所有 `hermes` 子命令(大文件)
│   ├── config.py             # DEFAULT_CONFIG、OPTIONAL_ENV_VARS、迁移
│   ├── commands.py           # COMMAND_REGISTRY——斜杠命令中央定义
│   ├── auth.py               # PROVIDER_REGISTRY、凭据解析
│   ├── runtime_provider.py   # Provider → api_mode + 凭据
│   ├── models.py             # 模型目录、provider 模型列表
│   ├── model_switch.py       # /model 命令逻辑(CLI + gateway 共用)
│   ├── setup.py              # 交互式设置向导(大文件)
│   ├── skin_engine.py        # CLI 主题引擎
│   ├── skills_config.py      # hermes skills——按平台启用/禁用
│   ├── skills_hub.py         # /skills 斜杠命令
│   ├── tools_config.py       # hermes tools——按平台启用/禁用
│   ├── plugins.py            # PluginManager——发现、加载、hook
│   ├── callbacks.py          # 终端回调(clarify、sudo、approval)
│   └── gateway.py            # hermes gateway 启动/停止
│
├── tools/                    # 工具实现(每个工具一个文件)
│   ├── registry.py           # 中央工具注册表
│   ├── approval.py           # 危险命令检测
│   ├── terminal_tool.py      # 终端编排
│   ├── process_registry.py   # 后台进程管理
│   ├── file_tools.py         # read_file、write_file、patch、search_files
│   ├── web_tools.py          # web_search、web_extract
│   ├── browser_tool.py       # 10 个浏览器自动化工具
│   ├── code_execution_tool.py # execute_code 沙箱
│   ├── delegate_tool.py      # 子 agent 委托
│   ├── mcp_tool.py           # MCP 客户端(大文件)
│   ├── credential_files.py   # 基于文件的凭据透传
│   ├── env_passthrough.py    # 沙箱环境变量透传
│   ├── ansi_strip.py         # ANSI 转义字符剥离
│   └── environments/         # 终端后端(local、docker、ssh、modal、daytona、singularity)
│
├── gateway/                  # 消息平台 gateway
│   ├── run.py                # GatewayRunner——消息分发(大文件)
│   ├── session.py            # SessionStore——对话持久化
│   ├── delivery.py           # 出站消息投递
│   ├── pairing.py            # DM 配对授权
│   ├── hooks.py              # Hook 发现与生命周期事件
│   ├── mirror.py             # 跨会话消息镜像
│   ├── status.py             # Token 锁、profile 范围的进程追踪
│   ├── builtin_hooks/        # 始终注册的 hook 扩展点(当前无内置)
│   └── platforms/            # 20 个适配器:telegram、discord、slack、whatsapp、
│                             #   signal、matrix、mattermost、email、sms、
│                             #   dingtalk、feishu、wecom、wecom_callback、weixin、
│                             #   bluebubbles、qqbot、homeassistant、webhook、api_server、
│                             #   yuanbao
│
├── acp_adapter/              # ACP 服务器(VS Code / Zed / JetBrains)
├── cron/                     # 调度器(jobs.py、scheduler.py)
├── plugins/memory/           # 记忆提供者插件
├── plugins/context_engine/   # 上下文引擎插件
├── skills/                   # 内置 skill(始终可用)
├── optional-skills/          # 官方可选 skill(需显式安装)
├── website/                  # Docusaurus 文档站点
└── tests/                    # Pytest 测试套件(3,000+ 个测试)

数据流

CLI 会话

用户输入 → HermesCLI.process_input()
AIAgent.run_conversation()
→ prompt_builder.build_system_prompt()
→ runtime_provider.resolve_runtime_provider()
→ API 调用(chat_completions / codex_responses / anthropic_messages)
→ tool_calls? → model_tools.handle_function_call() → 循环
→ 最终响应 → 显示 → 保存至 SessionDB

Gateway 消息

平台事件 → Adapter.on_message() → MessageEvent
→ GatewayRunner._handle_message()
→ 授权用户
→ 解析会话 key
→ 创建带会话历史的 AIAgent
AIAgent.run_conversation()
→ 通过适配器回传响应

Cron 任务

调度器触发 → 从 jobs.json 加载到期任务
创建全新 AIAgent(无历史)
→ 将附加的 skill 注入为上下文
→ 运行任务 prompt
→ 向目标平台投递响应
→ 更新任务状态与 next_run

Agent 循环

核心编排引擎是 run_agent.py 中的 AIAgent 类——这是一个大型文件(15k+ 行),负责处理从 prompt(提示词)组装到工具分发再到 provider 故障转移的所有逻辑。
agent loop 的每次迭代按以下顺序执行:

run_conversation()
  1. 若未提供则生成 task_id
  2. 将用户消息追加到对话历史
  3. 构建或复用已缓存的系统 prompt(prompt_builder.py)
  4. 检查是否需要预检压缩(上下文超过 50%)
  5. 从对话历史构建 API 消息
     - chat_completions:直接使用 OpenAI 格式
     - codex_responses:转换为 Responses API 输入项
     - anthropic_messages:通过 anthropic_adapter.py 转换
  6. 注入临时 prompt 层(预算警告、上下文压力提示)
  7. 若使用 Anthropic,应用 prompt 缓存标记
  8. 发起可中断的 API 调用(_interruptible_api_call)
  9. 解析响应:
     - 若有 tool_calls:执行工具,追加结果,回到步骤 5
     - 若为文本响应:持久化 session,按需刷写内存,返回

所有消息在内部均使用兼容 OpenAI 的格式:

{"role": "system", "content": "..."}
{"role": "user", "content": "..."}
{"role": "assistant", "content": "...", "tool_calls": [...]}
{"role": "tool", "tool_call_id": "...", "content": "..."}

agent loop 强制执行严格的消息角色交替规则:

  • 系统消息之后:User → Assistant → User → Assistant → ...
  • 工具调用期间:Assistant(含 tool_calls)→ Tool → Tool → ... → Assistant
  • 不允许连续出现两条 assistant 消息
  • 不允许连续出现两条 user 消息
  • 只有 tool 角色可以连续出现(并行工具结果)

Provider 会验证这些序列,并拒绝格式错误的历史记录。

多个工具调用 → 通过 ThreadPoolExecutor 并发执行

for each tool_call in response.tool_calls:
    1. 从 tools/registry.py 解析处理器
    2. 触发 pre_tool_call 插件 hook
    3. 检查是否为危险命令(tools/approval.py)
       - 若危险:调用 approval_callback,等待用户确认
    4. 使用参数 + task_id 执行处理器
    5. 触发 post_tool_call 插件 hook
    6. 将 {"role": "tool", "content": result} 追加到历史

压缩与持久化

压缩触发时机

预检(API 调用前):对话超过模型上下文窗口的 50%
Gateway 自动压缩:对话超过 85%(更激进,在轮次之间运行)

压缩过程

首先将内存刷写到磁盘(防止数据丢失)
将中间对话轮次摘要为紧凑的摘要内容
保留最后 N 条消息完整不变(compression.protect_last_n,默认:20)
工具调用/结果消息对保持完整(不拆分)
生成新的 session 血缘 ID(压缩会创建一个"子" session)

Session 持久化

每轮结束后:
消息保存到 session 存储(通过 hermes_state.py 使用 SQLite)
内存变更刷写到 MEMORY.md / USER.md
可通过 /resume 或 hermes chat --resume 恢复 session

Hermes Agent 使用双重压缩系统和 Anthropic prompt(提示词)缓存,在长对话中高效管理上下文窗口用量。
Hermes 有两个独立运行的压缩层:

                     ┌──────────────────────────┐
  Incoming message   │   Gateway Session Hygiene │  Fires at 85% of context
  ─────────────────► │   (pre-agent, rough est.) │  Safety net for large sessions
                     └─────────────┬────────────┘
                                   │
                                   ▼
                     ┌──────────────────────────┐
                     │   Agent ContextCompressor │  Fires at 50% of context (default)
                     │   (in-loop, real tokens)  │  Normal context management
                     └──────────────────────────┘
  1. Gateway 会话清理(85% 阈值)
    位于 gateway/run.py(搜索 Session hygiene: auto-compress)。这是一个安全网,在 agent 处理消息之前运行。它防止会话在两次交互之间增长过大时(例如 Telegram/Discord 中的隔夜积累)导致 API 失败。
    阈值:固定为模型上下文长度的 85%
    Token 来源:优先使用上一轮 API 实际报告的 token 数;回退到基于字符的粗略估算(estimate_messages_tokens_rough)
    触发条件:仅当 len(history) >= 4 且压缩已启用时
    目的:捕获逃过 agent 自身压缩器的会话
    Gateway 清理阈值有意高于 agent 压缩器的阈值。将其设置为 50%(与 agent 相同)会导致长 gateway 会话在每一轮都过早触发压缩。

  2. Agent ContextCompressor(50% 阈值,可配置)
    位于 agent/context_compressor.py。这是主要压缩系统,在 agent 的工具循环内运行,可访问准确的 API 报告 token 数。

压缩算法

ContextCompressor.compress() 方法遵循 4 阶段算法:

阶段 1:清除旧工具结果(廉价,无需 LLM 调用)
保护尾部之外的旧工具结果(>200 字符)将被替换为:

[Old tool output cleared to save context space]

这是一个廉价的预处理步骤,可从冗长的工具输出(文件内容、终端输出、搜索结果)中节省大量 token。

阶段 2:确定边界

┌─────────────────────────────────────────────────────────────┐
│  Message list                                               │
│                                                             │
│  [0..2]  ← protect_first_n (system + first exchange)        │
│  [3..N]  ← middle turns → SUMMARIZED                        │
│  [N..end] ← tail (by token budget OR protect_last_n)        │
│                                                             │
└─────────────────────────────────────────────────────────────┘

尾部保护基于 token 预算:从末尾向前遍历,累积 token 直到预算耗尽。如果预算保护的消息数少于固定的 protect_last_n,则回退到该固定数量。

边界对齐以避免拆分 tool_call/tool_result 组。_align_boundary_backward() 方法会跳过连续的工具结果,找到父级 assistant 消息,保持组的完整性。

阶段 3:生成结构化摘要
中间轮次使用辅助 LLM 以结构化模板进行摘要:

## Goal
[What the user is trying to accomplish]

## Constraints & Preferences
[User preferences, coding style, constraints, important decisions]

## Progress
### Done
[Completed work — specific file paths, commands run, results]
### In Progress
[Work currently underway]
### Blocked
[Any blockers or issues encountered]

## Key Decisions
[Important technical decisions and why]

## Relevant Files
[Files read, modified, or created — with brief note on each]

## Next Steps
[What needs to happen next]

## Critical Context
[Specific values, error messages, configuration details]

摘要预算随被压缩内容的量动态调整:

  • 公式:content_tokens × 0.20(_SUMMARY_RATIO 常量)
  • 最小值:2,000 token
  • 最大值:min(context_length × 0.05, 12,000) token

阶段 4:组装压缩后的消息
压缩后的消息列表为:

  • 头部消息(首次压缩时在系统提示词后追加一条说明)
  • 摘要消息(角色经过选择以避免连续相同角色违规)
  • 尾部消息(未修改)

_sanitize_tool_pairs() 清理孤立的 tool_call/tool_result 对:

  • 引用已删除调用的工具结果 → 删除
  • 结果已被删除的工具调用 → 注入存根结果

迭代重压缩
在后续压缩中,前一次摘要会连同指令一起传递给 LLM,要求其更新摘要而非从头摘要。这在多次压缩中保留了信息——条目从"进行中"移至"已完成",新进展被添加,过时信息被删除。
压缩器实例上的 _previous_summary 字段存储最后一次摘要文本以供此用途。

会话存储

Hermes Agent 使用 SQLite 数据库(~/.hermes/state.db)跨 CLI 和 gateway 会话持久化会话元数据、完整消息历史及模型配置。这替代了早期的逐会话 JSONL 文件方案。

源文件:hermes_state.py

~/.hermes/state.db (SQLite, WAL mode)
├── sessions              — 会话元数据、token 计数、计费信息
├── messages              — 每个会话的完整消息历史
├── messages_fts          — FTS5 虚拟表(content + tool_name + tool_calls)
├── messages_fts_trigram  — 使用 trigram tokenizer 的 FTS5 虚拟表(CJK / 子串搜索)
├── state_meta            — 键值元数据表
└── schema_version        — 单行表,跟踪迁移状态

多个 hermes 进程(gateway + CLI 会话 + worktree agent)共享同一个 state.db。SessionDB 类通过以下方式处理写入竞争:短 SQLite 超时(1 秒),而非默认的 30 秒
应用层重试,带随机抖动(20–150ms,最多 15 次重试)
BEGIN IMMEDIATE 事务,在事务开始时暴露锁竞争
定期 WAL checkpoint,每 50 次成功写入执行一次(PASSIVE 模式)
这避免了"护卫效应"——SQLite 确定性内部退避会导致所有竞争写入者在相同间隔重试。

回过头看开篇的几个问题:

核心逻辑

核心对话循环在 AIAgent.run_conversation() 里。真实实现比较复杂,涉及流式 API、重试、fallback、响应校验、tool call 修复、并发执行等,但骨架可以简化理解为:

Agent 接收用户消息 → 带上工具 schema 调用 LLM → 如果返回 tool_calls 就逐个执行并把结果追加到上下文 → 循环直到模型给出最终文本回复,或者达到迭代上限。

这个循环有两个值得注意的细节:
有一个 iteration_budget,防止 Agent 无限循环。默认最多 90 轮迭代。
循环结束后,会触发后台 review 流程(下一节会详细讲),这是 Skill 和记忆自动沉淀的入口。
从架构边界看,两者甚至可以互补:一个偏接入,一个偏执行与学习。

Skill 系统

这是 Hermes 最有意思的设计,也是最容易被讲偏的地方。

社区讨论里常见的说法是"Agent 完成 5 次以上工具调用后,会自动生成 Skill 文件"。这个描述不算错,但容易让人以为这是一个简单的"达到阈值就必写文件"的硬规则。

翻源码会发现,实际机制比这个更柔和,也更有意思。
第一层:系统提示里的引导
在 agent/prompt_builder.py 里,有一段写给 Agent 看的 SKILLS_GUIDANCE:

SKILLS_GUIDANCE = (
    "After completing a complex task (5+ tool calls), fixing a tricky error, "
    "or discovering a non-trivial workflow, save the approach as a "
    "skill with skill_manage so you can reuse it next time.\n"
    "When using a skill and finding it outdated, incomplete, or wrong, "
    "patch it immediately with skill_manage(action='patch') — don't wait to be asked. "
    "Skills that aren't maintained become liabilities."
)

注意,这里的 5+ tool calls 是写在提示语里的经验阈值,是对模型的建议,不是代码层面的硬触发器。

第二层:后台 review 流程
真正推动 Skill 沉淀的是后台 review 机制。在 run_agent.py 里,有一个 _skill_nudge_interval,默认值是 10:

self._skill_nudge_interval = int(
    skills_config.get("creation_nudge_interval", 10)
)

每当 Agent 累计执行了 10 轮工具迭代,在响应结束后(注意,不是响应过程中),会触发一个后台 review 流程。这个流程的核心在 _spawn_background_review():

_SKILL_REVIEW_PROMPT = (
    "Review the conversation above and consider saving or updating "
    "a skill if appropriate.\n\n"
    "Focus on: was a non-trivial approach used to complete a task "
    "that required trial and error, or changing course due to "
    "experiential findings along the way...?\n\n"
    "If nothing is worth saving, just say 'Nothing to save.' and stop."
)

它会 fork 出一个完整的子 AIAgent(静默模式,最多 8 轮迭代),把当前对话历史传进去,让这个子 Agent 自己判断"刚才这段对话有没有值得沉淀成 Skill 的经验"。
关键词是 "consider" 和 "if appropriate"。这是一个 best-effort 流程,不是硬编码必达动作。子 Agent 可能判断"Nothing to save"就直接结束了。
而且这个 review 流程跑在后台线程里,不会阻塞用户的下一轮对话,也不会竞争主 Agent 的模型注意力。
如下图,你会更容易看出:Hermes 不是“达到阈值就强制写 Skill”,而是把“复盘并沉淀经验”做成了一段后台工作流。

Hermes 的 Skill 沉淀闭环

第三层:Skill 索引与加载的两条链路
Skill 在系统里有两种工作方式,分别由不同的代码负责:

链路一:系统提示里的索引。build_skills_system_prompt() 在 agent/prompt_builder.py 里,负责扫描 ~/.hermes/skills/ 目录(以及外部 Skill 目录),构建一份技能索引注入系统提示。这样 Agent 在每轮对话开始时就知道"我有哪些技能可以用"。这个索引还做了两层缓存(进程内 LRU + 磁盘快照),避免每次都做文件系统扫描。

链路二:用户显式调用。skill_commands.py 负责把每个 Skill 注册成斜杠命令。当用户在对话中输入 /skill-name 时,Skill 的完整内容会作为用户消息(而不是系统提示)注入到对话中。这个设计是有意为之的,目的是保护 prompt caching 不被破坏。

对比 OpenClaw: OpenClaw 也有 Skill 系统,但主要依赖人工编写和社区贡献的 ClawHub 市场。Hermes 这边等于把"写 Skill"和"改 Skill"这两件事都交给了 Agent 自己,走的是更自动化的路线。

仓库里内置了 26 个目录的 Skill 模板,覆盖 DevOps、研究、社交媒体、智能家居、数据科学等场景;如果把整个仓库一起算进去,可以检到 122 个 SKILL.md。这说明 Skill 在 Hermes 里不是附属能力,而是一等公民。

记忆体系:不是笔记本,更接近搜索引擎

翻 Hermes 的官方文档和源码,默认内建记忆其实有三块:

组件 存储位置 内容 容量
MEMORY.md ~/.hermes/memories/ Agent 的个人笔记:环境事实、惯例、学到的东西 ~800 tokens(约 2,200 字符)
USER.md ~/.hermes/memories/ 用户画像:偏好、沟通风格、期望 ~500 tokens(约 1,375 字符)
state.db ~/.hermes/ 全量对话历史 + FTS5 全文检索 理论无限(受磁盘容量限制)

前两个文件在每次会话开始时作为冻结快照注入系统提示,不会在会话中途变化(为了保护 prompt caching)。Agent 在会话中通过 memory 工具修改的内容会立即写入磁盘,但要到下一次会话才会反映到系统提示里。

state.db 是一个 SQLite 数据库(WAL 模式,支持并发读写),在 hermes_state.py 里定义。它存储所有会话的完整消息历史,并通过 FTS5 虚拟表支持全文检索:
Agent 可以通过 session_search 工具搜索过去的对话,配合 LLM 做摘要召回。这意味着 Hermes 不是把所有记忆一次性塞给模型,而是只在需要时检索和加载。

这一层如果只靠文字描述,读者很容易把它理解成“又一个记笔记文件”。其实更准确的理解是:小而稳定的信息放在前缀里,长而杂的历史放进数据库里,只有需要时才搜出来。

安全模型:七层纵深防御

打开 tools/approval.py,核心是一张危险命令模式表 DANGEROUS_PATTERNS,包含 30+ 条正则匹配规则(以下是简化示意,非源码原样):

  • 递归删除(rm -r)
  • 世界可写权限(chmod 777)
  • 磁盘写入(dd if=、> /dev/sd)
  • SQL 破坏性操作(DROP TABLE、DELETE FROM 不带 WHERE、TRUNCATE)
  • 管道执行远程脚本(curl ... | bash)
  • 覆写系统配置(> /etc/)
  • fork 炸弹
  • 脚本语言 -e/-c 执行
  • 自杀保护:阻止 Agent 杀掉自己的进程

审批模式有三档:manual(默认,总是问人)、smart(用辅助 LLM 评估风险,低风险自动通过,高风险自动拒绝,不确定的才问人)、off(关闭所有审批)。

Hermes Agent
Hermes Agent 架构拆解

posted @ 2026-05-30 09:06  Jamest  阅读(20)  评论(0)    收藏  举报