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 后台维护员。本文将从架构到代码逐层拆解,最后给出客观的优缺点评估。
一、项目全貌:目录结构与模块划分
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"。其核心职责链:
工具分发(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`。```mermaidflowchart 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_mode 是 chat_completions 且模型需要 Responses API,会自动升级——但 Azure OpenAI 除外(Azure 不支持 Responses API,代码有 _is_azure_openai_url() 专门排除)。
2.2 迭代预算(IterationBudget)
classIterationBudget:def__init__(self, max_total: int):self.max_total = max_total # 父 Agent 默认 90self._used = 0self._lock = threading.Lock()defconsume(self) -> bool:withself._lock:ifself._used >= self.max_total:returnFalseself._used += 1returnTruedefrefund(self) -> None: # execute_code 轮次退款,不消耗预算withself._lock:ifself._used > 0:self._used -= 1设计亮点:父子 Agent 的预算是独立的。父 Agent 最多 90 次,每个子 Agent 最多 50 次(可配置)。execute_code(Python 脚本执行)调用会被退款——因为脚本执行本质是编程,而不是一次 LLM 决策。
三、上下文引擎:可插拔的压缩架构
context_engine.py 定义了一个抽象基类 ContextEngine,这是 Hermes 架构中最优雅的设计之一:
classContextEngine(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 条消息 @abstractmethoddefshould_compress(self, prompt_tokens: int = None) -> bool: ... @abstractmethoddefcompress(self, messages, current_tokens, focus_topic=None) -> List: ...内置实现是 ContextCompressor(基于 LLM 摘要),通过 config.yaml 的 context.engine 字段可以切换到第三方引擎(如 LCM),引擎目录:plugins/context_engine/<name>/。
值得注意的是,ContextEngine ABC 还定义了 get_tool_schemas() 接口——这意味着上下文引擎可以向 Agent 暴露自己的工具(如 LCM 的 lcm_grep、lcm_describe),Agent 在对话中可以直接调用这些工具与压缩引擎交互。这是一个非常优雅的设计:压缩引擎不只是被动执行压缩,它可以主动扩展 Agent 的能力边界。
3.1 ContextCompressor 的压缩策略
context_compressor.py 的注释中明确了核心设计决策:
• 结构化摘要模板:追踪 Resolved/Pending 问题 • 工具输出预剪枝:LLM 摘要前先做一次廉价的工具输出删减 • 比例摘要预算:摘要长度 = 被压缩内容长度 × SUMMARY_RATIO(0.20),上限_SUMMARY_TOKENS_CEILING(12000 tokens)• 图片占位:每张图片估算 1600 tokens,防止多模态对话压缩预算计算失准
压缩时插入的前缀明确告知模型这是"历史参考,非活跃指令":
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 截断的代码,注释说明了为什么不能简单地切断字符串:
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 的记忆系统分三层:
4.1 MemoryManager 的防注入机制
memory_manager.py 实现了一个 StreamingContextScrubber,专门应对流式输出中可能出现的 <memory-context> 标签注入问题:
classStreamingContextScrubber:""" 非流式 sanitize_context 无法处理跨 chunk 边界的标签: <memory-context> 在一个 delta 开启,在另一个 delta 关闭, 内容会泄露到 UI 界面。 该 scrubber 维护一个状态机,跨 delta 追踪标签开关状态, 挡住被 memory context 包裹的内容。 """这种防御是必要的:如果 AI 模型在生成响应时"输出"了一个 <memory-context> 标签,流式 UI 会直接把它渲染给用户,造成混乱甚至安全问题。
4.2 记忆写入与同步
每次对话轮次结束,MemoryManager.sync_all() 会被调用,把当前轮次的用户消息和助手响应同步给所有 provider:
defsync_all(self, user_content, assistant_content, *, session_id=""):for provider inself._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 的当前能力。
技能分三类:
~/.hermes/skills/ | hermes skills install 安装 | |
~/.hermes/skills/ |
5.2 Background Review:对话后的自我审视
这是"自进化"机制的核心实现,在 background_review.py 中:
_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 管理工具,让它审阅整个对话并决定:
• 是否有值得持久化的记忆(关于用户偏好、行为等) • 是否有值得写入技能库的经验(工作流、纠错记录、域知识)
技能更新的优先级顺序(来自 prompt):
1. 更新当前对话中已加载的技能 2. 更新现有伞形技能(umbrella skill) 3. 在现有伞形技能下添加支持文件(references/templates/scripts) 4. 创建新的类级别伞形技能(严格禁止创建以 PR 编号、bug 字符串命名的一次性技能)
这个设计的工程考量很细致:后台复审使用 auxiliary_client,共享父 Agent 的 prompt cache 前缀,实现近乎零额外成本的缓存命中。
5.3 Curator:技能库的"定期维护员"
curator.py 是一个后台维护系统,定期(默认每 7 天)在 Agent 空闲时自动运行:
DEFAULT_INTERVAL_HOURS = 24 * 7# 7 天DEFAULT_MIN_IDLE_HOURS = 2# 至少空闲 2 小时才触发DEFAULT_STALE_AFTER_DAYS = 30# 30 天未用标记为 staleDEFAULT_ARCHIVE_AFTER_DAYS = 90# 90 天未用归档Curator 的职责:
• 自动管理技能生命周期状态(active → stale → archived) • 合并重叠技能,归档冗余条目(不自动删除,只归档,可恢复) • 始终只操作用户创建的技能,绝不动 bundled/hub 技能
首次运行逻辑有一个有趣的 UX 设计:如果从未运行过,不会立即触发,而是把 last_run_at 设为当前时间,让用户先运行一个完整的 7 天周期。防止 hermes update 后立即触发一次不必要的库维护。
六、子 Agent 委派系统:真正的并行 Agent
tools/delegate_tool.py 实现了父子 Agent 委派架构:
DELEGATE_BLOCKED_TOOLS = frozenset(["delegate_task", # 禁止递归委派(MAX_DEPTH=1 默认)"clarify", # 禁止用户交互"memory", # 禁止写共享 MEMORY.md"send_message", # 禁止跨平台副作用"execute_code", # 子 Agent 委派旨在步骤分解,而非直接写脚本执行])委派工具的设计亮点:
1. 防死锁保护子 Agent 在 ThreadPoolExecutor 的 worker 线程中运行,而父 Agent 的 stdin 交互回调存储在 threading.local() 中,不会被 worker 线程继承。如果子 Agent 遇到需要用户确认的命令,直接调用 input() 会死锁。解决方案:为每个 worker 线程注入一个 _subagent_auto_deny 回调,遇到危险命令直接拒绝而非等待用户输入。
2. 深度限制
MAX_DEPTH = 1# 默认:父(0) -> 子(1);孙子在默认配置下被拒绝默认嵌套深度为 1(仅允许一层子 Agent),可通过配置调整,最大支持 3 层,防止无限递归。
3. 运行时暂停set_spawn_paused(True) 可以全局阻止新的委派,已运行的子 Agent 不受影响。这是 TUI 控制面板的功能。
七、多凭证池(CredentialPool)
credential_pool.py 实现了企业级的多凭证管理:
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 注释中有一段重要的依赖策略说明:
# 所有直接依赖使用精确版本锁(==X.Y.Z),禁止范围版本。# 原因:2026-05-12 mistralai 2.4.6 被注入恶意代码(Mini Shai-Hulud 蠕虫),# 如果我们用 "mistralai>=2.3.0,<3" 而不是精确锁定,每个安装都会自动拉取该版本。这是对真实供应链攻击事件的直接响应。所有依赖精确锁版本,只有 Hermes 主动升级时新版本才会进入用户环境。
另一个有趣的决策:execute_code 子 Agent 禁止递归委派,部分原因也是防止通过动态生成的代码绕过工具白名单。
十一、工具生态:40+ 工具的组织方式
工具通过"toolset"系统分组管理:
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.py 和 system_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 的质量依赖 PromptBackground 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 的规则引擎决定是否可以并行:
# 绝不并行(交互式工具)_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() 的决策流程:
路径重叠检测用的是前缀匹配而非 resolve(),因为目标文件可能还不存在——这是一个很细心的边界条件处理。
工具结果的多模态包装
computer_use 等工具返回的不是字符串,而是一个特殊 envelope:
{"_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 缓存策略:
defapply_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 的使用场景
5m | |
1h |
Background Review 的"近零成本"原理
Background Review 的 auxiliary agent 使用同一个 base_url + api_key 构建,加上同样的 system prompt(包含 SOUL.md、MEMORY.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 字符串匹配,而是查询结构化标志位:
classFailoverReason(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 的恢复标志设计
@dataclassclassClassifiedError: reason: FailoverReason retryable: bool = True should_compress: bool = False# True → 触发上下文压缩 should_rotate_credential: bool = False# True → 切换凭证 should_fallback: bool = False# True → 切换 provider/model这个设计的价值在于:重试循环不需要知道"为什么失败",只需要读取 should_compress、should_rotate_credential、should_fallback 三个标志来决定下一步动作。分类逻辑和恢复逻辑完全解耦。
几个有趣的分类细节
billing vs rate_limit 的模糊地带
_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 的懒加载代理
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. 模板变量替换
_SKILL_TEMPLATE_RE = re.compile(r"\$\{(HERMES_SKILL_DIR|HERMES_SESSION_ID)\}")SKILL.md 里可以写 ${HERMES_SKILL_DIR} 引用技能自己的目录路径,或 ${HERMES_SESSION_ID} 拿到当前会话 ID。设计原则:无法解析的 token 原样保留,不替换为空字符串——这让技能作者可以快速发现自己写错了变量名。
2. 内联 Shell 执行
_INLINE_SHELL_RE = re.compile(r"!`([^`\n]+)`")_INLINE_SHELL_MAX_OUTPUT = 4000# 输出上限,防止撑爆 contextSKILL.md 里的 !`date +%Y-%m-%d` 会在技能加载时执行,结果替换进内容:
# 每日报告技能今天是 !`date +%Y-%m-%d`,请帮我生成今日工作总结。执行后变成:
今天是 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.py 和 memory_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 作为普通文本传给用户,推理内容泄漏。
解决方案:
classStreamingThinkScrubber: _OPEN_TAG_NAMES = ("think", "thinking", "reasoning", "thought", "REASONING_SCRATCHPAD")deffeed(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.py、agent/agent_init.py、agent/context_engine.py、agent/context_compressor.py、agent/memory_manager.py、agent/curator.py、agent/background_review.py、agent/iteration_budget.py、agent/credential_pool.py、tools/delegate_tool.py、tools/tool_dispatch_helpers.py、agent/prompt_caching.py、agent/error_classifier.py、agent/auxiliary_client.py、agent/skill_preprocessing.py、agent/think_scrubber.py。
夜雨聆风