从入门到精通:架构、配置、扩展点与工程实践
版本说明:本书基于 Nous Research 官方开发者文档与官方 GitHub 仓库文档(截至 2026-09-30)整理,并参考少量公开的第三方插件仓库作为生态佐证。
依据与边界(请先读)
- 本书以官方文档为准,没有逐行通读仓库源码。凡涉及类构造参数、字段全集的地方,我会标注「⚠ 以源码为准」。
- 标注「伪代码」的片段是示意,不保证直接可运行;标注「官方示例」的取自或改编自官方文档。
- Hermes 迭代很快(2026-09 刚做过一次大规模模块拆分),落地前请先跑 §11 的自查命令。
目录
- 认识 Hermes 与二次开发的心智模型
- 架构全景
- 配置与目录体系
- 扩展面总览与选型
- 零代码扩展:配置、Skill、MCP、自定义模型、Cron
- Hook:三类钩子与事件全表
- Python 插件开发
- 专用插件:模型、平台、记忆、上下文引擎
- 把 Hermes 当服务:API Server 与嵌入
- 实战案例集
- 安全、测试与升级策略
学习路线与源码阅读地图
附录 A 命令速查 · B 排错手册 · C 上线前检查清单 · D 参考资料(ctx API 见 §7.3,Hook 全表见 §6.5)
第 1 章 认识 Hermes 与二次开发的心智模型
1.1 Hermes 是什么
Hermes Agent 是 Nous Research 开源的自主 Agent 框架:同一个 Agent 内核,可以从终端 CLI、Telegram/Discord/Slack 等 25+ 聊天平台、IDE(ACP)、HTTP API、定时任务等多种入口驱动;带工具系统、技能系统(Skill)、持久记忆、上下文压缩、子 Agent 委派。
1.2 二次开发的黄金法则
法则:所有定制都放在 HERMES_HOME(默认 ~/.hermes)和你自己的仓库里,永远不要修改 Hermes 的代码目录。
原因有三:
- 官方升级(
hermes update)会重建代码目录与虚拟环境,但不动 HERMES_HOME,你的插件、配置、技能升级后仍在。 - 官方把「插件必须改核心才能实现」视为设计缺陷——正确路径是向上游提 PR 扩大通用插件面,而不是各自打补丁。
- Agent 核心是「窄腰」:核心多一行代码,每次 API 调用都要付出代价,所以官方对核心改动极为克制。
1.3 侵入性阶梯
零侵入 配置 / SOUL.md / AGENTS.md ─ Skill ─ MCP ─ 自定义 Provider(YAML) ─ Cron
极低 Shell Hook ─ Gateway Hook ─ 通用 Python 插件
低 专用插件:Model Provider / Platform / Memory / Context Engine
外部 API Server / TUI Gateway / ACP(把 Hermes 当服务)
不建议 修改核心源码(fork 维护补丁)
选型口诀:能用左边的,就不用右边的。 每往右一步,你要维护的代码和兼容风险都会增加。
第 2 章 架构全景
2.1 三层结构
┌─────────────────────── 入口层 ───────────────────────┐
│ CLI(cli.py) │ Gateway(gateway/run.py) │ ACP │ Batch │ API Server │ Python 库 │
└───────────────────────────┬──────────────────────────┘
↓ 全部驱动同一个
┌─────────────────────── 核心层 ───────────────────────┐
│ AIAgent(run_agent.py 是门面;循环在 agent/conversation_loop.py 等)│
│ Prompt Builder │ Provider Resolution │ Tool Dispatch │ 压缩与缓存 │
└───────────────────────────┬──────────────────────────┘
↓
┌─────────────────────── 后端层 ───────────────────────┐
│ 会话存储 SQLite+FTS5 │ 终端后端(7) │ 浏览器(5) │ Web(4) │ MCP(动态) │
└──────────────────────────────────────────────────────┘
关键认知:入口不同,AIAgent 相同。所以你写一个插件(如命令守卫),CLI、聊天网关、Cron、API 全部生效。
2.2 十大子系统
| 子系统 | 主要位置 | 职责 | 你能怎么扩展 |
|---|---|---|---|
| Agent Loop | run_agent.py、agent/ |
同步循环:选 provider → 构造 prompt → 调 API → 执行工具 → 重试/回退 → 压缩 → 持久化 | Hook、Middleware |
| Prompt 系统 | agent/prompt_builder.py |
分层组装系统提示(SOUL.md 在第 1 槽) | SOUL.md、AGENTS.md、pre_llm_call 注入 |
| Provider 解析 | hermes_cli/runtime_provider.py |
(provider, model) → (api_mode, key, base_url) |
YAML 自定义 provider、Provider 插件 |
| 工具系统 | tools/、registry.py |
工具自注册,按 toolset 分组 | 插件 ctx.register_tool、MCP |
| 会话存储 | hermes_state.py |
SQLite+FTS5,压缩产生父子会话 | 只读查询(外部工具) |
| 消息网关 | gateway/、plugins/platforms/ |
长驻进程,适配 25+ 平台 | Platform 插件、Gateway Hook |
| 插件系统 | hermes_cli/plugins.py |
三个发现源:用户/项目/pip | —(它本身就是扩展机制) |
| Cron | cron/ |
一等公民的 Agent 定时任务 | Skill、脚本、hermes cron |
| ACP | acp_adapter/ |
stdio JSON-RPC 接 IDE | 直接使用 |
| 轨迹 | trajectory.py |
导出 ShareGPT 训练数据 | 直接使用 |
2.3 三条数据流
CLI 输入 → HermesCLI → AIAgent.run_conversation → 构造 prompt → 解析 provider → 调 API
→ 有 tool_calls? → dispatch 工具 → 回到调 API → 最终回复 → 写 SessionDB
Gateway 平台事件 → Adapter 归一化为 MessageEvent → 鉴权 → 解析 session key
→ 新建 AIAgent(带历史) → 回复经 Adapter 投递
Cron 每 60 秒 tick → 读 jobs.json → 筛选到期任务 → 新建无历史的 AIAgent
→ 注入 Skill → 执行 prompt → 投递结果 → 更新 next_run
2.4 工具系统内部(写插件前必懂)
- 每个工具通过
registry.register(name, toolset, schema, handler, check_fn, requires_env, is_async, description, emoji)自注册。 - 内置工具由
discover_builtin_tools()用 AST 扫描tools/*.py中「顶层」的registry.register()发现——这是核心开发路径,二次开发不走它(它需要改tools/和toolsets.py,会被升级覆盖)。你走插件的ctx.register_tool。 check_fn返回 False 时,工具被静默排除(用于「缺依赖/缺密钥就不出现」)。- 四个工具被 Agent 循环拦截而不经过 registry:
todo、memory、session_search、delegate_task(因为它们需要会话级状态)。你不能通过 registry 去覆盖它们的行为。 - Handler 契约:
handler(args: dict, **kwargs) -> str,必须返回 JSON 字符串,错误用{"error": "..."},不要抛异常;需要会话状态时从kwargs取task_id。
2.5 六条设计原则(决定扩展怎么写)
- Prompt 稳定:会话中系统提示不变,保护 prompt cache。→ 插件想影响模型,注入到用户消息(
pre_llm_call),别改系统提示。 - 可观察、可中断:每次工具调用、API 请求都有回调;可被取消。→ 你的插件也要能快速返回。
- 平台无关核心:平台差异留在入口。
- 松耦合:可选子系统靠注册表 +
check_fn。→ 你的插件依赖缺失时应降级而非崩溃。 - Profile 隔离:每个 profile 独立 HERMES_HOME。→ 插件状态必须 profile 感知。
- 核心是窄腰:见 §1.2。
第 3 章 配置与目录体系
3.1 HERMES_HOME 目录
~/.hermes/
├── config.yaml # 非密钥配置:模型、终端后端、压缩、插件、MCP、hooks
├── .env # 密钥(API Key、Token)
├── SOUL.md # Agent 主身份(系统提示第 1 槽)
├── auth.json # OAuth 凭据
├── state.db # 会话历史(SQLite)
├── memories/ # MEMORY.md、USER.md
├── skills/ # 技能:<category>/<name>/SKILL.md
├── plugins/ # 用户级插件(通用/专用)
├── hooks/ # Gateway 事件钩子:<name>/HOOK.yaml + handler.py
├── cron/ # jobs.json 与输出
├── plugin-data/ # 插件运行时数据(推荐位置)
├── logs/
└── profiles/<name>/ # 每个 profile 一整套上述结构
3.2 配置优先级与命令
- 非密钥项
config.yaml优先于.env;密钥只放.env(hermes config set会自动路由)。 config.yaml中可用${VAR_NAME}引用环境变量。- 命令:
hermes config(查看)、hermes config edit、hermes config set <key> <value>、hermes config check(升级后查缺失项)、hermes config migrate。
3.3 项目级上下文文件
| 文件 | 范围 | 说明 |
|---|---|---|
.hermes.md / HERMES.md |
向上找到 git 根 | 优先级最高 |
AGENTS.md |
递归目录 | 团队约定的主要载体 |
CLAUDE.md |
仅工作目录 | 兼容 |
合计受 context_file_max_chars(默认 20000)限制,写太长会被截断——规范要精炼。
3.4 Profile:多环境隔离
hermes profile list、hermes -p <name> ...。每个 profile 独立的 config/.env/SOUL.md/skills/memory/cron。用途:开发/测试/生产、多租户、多个「员工」并行。
⚠ Profile 不是安全沙箱,只隔离状态,不限制文件访问。真隔离靠终端后端(如 docker)。
3.5 与二次开发相关的配置段
model: {...} # 主模型
providers: # 自定义模型端点(零代码接入,见 §5.4)
work: {api: "https://gpu.corp/v1", key_env: CORP_API_KEY}
plugins:
enabled: [guard, kb-recall] # 通用插件 opt-in
hook_callback_timeout: 30 # 热路径 hook 超时(秒,0=禁用,最大 600)
entries:
kb-recall:
settings: {endpoint: "https://kb.corp/api"}
memory: {provider: ""} # 记忆后端,单选,空=仅内置
context: {engine: "compressor"} # 上下文引擎,单选
mcp_servers: {} # MCP,见 §5.3
hooks: [] # Shell hooks,见 §6.2
terminal: {backend: local} # local/docker/ssh/...
第 4 章 扩展面总览与选型
4.1 扩展面地图
| 你想做的事 | 用什么 | 代码量 | 章节 |
|---|---|---|---|
| 改人设、团队规范 | SOUL.md / AGENTS.md | 0 | 5.1 |
| 沉淀可复用流程 | Skill | 0(Markdown) | 5.2 |
| 接内部系统 API | MCP server | 任意语言 | 5.3 |
| 接私有/第三方模型 | providers: YAML |
0 | 5.4 |
| 定时任务 | Cron(可挂 Skill/脚本) | 0 | 5.5 |
| 事件触发脚本 | Shell / Gateway Hook | 少量 | 6 |
| 自定义工具/斜杠命令/CLI 子命令 | 通用 Python 插件 | 中 | 7 |
| 拦截/改写工具调用与模型请求 | Hook + Middleware | 中 | 6、7 |
| 新 LLM 后端(非 OpenAI 兼容) | Model Provider 插件 | 中 | 8.1 |
| 新聊天渠道 | Platform 插件 | 中-大 | 8.2 |
| 新记忆后端 | Memory Provider 插件 | 中 | 8.3 |
| 换压缩策略 | Context Engine 插件 | 中 | 8.4 |
| 用自己的程序驱动 Hermes | API Server / TUI Gateway / ACP | 小 | 9 |
4.2 决策树
需求是「让 Agent 知道/遵守某些东西」? ─是→ SOUL.md / AGENTS.md / Skill
否↓
需求是「让 Agent 能调用某个外部系统」? ─是→ 有现成 MCP?用;没有→ 自写 MCP server(首选)
否↓ └ 只需本地小函数?→ 通用插件 register_tool
需求是「拦截、审计、改写 Agent 行为」? ─是→ Hook / Middleware
否↓
需求是「换掉某个底层部件」(模型/渠道/记忆/压缩)? ─是→ 对应专用插件
否↓
需求是「在我的产品里用 Hermes」? ─是→ API Server(首选)→ TUI Gateway → 嵌入 AIAgent(最后)
4.3 MCP 还是插件工具?
| 维度 | MCP server | 插件 register_tool |
|---|---|---|
| 语言 | 任意 | 仅 Python |
| 进程隔离 | 独立进程,崩溃不影响 Hermes | 与 Hermes 同进程 |
| 复用 | 其他 Agent/IDE 也能用 | 仅 Hermes |
| 与 Hermes 深度集成 | 弱(只是工具) | 强(可配 hook、命令、状态) |
| 升级风险 | 极低(协议标准) | 低(依赖 ctx API) |
建议:对外部系统的能力封装,优先 MCP;需要与 Hermes 生命周期联动时才用插件。
第 5 章 零代码扩展
5.1 SOUL.md 与 AGENTS.md
SOUL.md(~/.hermes/SOUL.md)定义「Agent 是谁」,在系统提示第 1 槽,跨项目通用:
你是本公司的内部运维助手。
- 一律用中文回答,先结论后细节。
- 执行任何写操作前,先说明影响范围并等待确认。
- 任何情况下不得在回复中输出密钥、Token、内网 IP 全表。
AGENTS.md(项目根目录)定义「这个项目的规矩」:构建命令、代码风格、禁区。
# AGENTS.md
## 构建与测试
- 测试:`make test`;提交前必须通过。
## 禁区
- 不要修改 `infra/prod/` 下任何文件。
实践要点
- SOUL 写「性格与底线」,AGENTS 写「项目事实」,别混。
- 受
context_file_max_chars限制,写长了会被截断;长知识放 Skill(按需加载)。 - 用 profile 给不同角色配不同 SOUL(如
ops、reviewer)。
5.2 Skill:可复用的程序性记忆
Skill 存储「怎么做某件事」,遵循 agentskills.io 开放标准。
目录:~/.hermes/skills/<category>/<name>/SKILL.md,可选 references/、templates/、scripts/、assets/。
必填字段仅 name 与 description;常用可选字段:version、author、license、platforms、required_environment_variables、metadata.hermes.{tags, category, related_skills, requires_toolsets, fallback_for_toolsets, config}。
---
name: deploy-checklist
description: Pre-deploy verification for our Next.js app.
version: 0.1.0
platforms: [linux, macos]
metadata:
hermes:
category: devops
tags: [deploy]
requires_toolsets: [terminal]
required_environment_variables:
- name: VERCEL_TOKEN
---
# Deploy Checklist
## 何时使用
用户要求发布、上线、部署预发布环境时。
## 步骤
1. 运行 `git status`,确认工作区干净。
2. 运行 `npm run build`,失败则停止并汇报。
3. 使用 `vercel deploy --prebuilt`,记录预览 URL。
## 常见坑
- 分支不是 main 时不要部署到 production。
## 验证
- 访问预览 URL,确认返回 200。
渐进式加载:系统提示只放 Skill 的名称+描述索引(skills_list),需要时才 skill_view(name) 读全文,再按需读附属文件——所以 description 要写清「何时该用」,正文可以很长而不占常驻上下文。
平台与条件激活:platforms 让 Skill 在不兼容 OS 上自动隐藏;requires_toolsets/fallback_for_toolsets 让 Skill 依据工具集是否可用而显示/隐藏。
分发(自建 tap,无需服务端):
# 1) 建 GitHub 仓库,结构:skills/<name>/SKILL.md
# 2) 同事订阅
hermes skills tap add myorg/skills-repo
# 3) 也可直接从 URL 安装
hermes skills install https://example.com/SKILL.md --name my-skill
# 4) 发布
hermes skills publish skills/my-skill --to github --repo owner/repo
使用:会话中 /skill <name> 或启动时 hermes -s <name>;Cron 任务可挂 Skill(§5.5)。
注:官方仓库内置 Skill 的作者规范里对 description 长度有更严格的要求(≤60 字符),那是给「向官方仓库提交 Skill」用的,自用/自建 tap 不受此限,但简短仍是好习惯。
5.3 MCP:接入内部系统的首选
mcp_servers:
filesystem: # stdio 类型
command: "npx"
args: ["-y", "@modelcontextprotocol/server-filesystem", "/home/user/projects"]
linear: # 远程 URL 类型
url: "https://mcp.linear.app/sse"
auth: {type: "oauth"}
启动时 Hermes 连接各 server、列出工具并与内置工具并列注册。
自写一个内部工单 MCP server(伪代码,基于官方 MCP Python SDK 的 FastMCP 风格)
# ticket_mcp.py —— 独立进程,与 Hermes 解耦
from mcp.server.fastmcp import FastMCP
import httpx, os
mcp = FastMCP("corp-tickets")
API = os.environ["TICKET_API"]
@mcp.tool()
def search_tickets(query: str, status: str = "open") -> str:
"""按关键词搜索工单,返回 JSON。"""
r = httpx.get(f"{API}/search", params={"q": query, "status": status}, timeout=10)
return r.text
@mcp.tool()
def comment_ticket(ticket_id: str, text: str) -> str:
"""给工单追加评论。写操作,谨慎使用。"""
...
if __name__ == "__main__":
mcp.run() # stdio
mcp_servers:
corp-tickets:
command: "python"
args: ["/opt/mcp/ticket_mcp.py"]
env: {TICKET_API: "https://tickets.corp/api"} # ⚠ env 字段名以官方 MCP 文档为准
安全:MCP 写操作应配合 pre_tool_call 守卫(§10.1)做二次确认。
5.4 自定义模型端点(YAML,零代码)
只要服务实现了 /v1/chat/completions(OpenAI 兼容)或 Anthropic Messages,就无需写插件:
providers:
local:
api: http://localhost:8080/v1 # 无 key 的本地服务:省略 api_key
work:
api: https://gpu-server.internal.corp/v1
key_env: CORP_API_KEY # 密钥放 .env
transport: chat_completions
anthropic-proxy:
api: https://proxy.example.com/anthropic
key_env: ANTHROPIC_PROXY_KEY
transport: anthropic_messages # Anthropic 兼容代理
model.base_url 里的裸主机会自动补 /v1。需要非标准鉴权、特殊参数、模型目录时,才升级到 Model Provider 插件(§8.1)。
5.5 Cron:让 Agent 自己按时干活
- 由 Gateway 守护进程每 60 秒 tick 一次,到期任务在隔离的新会话中运行(无历史)。
- 任务存于
~/.hermes/cron/jobs.json。 - 模型解析顺序:任务级指定 →
cron.model→ 主模型。 - Cron 会话里的 Agent 不能创建/编辑/删除其他 Cron 任务(防止失控循环)。
# 自然语言 / cron 表达式均可
hermes cron create "every 2h" "检查各服务健康状态并汇总"
hermes cron create "0 9 * * *" "总结今天的告警" --deliver telegram
# 挂载 Skill:任务提示不用塞长文,流程在 Skill 里
hermes cron create "every 1h" "汇总新条目" --skill blogwatcher --name feed-digest
# 纯脚本模式:不调用 LLM,脚本 stdout 原样投递(默认超时 120s,可用 HERMES_CRON_SCRIPT_TIMEOUT 调)
hermes cron create "every 5m" --no-agent --script memory-watchdog.sh --deliver telegram --name mem-watch
hermes cron list | status | edit <id> | pause <id> | resume <id> | run <id> | remove <id>
选型:机械检查用 --no-agent(便宜、稳定);需要判断和总结才用 Agent;复杂流程写 Skill 再挂载。
声明式管理(生态工具):第三方 hermes-jobctl 支持把任务写成带 YAML 头的 Markdown,用 apply/status/delete 做 GitOps 式同步(生态项目,非官方)。
第 6 章 Hook:三类钩子
6.1 三类钩子对比
| 维度 | Shell Hook | Gateway 事件 Hook | Python 插件 Hook |
|---|---|---|---|
| 配置位置 | config.yaml 的 hooks: |
~/.hermes/hooks/<name>/(放置即生效) |
插件 register(ctx) |
| 语言 | 任意可执行命令 | Python | Python |
| 作用范围 | CLI + 网关 | 仅网关 | CLI + 网关 + Cron 等 |
| 能否阻断/改写 | 视事件而定 | 否(观察) | pre_tool_call 可阻断;Middleware 可改写 |
| 适合 | 通知、审计脚本 | 网关事件告警 | 策略、注入、深度集成 |
6.2 Shell Hook(无需 Python)
hooks:
- event: post_tool_call
command: "notify-send 'Tool ran: {tool_name}'"
when: {tools: [terminal, patch, write_file]}
Shell hook 通过 stdin 收到 JSON 载荷(含 hook_event_name 等字段),并有独立的逐条超时。首次启用时 Hermes 可能提示你授权(consent)该 hook。⚠ 字段细节以官方 Hooks 文档为准。
6.3 Gateway 事件 Hook
事件:gateway:startup、session:start、session:end、session:reset、agent:start、agent:step、agent:end、command:*(通配)。
~/.hermes/hooks/long-task-alert/
├── HOOK.yaml
└── handler.py
# HOOK.yaml
name: long-task-alert
events: [agent:end]
# handler.py
async def handle(event_type: str, context: dict) -> None:
if context.get("duration_seconds", 0) > 120:
... # 推送「任务耗时过长」通知
6.4 Python 插件 Hook:分三种角色
① 策略 Hook(能左右结果):pre_tool_call 返回 None 放行;{"action": "block", "message": "..."} 阻断;{"action": "approve", "message": "..."} 升级为人工审批。多个插件都注册时,第一个有效的 block 生效(生态插件文档的说法,⚠ 以官方为准)。
② 注入 Hook:pre_llm_call 每轮在工具循环前触发一次;返回 {"context": "..."} 注入到当轮用户消息,不改系统提示,因此不破坏 prompt cache。单次注入有大小上限(官方:10000 字符,超出落盘并留预览)。
③ 观察 Hook(只读,做遥测/审计):列表见 §6.5。所有观察类载荷带 telemetry_schema_version = "hermes.observer.v1"。
铁律
- 回调一律写 `kwargs
**:官方靠「只增字段不删字段」保证向前兼容,你的回调不接**kwargs` 就会在新增字段时崩。 - 回调要快:热路径 hook(
post_tool_call、pre_llm_call、pre_tool_call)受plugins.hook_callback_timeout约束(默认 30s,最大 600,0=禁用)。超时的pre_tool_call有专门处理,别让它卡住。 - 回调抛异常会被记录并跳过,不会拖垮主流程;但也意味着你的守卫代码有 bug 时是「静默失效」——必须写测试(§11)。
6.5 观察类 Hook 事件全表
| 类别 | 事件 |
|---|---|
| 工具 | pre_tool_call(策略)、post_tool_call、transform_tool_result(在 post 之后、结果进入模型上下文之前,可改写结果) |
| LLM(回合级) | pre_llm_call、post_llm_call(偏「提示性」信号) |
| LLM(请求级,权威) | pre_api_request、post_api_request、api_request_error |
| 辅助模型 | pre_auxiliary_call、post_auxiliary_call |
| 流式 | on_stream_start、on_stream_delta、on_stream_end、on_interim_message |
| 会话 | on_session_start(仅新会话,续聊不触发)、on_session_end、on_session_finalize、on_session_reset |
| 其他 | agent_loop_stopped、on_skill_lifecycle、subagent_start、subagent_stop、pre_approval_request、post_approval_response、pre_command、kanban_task_claimed/completed/blocked |
载荷要点
pre_tool_call:tool_name, args, task_id, session_id, tool_call_id, turn_id, api_request_id。post_tool_call:以上 +result(JSON 字符串)、duration_ms、status、error_type、error_message。- 做 LLM 用量/耗时观测请用
pre/post_api_request与api_request_error(每次 provider 调用一条;一个用户回合里可能有多次重试/回退),不要用pre/post_llm_call计费。(NVIDIA NeMo Relay 官方集成文档即此做法。)
第 7 章 Python 插件开发
7.1 三个发现源与启用方式
| 位置 | 范围 |
|---|---|
~/.hermes/plugins/<name>/ |
用户级(跨项目,升级不丢) |
<project>/.hermes/plugins/<name>/ |
项目级(随仓库提交) |
pip 包,entry point 组 hermes_agent.plugins |
可分发 |
通用插件是 opt-in:hermes plugins enable <name>(或 config.yaml 的 plugins.enabled)。专用插件(记忆/上下文引擎/模型/平台)走各自配置项或按 kind 自动加载。
7.2 最小结构
my-plugin/
├── plugin.yaml
└── __init__.py # 必须导出 register(ctx)
# plugin.yaml
name: my-plugin
version: 1.0.0
description: 示例插件
provides_tools: [my_tool]
provides_hooks: [post_tool_call]
requires_env: [MY_API_KEY] # 缺失时插件不加载并提示
python_dependencies: ["httpx>=0.27,<1"] # Hermes 校验但不会替你安装;给依赖加上界
7.3 register(ctx) 能力清单
| API | 作用 |
|---|---|
ctx.register_tool(name, toolset, schema, handler, check_fn=, override=) |
注册工具 |
ctx.register_hook(event, cb) |
订阅生命周期事件 |
ctx.register_middleware(kind, cb) |
改写请求/包装执行(§7.6) |
ctx.register_command(name, handler, description) |
会话内斜杠命令(CLI+各聊天平台通用) |
ctx.register_cli_command(name, help, setup_fn, handler_fn) |
hermes <plugin> <sub> 子命令 |
ctx.register_skill(name, path) |
附带只读 Skill(带命名空间,不污染内置) |
ctx.dispatch_tool(name, args) |
走完整审批/预算流水线调用任意工具 |
ctx.get_config / set_config、ctx.state |
插件设置 / 运行时状态(profile 隔离,≤10MiB) |
ctx.register_platform_handler(platform, factory) |
接管平台原生事件(按钮回调、reaction 等) |
ctx.register_platform(...) |
注册新聊天平台(§8.2) |
ctx.register_context_engine(engine) |
注册上下文引擎(§8.4) |
ctx.register_memory_provider(...) |
注册记忆后端(§8.3) |
7.4 写一个工具(完整示例)
# __init__.py
import json, os
SCHEMA = {
"name": "cmdb_lookup",
"description": "按主机名查询 CMDB 资产信息(负责人、机房、环境)。",
"parameters": {
"type": "object",
"properties": {"hostname": {"type": "string", "description": "主机名"}},
"required": ["hostname"],
},
}
def _available() -> bool:
return bool(os.getenv("CMDB_TOKEN")) # 缺密钥→工具静默不出现
def handle(args: dict, **kwargs) -> str: # 必须返回 JSON 字符串
try:
host = args["hostname"]
# ... 调 CMDB;失败也要返回 JSON
return json.dumps({"hostname": host, "owner": "team-a", "env": "prod"})
except Exception as e:
return json.dumps({"error": str(e)}) # 绝不抛异常
def register(ctx):
ctx.register_tool(name="cmdb_lookup", toolset="corp", schema=SCHEMA,
handler=handle, check_fn=_available)
要点:description 决定模型何时调用,写清「何时用、返回什么」;异步 handler 需声明异步;需要会话状态从 kwargs["task_id"] 取。⚠ 插件 register_tool 的异步参数名以官方 Plugins 文档为准(内置路径是 is_async=True)。
7.5 斜杠命令与 CLI 子命令
def register(ctx):
# 会话内 /scan *.log
ctx.register_command(
"scan",
lambda raw: ctx.dispatch_tool("terminal", {"command": f"find . -name '{raw}'"}),
description="按 glob 查找文件")
注意:拼接命令要防注入,示例仅演示;生产中请校验/转义 raw。
7.6 Middleware:改写与包装
四种 kind:
tool_request:改写工具调用参数(返回{"args": ..., "source": ..., "reason": ...})llm_request:改写发往模型的请求tool_execution/llm_execution:包装执行(next_call只能调用一次,用于计时、缓存、重试、降级)
# 官方示例:给 find 命令加输出上限
def cap_find(tool_name, args, **kwargs):
cmd = args.get("command", "")
if tool_name == "terminal" and cmd.startswith("find "):
return {"args": {**args, "command": cmd + " | head -100"},
"source": "my-plugin", "reason": "cap find output"}
return None
def register(ctx):
ctx.register_middleware("tool_request", cap_find)
回调抛异常会被记录并跳过,不会拖垮主链路。
7.7 状态、配置与数据存放
- 运行时数据写到
<HERMES_HOME>/plugin-data/<plugin>/(官方提供plugins.plugin_storage的plugin_data_dir/plugin_db辅助),绝不写进插件目录——hermes plugins update/remove会 git pull 或删除该目录。 - 用户可见设置放
plugins.entries.<id>.settings,在 manifest 用config_schema声明,桌面端会自动生成表单;密钥字段声明type: secret,存入.env。 - 共享单例用
plugins.plugin_utils.lazy_singleton——Hermes 多线程运行,手写全局变量有竞态。 - 可选依赖在
register()里再 import,check_fn做降级,确保没装 SDK 时插件不崩。
7.8 调试与测试
hermes plugins doctor ./my-plugin --ci # 校验 manifest、依赖等
hermes plugins compat ./my-plugin # 检查兼容性/弃用
HERMES_PLUGINS_DEBUG=1 hermes chat # 打印插件发现过程
hermes logs --level WARNING # 查看警告
常见发现失败原因:目录名与 manifest 不符、缺 register、kind 写错(平台插件必须 kind: platform;记忆后端自动识别为 exclusive 并走 memory.provider,不走 plugins.enabled)、requires_env 缺失、依赖没装。
7.9 分发
- 目录复制:小团队最简单,放进
~/.hermes/plugins/。 - Git 安装:
hermes plugins install <owner>/<repo>(社区有此类做法),hermes plugins update拉取更新。 - pip 包:声明
hermes_agent.pluginsentry point,pip install后启用。适合企业内部私有源。
第 8 章 专用插件
专用插件用于替换某个底层部件。它们仍然是插件,不需要改核心;但因为是「接口实现」,与内部契约耦合更深,升级时要更留意。
| 类型 | 选择方式 | 是否单选 |
|---|---|---|
| Model Provider | plugins/model-providers/<name>/,kind: model-provider |
多个共存 |
| Platform | kind: platform |
多个共存 |
| Memory Provider | memory.provider |
单选 |
| Context Engine | context.engine |
单选 |
⚠ 记忆与上下文引擎插件按文档需要
from agent.memory_provider / agent.context_engine import ...基类——这是官方文档化的契约路径,与「不要 import 内部模块」的一般规则不矛盾,但 2026-09 模块拆分后请务必用hermes plugins compat复查。
8.1 Model Provider 插件
先判断是否真的需要:OpenAI 兼容 / Anthropic 兼容端点用 §5.4 的 YAML 即可。需要「自定义环境变量组合、默认辅助模型、特殊鉴权、模型目录」时才写插件。
# ~/.hermes/plugins/model-providers/acme/__init__.py(官方示例)
from providers import register_provider
from providers.base import ProviderProfile
register_provider(ProviderProfile(
name="acme",
display_name="Acme Inference",
env_vars=("ACME_API_KEY", "ACME_BASE_URL"),
base_url="https://api.acme.example.com/v1",
auth_type="api_key",
default_aux_model="acme-small-fast", # 压缩/标题等辅助任务用的小模型
))
配套 plugin.yaml:kind: model-provider。用户插件与内置同名时用户插件覆盖内置。接入 3 种 API 模式(chat_completions / anthropic_messages / 其他)由 provider 声明,⚠ 细节参考官方 Model Provider Plugins 与 Adding Providers。
8.2 Platform 插件:接入新聊天渠道
契约:继承 BasePlatformAdapter:
async connect(self, *, is_reconnect: bool = False) -> bool:建立连接、启动监听async disconnect(self)async send(self, chat_id, text) -> SendResult:发消息- 入站:把平台原始事件转成
MessageEvent,调用self.handle_message(event)
# plugin.yaml
name: corp-im-platform
kind: platform
requires_env: [CORP_IM_TOKEN]
optional_env: [CORP_IM_ALLOWED_USERS]
# __init__.py —— 骨架(⚠ SendResult / MessageEvent 字段以源码为准,此处为示意)
from gateway.platforms.base import BasePlatformAdapter, SendResult
from gateway.platforms.event import MessageEvent, MessageType
class CorpIMAdapter(BasePlatformAdapter):
async def connect(self, *, is_reconnect: bool = False) -> bool:
self.client = await open_ws(self.token) # 伪代码
self.client.on_message(self._on_raw)
return True
async def _on_raw(self, raw):
event = MessageEvent( # 字段示意
text=raw["text"], chat_id=raw["chat"], user_id=raw["from"],
message_type=MessageType.TEXT)
await self.handle_message(event) # 进入网关统一流程
async def send(self, chat_id, text):
mid = await self.client.send(chat_id, text) # 伪代码
return SendResult(success=True, message_id=mid) # 示意
async def disconnect(self):
await self.client.close()
def _check(): # 缺凭据/依赖时隐藏该平台
import os; return bool(os.getenv("CORP_IM_TOKEN"))
def register(ctx):
ctx.register_platform(
name="corp-im", label="Corp IM",
adapter_factory=lambda cfg: CorpIMAdapter(cfg),
check_fn=_check,
required_env=["CORP_IM_TOKEN"],
install_hint="pip 安装 corp-im-sdk 后执行 hermes plugins enable corp-im-platform",
)
注册后自动获得(无需改核心):config.yaml 里的平台配置(经 apply_yaml_config_fn 的 YAML→环境变量桥接)、hermes config 中的 env 条目(来自 requires_env/optional_env)、hermes status 显示 (plugin)、hermes tools/hermes skills 的按平台配置、Cron 投递目标(cron_deliver_env_var)。多 profile 下用 acquire_scoped_lock() 防止同一 Token 被两个 profile 同时占用。
安全(必须):Hermes 对入站发送者默认拒绝。务必配置白名单(如 allow_from 或平台对应的 allowed-users 环境变量),否则要么没人能用,要么(若你放开)任何人都能驱动一个拥有终端权限的 Agent。参考官方 plugins/platforms/irc/(纯标准库,最好的最小样例)。
8.3 Memory Provider 插件
内置记忆是 MEMORY.md/USER.md。外接向量库/图谱/云记忆服务时,用 Provider 接管。同时只能启用一个:hermes config set memory.provider <name> 或 hermes memory setup,hermes memory status 验证。
必须实现:
| 方法 | 时机 | 备注 |
|---|---|---|
name(property) |
始终 | 与配置值一致 |
is_available() |
初始化前 | 禁止网络调用(只检查配置/依赖) |
initialize(session_id, **kwargs) |
Agent 启动 | 建立连接,取 profile/会话上下文 |
get_tool_schemas() |
初始化后 | 你暴露给模型的记忆工具 |
handle_tool_call(tool_name, args, **kwargs) |
模型调用你的工具 | 返回 JSON 字符串 |
可选钩子:system_prompt_block()(静态提示块)、prefetch(query, *, session_id="")(每次 API 调用前返回召回内容)、queue_prefetch(...)(回合后预热下一轮)、sync_turn(user_content, assistant_content, *, session_id="", messages=None)(回合结束同步)、on_memory_write(action, target, content)(镜像内置记忆写入)、shutdown()。
# 骨架(伪代码,基于官方方法表)
import json, threading
from agent.memory_provider import MemoryProvider # ⚠ 路径以官方为准
class CorpMemory(MemoryProvider):
name = "corp-memory"
def is_available(self):
import os; return bool(os.getenv("CORP_MEM_URL")) # 无网络
def initialize(self, session_id, **kwargs):
self.sid, self.cache = session_id, ""
self.client = make_client()
def get_tool_schemas(self):
return [{"name": "corp_mem_search", "description": "检索公司记忆库",
"parameters": {"type": "object",
"properties": {"q": {"type": "string"}}, "required": ["q"]}}]
def handle_tool_call(self, tool_name, args, **kw):
if tool_name == "corp_mem_search":
return json.dumps(self.client.search(args["q"]))
return json.dumps({"error": "unknown tool"})
def prefetch(self, query, *, session_id=""):
c, self.cache = self.cache, "" # 先吃上一轮预热结果
return c or self._recall(query)
def queue_prefetch(self, query, *, session_id=""): # 后台线程,不阻塞
threading.Thread(target=lambda: setattr(self, "cache", self._recall(query)),
daemon=True).start()
def sync_turn(self, user_content, assistant_content, *, session_id="", messages=None):
threading.Thread(target=self.client.ingest, # 写入放后台
args=(user_content, assistant_content), daemon=True).start()
def register(ctx):
ctx.register_memory_provider(CorpMemory())
工程经验(来自生态实现):网络调用放守护线程;召回失败要静默返回空串;对「Cron/子 Agent 场景是否写记忆」要有开关,避免污染长期记忆;注意 profile 隔离(按 session/profile 维度分区)。
8.4 Context Engine 插件
内置 ContextCompressor 在上下文用到约 50%(可配)时做有损摘要(4 阶段算法);网关另有一层「会话卫生」压缩作安全网。要换成无损方案(如 DAG 化知识、分层检索)就写引擎。单选,且永不自动激活,必须 context.engine: <name>。
# plugins/context_engine/my-engine/__init__.py(官方骨架)
from agent.context_engine import ContextEngine
class MyContextEngine(ContextEngine):
@property
def name(self) -> str:
return "my-engine" # 必须与 config 中的值一致
def update_from_response(self, usage) -> None:
# 每次 API 调用后:更新 self.last_prompt_tokens / last_completion_tokens / last_total_tokens
...
def should_compress(self, prompt_tokens: int = None) -> bool:
# 每回合检查:是否该压缩
...
def compress(self, messages, current_tokens=None, focus_topic=None,
force=False, memory_context="") -> list:
# 返回压缩后的 messages;focus_topic 来自手动 /compress <focus>
...
def register(ctx):
ctx.register_context_engine(MyContextEngine())
约束:必须维护类属性 context_length、compression_count(Agent 直接读取用于显示/日志);可通过 get_tool_schemas()/handle_tool_call() 暴露引擎自己的工具(如 lcm_grep);另有两个「逐回合选择/观察」的可选钩子(默认空实现),⚠ 名称与签名请查官方 Context Engine Plugins 页。生命周期:实例化 → update_from_response → should_compress → compress。
测试:assert isinstance(engine, ContextEngine)、engine.name == "your-name"、engine.compress(msgs) 返回合法消息列表。生态里的 hermes-context-tuner 就是「包装内置压缩器并记录审计」的典型,它通过公开 compress() 契约委派,升级抗性较好,但仍提示「升级移除垫片后需重跑安装」——这是专用插件的现实成本。
第 9 章 把 Hermes 当服务
9.1 三种协议,同一个 AIAgent
| 协议 | 传输 | 适用 |
|---|---|---|
| API Server | HTTP + SSE,OpenAI 兼容 | Web 前端、CI、非 Python 系统、Open WebUI 等 |
| TUI Gateway | JSON-RPC over stdio/WebSocket | 自研桌面/TUI 宿主,要全部功能(审批、澄清、分支) |
| ACP | JSON-RPC over stdio | VS Code/Zed/JetBrains |
9.2 API Server
# ~/.hermes/.env
API_SERVER_ENABLED=true
API_SERVER_KEY=<强随机串> # 必填,哪怕只绑定 127.0.0.1
# API_SERVER_PORT=8642 # 默认端口
hermes gateway # 启动,监听 http://127.0.0.1:8642
| 端点 | 用途 |
|---|---|
POST /v1/chat/completions |
无状态,OpenAI 格式;工具进度以 event: hermes.tool.progress 输出 |
POST /v1/responses |
服务端会话状态(previous_response_id);GET/DELETE /v1/responses/{id} |
POST /v1/runs |
长任务;GET /v1/runs/{id}、/events(SSE)、POST /stop、/approval |
POST /api/sessions/{id}/chat(/stream) |
会话式接口 |
GET /v1/models |
模型列表(客户端探测用) |
行为要点
- 前端的
system消息(Chat)或instructions(Responses)会叠加在核心系统提示之上;Agent 仍保留全部工具、记忆、Skill。 - Runs 支持
input、session_id、instructions、conversation_history、previous_response_id;session_id会回显便于关联你自己的会话。 - 裸
model值(如gpt-4o)默认被忽略并回落到网关默认模型,避免通用客户端硬编码模型名造成误配;需要按请求切模型要显式开启相应选项(⚠ 以官方 API Server 页为准)。 - Runs 端点不能为每次运行指定工作目录。
⚠ API Server 等于给调用方开放整套工具,包括终端命令。 必须:强 Key、只绑内网/回环、前置反向代理与鉴权、与 §10.1 的守卫插件配合、不同租户用不同 profile + 不同端口/Key(官方给出了多 profile 各自
API_SERVER_KEY的配置方式)。
curl -N http://127.0.0.1:8642/v1/chat/completions \
-H "Authorization: Bearer $API_SERVER_KEY" -H "Content-Type: application/json" \
-d '{"model":"hermes-agent","stream":true,
"messages":[{"role":"system","content":"回答请附带工单号"},
{"role":"user","content":"查一下昨天失败的部署"}]}'
# 用任何 OpenAI SDK 直连
from openai import OpenAI
c = OpenAI(base_url="http://127.0.0.1:8642/v1", api_key=API_SERVER_KEY)
r = c.chat.completions.create(model="hermes-agent",
messages=[{"role": "user", "content": "总结今天的告警"}])
长任务模式(伪代码):
POST /v1/runs {"input": "...", "session_id": "order-42"} → run_id
GET /v1/runs/{run_id}/events # SSE:工具/推理/用量事件
POST /v1/runs/{run_id}/approval {...} # 遇到审批时回应(字段以官方为准)
POST /v1/runs/{run_id}/stop # 中止
9.3 进程内嵌入(最后手段)
官方允许在 Python 里直接使用 AIAgent,但它走的是内部导入路径,是最容易随升级失效的一种集成方式:
from run_agent import AIAgent # 伪代码:构造参数、run_conversation 签名以源码为准
agent = AIAgent(...)
print(agent.run_conversation("总结今天的告警"))
能用 API Server 就不要用这个;确需嵌入,请把调用封装在你自己的一个薄适配层里,并加集成测试,升级时只改这一层。
9.4 与 Web/桌面前端整合
- Open WebUI、LobeChat、LibreChat 等:API Type 选 Chat Completions(推荐),
OPENAI_API_BASE_URL指向.../v1,Key 与API_SERVER_KEY一致。 - 官方 Desktop 与 Dashboard 有各自的插件/扩展 SDK(本书不展开)。
第 10 章 实战案例集
每个案例都遵循同一原则:只新增文件到 HERMES_HOME 或独立仓库,不碰核心。
案例 1:企业命令守卫(阻断 + 人工审批 + 审计)
目标:禁止高危命令;生产集群操作必须人工确认;所有决策留痕。 方案:pre_tool_call 策略 Hook(能阻断)+ 本地 JSONL 审计。
# ~/.hermes/plugins/guard/plugin.yaml
name: guard
version: 1.0.0
description: 企业命令策略
provides_hooks: [pre_tool_call]
# ~/.hermes/plugins/guard/__init__.py
import json, os, re, time, pathlib
HOME = pathlib.Path(os.environ.get("HERMES_HOME", "~/.hermes")).expanduser()
AUDIT = HOME / "plugin-data" / "guard" / "audit.jsonl" # 数据不放插件目录
DENY = [r"\bmkfs\b", r"\brm\s+-rf\s+/(?!tmp)", r"curl[^|]*\|\s*(ba)?sh", r"\bdd\s+if=.*of=/dev/"]
APPROVE = [r"\bkubectl\b.*\b(prod|production)\b", r"\bterraform\s+apply\b"]
def decide(tool_name: str, args: dict):
"""纯函数:便于单元测试。返回 None / block / approve 指令。"""
if tool_name != "terminal":
return None
cmd = args.get("command", "")
if any(re.search(p, cmd) for p in DENY):
return {"action": "block", "message": "公司策略禁止该命令"}
if any(re.search(p, cmd) for p in APPROVE):
return {"action": "approve", "message": "生产环境操作,需人工确认"}
return None
def _audit(**rec):
try:
AUDIT.parent.mkdir(parents=True, exist_ok=True)
with AUDIT.open("a") as f:
f.write(json.dumps({"ts": time.time(), **rec}, ensure_ascii=False) + "\n")
except Exception:
pass # 审计失败不能影响主流程
def pre_tool_call(tool_name, args, task_id="", **kwargs):
d = decide(tool_name, args)
_audit(tool=tool_name, args=args, task=task_id, decision=(d or {}).get("action", "allow"))
return d
def register(ctx):
ctx.register_hook("pre_tool_call", pre_tool_call)
测试(pytest):
from guard import decide
def test_block_rm(): assert decide("terminal", {"command": "rm -rf /"})["action"] == "block"
def test_allow_ls(): assert decide("terminal", {"command": "ls -la"}) is None
def test_approve_prod(): assert decide("terminal", {"command": "kubectl delete pod x -n prod"})["action"] == "approve"
注意:正则黑名单永远不完备(可被 bash -c、变量拼接绕过)。它是「减少事故的护栏」,不是安全边界。真正的边界是 Docker 终端后端 + 最小权限凭据 + 网络隔离(§11.2)。
案例 2:内部知识库 RAG(缓存友好注入)
目标:每轮把相关内部文档片段带给模型,不破坏 prompt cache。 方案:pre_llm_call 返回 {"context": ...},注入到当轮用户消息。
name: kb-recall
version: 1.0.0
provides_hooks: [pre_llm_call]
python_dependencies: ["httpx>=0.27,<1"]
import httpx
_endpoint = None
def recall(session_id="", user_message="", **kwargs):
if len(user_message) < 8: # 太短的问候不检索
return None
try:
r = httpx.post(_endpoint, json={"q": user_message, "k": 4}, timeout=3.0)
hits = r.json().get("results", [])
except Exception:
return None # 失败静默,绝不拖垮 Agent
if not hits:
return None
body = "\n".join(f"[{h['id']}] {h['text'][:400]}" for h in hits)
return {"context": f"以下为相关内部文档(回答时请引用编号):\n{body}"}
def register(ctx):
global _endpoint
_endpoint = ctx.get_config("endpoint", default="https://kb.corp/api/search")
ctx.register_hook("pre_llm_call", recall)
调优:超时要短(回调卡住会拖慢每一轮);控制注入总量(官方单次上限 10000 字符);按相关度阈值过滤,宁缺毋滥;不要每轮重复注入相同内容。
案例 3:审计与用量上报(观察类 Hook → SIEM/数据仓库)
目标:把每次工具调用与每次模型请求上报,用于合规与成本分析。 方案:观察 Hook + 后台队列,回调内只入队,绝不做网络 IO。
import queue, threading, json, httpx
Q: "queue.Queue" = queue.Queue(maxsize=10000)
def _worker(url):
while True:
rec = Q.get()
try:
httpx.post(url, content=json.dumps(rec, ensure_ascii=False), timeout=5)
except Exception:
pass # 可加重试/落盘
def _put(rec):
try: Q.put_nowait(rec) # 满了就丢,绝不阻塞 Agent
except queue.Full: pass
def on_tool(**kw):
_put({"type": "tool", "tool": kw.get("tool_name"), "status": kw.get("status"),
"ms": kw.get("duration_ms"), "session": kw.get("session_id"),
"turn": kw.get("turn_id"), "err": kw.get("error_type")}) # 不上报 args/result 原文,见下
def on_api(**kw): # 请求级权威 Hook:用量、耗时、重试
_put({"type": "llm", **{k: kw.get(k) for k in ("session_id", "turn_id", "api_request_id")}})
def on_api_err(**kw):
_put({"type": "llm_error", "session": kw.get("session_id")})
def register(ctx):
url = ctx.get_config("collector", default="https://siem.corp/ingest")
threading.Thread(target=_worker, args=(url,), daemon=True).start()
ctx.register_hook("post_tool_call", on_tool)
ctx.register_hook("post_api_request", on_api)
ctx.register_hook("api_request_error", on_api_err)
合规要点:args/result 可能含密钥或个人信息,默认只上报元数据;确需上报原文,先脱敏(案例 5)。线程建议用 lazy_singleton 保证只启动一次。
现成方案:NVIDIA NeMo Relay 提供官方 Hermes 集成(observability/nemo_relay 插件或 shell hook 转发),可输出 ATIF 轨迹格式,不想自研可直接评估。
案例 4:工单运维日报(MCP + Skill + Cron 三件套,零 Python 核心代码)
目标:每天 9:00 汇总昨天失败的部署与未关闭工单,推送到 Telegram。
- MCP:§5.3 的
corp-tickets(search_tickets);CI 系统同理封成corp-ci。 - Skill:把「怎么写日报」固化成流程(口径统一、可版本化):
---
name: ops-daily-report
description: Compile the daily ops report from CI and tickets.
metadata:
hermes:
requires_toolsets: []
---
# 运维日报
## 步骤
1. 调用 corp-ci 工具,取过去 24h 失败的部署。
2. 调用 search_tickets,status=open 且 severity>=P2。
3. 按「影响面 / 责任团队 / 建议动作」三段输出,总长不超过 400 字。
## 验证
- 每条失败部署都有对应处理建议。
- Cron:
hermes cron create "0 9 * * 1-5" "生成今日运维日报" --skill ops-daily-report \
--deliver telegram --name ops-daily
为什么这么拆:数据获取(MCP)、口径(Skill)、调度投递(Cron)三层各自可独立升级和测试,且全部不在 Hermes 代码里。
案例 5:工具结果脱敏(transform_tool_result)
目标:终端输出里的密钥/身份证号在进入模型上下文前被遮盖。 方案:transform_tool_result 在 post_tool_call 之后、结果进入上下文之前触发。
import re
PATTERNS = [(re.compile(r"AKIA[0-9A-Z]{16}"), "AKIA****"),
(re.compile(r"(?i)(password|token|secret)\s*[=:]\s*\S+"), r"\1=****")]
def redact(tool_name="", result="", **kwargs):
out = result
for pat, rep in PATTERNS:
out = pat.sub(rep, out)
return out if out != result else None # ⚠ 返回值约定(返回新字符串 or dict)以官方 Hook 文档为准
def register(ctx):
ctx.register_hook("transform_tool_result", redact)
注意:这只保护「进入模型」这一路;落盘到会话库的内容是否已脱敏,请以官方语义与实测为准。
案例 6:多租户 Agent 服务(Profile + API Server)
目标:给多个团队各提供一个隔离的 Hermes 服务。
profiles/team-a/ config.yaml .env(API_SERVER_KEY=a-secret, API_SERVER_PORT=8651) SOUL.md skills/
profiles/team-b/ config.yaml .env(API_SERVER_KEY=b-secret, API_SERVER_PORT=8652) SOUL.md skills/
hermes -p team-a gateway # 各自一个进程
hermes -p team-b gateway
- 前置反向代理做统一鉴权、限流、审计;每个 profile 使用独立的模型密钥/额度。
- 每个 profile 都启用案例 1 的守卫;终端后端一律
docker。 - 再次强调:profile 只隔离状态,不是沙箱。
案例 7:接入内部 IM(Platform 插件)
见 §8.2 骨架。落地清单:① 先实现 connect/send 跑通「收到消息→回复」;② 加白名单;③ 断线重连(is_reconnect);④ 消息去重与幂等;⑤ 限流与消息长度切分;⑥ 用 hermes gateway status 验证显示为 (plugin);⑦ 让 Cron 能投递到该平台(cron_deliver_env_var)。
第 11 章 安全、测试与升级策略
11.1 升级安全清单
该做
- 代码只放
~/.hermes/plugins/、<project>/.hermes/plugins/或独立 pip 包;数据放plugin-data/。 - 只用官方文档化的
ctxAPI;回调一律**kwargs。 - manifest 声明
requires_env、python_dependencies(依赖加上界);可选依赖用check_fn/延迟 import 降级。 - 每次升级前后都跑:
hermes plugins doctor <path> --ci、hermes plugins compat <path>、hermes config check。 - 记录并固定你验证过的 Hermes 版本(
hermes --version/git tag),先在 canary profile 验证再全量升级;保留回滚方案。 - 把「行为」写成测试:纯函数(如
decide())单测 + 一个端到端冒烟(启动 → 触发一条工具调用 → 断言被阻断/审计)。
不要做
- 不要 import 未文档化的内部模块,不要碰
ctx._cli_ref(网关、chat -q、kanban worker 里为None,会静默失效)。 - 不要 monkey-patch 适配器类;用
ctx.register_platform_handler/ 平台专属 handler 注册。 - 不要给 Telegram 注册无
pattern的 handler(会吞掉核心的审批/模型选择按钮)。 - 不要把运行时数据写进插件目录(
plugins update/remove会覆盖/删除)。 - 不要靠改系统提示影响模型(会破坏缓存):用 SOUL/AGENTS/
pre_llm_call。
兼容承诺(官方):PluginContext 文档化方法只增不删;Hook 载荷只增字段;弃用至少保留两个小版本并给一次性警告。
⚠ 2026-09 的模块拆分(PR #102117)使旧的「从内部路径 import」写法在 2026-09-14 后默认不再加载。若你有旧插件,先跑
hermes plugins compat并按提示迁移到ctxAPI。
如果非改核心不可:先确认能否用 Middleware/Hook 达成;不能就向上游提 PR 扩大通用插件面;实在要 fork,把补丁做成最小集合并在每次上游发版时 rebase,同时把「为什么需要」写进文档。
11.2 安全清单
- 终端后端隔离:生产/服务化一律
terminal.backend: docker(或其他隔离后端);主机直跑只用于个人开发机。 - 最小权限凭据:给 Agent 的云/数据库/代码托管凭据只读优先,按环境分离。
- 入站白名单:任何聊天平台/API 都必须有发送者控制;API Server 必须强 Key,不暴露公网。
- 插件供应链:插件与 Hermes 同进程运行,等同于给了它全部权限。第三方插件(含 pip 包)先审代码/锁版本/放私有源。
- 策略是护栏不是边界:黑名单、审批只能降低事故概率,不能替代隔离。
- 密钥管理:只放
.env/type: secret配置;日志与审计不要落原文。 - 可观测:接入案例 3 的上报;关注
agent.log中check_fn ... returned False一类警告,它们说明某些工具因依赖缺失被静默排除。
11.3 性能与稳定性
- 热路径回调要快(默认 30s 超时是兜底,不是预算);网络 IO 放线程/队列。
- 注入类 Hook 控制体积;重复内容会推高每轮 token 与成本。
- 异步适配器里别做阻塞调用。
- 多线程环境用
lazy_singleton;不要依赖模块级可变全局。 - 对外部依赖(KB、SIEM、记忆库)一律「失败静默 + 超时 + 熔断」,不能让辅助系统拖垮主 Agent。
11.4 CI 参考流水线
1. lint + 单测(纯函数)
2. hermes plugins doctor ./plugin --ci
3. hermes plugins compat ./plugin
4. 冒烟:临时 HERMES_HOME + 最小 config → 启动 → 触发工具 → 断言
5. 发布:打 tag → 私有 pip 源 / git 仓库
6. 部署:canary profile 先行 → 观察 24h → 全量
第 12 章 学习路线与源码阅读地图
12.1 路线
| 阶段 | 目标 | 练习 |
|---|---|---|
| 入门(1–2 天) | 会用、会配 | 安装 → hermes setup → 写 SOUL/AGENTS → hermes tools → 建 2 个 profile |
| 进阶(3–5 天) | 零代码扩展 | 写 2 个 Skill、接 1 个 MCP、配 1 个自定义 provider、建 1 个 Cron 日报 |
| 熟练(1–2 周) | 写插件 | 官方 calculator 教程 → 案例 1/2/3;练 plugins doctor;写测试 |
| 精通(持续) | 换后端、做平台 | Model Provider、Platform、Memory、Context Engine 插件各做一个 |
| 专家 | 产品化 | API Server 多租户;TUI Gateway 自研前端;Trajectory 做训练数据 |
12.2 源码阅读顺序(官方推荐)
Architecture → Agent Loop → Prompt Assembly → Provider Runtime → Adding Providers → Tools Runtime → Session Storage → Gateway Internals → Context Compression & Caching → ACP Internals。
读源码时的问题清单:这一段有哪些 Hook/注册点?它的契约是否在官方文档中承诺?我的插件是否只依赖被承诺的部分?
12.3 参与上游
发现「只能改核心才能做」的需求,恰是贡献机会:在 NousResearch/hermes-agent 提 Issue 描述场景,倡议新增通用扩展点(如新 Hook、新 ctx 方法)。官方的态度是:第三方集成作为独立插件仓库发布,不合并进核心树。
附录 A 命令速查
# 配置
hermes config | edit | set <k> <v> | check | migrate
hermes -p <profile> ... hermes profile list
# 插件
hermes plugins enable|disable <name>
hermes plugins install <owner/repo> hermes plugins update
hermes plugins doctor <path> --ci hermes plugins compat <path>
# Skill
hermes skills tap add owner/repo hermes skills install <url|name>
hermes skills publish <dir> --to github --repo owner/repo
# 记忆 / 上下文
hermes memory setup | status hermes config set memory.provider <name>
# Cron
hermes cron create|list|status|edit|pause|resume|run|remove
# 网关 / 诊断
hermes gateway | gateway status | gateway restart
hermes logs --level WARNING hermes doctor hermes dump
HERMES_PLUGINS_DEBUG=1 hermes chat
⚠ 子命令以 hermes --help 与官方 CLI 参考为准(版本间可能有差异)。
附录 B 排错手册
| 现象 | 常见原因 | 处理 |
|---|---|---|
| 插件不出现 | 未 enable;目录/manifest 不符;缺 register;kind 错误 |
hermes plugins enable;HERMES_PLUGINS_DEBUG=1;plugins doctor |
| 工具不出现 | check_fn 返回 False(缺密钥/依赖);toolset 被 hermes tools 关闭 |
看 agent.log 的 check_fn ... returned False;检查 requires_env |
| 守卫没生效 | 回调抛异常被静默跳过;未接 **kwargs 导致签名不匹配;未启用 |
加日志与单测;确认插件已启用 |
| 平台没人能用 | 默认拒绝所有发送者 | 配置白名单 |
| 记忆提供者不工作 | is_available() 为假;进程未重启;memory.provider 名称不符 |
hermes memory status;重启网关 |
| Hook 卡顿 | 回调里做了慢 IO | 改队列/线程;调 plugins.hook_callback_timeout 仅作兜底 |
| 升级后失效 | 内部路径被拆分;使用了未文档化 API | plugins compat;迁移到 ctx API |
| Telegram 按钮失灵 | 注册了无 pattern 的 handler |
加 pattern,只处理自己的回调 |
| 压缩后行为怪异 | 自定义引擎 compress() 破坏了消息结构 |
单测校验返回的 messages 合法 |
附录 C 上线前检查清单
- [ ] 所有定制都在 HERMES_HOME/项目
.hermes/独立包,核心无改动 - [ ] 回调均接
**kwargs,外部调用有超时且失败静默 - [ ]
plugins doctor与plugins compat通过 - [ ] 守卫/脱敏逻辑有单元测试与一条端到端冒烟
- [ ] 终端后端已隔离,凭据最小权限
- [ ] 入站白名单/API Key 已配置,API 不暴露公网
- [ ] 审计/用量上报已接入,且不落敏感原文
- [ ] canary profile 验证过,有回滚方案
- [ ] 已记录验证过的 Hermes 版本
附录 D 参考资料
官方(Nous Research)
- Architecture、Agent Loop、Tools Runtime、Adding Tools、Gateway Internals、Cron Internals、Context Compression and Caching:
https://hermes-agent.nousresearch.com/docs/developer-guide/… - Build a Hermes Plugin:
…/docs/developer-guide/plugins - Memory Provider Plugins:
…/docs/developer-guide/memory-provider-plugin - Context Engine Plugins:
…/docs/developer-guide/context-engine-plugin - Adding Platform Adapters:
…/docs/developer-guide/adding-platform-adapters - Observer Hooks:
…/docs/developer-guide/observer-hooks - Creating Skills:
…/docs/developer-guide/creating-skills - Plugins / Skills / API Server / Cron(用户指南):
…/docs/user-guide/features/{plugins,skills,api-server,cron} - 仓库:
https://github.com/NousResearch/hermes-agent(文档源在website/docs/)
生态佐证(第三方,非官方,仅作参考)
- NVIDIA NeMo Relay 的 Hermes 集成:
https://docs.nvidia.com/nemo/relay/ - blackwall-hermes-plugin(
pre_tool_call守卫)、ai-memory-hermes-plugin(记忆后端)、hermes-context-tuner(上下文引擎)、hermes-jobctl(声明式 Cron)等 PyPI/GitHub 项目
本书未逐项验证的部分(请以官方为准):Shell hook 载荷字段全集;transform_tool_result 返回值约定;SendResult/MessageEvent 字段;插件 register_tool 的异步参数;Context Engine 的两个逐回合可选钩子的名称;Runs 审批请求体;Desktop/Dashboard 插件 SDK;TUI Gateway 与 ACP 协议细节。