Hermes 中间件
Hermes 中间件是 observer hook 的、可改变行为的搭档。Observer hook 报告发生了什么;中间件能通过在执行前重写请求、或包裹执行回调本身来改变发生什么。
本契约刻意后端中立。插件可用它做本地策略、请求塑形、追踪、自适应路由、缓存控制、沙箱选择,或交接给 NeMo Relay 等运行时,而无需改动 Hermes 的规划器、模型 provider 适配器、工具注册表、记忆或 CLI UX。
启用中间件后,插件可以:
- 在 Hermes 调用 provider 之前重写 LLM provider 请求 kwargs。
- 在护栏、审批检查、hook 与工具执行看到工具参数之前重写它们。
- 包裹真实的 LLM 执行回调,同时保留 Hermes 的重试、流式、中断与 hook 行为。
- 包裹真实的工具执行回调,同时保留 Hermes 的护栏、审批、工具后 hook 与工具结果变换。
契约
插件从 register(ctx) 注册中间件:
def register(ctx):
ctx.register_middleware("llm_request", on_llm_request)
ctx.register_middleware("llm_execution", on_llm_execution)
ctx.register_middleware("tool_request", on_tool_request)
ctx.register_middleware("tool_execution", on_tool_execution)
每个中间件回调收到:
telemetry_schema_version:当前为hermes.observer.v1middleware_schema_version:当前为hermes.middleware.v1- 运行时上下文,如
session_id、task_id、turn_id、api_request_id、provider、model、api_mode、tool_name,以及适用时的tool_call_id。
支持的中间件种类:
| 种类 | 载荷 | 返回形状 | 用途 |
|---|---|---|---|
llm_request | request、original_request | {"request": {...}} | 在 provider 执行前替换生效的 provider kwargs。 |
tool_request | tool_name、args、original_args | {"args": {...}} | 在 hook、护栏、审批与执行之前替换生效的工具参数。 |
llm_execution | request、original_request、next_call | 任意 provider 响应 | 包裹或替换真实的 provider 调用。 |
tool_execution | tool_name、args、original_args、next_call | 任意工具结果 | 包裹或替换真实的工具调用。 |
请求中间件可返回可选的追踪字段:
return {
"request": updated_request,
"source": "my-plugin",
"reason": "selected fallback model",
}
Hermes 把这些追踪条目作为 middleware_trace 存入后续 observer hook 载荷。
执行中间件收到一个 next_call 回调。调用它以继续链条:
def on_tool_execution(**kwargs):
result = kwargs["next_call"](kwargs["args"])
return result
若多个插件注册同一种执行中间件,Hermes 按注册顺序把它们跑成嵌套链。中间件失败是 fail-open:Hermes 记一条 warning,继续下一个中间件或基础运行时路径。一个每次调用都以同样方式失败的回调(通常是签名命名了中间件不发送的字段)会在 WARNING 报一次——消息列出它确实提供的字段——相同重复进 DEBUG,因此一个错误声明的中间件不会刷爆日志;插件重载会重置该报告。
执行顺序
LLM 调用
对每个 provider 请求,Hermes 按此顺序应用中间件:
- 从当前对话构建 provider kwargs。
- 应用
llm_request中间件。 - 以生效请求发出
pre_api_requestobserver hook。 - 经
llm_execution中间件运行 provider 执行。 - 发出
post_api_request或api_request_errorobserver hook。
请求中间件看到完整 provider kwargs,包括 messages 或 Responses API input、模型设置、工具定义、流选项与 provider 专属选项。执行中间件收到同一生效请求外加 next_call。
工具调用
对每次工具调用,Hermes 按此顺序应用中间件:
- 解析并强转模型给出的工具参数。
- 应用
tool_request中间件。 - 对生效参数跑正常的 Hermes 执行前路径:工具可用性检查、observer 块指令、护栏与审批检查。
- 经
tool_execution中间件运行工具执行。 - 发出
post_tool_callobserver hook。 - 在结果追加回对话上下文之前应用
transform_tool_resulthook。
工具请求中间件在审批检查之前运行。谨慎使用它:被重写的路径、命令或 URL 就是下游策略将要评估的值。
启用
中间件只对已启用插件运行。对内置插件:
hermes plugins enable <plugin-name>
隔离本地测试时,用同一个 HERMES_HOME 做插件启用与 agent 运行:
export HERMES_HOME=$HOME/.hermes/cache/scratch/hermes-middleware-test
mkdir -p "$HERMES_HOME"
hermes plugins enable <plugin-name>
hermes chat --query 'Reply exactly ok'
对源码 checkout,用 PM 开发者工作流 和一个独立的开发 home,让运行时从工作树看到插件与中间件:
export HERMES_HOME="$HOME/hermes-middleware-test"
export HERMES_RUNTIME_DIR="$HERMES_HOME/tools"
source ./activate
python hermes plugins enable <plugin-name>
python hermes chat --query 'Reply exactly ok'
通用插件示例
下面的示例刻意很小。它们展示中间件契约形状,不依赖 NeMo Relay。
LLM 请求中间件
这个插件给 provider 请求打标签并记录一条中间件追踪条目:
def register(ctx):
ctx.register_middleware("llm_request", tag_llm_request)
def tag_llm_request(**kwargs):
request = dict(kwargs["request"])
extra_body = dict(request.get("extra_body") or {})
extra_body.setdefault("metadata", {})["hermes_middleware_demo"] = True
request["extra_body"] = extra_body
return {
"request": request,
"source": "middleware-demo",
"reason": "tagged provider request",
}
生效请求被传给 pre_api_request、provider 执行与 post_api_request。
工具请求中间件
这个插件把 terminal 调用约束到一个已知工作目录:
from pathlib import Path
def register(ctx):
ctx.register_middleware("tool_request", normalize_terminal_workdir)
def normalize_terminal_workdir(**kwargs):
if kwargs.get("tool_name") != "terminal":
return None
args = dict(kwargs["args"])
args.setdefault("workdir", str(Path.home() / ".hermes" / "cache" / "scratch" / "hermes-middleware-demo"))
return {
"args": args,
"source": "middleware-demo",
"reason": "defaulted terminal workdir",
}
因为这在 hook 与审批之前运行,下游遥测与策略观察到的是被重写的 workdir。
LLM 执行中间件
这个插件包裹 provider 调用并保留原始 provider 响应:
import time
def register(ctx):
ctx.register_middleware("llm_execution", time_llm_execution)
def time_llm_execution(**kwargs):
started = time.monotonic()
response = kwargs["next_call"](kwargs["request"])
elapsed_ms = int((time.monotonic() - started) * 1000)
print(f"llm_execution elapsed_ms={elapsed_ms}")
return response
返回 Hermes 从 provider 适配器期望的同一响应形状。不要把响应包进插件专属信封,除非运行时其余部分期望那个信封。
工具执行中间件
这个插件包裹工具执行,同时保留工具结果:
def register(ctx):
ctx.register_middleware("tool_execution", annotate_tool_execution)
def annotate_tool_execution(**kwargs):
result = kwargs["next_call"](kwargs["args"])
# 指标、日志或外部路由可在此发生。
return result
执行中间件可调 next_call(modified_args),把改变后的载荷传给后续中间件与基础工具分发器。
插件专属示例应与拥有该行为的插件放在一起。NeMo Relay 执行中间件经 Relay 的发现式用户与系统配置安装,或经 HERMES_NEMO_RELAY_PLUGINS_TOML 选择的显式 plugins.toml;见Relay 共享指标。
安全注意
- 除非明确路由到动态外部系统,中间件应对同一输入确定性。
- 请求中间件应返回完整替换载荷,而非部分补丁。
- 执行中间件应恰好调一次
next_call(...),除非它有意短路执行。 - 若执行中间件在调
next_call(...)之前抛错,Hermes 视其为中间件失败,继续其余中间件链与基础执行。 - 若执行中间件成功调了
next_call(...)然后在后置处理中抛错,Hermes 保留下游结果,不二次运行 provider 或工具。 - 若下游 provider 或工具执行失败,中间件可让该错误传播或有意翻译它。Hermes 不把下游失败转成成功的
None结果。 - 工具请求中间件在审批之前运行。若它改动文件路径、命令、URL 或参数,被改动的值就是护栏与审批评估的对象。
- Observer hook 仍是只读遥测的正确位置。仅当插件需要改动或包裹行为时才用中间件。