数据截至 (上游 commit 4ac938ddecce)
Hermes Agent — 架构与原理(总览)
30 秒导读: Hermes Agent 是 Nous Research 开源的自托管 AI agent。它把"终端里的 AI 助手""挂在 Telegram/Discord 上的机器人""半夜自己跑任务的定时器"做成了同一个大脑的不同外壳;更特别的是,它每回完一次话,会在后台再 fork 一个自己去复盘刚才那轮,把值得记的写进记忆、把值得沉淀的写成技能——所以它是"越用越熟"的。这个分叉在下文统称复盘 fork。
本页的位置: 六章正文与本页同目录,章末的"回总览"落在这里,所以本页是这套文档的导航中枢。上级的书架条目只负责把读者送到这儿,不承担章节导航。
1. 这是什么(零基础也能懂)
一句话定义。 Hermes Agent 是一个你自己部署、自己接模型的通用 AI agent:它能读写文件、开终端跑命令、上网、调 MCP 工具,并且能从任何你常用的聊天软件里被使唤。
给谁用、解决什么问题。 想象你想要一个"随身的技术助理":你在笔记本前用终端跟它结对写代码;出门在地铁上用 Telegram 让它去查个东西、改个配置;睡觉时它按点自己跑一遍晨间简报发给你。市面上多数 agent 只满足其中一条——要么绑死终端,要么是个云端聊天机器人。Hermes 的取舍是:agent 跑在你自己的机器上(一台 5 美元的 VPS 就够),外壳随便换(README.md:19-21)。
它能做什么。
| 能力 | 白话 |
|---|---|
| 终端里对话 | 全功能 TUI:多行编辑、斜杠命令补全、流式工具输出、随时打断改口 |
| 二十来个聊天平台 | Telegram / Discord / Slack / WhatsApp / Signal / 飞书 / 邮 件…… 全由一个网关进程托管(plugins/platforms/ 下 20 个插件目录 + gateway/platforms/ 内置若干) |
| 自我进化闭环 | 复杂任务后自动写技能,用的过程中自动改技能,定期提醒自己存记忆,还能搜自己的历史会话 |
| 定时自动化 | 内置 cron 调度器,用自然语言写任务,跑完投递到任意平台 |
| 到处能跑 | 六种终端后端:本地、Docker、SSH、Singularity、Modal、Daytona |
| 换模型不换代码 | Nous Portal / OpenRouter / OpenAI / 自建端点,hermes model 一句话切换 |
用起来什么样。 装完之后就三条命令起步(README.md:107-116):
hermes # 进终端界面,直接开聊
hermes model # 挑模型和 provider
hermes gateway # 起消息网关,然后从 Telegram 给机器人发消息
包安装时注册的可执行入口只有三个(pyproject.toml:303-306):
| 命令 | 指向 | 用 途 |
|---|---|---|
hermes | hermes_cli.main:main | 面向人的多命令 CLI,裸跑时进 TUI 对话 |
hermes-agent | run_agent:main | 直接跑 agent 本体,给脚本/容器用 |
hermes-acp | acp_adapter.entry:main | ACP 协议适配器,让编辑器把 Hermes 当后端 |
注意 hermes gateway 不是第四个可执行文件,它是 hermes 底下的子命令。
一句话直觉。 把它想成一台装了大脑的服务器:AIAgent 是大脑(只有一个类),CLI、网关、cron、编辑器适配器都只是插在大脑上的输入输出口;而"技能库 + 记忆文件 + 会话数据库"是它的长期记忆盘,每轮对话结束后由复盘 fork 负责往里写。
2. 顶层全景(它大概怎么转)
2.1 一张图看清分层
怎么读这张图:左边五个是"从哪进来",中间是唯一的大脑,右下是"活干在哪",最下面是"学到的东西存哪"。
┌── 终端 TUI ───┐
├── 聊天网关 ───┤
├── 定时任务 ───┼──► AIAgent ──► run_conversation(一轮)
├── 编辑器 ACP ─┤ 一个大脑 循环:问模型 ⇄ 跑工具
└── MCP 服务 ───┘ │ │
(入口层) │ ▼
│ 执行环境:本地 / 容器 / 远端
│ (命令真正落地的地方)
▼
回完话之后,复盘 fork 再跑一遍本轮
│
▼
回写层:技能库 · 记忆文件 · 会话数据库
"一个大脑"不是修辞,是可以 grep 验证的事实:所有外壳都在构造同一个 AIAgent 类——网关 gateway/run.py:22506 与 :16405、定时任务 cron/scheduler.py:6010、编辑器 acp_adapter/session.py:687、TUI tui_gateway/server.py:7177、一次性脚本 hermes_cli/oneshot.py:476。
关键是那条竖着往下的路径:用户拿到回复之后,主循环并没有结束——它会另起一条守护线程,让 agent 复盘刚才那一轮,决定要不要写记忆、要不要沉淀技能。这条路径是 Hermes 区别于同类 agent 的地方,细节见 自我进化闭环。
2.2 部件一句话职责
| 部件 | 干什么 | 文件与符号 |
|---|---|---|
| CLI 门面 | 解析 hermes xxx 子命令;裸跑时转交给对话主程序 | hermes_cli/main.py:12418 main(:2379 from cli import main as cli_main) |
| 终端对话程序 | TUI 交互、斜杠命令、把用户输入喂给 agent | cli.py:15186 main |
| 消息网关 | 一个进程同时托管全部聊天平台适配器,收消息→喂 agent→回消息 | gateway/run.py:6726 GatewayRunner、:18801 main |
| 平台适配器 | 每个聊天平台一份收发实现 | plugins/platforms/(telegram、discord、slack…)、gateway/platforms/ |
| 定时调度 | 到点取出任务、拼提示词、跑一轮、把结果投递到指定平台 | cron/scheduler.py:7199 tick、:1969 run_job |
| 编辑器适配 | 把 Hermes 包成 ACP 服务,供编辑器调用 | acp_adapter/entry.py:220 main |
| MCP 服务端 | 反过来把 Hermes 自己暴露成 MCP server | mcp_serve.py:623 create_mcp_server |
| AIAgent | 大脑本体:持有模型配置、工具集、会话状态、记忆句柄 | run_agent.py:412 class AIAgent |
| 一轮对话循环 | "问模型 → 有工具就跑 → 结果塞回去 → 再问",直到模型不再要工具 | agent/conversation_loop.py:1766 run_conversation |
| 模型接入 | 各家 API 的方言适配(Anthropic / chat-completions / Bedrock / Codex…) | agent/transports/ |
| 工具分发 | 按名字找到工具函数并调用,过一遍护栏与审批 | model_tools.py:1192 handle_function_call |
| 工具执行 | 并发或串行地执行本轮的一批 tool_calls | agent/tool_executor.py:1070 execute_tool_calls_concurrent |
| 执行环境 | 命令实际落到哪:本机、Docker、SSH、Modal…… 统一抽象 | tools/environments/base.py:597 BaseEnvironment |
| 系统提示装配 | 把身份/指南/技能/记忆拼成一整条系统提示,并整段缓存 | agent/system_prompt.py:903 build_system_prompt |
| 收尾器 | 一轮结束时同步记忆、判断要不要触发复盘 | agent/turn_finalizer.py:798(调用 _spawn_background_review) |
| 复盘 fork | fork 一个 agent 重放本轮,只许它动记忆和技能工具 | agent/background_review.py:1478 spawn_background_review_thread |
| 会话存储 | 所有会话消息落 SQLite,支持全文检索与跨会话回忆 | hermes_state.py:3258 class SessionDB |
2.3 主线走一遍(高层,不进代码)
跟着一条真实消息走一遍。假设你在 Telegram 里发了句"帮我看看这个仓库的测试为什么挂了"。
① 进门 Telegram 适配器收到消息 → 网关按聊天窗口找到/新建会话
gateway/run.py:16949 调 agent.run_conversation
② 拼提示 系统提示整段从缓存取(不重拼,保住 prefix cache)
agent/system_prompt.py:470
③ 一轮开始 进入 while 循环:调模型 → 拿回复
agent/conversation_loop.py:1922
④ 分岔 回复里有 tool_calls?
├─ 没有 → 这就是最终答复,跳到 ⑥
└─ 有 → 执行工具,把结果作为新消息追加,回到 ③
agent/conversation_loop.py:7237
⑤ 工具落地 比如 terminal("pytest") → 交给当前执行环境去跑
tools/environments/base.py:290
⑥ 回话 文本经网关发回 Telegram;消息写进 SessionDB
⑦ 复盘 收尾器判断该不该复盘;该的话拉起复盘 fork,
让它问自己"这轮有什么值得记/值得沉淀的?"
agent/turn_finalizer.py:455 → background_review.py:839
从终端进来的路径只有第 ① 步和第 ⑥ 步不同——cli.py 直接调同一个 run_conversation;cron 也一样,由 cron/scheduler.py:6109 调进来。这就是"一个大脑、多种外壳"的实际含义。
顺带一个容易忽略的事实:cron 的心跳跑在网关进程里(gateway/run.py:30153 _start_cron_ticker,默认 60 秒一跳),所以想让定时任务自动跑,得让网关活着。
3. 阅读地图
六章按"由浅入深"排。建议顺序:先 01 建立"一轮怎么跑"的骨架,再 02 看它怎么省 token,03 才是这个项目最有辨识度的部分。
| 顺序 | 章节 | 这章讲什么 |
|---|---|---|
| 1 | 一轮对话是怎么跑完的 | 一轮对话的完整生命周期:主循环、迭代预算、模型 transports、流式与中断、收尾 |
| 2 | 上下文工程 | 系统提示的三层结构、缓存不变量、上下文压缩、工具结果截断 |
| 3 | 自我进化闭环 | 复盘 fork、技能的产生与自改、记忆策展、会话全文检索 |
| 4 | 工具层与执行环境 | 工具注册/分发/并发执行,以及六种终端后端的统一抽象 |
| 5 | 到处都能找到它 | 网关多平台托管、SessionDB、cron 调度与投递路由 |
| 6 | 信任边界 | 危险命令审批、提示注入防线、凭据与沙箱边界 |
"我只想看 X" 的跳转表:
| 我想知道…… | 跳这章 |
|---|---|
| 模型返回 tool_calls 之后到底发生了什么 | 01 |
| 它凭什么能少烧 token / prompt cache 怎么保住的 | 02 |
| "自我进化"是营销词还是真机制 | 03 |
| 我想加一个自己的工具 / 换成 Docker 里跑命令 | 04 |
| 我想把它接到某个聊天平台 / 让它定时干活 | 05 |
| 它会不会被一封邮件里的指令劫持 | 06 |
4. 巧妙之处速览
这不是完整清单——每章末尾都有自己那一节。这里从六章里各挑一条最能说明设计品味的,一句话点出妙在哪,展开看对应章节。
-
复盘 fork 复用父进程的 prompt cache。 复盘 fork 默认就用主模型 + 父进程的活运行时,把整段对话重放一遍——因为这段前缀在上游缓存里还是热的,重放几乎只花 cache-read 的钱;只有当用户特意把复盘路由到另一个模型时(缓存键必然 miss)才改成重放一份压缩摘要(
agent/background_review.py:187-197的策略注释、:45_resolve_review_runtime)。连tools[]都要跟父进程逐字节一致,因为它进缓存键(agent/background_review.py:1128-1129)。→ 03 -
记忆快 照在会话内冻结。 系统提示每个会话只拼一次并整段缓存,只有上下文压缩才会重建;记忆块虽然属于"volatile"层,但既然整条提示不重拼,会话中途写的记忆就不会立刻改变提示,prefix cache 因此不被打断(
agent/system_prompt.py:903build_system_prompt的文档串、:496invalidate_system_prompt在压缩后才重载磁盘记忆)。同源的小心思:时间戳只精确到天(另附当天不变的时区/UTC 偏移),让压缩边界、网关新 agent、会话恢复这几条重建路径仍能共享同一段缓存前缀(agent/system_prompt.py:847-863)。→ 02 -
tool_search的渐进式工具披露。 MCP 和非核心插件工具不再全量塞进 tools 数组,而是换成tool_search/tool_describe/tool_call三个桥接工具按需捞取;核心工具永不延迟,且有个阈值闸门——可延迟工具占不到上下文窗口 10%(默认threshold_pct)时整套机制直接不启用(tools/tool_search.py:1-41设计约束、:234should_activate、:529assemble_tool_defs)。→ 04 -
execute_code让中间结果永不进上下文。 模型写一段 Python,脚本在子进程里跑,脚本里调 Hermes 工具是通过 RPC 回打到父进程(本地走 Unix domain socket,远端后端走文件轮询);只有脚本的 stdout 回给模型,十几次工具往返的中间结果一个字都不占上下文(tools/code_execution_tool.py:1-27架构说明、:1076execute_code)。→ 04 -
shadow-git 快照对模型完全不可见。 每轮第一次要改文件前,自动把工作目录快照进
~/.hermes/checkpoints/下的共享 shadow git 仓库,靠GIT_DIR+GIT_WORK_TREE+GIT_INDEX_FILE三件套做到"一点 git 状态都不漏进用户项目";而且它明确不是一个工具——模型从头到尾不知道有这回事(tools/checkpoint_manager.py:1-11与:38-39、:608class CheckpointManager)。共享单仓库还让多个 worktree 的相同 blob 被 git 自动去重。→ 05
5. 顶层代码地图
按"你想改什么"排,给人和 agent 当跳转表。这里只给符号不给行号:符号名比行号抗漂移,定位时优先 grep 符号;要精确到行的引用在各章正文里。
| 主题 | 文件 | 符号 |
|---|---|---|
| 可执行入口注册 | pyproject.toml | [project.scripts](303-306) |
| CLI 子命令分发 | hermes_cli/main.py | main |
| 终端对话主程序 | cli.py | main |
| agent 本体 | run_agent.py | AIAgent、main |
| 一轮对话循环 | agent/conversation_loop.py | run_conversation |
| 系统提示装配 | agent/system_prompt.py | build_system_prompt_parts、build_system_prompt、invalidate_system_prompt |
| 模型方言适配 | agent/transports/ | anthropic.py、chat_completions.py、bedrock.py、codex.py |
| 工具定义与分发 | model_tools.py | get_tool_definitions、handle_function_call |
| 工具执行 | agent/tool_executor.py | execute_tool_calls_concurrent、execute_tool_calls_sequential |
| 渐进式工具披露 | tools/tool_search.py | should_activate、assemble_tool_defs |
| 代码执行沙箱 | tools/code_execution_tool.py | execute_code、generate_hermes_tools_module |
| 执行环境抽象 | tools/environments/base.py | BaseEnvironment |
| 六种后端实现 | tools/environments/ | LocalEnvironment、DockerEnvironment、SSHEnvironment、SingularityEnvironment、ModalEnvironment、DaytonaEnvironment |
| 一轮收尾 | agent/turn_finalizer.py | finalize_turn、_spawn_background_review 调用点 |
| 复盘 fork | agent/background_review.py | spawn_background_review_thread、_run_review_in_thread、_resolve_review_runtime |
| 技能策展 | agent/curator.py | run_curator_review、apply_automatic_transitions |
| 记忆存储 | tools/memory_tool.py | MemoryStore、memory_tool |
| 技能读写 | tools/skills_tool.py、tools/skill_manager_tool.py | skills_list、skill_view、skill_manage |
| 跨会话检索 | tools/session_search_tool.py | session_search |
| 会话数据库 | hermes_state.py | SessionDB |
| 文件快照/回滚 | tools/checkpoint_manager.py | CheckpointManager、prune_checkpoints |
| 消息网关 | gateway/run.py | GatewayRunner、main、_start_cron_ticker |
| 聊天平台适配 | plugins/platforms/、gateway/platforms/ | 每个平台一个目录/模块 |
| 定时任务 | cron/scheduler.py | tick、run_job、_build_job_prompt |
| ACP 适配器 | acp_adapter/entry.py | main |
| MCP 服务端 | mcp_serve.py | create_mcp_server |
引用说明: 本文所有 path:line 相对克隆根 aiRef/repos/hermes-agent/,as-of sourceCommit: 9098f6777b93b7881216a9b7d8fb402899f62400。
从这里开始读: 01 一轮对话是怎么跑完的。