Hermes Agent 架构梳理
研究对象:NousResearch/hermes-agent(Python,~12k LOC 的 run_agent.py,~15k LOC 的 cli.py,82 个内置工具)。
按"自顶向下的机器人架构"组织——从最外层入口,逐层进入核心,最后是底层基础设施。
L0 进程入口与运行形态
一个 Python 进程,四种入口形态:
入口 | 文件 | 作用 |
|---|---|---|
交互 CLI |
| Rich + prompt_toolkit 终端 |
TUI |
| stdio JSON-RPC 分离前后端 |
多平台 gateway |
| Telegram/Discord/Slack/微信/邮件… 一个进程多平台 |
单次调用 |
| 程序化 / 测试 |
四个入口共用一个核心:AIAgent。Gateway 模式下每条消息 new 一个 AIAgent,靠 session_id 从 SQLite 反推历史。
进程模型亮点:dashboard 里的"web 聊天"不是 React 重写,而是把 hermes --tui 接进 PTY,xterm.js 渲染 PTY 输出。一份 TUI 实现,CLI/TUI/web 三处复用。
L1 单回合主循环(agent/conversation_loop.py::run_conversation)
整个 agent 的心脏。同步 while 循环:
while api_call_count < max_iterations and budget.remaining > 0 or grace_call:
if interrupt_requested: break
drain_pending_steer() # 中途引导注入
messages = sanitize(messages) # 修复 tool_call 错位/损坏
response = client.chat.completions.create(messages, tools=tool_schemas)
if response.tool_calls:
execute_tool_calls_concurrent(...) # ThreadPool 并发
api_call_count += 1
else:
return response.content外圈 5 个守门:
interrupt — 用户中断
iteration_budget — 软预算,父子 agent 共享池
grace_call — 预算耗尽给最后一次机会
steer drain — 异步引导文本注入到上一条 tool 消息末尾
tool_call sanitizer — 历史里的损坏 arguments 在请求前修复
L2 上下文构造
L2.1 三层系统提示词(agent/system_prompt.py)
build_system_prompt_parts() 返回:
层 | 内容 | 何时变 |
|---|---|---|
| 身份、工具用法、环境提示、模型家族操作守则、profile 提示 | 进程生命周期不变 |
| AGENTS.md / .cursorrules / caller system_message | 会话生命周期不变 |
| 记忆快照、用户画像、外部 memory provider、时间戳 | 会话生命周期不变 |
关键约束:agent._cached_system_prompt 一旦构建,整个 session 不再重建。中途记忆写盘也不动它——下一次 session 启动时刷新。这是为了 Anthropic prefix cache 命中。
Continuing session 里直接从 SQLite 读回旧的 system prompt,避免"模型自己写的记忆被重建到 prompt 里"产生 prefix mismatch。
L2.2 prompt cache 控制(agent/prompt_caching.py)
在哪些消息打 cache_control: { type: 'ephemeral' }:
系统提示词末尾 → 系统部分缓存
倒数第二个 user message 末尾 → 历史缓存
最新 user message 不打 → 当前输入一定变,不浪费
L2.3 上下文压缩(agent/context_compressor.py)
主循环前 preflight:
preflight_tokens = estimate_request_tokens_rough(
messages, system_prompt, tools # 工具 schema 也计入
)
if compressor.should_compress(preflight_tokens):
# 用 auxiliary LLM 摘要中间 N 轮,最多 3 次循环
messages = compress_middle_turns(messages)工具 schema token 也算——多工具能多吃 20-30k token
辅助 LLM 做摘要——主模型不浪费
首 N 条 + 尾 N 条保留,压中间
触发压缩后清掉历史 retry counter,给模型新预算
L2.4 长内容外置(agent/context_references.py)
50KB 文件不直接进 message —— 存 disk,message 里只放 <file ref preview />。模型要全文时调工具 fetch。
L2.5 子目录提示(agent/subdirectory_hints.py)
cwd 是某子目录时,该目录的 README/AGENTS.md/.cursorrules 追加进 system prompt。cd 出去自动撤掉。
L3 模型调用与错误处理
L3.1 凭证池(agent/credential_pool.py)
同 provider 多 key,按 last_failed_at 排序,失败自动切。状态 persist 到 disk —— 重启不会全员命中"刚被限流的那个"。
L3.2 错误分类(agent/error_classifier.py,1000+ 行)
API 错误不只是 status code,还要看:
vendor 特定 error code
error body JSON
error message regex
输出 FailoverReason 枚举:
分类 | 应对 |
|---|---|
| 切下一个 key |
| 等 retry-after,同 key 重试 |
| 触发压缩 |
| 切 fallback model |
| 指数退避重试 |
| 报告用户,不重试 |
L3.3 模型 fallback
fallback_model 参数:主模型连续失败 N 次切到备用。Codex/Anthropic/OpenAI/local 都可以做 fallback 目标。
L3.4 速率守护(agent/nous_rate_guard.py)
Nous Portal 订阅有自己的速率限制,本地缓存"距离下次允许请求还有多久",避免硬撞 429。
L4 工具系统
L4.1 注册中心(tools/registry.py)
每个 tools/*.py 模块顶层调 registry.register(name, toolset, schema, handler, check_fn, requires_env, ...)。
discover_builtin_tools() 启动时:
AST 扫描所有
tools/*.py只导入"顶层有
registry.register(...)调用"的模块(防止误导入辅助文件)import 副作用注册进 registry 单例
check_fn 用 30 秒 TTL 缓存——避免每轮都去探测 Docker/Modal/Playwright 是否可用。
L4.2 工具集(toolsets.py)
_HERMES_CORE_TOOLS 是默认集合,每个平台/模式可声明自己的子集或叠加。"注册"和"暴露给模型"是两步:自动发现负责前者,toolset 声明负责后者。
L4.3 调度链(model_tools.py::handle_function_call)
pre_tool_call hook (可 block)
→ ACP edit approval(IDE 集成时的人工授权)
→ registry.dispatch
→ post_tool_call hook(observational)
→ transform_tool_result hook(可改写结果)每个 hook 失败 fail-open,不挂主流程。duration_ms 一路传下去,方便插件做延迟监控。
L4.4 并发执行(agent/tool_executor.py)
execute_tool_calls_concurrent:一个 assistant message 内的多个 tool_call 走 ThreadPoolExecutor,结果按原顺序拼回 messages。
每个 worker 线程:
注册到
_tool_worker_threads用于中断 fan-out继承父线程的 approval/sudo callback(防止 prompt_toolkit 死锁)
在 destructive 操作前自动 checkpoint
L4.5 危险操作 checkpoint(tools/checkpoint_manager.py)
write_file / patch / 危险 terminal 命令前自动起影子 git 仓库 commit。失败可回滚。影子仓库不动用户的 .git。
L4.6 工具结果限长(tools/tool_output_limits.py)
每个工具有 max_result_size_chars。超限的结果存盘换成 ref。模型按需 fetch。
L5 记忆系统
L5.1 本地文件记忆(tools/memory_tool.py)
~/.hermes/memories/MEMORY.md— agent 自己的笔记~/.hermes/memories/USER.md— 关于用户的画像纯文本,
§分隔条目,字符数限制(不是 token,模型无关)
冻结快照模式:启动时整个文件作为快照塞 system prompt。中途 memory 工具调 add/replace/remove/read → 写盘但不改 system prompt。下次启动看到新版。
L5.2 跨会话搜索(tools/session_search_tool.py + hermes_state.py)
SQLite + FTS5,每条消息都进索引。session_search 工具暴露给模型,让它"想起以前在哪聊过这个"。
两个工具分工:
memory→ 读结论快照session_search→ 搜对话原文
模型语义清晰:要"我记下的结论"用前者,要"那次原话"用后者。
L5.3 Curator 学习闭环(agent/curator.py)
后台定时任务(默认 24h):
扫 agent-created skills 目录
自动状态转换:stale → archive(纯函数,无 LLM)
Fork 一个 LLM 子 agent 跑 review prompt:哪些 skill 重复、没人用、应该合并
子 agent 直接调
skill_manage改文件写报告到
~/.hermes/curator/reports/失败可从 pre-run snapshot 回滚
L5.4 Nudge 计数器
agent._turns_since_memory += 1
if agent._turns_since_memory >= memory_nudge_interval:
# 本轮 system prompt 末尾追加一条"考虑更新一下记忆"_iters_since_skill 同理——工具迭代超过阈值时 nudge "考虑沉淀成 skill"。
Gateway 反推:每条消息 new AIAgent 计数器丢失,启动时数 conversation_history 里 user 消息条数 mod N,恢复有效计数。
L5.5 Memory provider 插件化(agent/memory_manager.py)
MemoryManager 统一接口(prefetch_all / sync_all / on_turn_start / on_session_end)。可挂:
本地文件(默认)
Honcho(dialectic user modeling)
mem0
supermemory
任何第三方实现
L6 Skill 系统
L6.1 Skill 存储
~/.hermes/skills/<category>/<name>/,目录里有:
SKILL.md— 描述 + 使用条件可选脚本/配套文件
类别:apple、autonomous-ai-agents、creative、data-science、devops、diagramming、email、gaming、media、productivity、red-teaming、research、smart-home、social-media、software-development …
L6.2 注入方式
Skill 内容不进 system prompt——作为 user message 注入。
原因:保 prefix cache。system prompt 一旦因为"这一轮可能用到 skill A"被修改,整段缓存失效。注入为 user message 只影响当前回合。
L6.3 自主创建(tools/skill_manager_tool.py)
skill_manage 工具让模型自己 add/edit/archive skill。复杂任务完成后,agent 自我审视:"这套流程值得沉淀成 skill 吗?"——配合 _iters_since_skill nudge 触发。
L6.4 兼容标准
兼容 agentskills.io 开放格式,可以从外部目录导入。
L7 自主行为
L7.1 Subagent 委托(tools/delegate_tool.py)
父 agent 调 delegate_task spawn 隔离 AIAgent:
自己的 messages 列表,不污染父
共享父的 iteration_budget(防指数膨胀)
结果以字符串回父
约束:
max_concurrent_children、max_spawn_depth
L7.2 Mini-SWE-Runner(mini_swe_runner.py)
不是工具循环,而是模型写一段 Python 脚本,脚本里通过 RPC 调工具。
效果:原本要 10 轮的流水线变成 1 轮——模型只看到 1 个 tool_call + 1 个 tool_result,省 90% context。
代价:模型要会写代码 + 需要受控沙箱(local / docker / modal / daytona)。
L7.3 Kanban 多 Agent(plugins/kanban/)
Dispatcher 进程 + N 个 worker。任务卡分发给 worker,每个 worker 独立跑 AIAgent。worker 启动时会收到 kanban 专属的 system prompt 段(_kanban_worker_guidance)说明自己的角色。
L7.4 Cron 定时(cron/)
模型可以创建定时任务:"每天 9 点给我做日报"。Cron scheduler 到点 spawn AIAgent 跑一遍 prompt,结果通过 gateway 推到任意平台(Telegram/Slack/邮件)。
L7.5 Todo 工具(tools/todo_tool.py)
模型自维护 todo list。每次工具响应里附带当前 todos。Gateway 重启后从 conversation_history 反推 todo state——AIAgent 重建时 _hydrate_todo_store(history)。
L8 中途交互
L8.1 /steer 软中断
用户在模型流式输出 / 工具执行中输入"换个方向":
agent._pending_steer = "改用 X 方法"下一轮 pre-API-call 时 drain_pending_steer() 把文本追加到最后一条 tool 消息末尾("User guidance: ...")。模型自然在下一轮看到。
不打断当前回合 → 不丢已完成的工作。
L8.2 危险命令审批
terminal 工具执行 destructive 命令前触发 approval_callback:CLI 弹 prompt_toolkit 对话框,gateway 走平台对应的 reply markup(Telegram inline button、Slack interactive button),Ink 走 prompts.tsx。
callback 必须跨线程传播——worker 线程没继承会 fallback 到 input() 死锁 prompt_toolkit(GHSA 安全公告里就有过)。
L8.3 Clarify / Sudo / Secret prompt
类似 approval,但用于"模型反问用户"、"sudo 密码"、"secret 输入"。统一走 TUI/gateway 的 prompt 协议。
L9 多平台 Gateway
L9.1 平台适配(gateway/platforms/)
每个平台一个 adapter:telegram、discord、slack、whatsapp、homeassistant、signal、matrix、mattermost、email、sms、dingtalk、wecom、weixin、feishu、qqbot、bluebubbles、yuanbao、webhook、api_server…
公共接口:receive_message / send_message / request_approval / send_typing。
L9.2 Session 管理(hermes_state.py::SessionDB)
SQLite 存所有平台 session:
messages表(FTS5 索引)sessions表(session_id ↔ platform ↔ chat_id ↔ user_id)tool_calls表
Gateway 每条消息查 session_id → 读历史 → new AIAgent → 跑一回合 → 写回。
L9.3 跨平台连续性
用户从 Telegram 切到 Slack,只要 user_id 映射对得上,对话历史能继续。
L9.4 命令注册中心(hermes_cli/commands.py)
所有 /command 在 COMMAND_REGISTRY 一个列表声明。七种下游全部自动派生:
CLI dispatch
Gateway dispatch + help
Telegram BotCommand 菜单
Slack
/hermessubcommand 路由自动补全
帮助分组
加一个新 alias 只改一行(tuple 里追加)。
L10 流式输出处理
L10.1 Think Scrubber(agent/think_scrubber.py)
模型有 <think>...</think> 块时,流式 chunk 可能切在 tag 中间(收到 <thi 就要判断"是否还在 think 块内")。状态机维护 pending buffer,确保用户看不到半截 tag。
L10.2 Streaming Context Scrubber(agent/context_compressor.py)
类似处理但针对工具输出/上下文标记的流式过滤。每轮开始 reset 防止前一轮未闭合的状态泄漏到本轮。
L10.3 Trajectory 录制
save_trajectories=True → 完整对话 + tool_call + 中间状态存 JSONL。配合:
batch_runner.py批量回放trajectory_compressor.py压成训练样本
闭环:跑出来的真实交互反哺下一代模型训练。
L11 安全与策略
L11.1 Redact(agent/redact.py)
正则替换:API key / token / password / JWT / OAuth code。
短 token(< 18 字符)全遮
长 token 留前 6 后 4 方便调试
URL query string 里的
access_token=/api_key=也覆盖
应用点:日志、verbose 输出、gateway log、工具结果。
L11.2 路径安全(tools/path_security.py)
file_safety 约束:禁止跨 profile 写(
~/.hermes/profiles/<other>/)symlink 解析后检查
用户主目录外的写入需要显式允许
L11.3 URL 安全(tools/url_safety.py)
web_fetch / browser_navigate 前查 URL:
DNS 解析后检查不是内网/loopback
黑名单检查(防 SSRF)
L11.4 OSV 检查(tools/osv_check.py)
terminal 工具看到 pip install xxx / npm install xxx 时,OSV 漏洞数据库查包名——已知恶意/有 CVE 的包先警告。
L11.5 威胁模式(tools/threat_patterns.py)
记忆条目、上下文文件、工具结果三处共享同一套"提示注入/外泄"正则集。memory 用"strict"作用域(因为冻结快照影响整个 session)。
L11.6 Tirith Security(tools/tirith_security.py)
更深一层的策略引擎。
L12 辅助 LLM 池(agent/auxiliary_client.py)
每个"副任务"独立配置 provider/model:
auxiliary:
curator: # skill review
provider: openrouter
model: claude-3-5-haiku
vision: # vision_analyze 工具
provider: openai
model: gpt-4o-mini
embedding: # session_search 嵌入
provider: ...
title_generation: # 自动命名 session
provider: ...
session_search: # 搜索结果摘要
provider: ...
compression: # 中间轮摘要
provider: ...主模型用贵的,副任务用便宜的。_resolve_auto 决定 fallback 顺序。
L13 调用链总览
一个真实的 user message → response 完整路径:
[gateway/cli/tui] receive_message
→ SessionDB hydrate history
→ AIAgent.__init__(从 cache 或 fresh build)
→ AIAgent.run_conversation
├─ install_safe_stdio(防 broken pipe)
├─ sanitize_surrogates(防剪贴板 invalid UTF-8)
├─ hydrate todo / nudge counters from history
├─ build_system_prompt_parts → cache 一次
├─ preflight compression check
├─ pre_llm_call plugin hook → 注入 user context
└─ while loop:
├─ interrupt / budget / grace 检查
├─ drain_pending_steer
├─ sanitize_tool_call_arguments
├─ cache_control 注入
├─ client.chat.completions.create (with retry/fallback)
│ └─ error_classifier → key 池切换 / 压缩 / 重试
├─ stream → think_scrubber → 用户
├─ tool_calls?
│ yes → execute_tool_calls_concurrent
│ ├─ pre_tool_call hook (可 block)
│ ├─ checkpoint snapshot (file/destructive)
│ ├─ ThreadPool dispatch
│ ├─ registry.dispatch
│ │ ├─ redact result
│ │ └─ tool_output_limits 截断/转 ref
│ ├─ post_tool_call hook
│ └─ transform_tool_result hook
│ no → break,final response
└─ persist messages → SessionDB(增量 FTS5 索引)
→ check skill nudge / memory nudge 条件
→ curator scheduler 决定是否触发后台 review
→ send_message back to platform借鉴清单(按对 Chat Poi 项目的直接落地价值排序)
优先级 | 项目 | 文件 | 收益 |
|---|---|---|---|
★★★ | prompt cache 三层冻结 | L2.1 + L2.2 | 直接省钱,Anthropic prefix cache 命中 |
★★★ | Redact 敏感信息 | L11.1 | 30 行代码,安全收益巨大 |
★★★ | error_classifier 触发压缩 | L3.2 + L2.3 | 解决 context length 报错 |
★★ | 多 key 凭证池 | L3.1 | OpenRouter 重度用户必需 |
★★ |
| L8.1 | 长任务体验提升 |
★★ | 辅助 LLM 池 | L12 | title/search/memory 分流便宜模型 |
★★ | memory + session_search 两工具分工 | L5.1 + L5.2 | ✅ 已实施 |
★ | checkpoint snapshot | L4.5 | 删除/编辑可回滚 |
★ | transform_tool_result hook | L4.3 | 工具结果统一加工口 |
★ | Nudge 计数器 + history 反推 | L5.4 | 让模型自觉更新记忆 |
★ | Todo 工具 | L7.5 | 长任务自我跟踪 |
◯ | Subagent / delegate | L7.1 | 复杂分支任务 |
◯ | Curator 后台 LLM 复审 | L5.3 | 长期记忆质量维护 |
◯ | Mini-SWE-Runner 脚本化 | L7.2 | 沙箱成熟后再说 |