札记

Hermes Agent 架构梳理

研究对象:NousResearch/hermes-agent(Python,~12k LOC 的 run_agent.py,~15k LOC 的 cli.py,82 个内置工具)。

按"自顶向下的机器人架构"组织——从最外层入口,逐层进入核心,最后是底层基础设施。


L0 进程入口与运行形态

一个 Python 进程,四种入口形态:

入口

文件

作用

交互 CLI

cli.py::HermesCLI

Rich + prompt_toolkit 终端

TUI

tui_gateway/server.pyui-tui/(Ink/React)

stdio JSON-RPC 分离前后端

多平台 gateway

gateway/run.py

Telegram/Discord/Slack/微信/邮件… 一个进程多平台

单次调用

run_agent.py::AIAgent.chat()

程序化 / 测试

四个入口共用一个核心: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 个守门:

  1. interrupt — 用户中断

  2. iteration_budget — 软预算,父子 agent 共享池

  3. grace_call — 预算耗尽给最后一次机会

  4. steer drain — 异步引导文本注入到上一条 tool 消息末尾

  5. tool_call sanitizer — 历史里的损坏 arguments 在请求前修复


L2 上下文构造

L2.1 三层系统提示词(agent/system_prompt.py

build_system_prompt_parts() 返回:

内容

何时变

stable

身份、工具用法、环境提示、模型家族操作守则、profile 提示

进程生命周期不变

context

AGENTS.md / .cursorrules / caller system_message

会话生命周期不变

volatile

记忆快照、用户画像、外部 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 枚举:

分类

应对

auth_failed

切下一个 key

rate_limited

等 retry-after,同 key 重试

context_length

触发压缩

model_unavailable

切 fallback model

transient_network

指数退避重试

content_filter

报告用户,不重试

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() 启动时:

  1. AST 扫描所有 tools/*.py

  2. 只导入"顶层有 registry.register(...) 调用"的模块(防止误导入辅助文件)

  3. 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):

  1. 扫 agent-created skills 目录

  2. 自动状态转换:stale → archive(纯函数,无 LLM)

  3. Fork 一个 LLM 子 agent 跑 review prompt:哪些 skill 重复、没人用、应该合并

  4. 子 agent 直接调 skill_manage 改文件

  5. 写报告到 ~/.hermes/curator/reports/

  6. 失败可从 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_childrenmax_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

所有 /commandCOMMAND_REGISTRY 一个列表声明。七种下游全部自动派生

  • CLI dispatch

  • Gateway dispatch + help

  • Telegram BotCommand 菜单

  • Slack /hermes subcommand 路由

  • 自动补全

  • 帮助分组

加一个新 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 重度用户必需

★★

/steer 软中断

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

沙箱成熟后再说