1. 为什么 Agent 项目迟早要面对 Hook 与中间件做 Agent 系统的人大多经历过这个阶段第一版代码跑通很快模型调用、工具执行、消息回传全写在一个大函数里。等到要加内容审核、要按用户等级切换模型、要统计每次工具调用的耗时你会发现每加一个需求就得往主流程里塞一段 if-else改到最后没人敢动那段核心逻辑。Hook 与中间件模式解决的就是这个问题。它把「在什么时机插入什么逻辑」从主流程里抽出来变成可注册、可排序、可单独开关的扩展点。OpenClaw 的 Hook 系统就是围绕 Agent 生命周期设计的一套事件机制覆盖从消息进入、模型选择、Prompt 构建、工具调用到响应返回的完整链路。适合正在自研 Agent 框架、或者准备给现有 Agent 加审核/日志/灰度能力的开发者尤其是那些已经感觉到「主流程越来越臃肿」的团队。这篇文章不空谈概念我会把 OpenClaw Hook 的两层结构拆开给出可复制的注册骨架和中间件链式调用示例再带你本地跑一遍验证 Hook 触发顺序最后把几个高频报错逐个排掉。你可以直接对照自己的项目改。2. OpenClaw Hook 系统的两层设计2.1 内部 Hook 与插件 Typed Hook 的分工OpenClaw 的 Hook 分两层理解这个分层是后面所有配置的前提。内部 Hook 是框架自身生命周期的事件总线事件类型覆盖 command、session、agent、gateway、message 五类支持{type}:{action}的细粒度命名比如message:received、gateway:startup、agent:end。这一层偏底层主要给框架内部模块和需要感知全局状态的插件用。插件 Typed Hook 是暴露给业务开发者的强类型扩展点名字更语义化before_model_resolve决定用哪个模型、before_prompt_build注入上下文和 System Prompt、llm_input拦截或改写 LLM 输入、subagent_spawning拦截子 Agent 创建。这一层是你日常写业务逻辑主要打交道的地方。一个请求进来典型顺序是message:received接收消息 →before_model_resolve决定模型 →before_prompt_build注入上下文 →llm_input拦截或修改输入 → 模型调用 → 结果返回 → 后置 Hook 做日志和分析。记住这条链路后面排查触发顺序全靠它。2.2 Void Hook 与 Modifying Hook 的执行差异这是 OpenClaw Hook 设计里最关键的一个区分也是很多人踩坑的地方。Void Hook 是「发射即忘」型多个 handler 并行执行不关心返回值用于观测和记录。message_received、agent_end、llm_input都属于这类适合做日志、审计、监控这些不影响主流程的事情。因为并行所以快但你不能指望它改变数据流。Modifying Hook 是「可修改数据」型多个 handler 串行按优先级依次执行每个 handler 的返回值会和前面的结果合并最终产出一个修改后的结果。before_prompt_build可以往 prompt 里注入内容before_tool_call可以拦截或修改工具参数。因为串行所以能做管道式的数据变换但执行顺序完全依赖 priority。注意写 Modifying Hook 时你拿到的数据可能已经被前面的 Hook 改过了。不要假设输入是原始值。2.3 和 LangChain Callback 的对比LangChain 的 Callback 机制思路类似通过 CallbackManager 注册on_llm_start、on_llm_end这些回调。区别在于 LangChain 的 Callback 更偏向观察和记录而 OpenClaw 的 Hook 可以直接修改数据流——before_model_resolve能换掉模型、llm_input能改写输入内容拦截能力更强。如果你需要的是「在关键节点改变行为」而不只是「记录发生了什么」OpenClaw 这套设计更顺手。3. 可复制的 Hook 注册配置骨架3.1 基础注册与优先级插件注册 Hook 的方式很直观核心就是api.on// hooks/register.js export default function register(api) { // 高优先级先执行数字越大越靠前 api.on(before_prompt_build, injectUserPreference, { priority: 10 }); // 低优先级后执行 api.on(before_prompt_build, appendSafetyHint, { priority: 5 }); // Void Hook并行执行不返回值 api.on(message_received, logIncomingMessage); api.on(agent_end, recordTokenUsage); }priority 控制多个 Hook 的执行顺序源码里按(b.priority ?? 0) - (a.priority ?? 0)降序排列也就是数字越大越先执行。多个插件监听同一事件时按 priority 从高到低依次跑。3.2 Modifying Hook 的链式处理Modifying Hook 的 handler 拿到的是上一个 Hook 处理后的结果返回新值继续往下传// 注入用户语言偏好 async function injectUserPreference(ctx) { const lang ctx.user?.preferredLanguage || zh; return { ...ctx, prompt: ${ctx.prompt}\n\n请使用${lang zh ? 中文 : 英文}回复。, }; } // 追加安全提示注意这里拿到的是上一个 Hook 改过的 prompt async function appendSafetyHint(ctx) { return { ...ctx, prompt: ${ctx.prompt}\n\n不要输出任何个人隐私信息。, }; }两个 Hook 都改 promptpriority 10 的先跑priority 5 的后跑最终 prompt 里中文指令在前、安全提示在后。如果两个 Hook 互相冲突比如一个要加中文指令一个要加英文指令就得在 priority 上做好约定或者抽出一个更高层的配置统一管理。3.3 中间件链式调用示例如果你更习惯中间件风格可以把 Hook 包装成链式调用// middleware/chain.js function compose(middlewares) { return function (ctx, next) { let index -1; function dispatch(i) { if (i index) return Promise.reject(new Error(next() called multiple times)); index i; const fn middlewares[i]; if (!fn) return Promise.resolve(); return Promise.resolve(fn(ctx, () dispatch(i 1))); } return dispatch(0); }; } const chain compose([ async (ctx, next) { ctx.startTime Date.now(); await next(); ctx.cost Date.now() - ctx.startTime; }, async (ctx, next) { if (containsSensitive(ctx.input)) { ctx.blocked true; return; // 不调用 next中断链路 } await next(); }, async (ctx, next) { ctx.model pickModelByABTest(ctx.userId); await next(); }, ]); // 在 before_model_resolve 里挂载 api.on(before_model_resolve, async (ctx) { await chain(ctx, async () {}); return ctx; });这套 compose 就是经典 Koa 中间件模型next()之前是前置逻辑之后是后置逻辑不调用next()就中断。你可以把它和 OpenClaw 的 Hook 混用Hook 负责生命周期挂载点中间件负责单个挂载点内部的逻辑编排。4. 本地验证 Hook 触发顺序光看代码不够得实际跑一遍确认顺序。下面是我常用的验证步骤。第一步写一个只做打印的探针插件// hooks/probe.js export default function register(api) { const events [ message:received, before_model_resolve, before_prompt_build, llm_input, agent_end, ]; events.forEach((name, idx) { api.on(name, async (ctx) { console.log([PROBE] ${name} fired, seq${idx}); return ctx; }, { priority: 100 }); }); }第二步启动本地 Agent 并发一条测试消息node ./scripts/start-agent.js --plugin ./hooks/probe.js curl -X POST http://localhost:3000/agent/chat \ -H Content-Type: application/json \ -d {sessionId:test-1,message:你好}第三步观察控制台输出。正常应该看到[PROBE] message:received fired, seq0 [PROBE] before_model_resolve fired, seq1 [PROBE] before_prompt_build fired, seq2 [PROBE] llm_input fired, seq3 [PROBE] agent_end fired, seq4如果顺序不对八成是 priority 配错了或者某个 Hook 被注册成了 Void 类型导致并行乱序。实测下来把探针 priority 设成 100 能保证它排在最前面方便观察。5. 本篇常见错排查5.1 Hook 不触发最常见的原因是事件名拼错。OpenClaw 内部 Hook 用{type}:{action}格式比如message:received而 Typed Hook 用下划线比如before_model_resolve。两种命名混用是高频错误。另外检查插件是否真的被加载——在启动日志里搜插件名没加载上自然不会触发。5.2 修改不生效如果你在 Void Hook 里改数据改了也不会生效因为 Void Hook 不参与数据流。确认你用的是 Modifying Hook。还有一种情况是返回了新对象但没被合并检查 handler 是否return ctx漏了 return 等于没改。5.3 单个 Hook 报错拖垮整个请求比较稳妥的做法是每个 Hook 执行都用 try-catch 包住单个 Hook 报错不影响后续 Hook 和主流程。OpenClaw 的内部 Hook 就是这个策略报错打日志然后继续往下走。但某些关键 Hook 比如内容审核报错了可能应该直接终止请求不能放行。更好的设计是注册时声明critical: true/falsecritical 的报错就中断非 critical 的跳过api.on(before_prompt_build, auditHandler, { priority: 20, critical: true });5.4 多个 Hook 改同一字段结果混乱靠 priority 排序解决但更根本的是约定好职责边界。我的做法是每个 Modifying Hook 只改自己负责的字段prompt 注入统一走一个「prompt 组装器」其他 Hook 往ctx.extraContext里塞数据最后由组装器统一拼。这样避免多个 Hook 直接竞争同一个字符串。6. 接入与调试的下一步Hook 系统跑通之后建议先把日志和审核这两个 Void Hook 接上它们风险最低、收益最直接。等链路稳定了再上before_model_resolve做模型灰度、before_prompt_build做上下文注入这类 Modifying Hook。调试阶段最实用的技巧是给每个 Hook 加一个traceId把同一次请求经过的所有 Hook 串起来出问题时一眼能看出卡在哪个节点。如果你需要管理多个项目的 Key 和调用配额可以在控制台里按项目拆分https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。生成和管理 API Key 走这里https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。接入细节和字段说明查文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。想先验证模型返回是否符合预期可以直接在模型对话里试https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。如果是要长期跑编码类 Agent、需要稳定的调用计划看 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。API 入口统一在 https://taotoken.net/api 。最后留一个我踩过的坑Hook 的 priority 不要设得太密集留出间隔比如 10、20、30后面插入新 Hook 时不用大改。顺序一旦定下来就写进注释团队里其他人加 Hook 时才知道该插在哪。
阅读完成 · 觉得有帮助?