跳至主要内容

Hermes Agent 二次开发实战手册

从入门到精通:架构、配置、扩展点与工程实践

版本说明:本书基于 Nous Research 官方开发者文档与官方 GitHub 仓库文档(截至 2026-09-30)整理,并参考少量公开的第三方插件仓库作为生态佐证。

依据与边界(请先读)

  1. 本书以官方文档为准,没有逐行通读仓库源码。凡涉及类构造参数、字段全集的地方,我会标注「⚠ 以源码为准」。
  2. 标注「伪代码」的片段是示意,不保证直接可运行;标注「官方示例」的取自或改编自官方文档。
  3. Hermes 迭代很快(2026-09 刚做过一次大规模模块拆分),落地前请先跑 §11 的自查命令。

目录

  1. 认识 Hermes 与二次开发的心智模型
  2. 架构全景
  3. 配置与目录体系
  4. 扩展面总览与选型
  5. 零代码扩展:配置、Skill、MCP、自定义模型、Cron
  6. Hook:三类钩子与事件全表
  7. Python 插件开发
  8. 专用插件:模型、平台、记忆、上下文引擎
  9. 把 Hermes 当服务:API Server 与嵌入
  10. 实战案例集
  11. 安全、测试与升级策略
  12. 学习路线与源码阅读地图

  13. 附录 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 六条设计原则(决定扩展怎么写)

  1. Prompt 稳定:会话中系统提示不变,保护 prompt cache。→ 插件想影响模型,注入到用户消息(pre_llm_call),别改系统提示。
  2. 可观察、可中断:每次工具调用、API 请求都有回调;可被取消。→ 你的插件也要能快速返回。
  3. 平台无关核心:平台差异留在入口。
  4. 松耦合:可选子系统靠注册表 + check_fn。→ 你的插件依赖缺失时应降级而非崩溃。
  5. Profile 隔离:每个 profile 独立 HERMES_HOME。→ 插件状态必须 profile 感知。
  6. 核心是窄腰:见 §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.plugins entry 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。

  1. MCP:§5.3 的 corp-tickets(search_tickets);CI 系统同理封成 corp-ci。
  2. 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 字。
## 验证
- 每条失败部署都有对应处理建议。
  1. 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/。
  • 只用官方文档化的 ctx API;回调一律 **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 并按提示迁移到 ctx API。

如果非改核心不可:先确认能否用 Middleware/Hook 达成;不能就向上游提 PR 扩大通用插件面;实在要 fork,把补丁做成最小集合并在每次上游发版时 rebase,同时把「为什么需要」写进文档。

11.2 安全清单

  1. 终端后端隔离:生产/服务化一律 terminal.backend: docker(或其他隔离后端);主机直跑只用于个人开发机。
  2. 最小权限凭据:给 Agent 的云/数据库/代码托管凭据只读优先,按环境分离。
  3. 入站白名单:任何聊天平台/API 都必须有发送者控制;API Server 必须强 Key,不暴露公网。
  4. 插件供应链:插件与 Hermes 同进程运行,等同于给了它全部权限。第三方插件(含 pip 包)先审代码/锁版本/放私有源。
  5. 策略是护栏不是边界:黑名单、审批只能降低事故概率,不能替代隔离。
  6. 密钥管理:只放 .env / type: secret 配置;日志与审计不要落原文。
  7. 可观测:接入案例 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 协议细节。

此博客中的热门博文

Elasticsearch 读写原理指南

### 1. 什么是 segment,里面装了什么? 在 Lucene(也是 Elasticsearch)里,索引被切分成若干 **segment(段)**,每个 segment 是一个完整的、只读的倒排索引单元。一个 segment 包含: * **倒排词典** —— 用 **FST(Finite‑State Transducer)** 以高度压缩的形式保存每个字段出现的所有 term 以及 term→ord 的映射。对应的磁盘文件是 `*.tim`(新版)或 `*.tis/*.tii`(旧版)。 * **倒排列表(postings)** —— 保存每个 term 出现的文档 ID、频次、位置信息等,文件名通常是 `*.doc`、`*.pos`、`*.pay`。 * **存储字段**(_source、store:true 的字段)—— 以二进制块的形式写入 `*.fdt` / `*.fdx`。 * **doc‑values、norms、向量** 等辅助结构,分别保存在 `*.dv`、`*.norm`、`*.tv` 等文件里。 * **deleted‑docs bitmap**(`*.del`),标记哪些文档已被删除或被更新。 所有这些文件在 segment **写入磁盘后即成为只读**,后续的查询只能读取,永远不会在原文件上进行增删改。 --- ### 2. 原始文档和 FST 为什么都在 segment 里? * **原始文档**:Elasticsearch 默认把完整的 JSON(_source)以及任何 `store:true` 的字段写入 segment 的 `*.fdt/*.fdx` 文件。每个 segment 保存自己的那部分文档,旧的 segment 在合并前仍然保留,直到合并后被删除。 * **FST**:每个字段的词典在每个 segment 中单独维护,采用 FST 进行前缀共享和字节压缩。这样即使同一个 term 在多个 segment 中出现,也会在每个 segment 里拥有独立的映射,查询时只需要在对应 segment 的 FST 中定位即可。 --- ### 3. 查询时到底是怎么遍历 segment 的? 1. **请求入口**      客户端的搜索请求先到达 **协调节点**,协调节点把请求 ...

PyTorch学习路线图

  第一阶段:基础知识(2-3周) 第1周:Python与机器学习基础 Python数据结构与NumPy基础 张量概念与线性代数基础 机器学习基本概念 第2-3周:PyTorch核心 张量操作与计算图 自动微分(autograd)机制 数据加载与预处理(DataLoader, Dataset) 构建神经网络模块(nn.Module) 第二阶段:深度学习基础(4-6周) 第4周:线性模型与优化器 线性回归与逻辑回归实现 优化器(SGD, Adam等) 损失函数与评估指标 第5-6周:基础神经网络 多层感知机(MLP) 卷积神经网络(CNN) 循环神经网络(RNN, LSTM, GRU) 第7-8周:训练技巧 模型保存与加载 学习率调度 正则化技术 迁移学习 第三阶段:进阶应用(6-8周) 第9-10周:计算机视觉 图像分类 目标检测 图像分割 视觉Transformer 第11-12周:自然语言处理 词嵌入 序列到序列模型 Transformer架构 预训练语言模型应用 第13-14周:生成模型 自编码器 变分自编码器(VAE) 生成对抗网络(GAN) 扩散模型 第四阶段:工程实践(4-6周) 第15-16周:模型部署 模型量化与优化 TorchScript与ONNX导出 服务化部署 移动端部署 第17-18周:高级训练技术 分布式训练 混合精度训练 梯度累积与梯度裁剪 模型剪枝与蒸馏 第19-20周:项目实战 完整项目流程 模型性能优化 工业级代码实践

LLM缓存详解

 可以把“大模型缓存”理解成: 把已经算过的结果(或中间结果)存下来,下次尽量复用 。但这里面其实分几层,不只是简单的“问题→答案”缓存。 1️⃣ 常见的几种缓存类型 (1)KV Cache(推理内部缓存) Transformer 在生成时,会把前面 token 的 Key/Value 向量 缓存下来。 本质:避免重复计算 attention 作用: 同一请求内部加速 特点: 👉 只对“同一上下文继续生成”有效 👉 不跨用户、不跨请求 这类缓存是你体感“流式输出越来越快”的原因之一。 (2)Prompt Cache(提示词缓存) 缓存的是: 相同(或高度相似)的 prompt → 对应的中间表示 / 输出 典型场景: 系统提示词(system prompt)很长 多轮对话里前文基本不变 👉 这里能省掉 前缀计算成本(prefill) (3)Embedding / 语义缓存(Semantic Cache) 这个才是你问题的关键 👇 不是按“字符串完全一致”,而是: 把问题转成向量 → 找“语义相似”的历史问题 → 直接复用答案 2️⃣ 为什么命中缓存成本低很多? 因为大模型推理成本主要在两块: (1)Prefill(吃 prompt) 复杂度 ~ O(n²) 很贵(尤其长 prompt) (2)Decode(逐 token 生成) 每个 token 都要算一遍模型 而缓存命中后: KV cache:不用重复 attention Prompt cache:不用重新 encode 语义缓存: 直接跳过模型推理 👉 相当于从: 几十~几百毫秒 + GPU算力 变成: 一次向量检索(毫秒级)+ 直接返回 所以成本差一个数量级是正常的。 3️⃣ “每个人问法不同,怎么命中缓存?” 这是核心难点,也是工程重点👇 ❌ 不能靠字符串匹配 比如: “今天天气怎么样” “今天外面热不热” 字符串完全不同 → 必须 miss ✅ 用语义相似度(Embedding) 流程一般是: 把问题转 embedding(向量) 在向量数据库里找 TopK 相似问题 如果相似度 > 阈值(比如 0.9) 直接返回缓存答案 一个简单示意 Q1: 北京天气怎么样 → embedding A Q2: 北京今天热吗 → embedding B cosine(A, B) ≈ 0.95...