全文约 1557 字,读完大约 3 分钟。
Hermes 源码解读八:Plugin 与 Middleware,扩展点如何不污染核心
一个 Agent 项目发展到后面,一定会有人想加能力。
加一个工具。
加一个 provider。
加一个平台 adapter。
加一个 memory backend。
加一个 LLM 请求改写。
加一个工具执行审计。
如果每个需求都往核心代码里加分支,核心运行时很快就会变成插件坟场。
我读 Hermes 的 plugin 和 middleware 源码时,最值得看的一点是:
Hermes 在把扩展能力从核心主流程里拆出去,但仍然保留统一治理入口。
这篇看 hermes_cli/plugins.py、hermes_cli/middleware.py、docs/middleware/README.md。
Plugin 不是随便 import 一个 Python 文件

Hermes 的插件不是“把某个目录 import 进来”这么简单。
插件要有 plugin.yaml。
PluginManager 会扫描 bundled、user、project、pip entrypoint 等来源。
大致来源包括:
repo bundled plugins
~/.hermes/plugins
project .hermes/plugins
pip entry points源码里还有一个重要规则:后面的来源可以覆盖前面的同名插件。
这意味着用户或项目可以替换 bundled 插件行为。
同时,插件不是默认全量执行。启用、禁用、safe mode、分类发现、平台插件延迟加载,都会影响它是否真的进入运行时。
这比简单 import 安全得多。
因为扩展点的第一层治理就是:
系统必须知道它加载了什么、从哪里加载、是否启用、属于哪类能力。
PluginContext 是扩展能力的登记处
插件加载时会拿到 PluginContext。
它可以注册很多东西:
| 注册能力 | 典型用途 |
|---|---|
register_tool() | 新工具进入 ToolRegistry |
register_hook() | 观察生命周期事件 |
register_middleware() | 改写请求或包裹执行 |
register_skill() | 提供插件内 skill |
register_platform() | 新消息平台 adapter |
| provider 类注册 | web/search/browser/tts/image/video/model 等 backend |
这个设计很像一个“受控扩展总线”。
插件不是到处 monkey patch。
它要通过 context 把自己接到系统认可的扩展点上。
这点很关键。
如果插件能随便改核心对象,短期很灵活,长期一定不可维护。
Hermes 的方向是:核心暴露有限的登记接口,插件在这些接口里扩展。
Hook 和 Middleware 要分开
Hermes 文档里有一句很清楚:
Hook 是 observer。
Middleware 是 behavior-changing companion。
这两个概念一定要分开。
| 机制 | 能做什么 | 不该做什么 |
|---|---|---|
| Hook | 观察事件、记录 telemetry、在生命周期点响应 | 不应该随意改写主请求 |
| Middleware | 改写 LLM/tool request,包裹执行 callback | 不应该只拿来做只读日志 |
很多系统会把这两类混在一起,最后 hook 既记录日志又改参数,调试会非常痛。
Hermes 把 middleware 分成四类:
llm_request
llm_execution
tool_request
tool_execution它们分别对应:
- • provider 请求发出前改 kwargs;
- • provider 真正执行时包一层;
- • tool 参数进入 guardrail/approval 前改写;
- • tool 真正 dispatch 时包一层。
这就是扩展点的边界。
不是所有插件都可以在任意位置改任意东西。
Middleware 的 next_call 是关键约束

执行型 middleware 会收到 next_call。
它可以调用 next_call() 继续链路,也可以短路。
文档里专门强调:正常情况下应该调用一次。
这和 Web 框架中间件很像。
middleware A
→ middleware B
→ base execution但 Agent 的 middleware 更敏感。
因为它包裹的是:
- • LLM provider call;
- • tool execution;
- • approval 之前或之后的参数;
- • streaming、retry、interrupt、hook 行为。
如果 middleware 乱调用两次 next_call,可能导致同一个工具执行两次。
如果它改写 path 或 command,后续 approval 看到的是改写后的值。
这就是为什么 Hermes 文档反复写 safety notes。
扩展点越强,约束越要清楚。
fail-open 是取舍,不是绝对安全
Hermes middleware 失败时倾向 fail-open:记录 warning,然后继续后面的 middleware 或 base path。
这是一种可用性取舍。
它避免一个观测、路由或请求改写插件把整个 Agent 搞挂。
但它也意味着:如果某个插件本来承担强安全策略,不能只依赖普通 middleware fail-open 行为。
更严格的安全边界应该在 approval、guardrail、sandbox、hardline blocklist 这类核心路径里。
这也是一个值得借鉴的分层:
扩展策略
可以 fail-open
系统底线
不能 fail-open插件系统负责灵活。
安全底线负责硬约束。
两者不能混为一谈。
这套扩展系统给自建 Agent 的启发
Agent 平台越做越大,扩展点一定要提前设计。
但扩展点不是越多越好。
至少要回答这些问题:
| 问题 | 不回答会怎样 |
|---|---|
| 插件从哪里发现? | 来源不可控,排障困难 |
| 插件是否默认启用? | 能力边界不清 |
| 同名插件谁覆盖谁? | 用户定制不可预测 |
| 插件能注册什么? | 到处 monkey patch |
| hook 和 middleware 是否分开? | 观察和改行为混乱 |
| middleware 失败怎么办? | 一个插件拖垮核心 |
| 安全策略能否被插件绕过? | 扩展点变后门 |
Hermes 这部分源码给我的启发是:
扩展性不是把核心打开,而是定义哪些地方可以打开。
插件系统最怕两个极端。
一个是完全不开放,最后所有需求都进核心。
另一个是完全开放,最后核心行为没人说得清。
Hermes 在中间做了一个比较务实的选择:工具、平台、provider、skill、hook、middleware 都可以扩展,但都要经过注册和运行时管线。
这对后端工程师很熟悉。
好的框架不是没有扩展点,而是扩展点有位置、有顺序、有失败语义。
Plugin 解决“能力怎么接进来”,Skill 和 Memory 解决另一个问题:
Agent 怎么把经验、工作流和长期偏好沉淀下来。
如果你正在做自己的 Agent 框架,建议认真看这一层。扩展点一旦长歪,后面所有功能都会绕着它还债。

夜雨聆风