[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
└──────────────────────────┘
-
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 会话在每一轮都过早触发压缩。 -
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”,而是把“复盘并沉淀经验”做成了一段后台工作流。

第三层: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(关闭所有审批)。

浙公网安备 33010602011771号