1. Hermes 到底是什么?

Hermes 是一个与模型无关、能够自我进化的对话型智能体,它可以作为 CLI/TUI 在本地运行,也可以作为消息网关(Telegram/Discord/Slack/WhatsApp/Signal)跑在服务器上,或者作为定时 cron worker 运行。它最大的差异化在于闭环学习:在用工具解决问题的同时,它会写下可复用的 “skill” 文档,并维护一个持久化的记忆文件——这意味着这个 agent 真的会越用越聪明。从模型、工具、技能、记忆后端、执行环境到 UI,所有部分都是可插拔的。

(1)三个范式的演进

要看懂 Hermes 这种东西为什么存在,得先看 AI 工程范式过去三年走过的路。

AI 工程三范式演进

范式 核心问题 优化对象 交互模式
提示词工程 怎么把话说清楚 Prompt 的措辞、格式、示例 一问一答
上下文工程 怎么给 AI 喂信息 文档、代码片段、历史对话 信息注入 → 生成
驾驭工程 怎么让 Agent 可靠工作 约束、反馈回路、控制系统 人类掌舵,Agent 执行

一个好记的类比:

  • Prompt Engineering —— 对马喊话的技巧
  • Context Engineering —— 给马看的地图
  • Harness Engineering —— 给马造一条高速公路,配上护栏、限速牌和加油站,让马儿按规矩跑

这不是术语升级,是工程现实碾出来的。Demo 跑通和生产可靠之间差着好几个数量级——promptcontext 都填不平这个鸿沟,你需要约束、反馈回路、护栏、可观测性、回退机制。Hermes 就是一个 harness:它的 47 个工具、四层审批、迭代预算、SessionDB——所有这些都不是「让模型变聪明」,是「让一个会犯错的幽灵在生产里能用」。imo 这就是 agent harness 这一代基础设施的全部价值。

(2)两条原则

在动手构建之前,先把这两条原则刻进脑子里:

  1. 一个 agent,多个 surface。 一个核心类驱动所有界面。各种 surface(CLI、gateway、cron、batch、API)都只是薄薄的入口层,负责构造一个 agent 并调用 AIAgent.run_conversation()
  2. 程序化记忆 > 提示词花活。 大多数 “聪明 agent” 的行为不是来自提示词工程,而是来自 agent 拥有一个由它自己读写、可以持续生长的 markdown 文件夹(技能 + 记忆 + 人格)。

2. 核心原则

这些是 Hermes 遵循的设计规则。自己构建时也要记住——代码库里那些"奇怪"的决策,大多都能追溯到其中某一条。

(1)平台无关的核心
agent 自身并不知道它运行在终端、Telegram 聊天还是 cron 任务里。所有平台相关的逻辑都活在适配器里,适配器把平台事件翻译成 agent.run_conversation(...),再把响应翻译回去。如果你发现自己正在 agent 核心代码里加 if 分支处理某个特定平台,那你已经偏离架构了。

(2)提示词稳定性(缓存友好)
系统提示词在会话开始时一次性组装,对话过程中不再改动。这不是审美问题,而是经济问题。Anthropic 和 OpenAI 的提示词缓存都要求有稳定的前缀才能命中。会话中途切换工具集、重载记忆、换技能都会让缓存失效,导致成本翻 10 倍。默认情况下,所有改动都应推迟到 “下次会话”。

(3)渐进式披露
不要把所有技能、所有记忆、所有工具的完整文档都灌进系统提示词。只加载描述(Level 0)。当 agent 真的需要某个技能时,再让它拉取完整内容(Level 1)。只有当技能本身请求时,才加载被引用的文件(Level 2)。这就是为什么 Hermes 可以带着 47 个工具和几十个技能上路,同时还能不超出上下文限制。

(4)自注册优于集中式清单
工具和插件应该在导入时自己注册(registry.register(...)),而不是被加到一个手维护的 __all__ 列表里。新增工具 = 一个新文件,其他地方一行不用改。

(5)Profile 隔离
多个独立的 agent 实例可以共存——每个实例都拥有自己的目录(默认是 ~/.hermes/,可通过环境变量覆盖)。代码库里的每一个文件系统路径都要走 get_hermes_home()——绝不要硬编码 ~/.hermes

(6)Agent 拥有自己的学习产物
技能不是人类编辑源码加进去的。Agent 在解决了一个非平凡任务后,通过一个叫 skill_manage 的工具自己写。记忆也不是人类整理的——agent 在每一轮之间编辑 MEMORY.mdUSER.md。这就是那个闭环。


3. 高层架构

Hermes 高层架构

用大白话讲,三层:

  • 第 1 层 — Surfaces: 人或系统如何跟 agent 对话(CLI、聊天平台、cron)。
  • 第 2 层 — Agent 核心: 主循环 + 四个可插拔子系统(工具、技能、记忆、模型)。
  • 第 3 层 — 执行后端: shell / 代码运行类工具实际执行的地方。今天在本地笔记本,明天在沙箱化的 Docker,生产环境跑在 Modal 云上。

4. Agent Loop🔄

这是最重要的一块。AIAgent 类本质上就是下面这个循环:

1. 接收输入 → 来自 CLI / gateway / cron / ACP / web
2. 构建系统提示词 → persona + memory + skills + tools (每个会话一次)
3. 解析 provider → 给选定模型用哪个 API key + endpoint
4. 调用模型 → 四种 API 模式之一 (根据 endpoint/model 自动判断):
                              chat_completions | codex_responses |
                              anthropic_messages | bedrock_converse
5. 解析响应
   ├─ 如果有 tool calls → 通过 registry 派发每一个 → 追加结果 → 回到 4
   └─ 否则 → 最终的 assistant 消息 → 显示 → 持久化 → 结束
6. 持久化 → SQLite SessionDB (WAL 模式 + FTS5 索引)

Hermes Agent 主循环

几个不那么显而易见但很关键的细节:

  • 迭代预算 — 比简单计数器要细腻得多。 一个线程安全的 IterationBudget 被父 agent 及其派生出的所有子 agent 共享。execute_code 在完成时会退还迭代次数,避免一个程序化的工具循环耗尽预算。预算耗尽时:注入一条警告消息(_budget_exhausted_injected),允许恰好再一次最终 API 调用(_budget_grace_call),然后强制做总结。中间没有任何警告——这是刻意为之,避免模型过早放弃。
  • 推理内容是单独存储的,跟可见的 assistant 消息分开(OpenAI o 系列和 Anthropic 的 extended thinking 都会产生隐藏的 “reasoning” token)。把它们放在独立字段里——缓存有效性需要它们,但不应该展示出来。相关回调有:stream_delta_callbackinterim_assistant_callbackthinking_callbackreasoning_callback
  • 带状态的流式擦洗。 _stream_context_scrubber 会剥离 <memory-context> 段,即使这些段跨越了多个 chunk 也能正确处理——别低估这件事的复杂度,标签跨越网络边界时各种小问题。
  • 压缩,不是截断。 上下文满了的时候,context_compressor 会总结中间的轮次,而不是把它们丢掉。总结本身变成一条消息。有损是可以接受的;无损会直接 OOM。
  • 中断。 工具调用过程中按 Ctrl-C 必须能干净地取消正在执行的工具,向历史追加一条 “用户中断” 的 tool result,然后把控制权交回来。不要杀掉整个循环——让 agent 看到中断并作出响应。
  • 会话恢复。 --continue / --resume 通过 SessionDB.get_messages() 加载历史。SQLite 的 WAL 模式 + 自定义的重试层(20–150 毫秒抖动,BEGIN IMMEDIATE)处理多进程写竞争。继续会话前会给用户展示一段回顾。

5. 系统提示词的组装

系统提示词组装

prompt_builder.build_system_prompt() 函数按这个顺序拼接以下各段:

  1. 人格(Persona) — SOUL.md / DEFAULT_AGENT_IDENTITY。身份、口吻、价值观。
  2. 平台提示 — PLATFORM_HINTS。告诉模型它现在跑在 CLI、Telegram、Slack 还是别的什么地方——这会改变格式化规则(CLI 里不用 MarkdownV2,Telegram 里不能嵌套代码块……)。
  3. 记忆指引 — MEMORY_GUIDANCE。以 冻结快照 的形式把 MEMORY.mdUSER.md 嵌入为一个块(用 § 分隔符隔开)。有大小上限(MEMORY 约 2200 字符,USER 约 1375 字符)。
  4. 会话搜索指引 — SESSION_SEARCH_GUIDANCE。告诉 agent 它可以通过 FTS5 搜索过往会话,并给一个小例子。
  5. 技能指引 — SKILLS_GUIDANCE。Level-0 技能索引,加上启发式的散文,提醒 agent 在解决困难任务后主动创建技能。
  6. 上下文文件 — 来自工作目录的 AGENTS.md.hermes.md
  7. 工具使用强制 — TOOL_USE_ENFORCEMENT_GUIDANCE。关于并行调用、错误恢复等的硬规则。
  8. 工具 schema — 所有启用工具的 JSON schema。

然后 prompt_caching.py 插入缓存断点(Anthropic 用 cache_control: {type: ephemeral},其他 provider 各有等价物)。整个拼接出来的前缀就是可缓存区域。

冻结快照模式(这就是窍门)。 MEMORY.mdUSER.md 只在会话开始时读一次,然后在余下整个会话中作为不可变内容嵌入系统提示词。Agent 在会话过程中依然可以往这些文件里写——但系统提示词不会变。结果:缓存在整个对话中都保持有效,新记忆在下次会话生效。跳过这一招,你的前缀缓存就完蛋了。

记忆安全扫描。 在注入之前,MEMORY/USER 内容会被扫描,检测提示词注入模式、外泄企图(引用环境变量的 curl/wget)、持久化后门以及不可见 Unicode。被污染的记忆文件是 agent 的朊病毒——必须防御性扫描。

关键规则: 1–8 节在整个会话中保持冻结。用户消息和工具结果只会被追加到历史中,不会进入系统提示词。


6. 工具系统🛠️

(1)自注册式注册表
一个中央的 tools/registry.py 暴露:

registry.register(
    name="read_file",
    toolset="filesystem",
    schema={...JSON schema...},
    handler=read_file_handler,
    available=lambda ctx: True, # 准入判断函数
)

每个工具文件在模块导入时调用这个函数。注册表负责处理:

  • 给系统提示词收集 schema。
  • 当模型发出 tool call 时按名称派发。
  • 可用性过滤(按用户、按平台、按工具集)。
  • 错误包装 — handler 里抛出的任何异常都会被转换成 tool result,让模型可以看到并作出反应。永远不要让一个工具崩掉整个循环。

所有 handler 都返回 JSON 字符串,而不是 Python 对象。模型看到的永远只是文本。

(2)工具集(Toolsets)
工具按逻辑分组(filesystem、web、browser、code、mcp、vision、audio……)——Hermes 自带约 40+ 个工具(文档里有些地方写 “47 个内置工具”,有些地方写 “40+”;AGENTS.md 里写文件系统是规范来源,因为这个数量一直在变——你自己的版本里别硬编码数字)。用户按工具集而不是逐个工具地启用/禁用。被禁用的工具集完全不会出现在系统提示词里——既省 token,也防止模型甚至知道有这些工具。

(3)执行环境
跑 shell 命令或代码的工具,都通过一层环境抽象(tools/environments/):

后端 用途
local 在你笔记本上开发用。最快,零隔离。
docker 共享开发机。每个会话一个容器。
ssh 远程 VM。把 VM 当作 agent 的 “电脑”。
daytona / modal 给生产用的无服务器沙箱。自动开机。
singularity HPC 集群。

同一个工具,爆炸半径不同。Agent 自己并不知道——只是底下的环境不同。

(4)Agent 级工具
少数几个工具(todo_*memory_*skill_manageskills_listskill_view)会在通用工具派发之前被拦截,由 agent 自己处理,因为它们修改的是 agent 自身的状态(记忆、技能、待办列表)而不是外部世界。这一类要保持小而明确。

(5)MCP 集成
Model Context Protocol 服务器可以作为额外的工具来源插进来。Hermes 把每个 MCP server 当作一个虚拟工具集,允许用户过滤其中的单个工具,并通过同一套 registry 派发调用。这样你就能不写一行集成代码,得到一长串的集成(GitHub、Slack、Linear……)。

(6)工具审批与安全(分层防御)
Shell 类工具很危险。Hermes 分了四层:

  1. Tirith — 一个外部的 Rust 扫描器,带自动安装 + SHA-256 校验。它能检测同形字 URL、终端注入攻击(用 ANSI 转义隐藏命令)以及已知的危险模式。
  2. 正则危险命令检测 — 在 规范化(大小写不敏感,空白压缩)的命令串上跑,这样攻击者就没法用 RM -RF 绕开了。
  3. 智能审批 — 由一个 LLM 对每条命令进行风险评级。低风险自动通过;中/高风险阻塞,等人类批准。
  4. 审批作用域 — 人类批准时可以选 本次 / 本会话 / 永久。信任会累积,不用每次都问。

当 agent 跑在消息网关上需要审批时,它用一个 threading.Event 阻塞,等人在聊天里回复。/yolo 命令可以让受信任的会话完全跳过审批。沙箱后端会自动跳过审批(Docker/Modal 沙箱本身就是安全边界,再问一遍只是徒增摩擦)。

工具审批四层分层防御


7. 技能系统

(1)什么是 skill
一个 skill 就是一个带 YAML frontmatter 的 markdown 文档,教 agent 怎么把某件事做好。不是代码。不是配置。是 agent 阅读的操作手册

---
name: deploy-staging
description: 把当前分支通过 Vercel 推到 staging 并校验健康状态。
version: 1.2.0
platforms: [macos, linux]
requires_toolsets: [shell, web]
fallback_for_toolsets: []
required_environment_variables: [VERCEL_TOKEN]
tags: [deploy, vercel]
category: devops
---

## 何时使用
用户说 "ship"、"deploy to staging"、或 "preview this branch"。

## 步骤
1. 运行 \`git status\` — 如果有未提交改动则中止。
2. 运行 \`vercel --token=$VERCEL_TOKEN\`。
3. 轮询 \`/healthz\` 直到返回 200 或 60 秒超时。
4. 汇报 preview URL。

## 陷阱
- 不要从 \`main\` 部署——只部署 feature 分支。
- 构建失败时,用 \`vercel logs <deployment>\` 抓日志。

## 验证
healthz endpoint 返回 \`{"status":"ok"}\`。

(2)skill 存放在哪里

~/.hermes/skills/
├── devops/deploy-staging/
│ ├── SKILL.md ← 上面那个文件
│ ├── references/ ← skill 可以拉取的额外文档
│ ├── templates/ ← 文件模板
│ ├── scripts/ ← agent 可运行的辅助脚本
│ └── assets/ ← 图片等
├── .hub/ ← 从 skills hub 安装而来的
└── .bundled_manifest ← Hermes 自带的

(3)渐进式披露(3 个层级)
这是 token 用量保持理智的关键:

层级 加载什么 何时
0 name、description、category 始终加载——在系统提示词中
1 完整的 SKILL.md 内容 agent 决定使用该技能时
2 references/scripts/ 里的文件 skill 正文中说 “见 references/foo.md” 时

Agent 通过调用 read_skill(或等价工具)从 L0 升级到 L1 再到 L2。

Skill 渐进式披露

(4)触发方式
技能被激活的三种方式:

  1. 斜杠命令 — 用户输入 /deploy-staging please ship #123
  2. 自然语言 — “deploy this to staging”;agent 基于 L0 描述做匹配,拉取 L1。
  3. 程序化 — cron 任务显式挂载技能。

(5)条件激活
Frontmatter 字段控制可见性:

  • platforms: [linux] — 在 macOS 上隐藏。
  • fallback_for_toolsets: [web] — 仅当没有高级 web 工具启用时可见(比如一个 DuckDuckGo 技能,只在 Brave Search 没配置时顶上)。
  • requires_toolsets: [shell] — 如果 shell 工具被禁用则隐藏。

这样技能目录就会随着部署环境自动适配。

(6)自我进化:skill_manage 工具
Agent 用两个互补的工具:

  • 读取路径: skills_list(浏览 Level-0 索引)和 skill_view(升级到 Level-1/2 内容)。
  • 写入路径: skill_manage,一个带子操作的 meta-tool:
动作 效果
create 从零创建新技能
patch 精准文本替换(更新时首选)
edit 全量重写
delete 删除技能(仅限用户/agent 创建的技能 — 自带技能不能删)

注意:技能内部的文件管理(references/scripts/)走通用的 write_file / remove_file 工具,作用域限定在该技能目录内。

系统提示词中的 SKILLS_GUIDANCE 块显式地推动 agent在以下情况后创建技能:

  • 解决了一个用了 5+ 次工具调用的任务。
  • 找到了一个不显然的变通方案。
  • 发现了一个可能重复使用的工作流。

来自 hub 的技能安装只能由用户发起(安全考虑)。Agent 永远不会自己安装未受信任的技能——它只能通过 skill_manage create 从自己的经验中创建。

这就是那个闭环学习。Agent 边工作边写自己的剧本。

Skill 自我进化闭环

(7)Skills hub 与共享
技能是可移植的 markdown ——天然适合分享。Hermes 集成了多个来源(official/skills-sh/github/well-known/urlclawhublobehub)。安装时每个技能都会被安全扫描,检测提示词注入、数据外泄和破坏性命令,然后才被信任。信任层级:builtin > official > community

格式遵循开放的 agentskills.io 标准——这意味着为 Hermes 写的技能可以在其他兼容的 agent 里运行。


8. 记忆系统

三个独立机制协同工作(所谓 “3 层” 是教学上的简化——在代码里它们是正交的):

Hermes 记忆系统

(1)冻结快照式持久记忆
两个 markdown 文件,都由 agent 自己维护:

  • MEMORY.md — 事实。“项目每周二发版。” “测试 DB 密码在 1Password vault X 里。” (约 2200 字符上限)
  • USER.md — 用户画像。“偏好简短回答。” “资深 Go 工程师,刚开始接触 React。” (约 1375 字符上限)

一个 MemoryStore 在会话开始时读一次,然后把它们作为一个不可变块嵌入系统提示词(由 § 分隔)。Agent 在会话过程中可以往这两个文件里写(写入会落盘),但系统提示词里的那份副本要到下次会话才会更新。这就是保持前缀缓存有效的关键。

(2)通过 SessionDB 进行跨会话回忆
一个 SessionDB(SQLite,WAL 模式,FTS5 全文索引)存储每一轮过往对话。需要时,agent 用 session_search 工具查询;一个 LLM 总结器把命中的内容压缩成一段适合放进上下文的文字。多进程写竞争用 BEGIN IMMEDIATE + 自定义重试循环(20–150 毫秒抖动)来处理。

(3)可插拔的 provider(Honcho / mem0 / supermemory)
这是个替换式机制,不是额外加一层。一个 ABC 基类(MemoryProvider,在 agent/memory_provider.py);编排逻辑在 agent/memory_manager.py。生命周期钩子:prefetch()(模型调用前)、sync_turn()(一轮之后)、shutdown()

要关心的 provider 旋钮:

  • 召回模式: hybrid / context / tools。Tools 模式让模型决定何时查询;context 模式每轮都注入相关记忆。
  • 写入频率: async / turn / session / 数值(每 N 轮)。

Honcho 的 “dialectic” 值得说明一下,因为它听起来玄乎,其实并不:它跑三轮顺序推理 — Initial Assessment → Self-Audit → Reconciliation — 深度由 dialecticDepth(1–3)控制。本质上就是链式自我批判,用于生成更高质量的用户模型。

任何时候只有一个 provider 在跑。挑一个适合你场景的抽象(要深度用户建模选 Honcho,要向量召回选 mem0/supermemory,文件 + FTS5 够用就别上)。


9. 插件系统

一个 PluginManager 从三个地方发现插件:

  1. ~/.hermes/plugins/(用户级)
  2. ./.hermes/plugins/(项目级)
  3. pip entry points(hermes.plugins

每个插件定义一个 register(ctx) 函数,可以挂载到生命周期事件:

  • pre_tool / post_tool
  • pre_llm / post_llm
  • session_start / session_end

……可以注册新工具、新 CLI 命令,或者替换记忆 provider。

铁律: 插件绝不修改核心文件。如果一个插件需要框架没暴露的东西,那框架应该长出一个通用的钩子——而不是开个特例 import。这样插件接口才能保持稳定。

(1)COMMAND_REGISTRY 模式(值得借鉴)

hermes_cli/commands.py 里有一个常量 COMMAND_REGISTRY,是所有斜杠命令的唯一来源。从这一个结构里,代码库自动派生出:

  • CLI 派发
  • Gateway 钩子(这样 /skill foo 在 Telegram 里也能用)
  • Telegram inline 菜单条目
  • Slack slash 子命令
  • prompt_toolkit 自动补全
  • /help 文本

新增一个斜杠命令 = 一个新的 CommandDef 条目 + 一个 handler。零散落的修改。 这跟工具注册表是同一个模式,只是应用在 UI 命令上。给自己的项目偷一个吧——这就是 Hermes 表面积可以扩展、但维护成本却不爆炸的秘诀。


(2)把主题做成数据

~/.hermes/skins/ 下的 YAML 文件(从 default 继承)。一个 YAML 控制 18 种命名颜色、spinner 的脸和动词、agent 名字和问候/告别语、提示符号、工具 emoji、带 Rich markup 的 ASCII banner。10 个内置皮肤(default、daylight、mono、poseidon、charizard……)。Hermes Mod 还自带一个带实时预览和图像→ASCII 转换的 web 编辑器。

架构上的启发:品牌存在 YAML 里,不存在代码里。 用户可以不动 Python 就 fork 一套外观。对一个用户一坐就是几小时的 agent 来说,这件事比你想象的更重要。

(3)多模态与流式

  • 视觉: vision_analyze 工具。Anthropic 通过 _anthropic_image_fallback_cache 做图像转文本的兜底缓存(模型本身看不懂图像时,缓存能避免重复描述同一张图)。
  • 音频输出: text_to_speech 工具。
  • 音频输入: 输入侧的语音备忘录转写。
  • 浏览器工具: 注入多模态上下文(截图 + DOM + 提取的文本)。
  • 流式: _stream_callback_current_streamed_assistant_text,加上有状态的 _stream_context_scrubber——后者即使在 chunk 边界上也能稳妥地剥离 <memory-context> 段。

(4)RL / Atropos 训练集成(environments/)

对 Nous Research 来说,这某种程度上才是项目的重点,而不是边角料。environments/ 目录把 Hermes 包装起来用于强化学习训练:

  • HermesAgentBaseEnv — 抽象工具解析和沙箱接线。
  • HermesAgentLoop — 用 RL rollout 能驱动的方式跑工具调用循环。
  • ToolContext — 把沙箱暴露给奖励函数(这样奖励函数可以 grep 文件系统来验证 agent 是否真的完成了工作)。
  • resize_tool_pool — 防止并行 rollout 时线程池死锁。
  • 两阶段训练流水线:
    • 阶段 1: VLLM/SGLang 原生 tool-call 解析。
      • 阶段 2: 原始 token 解析——Hermes 的 XML 风格 tool 标签和 DeepSeek 的 Unicode 分隔符需要这一阶段,用 ManagedServer
  • 三层 tool result 预算: 每个工具截断 → 沙箱溢出附带预览 → 每轮预算。没有这套,一个 ls / 就能炸掉训练 rollout 的上下文窗口。
  • 预集成 benchmark: TerminalBench 2.0、YC-Bench、WebResearch。

你的 v1 大概率用不上这些。知道这些钩子在那里就好——以后真要拿自己 agent 的轨迹来微调模型,你能找到入口。


10. Surfaces

同一个 AIAgent 驱动六个不同的 surface。每一个都是一层薄薄的适配器,不是重新实现。

Surfaces 扇入

(1)CLI(经典)
cli.py(约 11k 行)。基于 Rich 的面板、用 prompt_toolkit 做输入加自动补全、动画 spinner(KawaiiSpinner)、API 调用时的活动 feed。

(2)TUI(hermes --tui)— 真正新颖

不只是更花哨的 CLI。 架构:

  • 前端: Node.js + React Ink。
  • 后端: Python tui_gateway/server.py
  • 通信格式: 基于 stdio 的换行分隔 JSON-RPC 2.0。

Python 这边把 print 重定向到 stderr,让 stdout 保持纯净给协议用。一个持久子进程跑斜杠命令;慢 handler 走 _SlashWorker(一个 ThreadPoolExecutor),这样中断响应才能保持灵敏。亮点功能:盲文 spinner 流式展示思考链、ToolTrail 树状可视化、虚拟历史视窗(只渲染可见行)、鼠标选择。

来自 AGENTS.md 的设计规则: *不要在 React 里重新实现聊天界面。*对话记录、输入框、斜杠命令行为属于嵌入的 TUI。侧边栏和检查器没问题——替换式视图不行。

(3)Gateway(消息平台)
Telegram、Discord、Slack、WhatsApp、Signal。每个适配器:

  1. 连接到平台(websocket / long-poll / webhook)。
  2. 收到消息时:对用户做鉴权,推导出稳定的 session_key,从 SessionDB 找会话,用这段历史实例化一个 AIAgent
  3. 调用 agent.run_conversation()
  4. 格式化并发回响应(Telegram 的 MarkdownV2 vs Discord 的 markdown 风格 vs Slack 的 mrkdwn —— 这些都活在适配器里)。

(4)ACP(Agent Client Protocol)— 面向 AI 原生编辑器
ACP 是 Zed 和正在兴起的 VS Code 集成跟 agent 对话所用的标准协议。Hermes 实现了 HermesACPAgent。ACP 会话绑定到编辑器的 cwd,并在同一个共享的 SessionDB 中持久化。Hermes 的工具映射到 ACP 的语义类型(比如 read_fileread),IDE 可以注册 MCP server,agent 就会把它们当作额外的工具集看见。

(5)Web UI(hermes web)
web/ 里的 React SPA + hermes_cli/web_server.py 里的 FastAPI。标签页:Status、Sessions(FTS5 搜索 UI)、Config(表单 + 原始 YAML)、Cron、Skills。安全:临时 session token、DNS rebinding 防护、CORS、限流。EN/中文 本地化。

(6)Cron 调度器(~/.hermes/cron/)

不是 APScheduler。 一个自定义调度器,在 gateway 进程内的后台线程上跑一个 60 秒的 tick() 循环。任务以 JSON 形式存在 ~/.hermes/cron/jobs.json 里(不是 SQLite)。输出落到 ~/.hermes/cron/output/{job_id}/{timestamp}.md

任务定义支持:

  • 间隔(every 30m)、5 字段 cron、一次性持续时间、ISO 时间戳。
  • 一个 prompt 字段(要发送给 agent 的用户消息)。
  • 一个可选的 skills 列表,在执行前附加(这样一个 “review-PRs” 的 cron 任务可以预加载一个 pr-review 技能)。
  • 投递目标:local(只写入)、origin(回到任务创建的地方)、platform:chat_id(发到指定的 Telegram/Slack 频道)。

每个 tick:创建一个全新、无历史的 AIAgent,加载附加的技能,执行 prompt,投递输出,更新任务状态。

(7)批处理 runner(训练数据流水线)
repo 根目录下的两兄弟:

  • batch_runner.py — 一个 BatchRunner,跑在 multiprocessing.Pool 上,每个 worker 一个隔离的 AIAgenttoolset_distributions.py 按独立的包含概率为每个 prompt 采样工具集。checkpoint.json 的 checkpoint 是按 prompt 文本 而不是索引来键的(这样 prompt 列表的编辑不会让 checkpoint 失效)。输出按 HuggingFace 的轨迹格式;推理通过 <REASONING_SCRATCHPAD> 标签或原生 thinking token 检测——没有推理的轨迹会被丢弃。
  • mini_swe_runner.py — 一个用于 SWE 风格 benchmark 的兄弟 runner。

Nous 就是用这套从真实的 agent 跑流中生成训练数据的。


11. Profile 与多实例

想在同一台机器上同时跑一个 “个人” agent 和一个 “工作” agent,又不希望它们的记忆串味?用 profile。

实现简单得可笑,但时机至关重要:

  • 每个 profile 拥有自己的 HERMES_HOME 目录。
  • hermes_cli/main.py 里的 _apply_profile_override() 在任何其它模块导入运行之前就设定 HERMES_HOME。如果你在导入之后才设,那些在 import 时读取路径的模块就会用到错的 home。
  • 每一个路径查找都走 get_hermes_home()。代码库里任何地方硬编码的 ~/.hermes 都会破坏 profile 隔离。

要做对的几件事:

  • 测试必须同时 mock Path.home() 和环境变量——只 mock 其中一个会导致测试莫名其妙地抖动。
  • Gateway 适配器要拿一个 per-profile 的 token 锁,避免两个 profile 同时想消费同一个 Telegram bot token。
  • Honcho identity(以及其它记忆 provider 的 ID)都是 profile 作用域的——别跨 profile 共享,否则用户模型会互相污染。

12. 提示词缓存💰

这是你的 agent 在生产环境里到底是便宜还是昂贵的最大单一原因。

提示词缓存对比

要做:

  • 每个会话只构建一次系统提示词。
  • 插入 provider 专属的缓存断点(Anthropic:在前缀里最后一条静态消息上加 cache_control: {type: "ephemeral"})。
  • 对记忆使用冻结快照模式:会话开始时读一次 MEMORY/USER 文件,然后即使磁盘上变了,也要不可变地嵌入。
  • 把配置变更(“开/关某工具集”、“换模型”)默认推迟到下次会话。修改状态的斜杠命令可以接受一个可选的 --now 标志,但默认要延迟。

别做:

  • 会话中途重新加载记忆。
  • 会话中途增删工具。
  • 因为用户"换了话题"就改系统提示词。
  • 让系统提示词依赖当前时间、随机 ID 或任何按轮变化的东西。

缓存命中的前缀的读取成本大约只有写入成本的 1/10。前缀稳定的话,一段 10 轮的对话总成本约为单轮的 1.5 倍。前缀不稳定的话,是 10 倍。


13. 参考资料

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐