Hermes Agent 是 NousResearch 开源的个人 Agent 运行时(MIT,提交 8f5693a)。如果只把它看成”提示词套壳”会看错重点:它更像一台回合制状态机——一次用户输入就是一个 turn,turn 内部是”模型调用 → 工具执行 → 再调用”的内循环,外层受预算、闸门与压缩约束。整个仓库约两万文件,其中 agent/ 目录 319 个 Python 文件、约 6.5 MB,这些复杂度几乎都在回答同一组问题:这次请求该发什么、发出去之后坏了怎么办、历史该怎么变短。

三条主线贯穿全部代码。第一,回合是唯一执行单元,任何层都不允许越权改写历史;第二,上下文是最稀缺的资源,系统提示词在会话期间按字节冻结,压缩是唯一被允许的改写,token 记账优先信 provider 返回的真实用量;第三,能力长在边缘、核心保持收窄,新能力默认走 skill、plugin、MCP,而不是往核心工具里加,学习靠”回合后 fork 一份自己复盘”。

所以它的架构定位可以概括成一句话:Prompt-cache-first 的同步 ReAct 内核,外面套着可插拔的能力与记忆外设。这个定位不是口号,而是代码评审的硬依据——没有真实消费者的 hook、为普通配置新增环境变量、terminal 能干的事却新增核心工具,都属于会被驳回的提交。

一、七层窄腰:分层与回合引擎

Hermes 的分层是自下而上七层,每层只依赖下一层的抽象:

层 内容 关键源码
L6 宿主面 CLI、TUI(Ink)、Desktop(Electron)、Gateway(20+ 消息平台)、ACP、Cron —
L5 会话与状态 state.db(SQLite + FTS5)、单一写者、profile 隔离、lineage hermes_state*.py
L4 回合引擎 preflight → prepare → assemble → call → intake → tool round → finalize conversation_loop.py、turn_*.py
L3 上下文工程 三层冻结、双阈压缩、四阶段算法、缓存断点 context_compressor.py、prompt_builder.py
L2 能力层 70+ 工具 / 28 toolset、Skills、Plugins、MCP、Subagents tools/registry.py、toolsets.py
L1 模型接入 3 种 API mode、30+ 失败分类、fallback 链、auxiliary 旁路 error_classifier.py、provider_registry.py
L0 运行时 deadline、中断控制、7 种终端后端、secret scope、审批 deadline.py、interrupt_control.py

主循环本身很朴素,值得看的是三个非教科书细节:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# agent/conversation_loop.py::_run_conversation_turn(简化,保留控制流)
while ((s.api_call_count < agent.max_iterations and agent.iteration_budget.remaining > 0)
or agent._budget_grace_call):
begin_iteration() # 预算 / 重试状态初始化
prepare_iteration() # 预算警告,/steer 插在最新 tool 结果之后
assemble_api_request() # messages + 工具 schema → 请求体
if run_preflight_gate(): # 调用前压缩闸门,也可能直接结束 turn
continue
try:
perform_api_call() # 可中断的真实 HTTP
check_api_response() # 空响应、截断、拒答
except Exception:
handle_api_error() # 分类 → 重试 / 换 key / 换模型 / 压缩
if response.tool_calls:
run_tool_round() # 执行 → 追加 tool 消息 → 回到循环头
else:
finish_text_response() # 过 stop gates,可能被拒绝收尾
finalize_turn()
  • _budget_grace_call:预算耗尽后仍给一次”恩典调用”,让模型把话说完,而不是被硬切断在半句话上。
  • run_tool_round 不是”执行完就继续”:内部做参数校验、去重、截断、并发分段、结果分类,失败走 guardrail。
  • finish_text_response 不保证收尾:LSP 延迟诊断、verify-on-stop、pre_verify hook、看板终态四道闸门可以把”看起来完成”的答案降级为 interim,追加一条 user nudge 后 continue。被拦下的回答不会丢,存进 pending_verification_response,预算耗尽时兜底返回。

这 13 个阶段基本一阶段一文件、平均约 600 行:改溢出不会碰到压缩,改压缩不会碰到工具调度。这是几十万行级 Agent 项目还能维护的关键组织手法。

二、上下文工程:把 prompt 缓存当成不变式

三层冻结

系统提示词按 stable → context → volatile 拼接,并在整个会话期间字节不变:

1
2
3
4
5
stable   —— 身份(SOUL.md) → 工具使用准则 → 编码守则 → 平台提示
context —— 调用方 system_message → 项目上下文(.hermes.md / AGENTS.md /
CLAUDE.md / .cursorrules) → git worktree 快照 → 运维指令
volatile —— 技能索引 → MEMORY.md 快照 → USER.md 快照 → 外部记忆块
→ 时间/会话/模型/供应商 → 运行时环境(host / home / cwd)

顺序有讲究:项目共享上下文排在依赖 worktree 的内容之前,同项目不同 worktree 的会话就能共享同一段 prompt 前缀,正好是最长前缀缓存能复用的部分;运行时环境块永远压最后,避免项目文件里的路径样例被误认为”当前目录”。平台提示(CLI 别说 Markdown、Telegram 别发长消息)不算身份字段,桌面切 TUI 时系统提示不动,改用一次性 user note 传达。

不变式很硬:系统提示词在会话生命周期内字节稳定,唯一的上下文变更是压缩;任何必须中途注入的内容,走 user 消息或 tool 结果,绝不进 system prompt。后果很具体——/skills install 默认下个会话才生效(要立刻生效得加 --now);子目录 AGENTS.md 的提示不插进 prompt,而是追加到 tool 结果尾部(超 32,000 字符头尾截断);插件 pre_llm_call 返回的上下文拼进本回合 user 消息;会话中写记忆只落盘,不改已构建好的 prompt。代价是产品上必然出现”我改了设置怎么没反应”,收益是长会话成本线性而非平方级。

双层压缩与四阶段算法

网关层在 85% 上下文时做粗估兜底(防炸,处理 Telegram 里过夜堆积的会话),Agent 的 ContextCompressor 在 50% 时接管日常管理,此时能拿到 provider 返回的准确 usage。把网关也设成 50% 会让每个长会话一开始就被压缩,源码注释明确写了这是踩过的坑。

压缩分四阶段,全程不切断工具配对:

  1. 剪枝(不调 LLM):保护尾之外的旧 tool 结果超过 200 字符就替换成 [Old tool output cleared to save context space]。文件内容、终端输出、搜索结果通常占大头,这一步能省掉近一半。模型”还没回应的那一轮工具”结果必须原样保留。
  2. 定界:尾部保护按 token 预算走而不是按条数,protect_last_n(默认 20)只是下限;边界要对齐到 assistant(tool_calls) → tool... 的完整组。
  3. 结构化摘要:用 auxiliary 小模型生成固定的分节摘要(Goal / Constraints & Preferences / Progress(Done|In Progress|Blocked) / Key Decisions / Relevant Files / Next Steps / Critical Context),预算 = 被压缩内容 × 0.20,下限 2,000 token,上限 min(上下文 × 5%, 12,000)。
  4. 拼装:头 + 摘要 + 尾,再跑 _sanitize_tool_pairs()——孤立 tool_result 删掉,丢了结果的 tool_call 补 stub。

第二次压缩不是重来,而是把上次摘要交给模型”更新”:条目从 In Progress 挪到 Done,新进展追加,过期信息删除。这是长会话不丢线索的关键。

记账策略是先信 provider、再信估算:每个压缩闸门先问 usage anchor(provider 上次返回的 prompt/completion tokens,加”仅对新增消息”的粗估),这样网关每回合从 DB 重读历史、--resume 换进程启动都对得上;没有 anchor 时先等一次请求,让 provider 自己裁决,而不是凭估算就压缩。摘要调用失败有分级冷却:超时与流停滞 60s → 300s → 900s,JSON 解析失败固定 30s;连续两次同因停滞就跳过 LLM,直接提交确定性兜底摘要,宁可丢中段,也不让会话涨到被网关整段重置。Anthropic 那边用 system_and_3(system 块 + 最近 3 个 user 轮打 cache_control),长会话命中率才立得住。

三、工具、委托与模型接入

工具层

注册与暴露是两件事。tools/*.py 在模块级调过 registry.register() 就被自动发现,没有手工 import 清单;但只有被某个 toolset 命名的工具才会送给模型,默认包是 _HERMES_CORE_TOOLS,一共 28 个 toolset。check_fn 只回答”这个 profile 能不能用”,绝不按会话面区分,GUI 专属工具要走独立 toolset。

一次工具调用拆成 8 步:解析参数(容错 JSON 修复)→ 查 INLINE 表(todo、memory、session_search、delegate_task 由 agent 直接处理)→ registry 找 handler → pre_tool_call hook → 危险命令审批 → 执行(带 deadline、可取消)→ post_tool_call(一个 tool_call_id 只发一次)→ 结果按原文顺序追加进历史。所有 handler 返回 JSON 字符串,需要回图时返回 {"_multimodal": True, "content": [...], "text_summary": ...} 信封。

并发不是”有多个就并发”:_plan_tool_batch_segments 先把整批切成有序段。遇到 _NEVER_PARALLEL_TOOLS(含交互型 clarify)、参数解析失败或非并发安全的 MCP 工具就下屏障;带路径的工具做读写冲突检测,读读可并发,任一方是写且路径重叠就把该调用另起一段排到后面;一段不足 2 个调用降级为顺序执行,相邻顺序段合并。结论是结果顺序与副作用边界等价于全顺序执行,但读多写少的典型场景仍能提速。护栏按指标分级:完全相同的失败 2 次警告 / 5 次硬停,同工具失败 3/8,无进展轮次 2/5;交互平台默认只警告,gateway 与 cron 这类无人值守平台默认开硬停。

新增能力要按 Footprint Ladder 从最省的一级解决:扩展现有代码 → CLI 命令 + Skill → 服务门控工具 → 插件 → 目录里的 MCP server → 新核心工具。最后一级门槛极高,理由是物理的:每个工具 schema 在每次 API 调用里都要发一遍,工具越多固定成本越高、注意力越散。

委托层与模型接入

Subagent 的出口是 delegate_task(tasks=[{goal, context, output_schema}, ...]),单任务或并行批。子代理上下文全新、只从 goal + context 构建,task_id 独立(终端会话、文件缓存隔离),工具是父的 toolset 减去禁止子用的,max_iterations 默认 50(父默认 500)且独立记账,中间工具调用与推理过程不上浮。对外暴露的是不可变契约而不是 AIAgent 对象:SubagentLaunchRequest / SubagentHandle / SubagentResult / SubagentStatus,插件只能通过这些值对象操作。并发上限与 max_spawn_depth 会动态重写进工具 schema 的 description,让模型看到的限制就是真实限制。

模型接入支持三种 API mode:chat_completions(OpenAI 兼容端点)、codex_responses(Codex / Responses API)、anthropic_messages(原生 SDK + 适配器)。解析优先级是显式参数 > provider 判定 > base_url 启发式 > 默认,三者进出都收敛到同一套内部 message 形状,回合引擎不必知道底下是哪个协议。

这套系统里最”脏”也最值钱的是 FailoverReason:30+ 种失败分类,而不是一个 try/except。auth 401/403 刷凭据后重试;rate_limit、overloaded、server_error 指数退避;model_not_found、model_entitlement 走 fallback 链换模型;context_overflow、413 该压缩而不是切模型;image_too_large 缩图、image_corrupt 剥图、role_alternation 合并同角色,本地修复后重试;content_policy_blocked 直接失败并给指引。说到底,”重试”这个动作至少有 8 种不同语义,混成一个 except: retry 就是线上事故来源。

压缩、视觉、标题、session_search、curator、embedding 都走 auxiliary_client 旁路,每个任务能独立钉 provider / model / base_url / reasoning_effort。两条纪律:辅助调用的 pre/post_api_request 事件不得触发,否则监控指标被污染;压缩用的摘要模型上下文必须不小于主模型,否则中段一次性塞不进去,压缩会静默丢内容。

四、能力演化、状态与通用模型

记忆、技能与复盘

记忆与技能有一条清晰的分工线:Memory 回答”用户是谁、当前处境”,落在 MEMORY.md 与 USER.md,会话开始时冻结快照进 volatile 层;Skill 回答”这类任务怎么做”,落在 skills/<name>/SKILL.md 加 references/ templates/ scripts/,索引常驻、正文按需 skill_view。源码原话是:”Memory is the narrow exception for facts that apply to EVERY session regardless of task.”

SKILL.md 的硬标准由测试强制:description 不超过 60 字符、一句话、句号结尾、不用营销词;正文只能写原生工具名(read_file 而不是 cat,patch 而不是 sed);platforms: 要和脚本实际用的 POSIX 原语对得上;章节顺序固定为 When to Use → Prerequisites → How to Run → Quick Reference → Procedure → Pitfalls → Verification;技能加载工具不允许分页,因为模型读了第一页就会跳过剩下的。

技能生命周期由 Curator 管:active → stale → archived,可 restore,pinned 豁免一切自动流转。触发方式是闲置触发而非 cron 守护进程——agent 空闲且距上次运行超过 interval_hours 才跑;只动 created_by: "agent" 的技能,内置与 hub 安装的免谈;永不删除只归档到 ~/.hermes/skills/.archive/;每次运行前打 tar.gz 快照支持 rollback;用量记在 .usage.json。

学习闭环是每回合结束后起一个 daemon 线程,fork 一个 AIAgent 重放本回合快照,只问一个问题:”Review the conversation above and update the skill library. Be ACTIVE — most sessions produce at least one skill update, even if small.”。它继承父的 provider、模型、凭据与缓存提示词,命中同一段前缀缓存,所以复盘几乎不额外花钱;它只能写 memory / skill 存储,绝不碰主会话与主 prompt 缓存;派发侧有工具白名单。更新优先级是改已加载技能 → 改已有伞形技能 → 加支撑文件 → 才新建,防止技能库退化成一堆一次性条目;patch 前必须本轮 skill_view 一次(read-before-write),”Nothing to save.” 是合法答案但不该是默认。用户不满是最高优先级信号,”stop doing X”、”this is too verbose” 这类话必须落到技能正文,而不只是记忆里。

状态层与可靠性

状态目录 ~/.hermes/ 里,config.yaml 放行为、.env 只放密钥、state.db 是可检索的会话库、SOUL.md / MEMORY.md / USER.md 是身份与记忆快照、profiles/<name>/ 是完全隔离的上述一切。三条纪律:单一写者,一个 profile 的 gateway 是唯一规范写者,CLI / TUI / Desktop / ACP / cron 都通过 /api/ws 提交输入,进程外 worker 走 RuntimeSessionStore RPC,不自己开库写 transcript;历史 append-only,序列坏了用 repair_message_sequence 修而不是重写;压缩用世代标记,旧行 active=0、新行 active=1,同一条消息在两代里同时出现是正常现象。session_search 直接在 FTS5 上做四种查询形状(discovery / scroll / read / browse),全程不调 LLM。

可靠性层最关键的不是”重试”,而是中断语义:API 调用在后台线程跑,主线程等”响应 / 中断 / 超时”三者之一;被放弃的响应直接丢弃,绝不把半截响应塞进历史,否则下次请求会带着一个”幽灵助手轮次”污染角色交替。其余机制包括统一的 deadline 原语(run_bounded_sync/async、clamp_timeout、kill_process_tree)、线程安全的 iteration budget(父子各自记账,父的额度不被子吃光)、repetition_guard 打断复读,以及错误串回写 DB 前强制脱敏。

抽象成通用 Agent Model

剥掉实现细节,这套东西可以提炼成 9 个对象和 12 条不变式:Agent(无状态执行器 + 配置)、Turn、Message、Tool、ContextEngine、MemoryProvider、Skill、Route、Delegation。其中几条最值得抄:系统提示词字节稳定、中途注入只走 user / tool 通道、角色严格交替(只有 tool 可连续)、历史 append-only、压缩不切断 tool_call 与 tool_result 配对、工具结果必须结构化、token 记账先信 provider、记忆快照会话开始冻结、子代理上下文隔离且父只见摘要、能力默认放边缘、永不删除只归档、密钥读取失败关闭。

复刻路线可以按 M0–M5 逐级走:M0 最小内核是 messages[] 加一个 while 循环加读 / 写 / 跑三个工具;M1 可用是 system prompt 组装、工具 registry、先做 5 类错误重试;M2 长会话是估算 + 剪枝 + 摘要压缩;M3 稳定是预算、中断、deadline、落盘;M4 会成长是记忆文件 + 技能库 + 回合后复盘;M5 规模化才是多 provider 降级、子代理与缓存断点。建议的砍法是 M0–M2 之间不要碰多 provider 和插件系统,先把”一轮对话稳定跑到 50 次工具调用还不炸”做出来。代价也要说清楚:agent/ 319 个文件带来的复杂度、缓存不变式反噬产品体验、同步循环 + 线程池不是高并发设计、压缩有损只能缓解不能根治、30+ 失败分类的 provider 税、以及”Be ACTIVE”式复盘长期可能让技能库膨胀。它适合单用户 / 小团队、长会话、要长期记忆、要操作真实机器的个人 Agent,不适合高并发多租户 API 服务或毫秒级延迟场景。

源码索引可以直接按图索骥:主循环看 agent/conversation_loop.py::_run_conversation_turn,回合各阶段看 agent/turn_*.py(13 个文件按阶段名找),系统提示词看 system_prompt.py 与 prompt_builder.py,压缩算法看 context_compressor.py(330 KB,compress() 与 4 个 Phase mixin),并发调度看 tool_dispatch_helpers.py::_plan_tool_batch_segments,失败分类看 error_classifier.py::FailoverReason,复盘 fork 看 background_review.py,技能生命周期看 curator.py 与 skill_usage.py,子代理看 delegate_tool.py 与 subagent_lifecycle.py。

三种读法

架构详解

完整的源码拆解另有长文:Hermes Agent 源码深度解析,逐章覆盖分层、13 个阶段、压缩算法公式与复刻路线,适合当参考手册。

动画:原理模型

七幕动画把上面这些机制压成了可步进的模型:七层窄腰、一次 turn 的 7 个可观察步骤、三层 prompt 冻结、分段并发、上下文压缩、学习闭环 fork、失败分类与降级路径。按空格播放,或用左右方向键逐帧看。

在新窗口打开完整页面 →

一条消息的 12 站旅程

如果更想看”一条消息从进入到交付”的完整轨迹,这个页面沿回合引擎的轨道排了 12 站:入口与会话、回合准备、请求装配、调用模型、响应摄入、分支判断、工具轮、预算回边、收尾闸门、回合落盘、后台复盘、交付与 resume。每一站右侧会显示这一刻的状态、护栏与持久化动作,第 8 站的回边能直观看到工具调用如何回到请求装配而不是重开一个 turn。

在新窗口打开完整页面 →

参考与延伸