乐于分享
好东西不私藏

Hermes 源码解读八:Plugin 与 Middleware,扩展点如何不污染核心

Hermes 源码解读八:Plugin 与 Middleware,扩展点如何不污染核心

全文约 1557 字,读完大约 3 分钟。

Hermes 源码解读八:Plugin 与 Middleware,扩展点如何不污染核心

一个 Agent 项目发展到后面,一定会有人想加能力。

加一个工具。

加一个 provider。

加一个平台 adapter。

加一个 memory backend。

加一个 LLM 请求改写。

加一个工具执行审计。

如果每个需求都往核心代码里加分支,核心运行时很快就会变成插件坟场。

我读 Hermes 的 plugin 和 middleware 源码时,最值得看的一点是:

Hermes 在把扩展能力从核心主流程里拆出去,但仍然保留统一治理入口。

这篇看 hermes_cli/plugins.pyhermes_cli/middleware.pydocs/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 框架,建议认真看这一层。扩展点一旦长歪,后面所有功能都会绕着它还债。