🤖HermesBlog
Hermes 功能详解 · 第16篇2026/8/9· Easy Understand Hermes Agent

第16篇:事件钩子——关键时刻自动触发

Hermes 功能详解第16篇:事件钩子。AI 每步动作都可以挂上钩子,实现自动化的精细控制。

事件钩子:给智能体装门铃

第16篇:事件钩子——关键时刻自动触发

各位小伙伴,欢迎回来!今天咱们聊一个特别实用的功能——事件钩子。简单说,就是让 Hermes 在“关键时刻”自动帮你干活,不用你手动盯着。

什么是事件钩子?

想象一下:你正在跟 Hermes 聊天,突然它卡在一个工具调用循环里出不来了。或者,你想在每次对话开始时自动加载某个记忆。这些场景,事件钩子都能派上用场。

事件钩子就是给 Hermes 装上的“小闹钟”——当特定事件发生时,自动触发预设的动作。就像你设置手机闹钟一样,到点就响,不用你操心。

Hermes 一共有四套钩子系统:Gateway hooks(放在 ~/.hermes/hooks/ 里的 HOOK.yaml + handler.py,只在网关里跑)、Plugin hooks(插件里用 ctx.register_hook() 注册,CLI 和网关都能跑)、Shell hooks(在 profile 的 config.yaml 里用 hooks: 块指向 shell 脚本,CLI、网关以及 Desktop/TUI/dashboard 聊天都能跑),以及 Outbound webhooks(在 ~/.hermes/config.yaml 的 hooks.outbound: 列表里配置,把签名的生命周期事件推送到外部 HTTP 端点)。钩子回调出错时会被隔离并记录日志,不会把 agent 弄崩。

顺带说一句,钩子可不全是“只读观察”的:指令类钩子能改变流程,转换类钩子能替换内容,shell 的 pre_tool_call 钩子甚至能直接拦截或失败关闭。pre_tool_call 的指令优先级是:任何有效的 block 都优先于任何 approve,其次才是第一个有效的 approve;modify 的返回值会浅合并进工具参数。

钩子什么时候触发?

除了前面说的那些场景,钩子还会在这些“收尾时刻”触发:CLI/TUI 的 teardown 阶段(包括 hermes -z 一次性运行结束,成功或失败都算),以及网关的 reset 或 shutdown 路径。这里有个小坑要注意:网关 shutdown 时可能直接收尾,不一定会配一个对应的 on_session_reset 事件——所以别指望在 shutdown 时一定能收到 reset 回调。

另外,on_session_finalize 这个钩子虽然名字里带“finalize”,但它其实是在每一轮对话结束后都会触发,所以它没法给用户显示任何东西——毕竟一轮对话刚结束,用户还在等着下一句呢。想在会话真正结束时给用户留句话,得改用 on_session_finalize 的返回值:返回一个非空字符串,或者带 "message" 字段的字典,Hermes 就会把它当通知展示给用户。它不会变成模型的一轮对话,也不会改写助手的回复;返回 None 就什么都不显示。展示位置随场景不同:CLI 的 /new 会在新会话开始前打印在终端;CLI 退出时会打印在退出摘要上方;hermes chat -q / -z 一次性运行会打到 stderr,这样 stdout 里还是只有答案;TUI 的 /new 会作为新会话记录里的一条系统行;网关的 /new 或 /reset 会作为独立消息发给拥有该会话的聊天;网关 shutdown / restart 会在适配器断开前发给每个活跃聊天。没人看的时候它就不发了——比如网关里的空闲过期和缓存淘汰、退出 TUI、被 reaper 或断线关掉的 TUI/Desktop 会话、Desktop 的新建聊天或删除聊天,以及 cron 任务(cron 不做会话 finalize)。这些情况下消息会被丢弃,但钩子本身照常运行。

出错时怎么办?默认“失败开放”

钩子跑挂了会怎样?默认情况下是失败开放(fail open):JSON 格式写错、普通的非零退出码、超时——这些都会记一条警告,但不会中断 agent 循环,agent 该干嘛干嘛。

唯一的例外是 pre_tool_call 退出码为 2 的情况(下面会讲),它会拦截工具调用。另外,如果你给钩子配了 fail_closed,行为就会变,具体看下面的说明。

情况 默认行为 fail_closed 时
非零退出码且没有可识别的指令 警告,继续 拦截

网关钩子的信任模型:把文件放进去就是“同意”

网关钩子目录是一个**“放置即信任”的扩展点:它没有启用列表,plugins.enabled / plugins.disabled 对它也不适用——网关钩子不是插件。这个信任模型从 3988c3c245f(2026 年 4 月)起就是文档化的约定,当时对比表里首次把它的同意模型记为“隐式(目录信任)”。加载规则很简单:网关启动时(HookRegistry.discover_and_load(),由 gateway/run_startup.py 调用)扫描一次 <profile home>/hooks/(默认 profile 就是 ~/.hermes/hooks/),每个同时包含 HOOK.yaml(能解析成带非空 events 列表的映射)和 handler.py 的子目录都会被加载;缺文件的目录静默跳过,清单无效或 events 为空的会打一条 [hooks] Skipping … 日志。handler.py 是在进程内导入的——模块体在导入时就执行,handle 函数被注册到声明的事件上,运行权限跟网关本身一样(凭据、工具、插件状态全都有),没有沙箱、没有首次确认,HERMES_SAFE_MODE 也不会跳过这个加载器。加载过程不查任何启用列表。多 profile 网关下,每个被服务的 profile 各自的 hooks/ 会在该 profile 内首次触发事件时加载;CLI、TUI、Desktop 和 cron 从不加载网关钩子。把两个文件丢进目录就是**开启,删掉(或改名)HOOK.yaml 或整个目录就是关闭。能写进你 profile home 的人,本来就能通过 config.yaml 的 shell 钩子或 plugins.enabled 以你的身份跑代码,所以这个目录跟 ~/.hermes/ 的其余部分处在同一个信任边界内。所以,放进去之前先读一遍 handler.py,就像启用插件前先看一遍插件代码一样。

硬停止:防止“死循环”的保险丝

新版文档里特别提到了一个实用场景:工具循环硬停止(Tool-loop hard stops)。

默认情况下,tool_loop_guardrails.hard_stop_enabled 是 false。这在交互式 CLI 里没问题——你能看到警告,自己手动干预。但在无人值守的网关模式下(比如 Telegram 机器人),如果 Hermes 陷入重复调用工具的循环,光有警告可不够。

这时候,你需要主动打开“保险丝”:

tool_loop_guardrails:
  hard_stop_enabled: true
  hard_stop_after:
    exact_failure: 5
    idempotent_no_progress: 5

配置好之后,如果连续 5 次完全相同的失败,或者 5 次“看似在干活但毫无进展”的调用,Hermes 就会自动刹车,避免浪费你的 API 额度。

钩子用在哪?

除了硬停止,事件钩子还能用在:

  • 会话开始时自动加载上下文
  • 收到特定关键词时触发特定技能
  • 工具调用失败时自动重试或切换方案

新版文档里还新增了几个值得留意的观察者事件:

  • agent_loop_stopped:当一个真正在运行的 agent 被中断时(比如网关的 _interrupt_and_clear_session,或者 TUI/desktop 的 session.interrupt)会立即触发,回调里能拿到 session_key、platform、reason、invalidation_reason 这些信息,方便你记录或告警。
  • on_human_input_request:当 Hermes 即将阻塞等待真人输入时触发——不管是 sudo 密码提示、clarify 提问,还是危险命令审批,CLI、Ink TUI/Desktop、ACP 和网关平台都覆盖。回调里能拿到 kind("sudo" / "clarify" / "approval")、request_id、session_id、session_key、platform、prompt。注意 prompt 永远会被强制脱敏,你输入的密码或答案不会传进去。approvals.mode=smart 由辅助 LLM 做的决定不会触发它,因为压根没问人。
  • on_human_input_resolved:每个 on_human_input_request 恰好对应一次,等待结束时触发(已回答、跳过、超时、撤回或失败都算)。字段跟请求一样,外加一个 outcome——只给结果,永远不给答案本身。
  • pre_auxiliary_call:辅助 LLM 调用(起标题、压缩、MoA、视觉、审批等)的每次 provider 尝试都会在请求发出前触发,返回值被忽略。回调里能拿到 aux_task、task_id、turn_id、session_id、platform(父轮次的,轮次外为空)、api_request_id、api_call_count、retry_count、streaming、model、provider、base_url、api_mode、request_messages、system_prompt、message_count、tool_count、approx_input_tokens、request_char_count、max_tokens、started_at、request。注意 request_messages 是原始内容(压缩时能看到整段对话记录),优先用脱敏过的 request。
  • post_gateway_admission:网关里每条被接纳的非内部消息触发一次,位置在鉴权、机器人准入、pause/drain、待回复拦截、运行中会话通道和空闲斜杠命令分发之后。被拒绝、被忽略、内部和控制类流量永远到不了它。它在已认领的会话槽里运行,所以同一个聊天的并发消息会排队等它;同时跑在该消息的路由 profile 作用域下,只有那个 profile 的插件会执行。返回 {"action": "handled", "reply": "..."} 就能消费掉这条消息:agent 那一轮被跳过,reply(非空字符串时)走正常路由发出去;省略 reply 就静默消费。返回其它任何值都会照常跑 agent 那一轮。它是失败开放的:回调抛异常、超时(plugins.hook_callback_timeout)或返回别的东西,都会被记录,然后消息继续交给 agent——一个坏插件拦不住整个 profile 的流量。需要失败关闭的插件得自己捕获错误,然后返回带自己失败回复的 handled。传进去的是快照(source 是 SessionSource.to_dict()),别直接往适配器里发东西,要发就返回 reply。去重和持久化接收是插件自己的责任。

写插件钩子的时候有个小细节值得留意:如果某个回调每次都因为同样的原因失败(通常是签名里写了个钩子根本不发的字段,比如把 tool_name/args 写成了 tool_data),Hermes 只会在 WARNING 级别提醒你一次,消息里还会列出这个钩子实际提供的字段;之后重复的相同错误就降到 DEBUG 了,免得一个写错的插件把日志刷爆。

还有一点值得注意:pre_tool_call 回调如果超过 plugins.hook_callback_timeout(或者上一次超时后还在跑),Hermes 会失败关闭——直接拦截这次工具调用并返回超时消息,而不是在没有策略决策的情况下放行;回调抛异常也一样,拦截消息里会点名是哪个回调、报了什么错。卡死的回调会被跳过 60 秒的抑制窗口,之后新的工具调用会再试一次(每个回调最多留三个被放弃的 worker,所以一个永久卡死的插件会带着警告拦住工具调用,而不是悄悄把 agent 卡到重启)。

小贴士

如果你是跑在 Docker 里的网关模式,记得检查 docker logs 里的输出。新版镜像用 s6-overlay 做进程守护,网关崩了会自动重启,日志里会有一条面包屑提示,方便你确认钩子是否生效。

总结

事件钩子就是给 Hermes 装上的“条件反射”——在关键时刻自动响应,不用你时刻盯着。特别是硬停止功能,强烈建议网关用户开启,给无人值守的机器人上一道保险。

下期见!

📖 官方文档

本文根据 Hermes Agent 官方文档编写,原文见:官方文档 › user-guide/features/hooks