Hermes Agent 源码深度解构:一个“自进化“AI Agent的完整架构拆解

Hermes Agent 源码深度解构:一个"自进化"AI Agent的完整架构拆解


引言:为什么值得深读这份源码

Hermes Agent 是由 Nous Research 开源的 AI Agent 框架,口号是"self-improving AI agent"------自进化。这不是营销词汇,而是一套有真实代码支撑的设计目标:Agent 会在每次对话结束后自动评估自己,决定哪些经验值得写入技能库,哪些事情应该记录进记忆。

截止 v0.14.0,整个项目约 3 万行 Python 代码(不含 website 和测试),支持 200+ 模型接入,7 种终端后端,6 大即时通信平台,一套完整的技能(Skills)体系,以及一个 Curator 后台维护员。本文将从架构到代码逐层拆解,最后给出客观的优缺点评估。
输入层
CLI / TUI
消息网关

TG/Discord/Slack...
核心 Agent
conversation_loop.py

对话主循环
ContextEngine

上下文压缩
MemoryManager

记忆管理
Skills System

技能系统
基础设施层
CredentialPool

多凭证池
AuxiliaryClient

后台 LLM
ErrorClassifier

错误分类
LSP Client

代码理解
模型层
chat_completions
anthropic_messages
bedrock_converse
codex_responses


一、项目全貌:目录结构与模块划分

复制代码
hermes-agent-main/
├── agent/                   # 核心 Agent 逻辑(~80 个模块)
│   ├── conversation_loop.py  # 对话主循环(3900 行,全项目最大)
│   ├── agent_init.py         # AIAgent.__init__ 实现(提取为独立模块)
│   ├── context_engine.py     # 上下文引擎抽象基类
│   ├── context_compressor.py # 默认压缩实现(LLM 摘要)
│   ├── memory_manager.py     # 记忆管理器(多 Provider 编排)
│   ├── curator.py            # 技能库后台维护员
│   ├── background_review.py  # 对话后后台复审(写记忆/技能)
│   ├── iteration_budget.py   # 迭代预算(线程安全计数)
│   ├── credential_pool.py    # 多凭证池(同 Provider 故障转移)
│   ├── lsp/                  # Language Server Protocol 集成
│   └── ...(40+ 其他模块)
├── tools/                   # 40+ 工具实现
│   ├── delegate_tool.py      # 子 Agent 委派与并行
│   ├── memory_tool.py        # 持久化记忆工具
│   ├── browser_tool.py       # 浏览器自动化
│   ├── mcp_tool.py           # MCP 协议集成
│   ├── kanban_tools.py       # Kanban 任务管理
│   └── ...
├── acp_adapter/             # Agent Communication Protocol 适配器
├── optional-skills/         # 可选技能包(迁移、MCP 等)
├── toolsets/                # 工具集配置(分组管理工具)
├── hermes_cli/              # CLI 入口与配置管理
└── pyproject.toml           # 项目依赖(精确锁版本)

关键架构决策:单文件提取

agent_init.py 的开头注释直接说明了一个有趣的工程决策:

AIAgent.__init__ 是 60+ 参数、~1400 行的属性初始化代码。把它放在 run_agent.py 里会让那个文件无法管理,所以把它提取成 init_agent(agent, ...) 独立函数,AIAgent.__init__ 变成一个薄薄的转发器。

这反映了项目的演化路径------代码库是有机增长的,大模块在达到阈值后被拆分,同时通过 _ra() 懒加载保持测试 mock 路径不变。


二、对话主循环:conversation_loop.py 的设计

这是整个项目最复杂的文件,约 3900 行,负责驱动"一个用户轮次通过 Agent"。其核心职责链:
需要压缩
无需压缩
纯文本
工具调用


有余量
耗尽
有错误
compress
rotate_credential
fallback
abort
无错误
用户消息输入
系统提示构建

system_prompt.py
预压缩检查

should_compress_preflight
上下文压缩

ContextEngine.compress
调用 LLM

chat_completions /

anthropicmessages /

bedrock_converse /

codex_responses
流式响应解析
流式输出给用户
并行可行?
ThreadPoolExecutor

并行执行工具
串行执行工具
工具结果汇总
迭代预算

IterationBudget.consume
强制结束
API 错误?
ErrorClassifier

分类错误
恢复策略
后置钩子

Background Review
轮次结束

工具分发(tool_dispatch_helpers.py)

迭代预算消耗(IterationBudget.consume)

错误分类 & 重试(error_classifier.py)

后置钩子(后台记忆/技能复审、背景审查)

复制代码
### 2.1 多传输层架构

Hermes 支持 4 种 API 传输模式,通过 `api_mode` 字段在初始化时确定:

| api_mode | 对应接口 | 适用场景 |
|---|---|---|
| `chat_completions` | OpenAI Chat API | OpenRouter、大多数三方 |
| `anthropic_messages` | Anthropic Messages API | 原生 Anthropic、AWS 兼容端 |
| `bedrock_converse` | AWS Bedrock Converse API | AWS 原生部署 |
| `codex_responses` | OpenAI Responses API | GPT-5.x、xAI Grok |

自动检测逻辑在 `agent_init.py` 中,通过 base_url 的 hostname 和 provider 名称推断------例如 `api.anthropic.com` 自动选 `anthropic_messages`,`bedrock-runtime.*.amazonaws.com` 选 `bedrock_converse`。

```mermaid
flowchart LR
    START([api_mode=?]) --> A{api_mode\n显式传入?}
    A -- 是 --> DONE[使用指定值]
    A -- 否 --> B{provider=anthropic\nor hostname=\napi.anthropic.com?}
    B -- 是 --> C[anthropic_messages]
    B -- 否 --> D{hostname 匹配\nbedrock-runtime\n*.amazonaws.com?}
    D -- 是 --> E[bedrock_converse]
    D -- 否 --> F{provider=openai-codex\nor hostname=api.x.ai\nor chatgpt.com/codex?}
    F -- 是 --> G[codex_responses]
    F -- 否 --> H{是 OpenAI 直连\n且模型需要\nResponses API?}
    H -- 是 --> G
    H -- 否 --> I[chat_completions\n默认]

    style C fill:#d4edda
    style E fill:#d1ecf1
    style G fill:#fff3cd
    style I fill:#f8d7da

对于 GPT-5.x 模型,代码还有一个特殊升级逻辑:如果 api_modechat_completions 且模型需要 Responses API,会自动升级------但 Azure OpenAI 除外(Azure 不支持 Responses API,代码有 _is_azure_openai_url() 专门排除)。

2.2 迭代预算(IterationBudget)

python 复制代码
class IterationBudget:
    def __init__(self, max_total: int):
        self.max_total = max_total   # 父 Agent 默认 90
        self._used = 0
        self._lock = threading.Lock()

    def consume(self) -> bool:
        with self._lock:
            if self._used >= self.max_total:
                return False
            self._used += 1
            return True

    def refund(self) -> None:  # execute_code 轮次退款,不消耗预算
        with self._lock:
            if self._used > 0:
                self._used -= 1

设计亮点:父子 Agent 的预算是独立的。父 Agent 最多 90 次,每个子 Agent 最多 50 次(可配置)。execute_code(Python 脚本执行)调用会被退款------因为脚本执行本质是编程,而不是一次 LLM 决策。


三、上下文引擎:可插拔的压缩架构

context_engine.py 定义了一个抽象基类 ContextEngine,这是 Hermes 架构中最优雅的设计之一:

python 复制代码
class ContextEngine(ABC):
    # 状态字段(run_agent.py 直接读取)
    last_prompt_tokens: int = 0
    threshold_percent: float = 0.75    # 75% 触发压缩
    protect_first_n: int = 3           # 保护前 N 条消息
    protect_last_n: int = 6            # 保护后 N 条消息

    @abstractmethod
    def should_compress(self, prompt_tokens: int = None) -> bool: ...

    @abstractmethod
    def compress(self, messages, current_tokens, focus_topic=None) -> List: ...

内置实现是 ContextCompressor(基于 LLM 摘要),通过 config.yamlcontext.engine 字段可以切换到第三方引擎(如 LCM),引擎目录:plugins/context_engine/<name>/
默认实现
第三方插件
<<abstract>>
ContextEngine
+last_prompt_tokens: int
+threshold_percent: float = 0.75
+protect_first_n: int = 3
+protect_last_n: int = 6
+context_length: int
+compression_count: int
+update_from_response(usage)
+should_compress(prompt_tokens)
+compress(messages, focus_topic)
+get_tool_schemas() : List
+handle_tool_call(name, args) : str
+on_session_start(session_id)
+on_session_end(session_id, messages)
+update_model(model, context_length)
ContextCompressor
+name = "compressor"
-SUMMARY_RATIO = 0.20
-_SUMMARY_TOKENS_CEILING = 12000
-_IMAGE_TOKEN_ESTIMATE = 1600
+compress(messages)
-_prune_tool_outputs(messages)
-_summarize_middle(messages)
LCMEngine
+name = "lcm"
+get_tool_schemas() : ← lcm_grep/describe/expand
+compress(messages)
-_build_dag(messages)

值得注意的是,ContextEngine ABC 还定义了 get_tool_schemas() 接口------这意味着上下文引擎可以向 Agent 暴露自己的工具 (如 LCM 的 lcm_greplcm_describe),Agent 在对话中可以直接调用这些工具与压缩引擎交互。这是一个非常优雅的设计:压缩引擎不只是被动执行压缩,它可以主动扩展 Agent 的能力边界。

3.1 ContextCompressor 的压缩策略

context_compressor.py 的注释中明确了核心设计决策:

  • 结构化摘要模板:追踪 Resolved/Pending 问题
  • 工具输出预剪枝:LLM 摘要前先做一次廉价的工具输出删减
  • 比例摘要预算 :摘要长度 = 被压缩内容长度 × SUMMARY_RATIO(0.20),上限 _SUMMARY_TOKENS_CEILING(12000 tokens)
  • 图片占位:每张图片估算 1600 tokens,防止多模态对话压缩预算计算失准

压缩时插入的前缀明确告知模型这是"历史参考,非活跃指令":

python 复制代码
SUMMARY_PREFIX = (
    "[CONTEXT COMPACTION --- REFERENCE ONLY] Earlier turns were compacted "
    "into the summary below. This is a handoff from a previous context "
    "window --- treat it as background reference, NOT as active instructions. "
    "Do NOT answer questions or fulfill requests mentioned in this summary; "
    "they were already addressed. "
    "IMPORTANT: Your persistent memory (MEMORY.md, USER.md) in the system "
    "prompt is ALWAYS authoritative and active --- never ignore or deprioritize "
    "memory content due to this compaction note. "
)

这个细节很关键:如果不显式区分,模型可能把摘要里的历史任务当作当前指令重新执行。

3.2 工具调用参数截断的坑

context_compressor.py 中有一段专门处理工具调用参数 JSON 截断的代码,注释说明了为什么不能简单地切断字符串:

python 复制代码
def _truncate_tool_call_args_json(args: str, head_chars: int = 200) -> str:
    """
    MiniMax 等 provider 严格校验 tool call 参数 JSON,直接截断会产生:
        {"path": "/foo/bar", "content": "# long markdown
        ...[truncated]
    即未闭合字符串 + 缺少结束括号 → 400 错误 → 会话永久卡住。
    
    正确做法:解析 JSON,截断长字符串叶节点,重新序列化。
    """

这是从真实 Bug(issue #11762)中提炼出的防御性代码。


四、记忆系统:三层架构

Hermes 的记忆系统分三层:
Agent 调用接口
MemoryManager

编排层(memory_manager.py)

最多 1 个外部 Provider
Provider 层
BuiltinProvider

MEMORY.md / USER.md

文件读写 + § 分隔符
ExternalProvider(可选)

如 Honcho 辩证用户建模

通过 HTTP API 同步
安全层
StreamingContextScrubber

流式输出防 memory-context 标签泄漏
sanitize_context()

非流式内容清洗

4.1 MemoryManager 的防注入机制

memory_manager.py 实现了一个 StreamingContextScrubber,专门应对流式输出中可能出现的 <memory-context> 标签注入问题:

python 复制代码
class StreamingContextScrubber:
    """
    非流式 sanitize_context 无法处理跨 chunk 边界的标签:
    <memory-context> 在一个 delta 开启,在另一个 delta 关闭,
    内容会泄露到 UI 界面。
    
    该 scrubber 维护一个状态机,跨 delta 追踪标签开关状态,
    挡住被 memory context 包裹的内容。
    """

这种防御是必要的:如果 AI 模型在生成响应时"输出"了一个 <memory-context> 标签,流式 UI 会直接把它渲染给用户,造成混乱甚至安全问题。

4.2 记忆写入与同步

每次对话轮次结束,MemoryManager.sync_all() 会被调用,把当前轮次的用户消息和助手响应同步给所有 provider:

python 复制代码
def sync_all(self, user_content, assistant_content, *, session_id=""):
    for provider in self._providers:
        try:
            provider.sync_turn(user_content, assistant_content, session_id=session_id)
        except Exception as e:
            logger.warning("Memory provider '%s' sync_turn failed: %s", provider.name, e)

注意容错设计:一个 provider 失败不会阻断其他 provider。这在分布式系统设计中是标准模式,但在一个单进程 Agent 里做到这一点说明开发者考虑了外部 provider(如 Honcho HTTP API)可能不可用的场景。


五、技能系统(Skills):从"配方"到"自进化"

技能系统是 Hermes 区别于其他 Agent 框架的核心特性。每个技能是一个 SKILL.md 文件,遵循 agentskills.io 开放标准。

5.1 技能的加载机制

技能的触发(skill_preprocessing.py)在用户消息进入主循环前执行。当用户输入 /skill-name 时,对应的 SKILL.md 内容被注入到系统提示中,扩展 Agent 的当前能力。

技能分三类:

类型 位置 说明
Bundled 代码内置 随 Hermes 发布,不可修改
Hub ~/.hermes/skills/ 通过 hermes skills install 安装
User-created ~/.hermes/skills/ Agent 自动创建/人工编写

5.2 Background Review:对话后的自我审视

这是"自进化"机制的核心实现,在 background_review.py 中:

python 复制代码
_SKILL_REVIEW_PROMPT = (
    "Review the conversation above and update the skill library. Be "
    "ACTIVE --- most sessions produce at least one skill update, even if "
    "small. A pass that does nothing is a missed learning opportunity, "
    "not a neutral outcome.\n\n"
    # ... 超过 80 行的详细提示 ...
)

每次对话轮次结束后,Hermes 会 fork 一个子 Agent(使用 auxiliary_client,命中同一个 prompt cache),限制工具白名单为仅 memory 和 skill 管理工具,让它审阅整个对话并决定:

  • 是否有值得持久化的记忆(关于用户偏好、行为等)
  • 是否有值得写入技能库的经验(工作流、纠错记录、域知识)

Skill Library\n~/.hermes/skills/ Memory Store\nMEMORY.md / USER.md Background Review\n(auxiliary fork) Prompt Cache 主 Agent 用户 Skill Library\n~/.hermes/skills/ Memory Store\nMEMORY.md / USER.md Background Review\n(auxiliary fork) Prompt Cache 主 Agent 用户 轮次结束 工具白名单:\nmemory / skill_manage\n禁止:delegate/clarify/send_message alt [有值得保存的记忆] alt [有值得更新的技能] 发送消息 查询缓存(system prompt + 历史) 命中缓存(75% cost reduction) 调用 LLM + 工具执行 流式返回响应 fork(daemon 线程)\n传入:会话快照 + 工具白名单 共享前缀缓存命中 命中(近零增量成本) 审阅对话\n决策:记忆 / 技能更新 memory tool 写入 skill_manage 写入/更新 完成(异步,不阻塞主对话)

技能更新的优先级顺序(来自 prompt):

  1. 更新当前对话中已加载的技能
  2. 更新现有伞形技能(umbrella skill)
  3. 在现有伞形技能下添加支持文件(references/templates/scripts)
  4. 创建新的类级别伞形技能(严格禁止创建以 PR 编号、bug 字符串命名的一次性技能)

这个设计的工程考量很细致:后台复审使用 auxiliary_client,共享父 Agent 的 prompt cache 前缀,实现近乎零额外成本的缓存命中。

5.3 Curator:技能库的"定期维护员"

curator.py 是一个后台维护系统,定期(默认每 7 天)在 Agent 空闲时自动运行:

python 复制代码
DEFAULT_INTERVAL_HOURS = 24 * 7   # 7 天
DEFAULT_MIN_IDLE_HOURS = 2         # 至少空闲 2 小时才触发
DEFAULT_STALE_AFTER_DAYS = 30      # 30 天未用标记为 stale
DEFAULT_ARCHIVE_AFTER_DAYS = 90    # 90 天未用归档

Curator 的职责:

  • 自动管理技能生命周期状态(active → stale → archived)
  • 合并重叠技能,归档冗余条目(不自动删除,只归档,可恢复)
  • 始终只操作用户创建的技能,绝不动 bundled/hub 技能

技能创建\n(用户手动 or Background Review)
30天未被调用
被调用\n(自动恢复)
90天未被调用\nCurator 每7天巡检
手动恢复\n(hermes curator restore)
hermes curator pin
hermes curator unpin
active
stale
archived
pinned
Pinned 技能跳过\n所有自动状态转换
只归档不删除\n~/.hermes/skills/.archive/
Bundled/Hub 技能\n永远保持 active\nCurator 不干预

首次运行逻辑有一个有趣的 UX 设计:如果从未运行过,不会立即触发,而是把 last_run_at 设为当前时间,让用户先运行一个完整的 7 天周期。防止 hermes update 后立即触发一次不必要的库维护。


六、子 Agent 委派系统:真正的并行 Agent

tools/delegate_tool.py 实现了父子 Agent 委派架构:

python 复制代码
DELEGATE_BLOCKED_TOOLS = frozenset([
    "delegate_task",   # 禁止递归委派(MAX_DEPTH=1 默认)
    "clarify",         # 禁止用户交互
    "memory",          # 禁止写共享 MEMORY.md
    "send_message",    # 禁止跨平台副作用
    "execute_code",    # 子 Agent 委派旨在步骤分解,而非直接写脚本执行
])

delegate_task

子任务 A
delegate_task

子任务 B
delegate_task

子任务 C
子 Agent 禁用工具
delegate_task ❌
clarify ❌
memory ❌
send_message ❌
execute_code ❌
父 Agent (depth=0)

IterationBudget: 90
子 Agent A (depth=1)

IterationBudget: 50

独立上下文
子 Agent B (depth=1)

IterationBudget: 50

独立上下文
子 Agent C (depth=1)

IterationBudget: 50

独立上下文
结果 A 汇总
结果 B 汇总
结果 C 汇总
默认 MAX_DEPTH=1

可配置最大 3 层

默认最多 3 个并发子 Agent

委派工具的设计亮点:

1. 防死锁保护

子 Agent 在 ThreadPoolExecutor 的 worker 线程中运行,而父 Agent 的 stdin 交互回调存储在 threading.local() 中,不会被 worker 线程继承。如果子 Agent 遇到需要用户确认的命令,直接调用 input() 会死锁。解决方案:为每个 worker 线程注入一个 _subagent_auto_deny 回调,遇到危险命令直接拒绝而非等待用户输入。

2. 深度限制

python 复制代码
MAX_DEPTH = 1  # 默认:父(0) -> 子(1);孙子在默认配置下被拒绝

默认嵌套深度为 1(仅允许一层子 Agent),可通过配置调整,最大支持 3 层,防止无限递归。

3. 运行时暂停
set_spawn_paused(True) 可以全局阻止新的委派,已运行的子 Agent 不受影响。这是 TUI 控制面板的功能。


七、多凭证池(CredentialPool)

credential_pool.py 实现了企业级的多凭证管理:

python 复制代码
STRATEGY_FILL_FIRST = "fill_first"    # 优先用第一个,满了再换
STRATEGY_ROUND_ROBIN = "round_robin"  # 轮询
STRATEGY_RANDOM = "random"            # 随机
STRATEGY_LEAST_USED = "least_used"    # 用得最少的优先

# 不同错误码的冷却时间
EXHAUSTED_TTL_401_SECONDS = 5 * 60    # 401: 5 分钟冷却(可能是刷新过期)
EXHAUSTED_TTL_429_SECONDS = 60 * 60   # 429: 1 小时冷却(速率限制)
EXHAUSTED_TTL_DEFAULT_SECONDS = 60 * 60  # 其他: 1 小时

PooledCredential 数据类设计了一个特殊的 extra 字段存储 JSON 只读字段(如 token_type, scope),通过重载 __getattr__ 透明暴露,保持 API 的整洁性而不需要为每个边缘字段都定义属性。


八、多平台消息网关

Hermes 通过一个 Gateway 进程支持 Telegram、Discord、Slack、WhatsApp、Signal、Email,关键设计是"统一会话管理":

  • 每个平台聊天(chat_id)对应一个独立的 Agent 实例
  • gateway_session_key 格式:agent:main:telegram:dm:123456
  • 跨平台对话连续性:同一个 Agent 实例可以在 CLI、Telegram、Discord 之间切换,session 状态持久化

ACP 适配器

acp_adapter/ 目录实现了 Agent Communication Protocol(ACP),这是一个更底层的 Agent 间通信协议,支持 Copilot 等外部 Agent 系统的集成。


九、LSP 集成:代码理解的基础设施

agent/lsp/ 目录是 Language Server Protocol 的完整客户端实现,这在 AI Agent 里不常见:

复制代码
lsp/
├── client.py       # LSP 客户端(JSON-RPC over stdio)
├── manager.py      # LSP 服务器生命周期管理
├── workspace.py    # 工作区管理(诊断、补全)
├── range_shift.py  # 文件编辑后的范围偏移计算
└── servers.py      # 已知 LSP 服务器配置(pyright, rust-analyzer 等)

这让 Hermes 具备了 IDE 级别的代码分析能力:在编辑文件后可以通过 LSP 获取实时类型错误、符号引用,而不是等模型"猜测"代码是否正确。


十、依赖管理:供应链安全的工程实践

pyproject.toml 注释中有一段重要的依赖策略说明:

toml 复制代码
# 所有直接依赖使用精确版本锁(==X.Y.Z),禁止范围版本。
# 原因:2026-05-12 mistralai 2.4.6 被注入恶意代码(Mini Shai-Hulud 蠕虫),
# 如果我们用 "mistralai>=2.3.0,<3" 而不是精确锁定,每个安装都会自动拉取该版本。

这是对真实供应链攻击事件的直接响应。所有依赖精确锁版本,只有 Hermes 主动升级时新版本才会进入用户环境。

另一个有趣的决策:execute_code 子 Agent 禁止递归委派,部分原因也是防止通过动态生成的代码绕过工具白名单。


十一、工具生态:40+ 工具的组织方式

工具通过"toolset"系统分组管理:

python 复制代码
TOOLSETS = {
    "core": ["read_file", "write_file", "bash", ...],   # 核心工具(默认启用)
    "browser": ["browser_navigate", "browser_click", ...],  # 浏览器工具
    "moa": ["mixture_of_agents"],                        # MoA 混合 Agent
    "delegation": ["delegate_task"],                     # 子 Agent 委派
    "kanban": ["kanban_create", "kanban_move", ...],     # 看板管理
    "computer_use": [...],                               # 计算机控制
    # ...
}

MCP(Model Context Protocol)支持通过 tools/mcp_tool.py 实现,让 Hermes 能接入任意 MCP 服务器扩展工具集。这是一个合理的扩展点------不需要等 Hermes 自己实现特定工具,只要有对应的 MCP 服务器即可。


十二、系统提示构建:多层叠加

agent/system_prompt.py(在 agent_init.py 中引用)负责构建最终的系统提示。以下优先级顺序基于对 agent_init.pysystem_prompt.py 的源码阅读推断:

  1. SOUL.md(个人身份/人格文件)
  2. AGENTS.md(工作区指令)
  3. .cursorrules / CLAUDE.md 等(项目级上下文文件)
  4. 技能内容(当前轮次激活的技能)
  5. 记忆上下文(从 MemoryManager 拉取的相关记忆)
  6. 平台提示(CLI / Telegram / Discord 的格式化提示)
  7. 工具说明

ephemeral_system_prompt 参数允许注入一个临时系统提示,它不会被保存进 trajectory 文件,专门用于批处理场景,避免用户个人数据(SOUL.md 等)污染训练数据。


十三、Trajectory 系统:训练数据管道

agent/trajectory.py 负责把对话记录保存为 JSONL 格式:

复制代码
每条 trajectory:
{
  "role": "user"|"assistant"|"tool",
  "content": "...",
  "tool_calls": [...],  // 可选
  "tool_call_id": "...", // 可选
}

这直接服务于 Nous Research 的核心业务:用真实用户 + Agent 的交互轨迹训练更好的工具调用模型。ephemeral_system_prompt 的设计和 skip_context_files 参数都是为了保证收集到的轨迹数据是"干净的"(不含个人身份信息)。


十四、优缺点分析

优点

1. 真正的自进化机制

Background Review + Curator 是实实在在的代码,不是宣传语。每次对话后 Agent 评估自己,技能库会持续演化。这是目前开源 Agent 中最完整的学习闭环实现之一。

2. 传输层抽象彻底

4 种 API 传输模式 + 自动检测,200+ 模型接入。切换模型不改任何业务逻辑,hermes model 一条命令完成。对于需要多模型协同(主模型 + 压缩模型 + 辅助模型)的复杂任务,这个架构相当灵活。

3. 多凭证池与故障转移

生产级的凭证管理:多账号轮询、错误码感知的冷却时间、provider 级故障自动转移。这在个人 Agent 项目里很少见。

4. 上下文引擎可插拔
ContextEngine 抽象基类让第三方可以实现自己的压缩策略(如 DAG-based LCM)。内置的 ContextCompressor 在工程细节上很扎实(JSON 截断修复、图片 token 估算、摘要比例预算)。

5. LSP 集成

原生 LSP 客户端让代码理解能力超越简单文件读写,达到 IDE 级别的符号分析。

6. 供应链安全意识

精确锁版本、Scope 规则(只有全会话必需的包才进 dependencies)、懒加载(lazy_deps.py),体现了对供应链攻击的认真态度。

7. 多平台消息网关

一套 Agent 逻辑,6 个 IM 平台接入,统一会话管理。适合需要让 AI 助手"住"在已有的通信工具里的用户。


缺点

1. 代码体量大,学习曲线陡
conversation_loop.py 一个文件 3900 行,agent_init.py 60+ 参数。即便经过模块化拆分,核心流程的复杂度仍然很高。新贡献者很难快速定位"我要改哪里"。

2. 后台 Review 的质量依赖 Prompt

Background Review 完全靠 prompt engineering 驱动,_SKILL_REVIEW_PROMPT 超过 80 行。如果 LLM 不严格遵循优先级规则(比如总是创建新技能而不是更新现有技能),会导致技能库膨胀甚至质量下降。项目有一定的"写技能的能力依赖写技能的技能(meta-skill)"的自举脆弱性。

3. 单进程设计,并发子 Agent 有限

子 Agent 通过 ThreadPoolExecutor 运行,GIL 限制了真正的 CPU 并行。默认最大并发子 Agent 数 3(_DEFAULT_MAX_CONCURRENT_CHILDREN),复杂的并行编排场景有天花板。

4. 状态管理复杂

Session ID、gateway_session_key、parent_session_id、iteration_budget 跨多个层次传递。在大量并发 gateway 会话的生产环境中,状态泄漏风险需要仔细管控。

5. 自进化的不可预期性

技能自动写入是一把双刃剑。如果 Agent 从错误的操作中"学到了错误的经验",可能在未来会话中持续重现错误。Curator 的"只归档不删除"策略保守了一些,在技能库积累大量低质量条目后,清理工作会比较麻烦。

6. Windows 支持仍是 Beta

尽管 v0.14.0 有原生 Windows 支持,但文档多次标注"early beta",PTY、browser dashboard 等功能依然需要 WSL2。

7. 依赖精确锁版本的维护成本

精确锁版本是安全最佳实践,但意味着每个依赖升级都需要手动操作,维护负担较重。对于快速迭代的 AI 生态(openai SDK、anthropic SDK 频繁更新),这会成为一定的摩擦。


结语

Hermes Agent 是一个工程实现质量相当扎实的开源 AI Agent 框架。它不是一个"玩具"------credential pool、LSP 集成、供应链安全、多平台统一网关,这些都是生产级组件。

它的"自进化"设计思路在 AI Agent 领域也是前沿的:不满足于让模型"记住"上下文,而是让 Agent 把经验沉淀成可复用的"技能",并由后台 Curator 持续维护这个技能库的健康度。

如果你在寻找一个可以运行在云端、通过 IM 交互、能够自我学习的个人/团队 AI 助手框架,Hermes Agent 是目前开源方案中架构最完整的选择之一。


十五、工具执行管线:并行调度的规则引擎

当 LLM 一次性返回多个工具调用(batch tool calls)时,Hermes 不是简单地串行执行,而是通过 tool_dispatch_helpers.py 的规则引擎决定是否可以并行:

python 复制代码
# 绝不并行(交互式工具)
_NEVER_PARALLEL_TOOLS = frozenset({"clarify"})

# 无状态只读,直接并行
_PARALLEL_SAFE_TOOLS = frozenset({
    "read_file", "web_search", "web_extract",
    "skill_view", "skills_list", "session_search", ...
})

# 路径作用域工具:目标路径不重叠则可并行
_PATH_SCOPED_TOOLS = frozenset({"read_file", "write_file", "patch"})

_should_parallelize_tool_batch() 的决策流程:



重叠
不重叠







LLM 返回 batch

多个工具调用
batch 中

有 clarify?
串行执行
逐一检查每个工具
工具在

_PATH_SCOPED_TOOLS?
提取目标路径
与已记录路径

前缀重叠?
记录路径,继续
工具在

_PARALLEL_SAFE_TOOLS?
是 MCP 工具且服务器

声明 parallel_safe?
还有下一个工具?
并行执行

ThreadPoolExecutor

路径重叠检测用的是前缀匹配而非 resolve(),因为目标文件可能还不存在------这是一个很细心的边界条件处理。

工具结果的多模态包装

computer_use 等工具返回的不是字符串,而是一个特殊 envelope:

python 复制代码
{
    "_multimodal": True,
    "content": [                      # OpenAI-style content parts
        {"type": "image_url", "image_url": {"url": "data:image/png;base64,..."}},
        {"type": "text", "text": "Screenshot taken"}
    ],
    "text_summary": "Screenshot taken"  # 给不支持多模态的 provider 的纯文本回退
}

_is_multimodal_tool_result() + _multimodal_text_summary() 这对函数让工具层和模型层的适配逻辑彻底解耦:工具只负责产出结构化内容,传输层决定如何把它发给具体的 provider。


十六、Prompt Caching:75% Token 成本压缩的实现

agent/prompt_caching.py 是一个纯函数模块(无类状态),实现的是 Anthropic 的 system_and_3 缓存策略:

python 复制代码
def apply_anthropic_cache_control(
    api_messages: List[Dict],
    cache_ttl: str = "5m",       # "5m" 或 "1h"
    native_anthropic: bool = False,
) -> List[Dict]:
    """
    放置最多 4 个 cache_control breakpoint:
      - system prompt(1 个)
      - 最近的 3 条非系统消息(3 个)
    所有 breakpoint 使用相同 TTL。
    """

为什么是"最近 3 条"而不是"前几条"?

Anthropic 的 prompt cache 是前缀缓存------只要消息序列的前缀没变,就能命中缓存。system prompt 永远不变,天然适合缓存。而对话历史中,越靠前的消息越稳定(用户不会修改),最近 3 条反而是当前轮次刚产生的,打上 breakpoint 让下一轮可以直接命中前面所有内容。

两个 TTL 的使用场景

TTL 场景
5m(默认) 交互式对话,用户不长时间离开
1h background review / Curator 等长周期任务,防止 5 分钟过期重算

Background Review 的"近零成本"原理

Background Review 的 auxiliary agent 使用同一个 base_url + api_key 构建,加上同样的 system prompt(包含 SOUL.mdMEMORY.md 等)。这意味着它和主 Agent 共享前缀缓存------system prompt 那一层已经被主 Agent 缓存过,background review fork 直接命中,只需支付少量增量 token 费用。

这不是营销说法,是有代码支撑的:auxiliary_client.py 在解析 provider 配置时,Step 1 就是"用户的主 provider + 主 model",刻意和主 Agent 保持一致。


十七、错误分类器:结构化故障恢复的神经中枢

error_classifier.py 是 v0.14.0 里一个相当成熟的设计,用 FailoverReason 枚举把所有 API 错误分类,让重试循环不再靠 if-else 字符串匹配,而是查询结构化标志位:

python 复制代码
class FailoverReason(enum.Enum):
    auth = "auth"                    # 401/403 瞬态 → 刷新/轮换凭证
    auth_permanent = "auth_permanent" # 刷新后仍失败 → 终止
    billing = "billing"              # 402/额度耗尽 → 立即轮换凭证
    rate_limit = "rate_limit"        # 429/限流 → 退避后轮换
    overloaded = "overloaded"        # 503/529 → 退避
    server_error = "server_error"    # 500/502 → 重试
    timeout = "timeout"              # 超时 → 重建 client + 重试
    context_overflow = "context_overflow"  # 上下文超限 → 压缩,不切 provider
    payload_too_large = "payload_too_large"  # 413 → 压缩 payload
    image_too_large = "image_too_large"   # 图片超限 → 缩图后重试
    model_not_found = "model_not_found"  # 404/模型不存在 → fallback 模型
    provider_policy_blocked = "provider_policy_blocked"  # OpenRouter 隐私策略 → 终止并提示
    format_error = "format_error"    # 400 格式错误 → 终止或清理后重试
    thinking_signature = "thinking_signature"  # Anthropic thinking block 签名失效
    long_context_tier = "long_context_tier"    # Anthropic 长上下文增量计费门槛
    oauth_long_context_beta_forbidden = "..."  # Anthropic OAuth 不支持 1M context
    llama_cpp_grammar_pattern = "..."          # llama.cpp JSON schema regex 不兼容
    unknown = "unknown"              # 无法分类 → 带退避重试

ClassifiedError 的恢复标志设计

python 复制代码
@dataclass
class ClassifiedError:
    reason: FailoverReason
    retryable: bool = True
    should_compress: bool = False      # True → 触发上下文压缩
    should_rotate_credential: bool = False  # True → 切换凭证
    should_fallback: bool = False      # True → 切换 provider/model

这个设计的价值在于:重试循环不需要知道"为什么失败",只需要读取 should_compressshould_rotate_credentialshould_fallback 三个标志来决定下一步动作。分类逻辑和恢复逻辑完全解耦。
auth
auth_permanent
billing
rate_limit
overloaded
server_error
timeout
context_overflow
image_too_large
model_not_found
provider_policy_blocked
format_error
unknown
API 错误
ErrorClassifier

分类错误
FailoverReason
rotate_credential

5分钟冷却后重试
终止会话
rotate_credential

立即切换
退避等待

1小时冷却
退避等待

503/529
直接重试

500/502
重建 HTTP client

  • 重试
    压缩上下文

ContextEngine.compress

不切换 provider
缩小图片

  • 重试
    fallback model

切换备用模型
终止并提示

修改 OpenRouter 隐私设置
清理 tool schema

如剥离 pattern 字段

  • 重试
    带退避重试
    重试 LLM 调用

几个有趣的分类细节

billing vs rate_limit 的模糊地带

python 复制代码
_USAGE_LIMIT_PATTERNS = ["usage limit", "quota", "limit exceeded"]
_USAGE_LIMIT_TRANSIENT_SIGNALS = ["try again", "retry", "resets at", "window"]

"usage limit exceeded" 可能是账单问题(永久)或速率限制(临时)。分类器先匹配 _USAGE_LIMIT_PATTERNS,再检查错误消息里是否有 "try again" / "resets at" 等瞬态信号------有就是 rate_limit,没有就是 billing

provider_policy_blocked

这是专门针对 OpenRouter 的一个枚举值。当用户的账户隐私设置排除了某个模型唯一可用的 endpoint 时,OpenRouter 返回 404 但消息是:

"No endpoints available matching your guardrail restrictions and data policy."

这和"模型不存在"(model_not_found)是完全不同的错误原因------切换 provider 无法解决,只能提示用户去修改 OpenRouter 账户设置。

llama_cpp_grammar_pattern

llama.cpp 的 JSON schema → grammar 转换器对 pattern / format 字段里的某些正则有兼容性问题。分类器识别出这类错误后,触发"从 tool schema 里剥离 pattern/format 字段后重试"------这是从真实兼容性 Bug 中沉淀出来的针对性处理。


十八、Auxiliary Client:背景任务的多 Provider 解析链

auxiliary_client.py 是所有"不打扰用户的后台 LLM 调用"的统一入口:上下文压缩、Session 搜索、Web 提取、Vision 分析、Background Review 都经过它。

自动解析链

复制代码
文本任务(auto 模式)解析顺序:
  1. 用户主 provider + 主 model(命中 prompt cache 关键)
  2. OpenRouter(OPENROUTER_API_KEY 环境变量)
  3. Nous Portal(~/.hermes/auth.json)
  4. 自定义端点(config.yaml model.base_url)
  5. 原生 Anthropic
  6. 直连 API-key provider(z.ai/GLM、Kimi、MiniMax 等)

视觉/多模态任务:额外检查主 provider 是否支持视觉,再走上面的链

注意"Codex OAuth(ChatGPT 账号 auth)不在备用链里"------注释明确解释了原因:OpenAI 对这个端点有个不公开、持续变化的模型白名单,硬编码一个模型名会自行腐烂。

OpenAI SDK 的懒加载代理

python 复制代码
class _OpenAIProxy:
    """模块级代理,模拟 openai.OpenAI 类行为,SDK 在首次调用时才加载。"""
    def __call__(self, *args, **kwargs):
        return _load_openai_cls()(*args, **kwargs)

OpenAI = _OpenAIProxy()  # 模块全局名称

openai SDK 的冷启动大约需要 240ms(包括 responses/*graders/* 等类型树)。用代理对象把真实 import 推迟到第一次 OpenAI(...) 调用时,避免在每次启动 Hermes 时都付出这个冷启动代价------即便当前会话根本用不到 auxiliary client。

同时,from openai import OpenAI 的测试 patch 路径(patch("agent.auxiliary_client.OpenAI", ...))通过代理对象完全透明兼容。


十九、Skill 预处理:动态注入与内联 Shell

skill_preprocessing.py 在技能内容被注入 system prompt 前做两件事:

1. 模板变量替换

python 复制代码
_SKILL_TEMPLATE_RE = re.compile(r"\$\{(HERMES_SKILL_DIR|HERMES_SESSION_ID)\}")

SKILL.md 里可以写 ${HERMES_SKILL_DIR} 引用技能自己的目录路径,或 ${HERMES_SESSION_ID} 拿到当前会话 ID。设计原则:无法解析的 token 原样保留,不替换为空字符串------这让技能作者可以快速发现自己写错了变量名。

2. 内联 Shell 执行

python 复制代码
_INLINE_SHELL_RE = re.compile(r"!`([^`\n]+)`")
_INLINE_SHELL_MAX_OUTPUT = 4000  # 输出上限,防止撑爆 context

SKILL.md 里的 ``!`date +%Y-%m-%d``` 会在技能加载时执行,结果替换进内容:

markdown 复制代码
# 每日报告技能
今天是 !`date +%Y-%m-%d`,请帮我生成今日工作总结。

执行后变成:

markdown 复制代码
今天是 2026-05-22,请帮我生成今日工作总结。

这个功能默认关闭skills.inline_shell: false),需要在 config.yaml 里显式开启。关闭是对的------允许 SKILL.md 执行任意 shell 命令是一个明显的安全风险,只给有意识地选择了这个选项的用户开启。

失败处理也很细致:bash 不存在返回 [inline-shell error: bash not found],超时返回 [inline-shell timeout after Xs: cmd],测试环境的"live-system guard"拦截也有专门处理------一个 snippet 失败不影响其他 snippet 或整体技能加载。


二十、StreamingThinkScrubber:推理块过滤的状态机

think_scrubber.pymemory_manager.py 里的 StreamingContextScrubber 是同一类设计------都是为了解决流式输出无法用正则做跨 chunk 匹配的问题,用状态机替代。

背景 :MiniMax-M2.7 等模型会在流式输出中夹带 <think> 推理块:

复制代码
delta1 = "<think>"
delta2 = "Let me check their config"
delta3 = "</think> Here is my answer"

如果每个 delta 独立做 re.sub(r"<think>.*?</think>", "", delta),delta1 被抹掉(open tag 匹配失败),delta2 作为普通文本传给用户,推理内容泄漏。

解决方案

python 复制代码
class StreamingThinkScrubber:
    _OPEN_TAG_NAMES = ("think", "thinking", "reasoning", "thought", "REASONING_SCRATCHPAD")
    
    def feed(self, text: str) -> str:
        # 状态机:_in_block=True 时吞掉所有内容直到 close tag
        # 跨 chunk 边界的 partial tag 缓存到 _buf 等下一个 delta 再判断

Block-boundary 规则 :开标签只在"行首"(流开始、换行后、当前行只有空白)才触发推理块模式。这防止了模型在行内提到 <think> 标签名时被误过滤,比如:

"use <think> tags to show reasoning"

这个设计和 StreamingContextScrubber 对比值得一提:两者都是流式状态机,但触发条件不同------Think Scrubber 有 block-boundary 规则,Context Scrubber 有更宽松的匹配(memory-context 标签通常只出现在换行处)。


本文基于 hermes-agent v0.14.0 源码,核心文件包括 agent/conversation_loop.pyagent/agent_init.pyagent/context_engine.pyagent/context_compressor.pyagent/memory_manager.pyagent/curator.pyagent/background_review.pyagent/iteration_budget.pyagent/credential_pool.pytools/delegate_tool.pytools/tool_dispatch_helpers.pyagent/prompt_caching.pyagent/error_classifier.pyagent/auxiliary_client.pyagent/skill_preprocessing.pyagent/think_scrubber.py

相关推荐
阿坤带你走近大数据1 小时前
Hadoop中的MapReduce介绍
大数据·hadoop·mapreduce
爱喝热水的呀哈喽1 小时前
hypermesh教程分类
大数据
JAVA学习通1 小时前
Sub2API + CCSwitch 实现 Codex 反向代理:多账号流量分发实战(解决codex手机号验证)
人工智能·codex·反代
qq_452396231 小时前
第十篇:《软件测试的未来:AI测试、DevOps与测试左移》
运维·人工智能·devops
青云计划1 小时前
多智能体路由:从场景定义到Agent解析的工程实践
人工智能
IPHWT 零软网络1 小时前
从选型角度看语音网关国产化:以MX8G-A为列的架构与价值分析
人工智能·架构·信创·国产化·语音网关
武子康1 小时前
调查研究-142 全球机器人产业深度调研报告【04篇】机器人产业利润池全景:谁最容易赚钱与十大判断指标
大数据·人工智能·ai·机器人·具身智能·openclaw
逸Y 仙X1 小时前
文章二:Elasticsearch跨集群能力探查
java·大数据·服务器·elasticsearch·搜索引擎·全文检索
阿标在干嘛1 小时前
政策快报如何让推荐准确率从8%提升到16%?画像系统实践
java·大数据·人工智能