☰
拆解 Hermes Agent 五层架构:从源码到 OpenClaw 的 Agent 落地实践
2026/10/1 20:28:21 网站建设 项目流程

1. 从一条消息出发:Hermes Agent 五层架构到底解决什么问题

Hermes Agent 最近在 Agent 圈子里讨论度很高,很多人第一反应是"又一个套壳 Agent"。但如果你真的把它的源码拉下来,跟着一条消息从终端输入走到最终输出,会发现它内部的分层设计比表面功能清单有意思得多。这篇文章不聊功能罗列,只做一件事:把 Hermes Agent 的五层架构拆开,看看每一层负责什么、层与层之间怎么调用,再结合 OpenClaw 这类本地 Agent 场景,给出可以复制到你自己项目里的分层配置和验证步骤。

先说清楚 Hermes Agent 是什么、能做什么、适合谁。它是一个支持多平台接入的通用 Agent 运行时,入口层同时提供 CLI 和二十多个消息平台适配器(飞书、钉钉、Telegram、Discord、Slack、Email、SMS 等),网关层用常驻进程管理连接和会话生命周期,执行层是核心的 AIAgent 主循环,负责组装上下文、调用模型、执行工具、处理错误,扩展层提供工具注册、技能系统、子 Agent 委托和 MCP 客户端,存储层用 SQLite + FTS5 加 Markdown 文件做持久化。适合谁?适合正在自建 Agent 项目、需要多平台接入、需要长会话记忆和技能沉淀的开发者,也适合想理解 Agent 运行时内部调用链路的工程师。

五层架构这个说法不是营销词,它对应的是消息在系统里的真实流转路径:终端输入 → CLI 解析 → 会话加载 → 上下文组装 → 模型推理 → 工具执行 → 流式输出 → 状态落盘。每一段都归属某一层,层与层之间通过明确的数据结构通信。理解这条链路,你排查"为什么我的 Agent 调不动工具""为什么上下文突然爆了""为什么子任务没返回"这类问题时,就能直接定位到具体层,而不是在几千行代码里瞎翻。

我试过用"跟一条消息走一遍"的方法读它的源码,这是读大型 Agent 项目最省力的路径。下面按五层顺序展开,每层给出职责、关键代码结构、可复制的配置片段,最后给本地验证和排障方法。

2. 入口层与网关层:适配器模式如何统一二十多个平台

入口层和网关层是 Hermes Agent 五层架构里最容易被低估的两层。很多人以为入口就是"接收消息",网关就是"转发消息",实际上这两层承担了平台差异屏蔽、会话隔离、凭证加载、服务生命周期管理这些脏活。

入口层的核心是适配器模式。每个平台的消息格式都不一样:Telegram 走长轮询,Slack 走 WebSocket,Email 走 IMAP,SMS 走 HTTP Webhook。Hermes 给每个平台写了一个适配器,全部继承自BasePlatformAdapter,基类只定义三个抽象方法:

class BasePlatformAdapter(ABC): @abstractmethod async def connect(self) -> bool: ... @abstractmethod async def disconnect(self) -> None: ... @abstractmethod async def send(self, chat_id, content, reply_to=None, metadata=None) -> SendResult: ...

注意这里没有定义"消息转换"接口。转换逻辑藏在每个适配器的connect()里,监听回调拿到平台原始消息后,自己构造MessageEvent,再交给基类统一处理。SMS 适配器收到 Twilio webhook 时是这样构造的:

event = MessageEvent( text=text, message_type=MessageType.TEXT, source=source, raw_message=form, message_id=message_sid, )

这是"约定而不是约束"的设计:各平台各自监听、各自构造MessageEvent,后续所有代码对着同一个内部对象干活。进来时把外部差异统一成内部对象,出去时反向拆回各平台格式。想接新平台,写一个适配器就够,核心代码不用动。

网关层的核心是GatewayRunner常驻进程。它启动时按顺序做四件事:SSL 证书自动探测(在/etc/ssl/certs/ca-certificates.crt等路径逐个试,必须在任何 HTTP 库导入之前完成)、加载~/.hermes/.env、把config.yaml桥接到环境变量(YAML 支持${ENV_VAR}引用)、启动启用的平台适配器。

网关层最值得学的是 Profile 隔离。每个 Profile 有独立配置、密钥、记忆、会话历史,实现靠一个HERMES_HOME环境变量,在 CLI 入口处、任何模块导入之前就设置好,所有后续代码通过get_hermes_home()拿主目录。切换时全自动生效:

hermes profile create coder --clone # 复制当前 profile 的配置、密钥、记忆 hermes -p coder chat # 一次性切换 hermes profile use coder # 设为默认 coder chat # 别名脚本,等同上面

删除 Profile 时会彻底清理:停 Gateway 进程 → 清理 systemd/launchd 服务 → 移除别名 → 删目录 → 如果是当前活跃 profile 就重置为 default。一个环境变量控制整棵目录树,你可以在同一台机器上同时跑"工作 Agent"和"个人 Agent",互不打扰。

结合 OpenClaw 场景,这一层的价值更明显。OpenClaw 作为本地 Agent 运行时,同样需要多入口接入和会话隔离。如果你在 OpenClaw 里做类似的分层,建议把"平台适配"和"会话管理"彻底分开:适配器只负责把外部消息转成内部MessageEvent,网关只负责会话生命周期和凭证加载,两者通过明确的数据结构通信。这样你换平台时不用动会话逻辑,改会话策略时不用碰适配器。

3. 执行层主循环:迭代预算、工具并行与子 Agent 委托的可复制配置

执行层是 Hermes Agent 五层架构的心脏,对应AIAgent(run_agent.py)。消息到这里,进入整个项目最核心的地方。主循环骨架大致是这样:

while iteration_budget.remaining > 0: response = client.chat.completions.create( model=model, messages=messages, tools=tool_schemas, stream=True ) if response 有 tool_calls: 执行工具(可能并行) iteration_budget.consume() else: return response.content # 没有工具调用,返回最终结果

主循环有三种退出路径:模型给最终文本(本轮没有 tool_calls,正常完结)、预算耗尽(iteration_budget.remaining归零,硬上限防止模型在错误循环里烧 token)、用户中断(_interrupt_requested被外部置位,收到中断后 break 出循环,持久化已有结果并补齐消息结构)。

迭代预算的设计有个细节值得抄:父 Agent 上限 90 轮,子 Agent 50 轮,模型每推理一轮消耗 1 次迭代预算,不管这一轮并行调了几个工具。但refund()有个特殊触发条件:

_tc_names = {tc.function.name for tc in assistant_message.tool_calls} if _tc_names == {"execute_code"}: self.iteration_budget.refund()

当本轮工具调用里只有execute_code一种,刚扣掉的那 1 次迭代会被退还。execute_code是 PTC(Programmatic Tool Calling):模型不是直接挨个调工具,而是写一段 Python 脚本,脚本内部通过 RPC 把web_search、read_file、write_file这些工具串起来跑。同样做 8 次信息获取,走普通工具调用要 8 轮模型推理,走 PTC 只要 1 轮推理就打包干完。退还的真正作用是预算管理:脚本密集型任务可能要连写十几个脚本,让脚本执行零成本,预算就能全留给需要推理的轮次。

工具并行执行靠三个集合决定:

_NEVER_PARALLEL_TOOLS = frozenset({"clarify"}) # 会跟用户交互 _PARALLEL_SAFE_TOOLS = frozenset({ # 只读,无共享状态 "read_file", "search_files", "session_search", "skill_view", "skills_list", "vision_analyze", "web_extract", "web_search", "ha_get_state", "ha_list_entities", "ha_list_services", }) _PATH_SCOPED_TOOLS = frozenset({"read_file", "write_file", "patch"}) # 路径不重叠才能并行

路径工具的冲突检查原理:提取每次调用的目标路径,两两比对看有没有重叠。重叠判定包括同一路径,或一个路径是另一个的祖先(比如/a和/a/b.txt)。只要重叠就可能撞上读写竞态,必须排队串行;路径完全独立则放并行。并行池最多 8 个工作线程同时跑。

delegate_task是执行层另一个关键设计。模型选它时会 fork 一个新的AIAgent,子 Agent 有自己独立的上下文和 50 轮迭代预算,父子之间只通过任务描述(传入)和最终摘要(传出)通信。子 Agent 被禁用 5 个工具:delegate_task(防套娃)、clarify(子 Agent 不能反问人)、memory(不能写共享记忆)、send_message(不能直接往平台发消息)、execute_code(定位就是一步步推理把事做完)。委托深度只有 1 层,并发上限 3 个。父 Agent 每 30 秒给子发一次心跳,父被中断或挂了,心跳断开,子 Agent 连锁停下,这就是"级联中断"。

如果你要在自己的 Agent 项目里落地这套分层,下面是一份可复制的config.yaml片段,路径和字段名对照 Hermes 的约定:

# ~/.hermes/config.yaml agent: max_iterations: 90 # 父 Agent 迭代预算上限 sub_agent_max_iterations: 50 # 子 Agent 迭代预算上限 parallel_workers: 8 # 并行工具执行线程池上限 delegate_max_concurrency: 3 # 子 Agent 并发上限 delegate_max_depth: 2 # 委托深度硬约束 tools: never_parallel: - clarify parallel_safe: - read_file - search_files - session_search - skill_view - skills_list - vision_analyze - web_extract - web_search path_scoped: - read_file - write_file - patch model: provider: ${MODEL_PROVIDER} name: ${MODEL_NAME} base_url: ${MODEL_BASE_URL} api_key: ${MODEL_API_KEY}

注意base_url和api_key用环境变量引用,避免把凭证写进配置文件。如果你用 TaoToken 这类统一接入服务,Base URL 填https://taotoken.net/api,Key 在控制台生成,Model ID 按你实际调用的模型填。这三件套(Base URL + Key + Model ID)在 Cline MCP、Codexauth.json、CC Switch 里都是同样的结构,配一次就能复用。

4. 系统提示词与记忆层:冻结快照、安全扫描与本地验证

系统提示词和记忆层是 Hermes Agent 五层架构里信息密度最高的部分。模型推理之前,系统提示词要按固定顺序拼好:身份 → 工具行为引导 → 外部系统提示 → 记忆 → 技能索引 → 项目上下文 → 运行时元数据(时间/环境/平台)。

这个顺序的门道是:越稳定的内容越靠前,动态内容靠后。配合前缀缓存,前缀不变就能命中,只有尾巴会变。身份层默认是一段"You are Hermes Agent…"的声明,用户在~/.hermes/SOUL.md里写了自定义人格就会替换掉默认那段。工具行为引导层会根据模型家族(GPT/Gemini/Grok/Claude)注入不同内容,对 GPT、Gemini、Grok 家族额外注入TOOL_USE_ENFORCEMENT_GUIDANCE,核心一句话:说做就做,别光说不动。Claude 不需要这段,不是偏见,是不同模型在工具调用行为上确实有差异。

项目上下文注入前要过 10 条正则安全扫描:

_CONTEXT_THREAT_PATTERNS = [ (r'ignore\s+(previous|all|above|prior)\s+instructions', "prompt_injection"), (r'do\s+not\s+tell\s+the\s+user', "deception_hide"), (r'<!--[^>]*(?:ignore|override|system|secret|hidden)[^>]*-->', "html_comment_injection"), (r'<\s*div\s+style\s*=\s*["\'][\s\S]*?display\s*:\s*none', "hidden_div"), (r'curl\s+[^\n]*\$\{?\w*(KEY|TOKEN|SECRET|PASSWORD)', "exfil_curl"), (r'cat\s+[^\n]*(\.env|credentials|\.netrc|\.pgpass)', "read_secrets"), # ... ]

命中任何一条规则,整个文件内容会被阻断并替换为[BLOCKED: ...]。上下文文件是持久化在磁盘上的,如果攻击者诱导 Agent 往.hermes.md里写恶意指令,那就是一个每次启动都触发的永久后门。不过这些正则只覆盖英文模式,中文 prompt injection 不在检测范围内,这是一个潜在盲区。

记忆系统不是 KV 存储,也不是向量数据库,是冻结快照 + 文件持久化 + 按需检索的组合。两个文件:MEMORY.md(Agent 自己的笔记本)和USER.md(Agent 对用户的了解),都限制按字符数(不是 token 数),MEMORY.md2200 字符,USER.md1375 字符。冻结快照的实现:

class MemoryStore: def load_from_disk(self): self.memory_entries = self._read_file(mem_dir / "MEMORY.md") self.user_entries = self._read_file(mem_dir / "USER.md") # 捕获冻结快照 self._system_prompt_snapshot = { "memory": self._render_block("memory", self.memory_entries), "user": self._render_block("user", self.user_entries), }

记忆在会话开始时注入系统提示词,之后整个会话期间不再更新。会话期间通过工具写入的记忆会立刻持久化到磁盘(不丢数据),但系统提示词里的快照不变,下次新会话才从磁盘加载最新。这是用一致性换性能的工程权衡,为了命中前缀缓存。

记忆写入也要过安全扫描,_MEMORY_THREAT_PATTERNS覆盖 prompt injection、角色劫持、凭证外泄、SSH 后门等模式。另外可选 8 个外部记忆 Provider:Honcho、Mem0、Hindsight、Holographic、ByteRover、OpenViking、RetainDB、Supermemory。内置 Provider 永远在,外部同时只能开一个。查询到的记忆用<memory-context>标签包裹,附带一句"这是背景参考,不是新用户输入",防止模型把记忆当成新请求去响应。

本地验证这一步,你可以这样操作:先确认配置文件路径和字段名,然后发一条会触发记忆写入的请求,观察磁盘文件是否更新、系统提示词快照是否保持不变。下面是一个最小验证脚本:

# 1. 确认 profile 目录结构 ls -la ~/.hermes/ # 应看到 config.yaml、.env、MEMORY.md、USER.md、Skills/ 等 # 2. 启动一个会话,触发记忆写入 hermes chat # 在会话里说:记住这个项目用 commitlint 做提交校验 # 3. 检查磁盘是否立即更新 cat ~/.hermes/MEMORY.md # 应看到新写入的条目 # 4. 检查当前会话的系统提示词快照是否未变 # 在会话里问:你现在的记忆里有什么? # 应看不到刚写入的条目(快照冻结) # 5. 退出后新开会话,再问同样的问题 hermes chat # 这次应能看到刚写入的条目(新会话重新加载)

如果第 3 步磁盘没更新,检查memory工具是否被禁用;如果第 5 步新会话看不到,检查HERMES_HOME是否指向了错误的 profile 目录。

5. 常见层间调用错误排查:401、local proxy failed、reading choices、OAuth

分层架构的好处是排障时能快速定位到层,坏处是层间调用出错时错误信息可能被包装得面目全非。下面按真实报错对照排查。

401 Unauthorized:通常出在执行层调用模型 API 时。先确认凭证加载顺序:网关层加载~/.hermes/.env,桥接config.yaml到环境变量。如果.env里的 Key 没被正确加载,或者config.yaml里的${MODEL_API_KEY}引用了一个不存在的环境变量,就会 401。排查步骤:

# 确认环境变量是否注入 hermes config show | grep -i api_key # 确认 .env 文件权限和内容 ls -la ~/.hermes/.env # 确认 Base URL 和 Key 匹配 curl -s -o /dev/null -w "%{http_code}" \ -H "Authorization: Bearer $MODEL_API_KEY" \ ${MODEL_BASE_URL}/models

如果返回 401,检查 Key 是否过期、Base URL 是否写错(注意结尾不要多加/v1,除非你的服务商要求)。用 TaoToken 的话,Base URL 是https://taotoken.net/api,Key 在控制台 API Keys 页面生成,Model ID 按实际模型填。

local proxy failed:这个报错通常出在网关层启动适配器时。网关启动顺序里 SSL 证书探测必须在任何 HTTP 库导入之前完成,如果证书路径探测失败,后续 HTTP 请求会走本地代理失败。排查:

# 确认证书路径存在 ls -la /etc/ssl/certs/ca-certificates.crt # 确认没有残留的代理环境变量 env | grep -i proxy # 如果有,清掉 unset HTTP_PROXY HTTPS_PROXY ALL_PROXY

注意:这里说的代理是系统环境变量层面的网络配置,不是让你去搭什么通道。企业内网环境下如果必须走网关,按公司 IT 规范配置,不要自己乱设。

reading choices 报错:这个报错出在执行层解析模型响应时。response.choices为空或结构不符合预期,常见原因有三个:模型返回了非标准格式(某些兼容接口在流式模式下返回结构不同)、请求被中间层拦截返回了错误页、stream=True但客户端没正确处理流式分块。排查:

# 临时关掉流式,看原始响应 response = client.chat.completions.create( model=model, messages=messages, tools=tool_schemas, stream=False # 先关流式 ) print(response)

如果关掉流式正常,说明是流式解析问题,检查stream_consumer的分块处理逻辑。如果关掉流式也报错,检查 Base URL 是否指向了正确的 API 端点。

OAuth 相关报错:如果用了需要 OAuth 的模型服务或 MCP 客户端,报错通常出在扩展层。检查 token 是否过期、回调地址是否配置正确。Codex 的auth.json结构里,Base URL、Key、Model ID 三件套必须齐全:

{ "base_url": "https://taotoken.net/api", "api_key": "sk-xxxxxxxx", "model": "your-model-id" }

Cline MCP 的配置类似,在 MCP 服务器配置里填同样的三件套。CC Switch 切换配置时,确认切换后的 profile 里这三项都正确。

层间调用通用排查思路:先确认消息走到哪一层断了。入口层问题看适配器日志,网关层问题看GatewayRunner启动日志,执行层问题看主循环迭代日志,扩展层问题看工具注册和 MCP 客户端日志,存储层问题看 SQLite 和文件读写权限。每一层都有明确的日志输出点,按链路顺序排查,不要跳层。

6. 把五层架构落到你自己的 Agent 项目

拆完 Hermes Agent 的五层架构,回到落地。如果你要在自己的 Agent 项目里对照实现,建议按这个顺序推进:先把入口层和网关层的边界划清楚,适配器只做消息转换,网关只做会话生命周期和凭证加载;再实现执行层主循环,把迭代预算、工具并行、子 Agent 委托这三个机制做进去;然后做系统提示词和记忆层,重点解决前缀缓存命中和安全扫描;最后补存储层和自进化机制。

几个容易踩的坑:一是迭代预算的计数口径要统一,每轮推理 +1,不管并行调了几个工具,execute_code的退还逻辑要单独处理;二是工具并行的路径冲突检查要覆盖祖先路径重叠的情况,不然会出现读写竞态;三是记忆快照的冻结策略要和前缀缓存配合,会话期间不更新系统提示词,但磁盘要立即持久化;四是子 Agent 的工具黑名单要硬编码,不能只靠提示词约束。

如果你需要统一接入多家模型服务,可以用 TaoToken 做 Base URL 和 Key 的统一管理,模型对话、Coding Plan、API Keys 都在控制台里配置。接入文档里有各客户端的配置示例,Claude Code、Cline、Codex 都能直接对照填。长期做编码和 Agent 任务的话,Coding Plan 比按量计费更划算,具体在控制台里看套餐说明。

最后留一个思考题:Hermes 的会话链设计(压缩后开新会话,parent_session_id指回旧会话)解决了"省成本"和"不丢历史"的矛盾,但"能搜到历史"和"Agent 记住了"是两回事。session_search是按需检索,搜索结果只是当次推理的临时上下文,不会自动写入记忆。真正持久的记忆只有模型主动调memory工具写入这一条路。你的项目里,这两条通道分清楚了吗?

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询