Appearance
🐣 小a的安检想法
循环转起来了,小a却想到一个问题:"如果我想在工具执行前拦一下——比如模型要跑
rm -rf,或者想确认一下——该怎么办?难道要改循环的代码?"老z笑了:"好问题。这正是一个成熟 Agent 该有的能力——Hooks(钩子)。"
9.1 Hooks 是什么:循环里的"插手点"
🧙 Hooks 是 Agent 在循环的关键节点上预留的"插手点"。
你不用改循环本身,只要把一段逻辑"挂"上去,Agent 跑到那个节点就会自动调用它。
老z打比方:hook 就像路口的红绿灯——工具是车,红绿灯在车通过前检查它。
两个最核心的 Hook
| Hook | 触发时机 | 能干什么 |
|---|---|---|
beforeToolCall | 工具执行前 | 拦截危险操作、审查参数、改写参数、二次确认 |
afterToolCall | 工具执行后 | 修改结果、标记错误、决定是否提前结束 |
🧙 beforeToolCall 的拦截能力(重点):
它能返回"阻止执行"——模型想调一个危险工具,你在这里拦下来:
模型要调 bash("rm -rf /") ↓ beforeToolCall 检查: • 命令危险?→ 返回"阻止" • 命令安全?→ 放行 ↓ 被阻止时,模型收到"错误结果"而非静默忽略
9.2 为什么需要 Hooks:三种用途
🧙 Hooks 解决三类问题:
用途一:安全(最重要)
Agent 会执行命令、读写文件。没有拦截点,Agent 就是裸奔。 Hooks 让"危险操作前先确认/拦截"成为可能——这是第 15 章"安全与沙箱"的底层机制。
用途二:定制行为
不改循环源码,就能:给工具输出加日志、改结果格式、加二次确认弹窗。扩展逻辑全在外面,循环核心保持纯净。
用途三:解耦
循环是"发动机",Hooks 是"可插拔的仪表盘"。不加 Hooks,循环照跑;加了,行为就变。 这正是"开放扩展,封闭修改"——不用改核心代码,就能定制行为。
9.3 Hooks 的完整家族
🧙 不止 before/afterToolCall,成熟 Agent 在循环的每个节点都有 Hook:
| Hook | 触发时机 | 典型用途 |
|---|---|---|
beforeToolCall | 工具执行前 | 拦截/审查/二次确认 |
afterToolCall | 工具执行后 | 改结果/标错/终止 |
shouldStopAfterTurn | 一轮对话结束时 | 自定义"要不要继续" |
prepareNextTurn | 准备下一轮前 | 改下一轮的模型/上下文 |
convertToLlm | 每次调 LLM 前 | 过滤/转换消息格式 |
🐣 小a:"这么多 Hook,它们覆盖了啥?"
🧙 "覆盖了循环的每个关键节点——工具前后、轮次前后、调 LLM 前。 等于循环被'拆开了接口',你可以在任何环节插手,而不用改循环本身。这就是 Hook 的威力:开放扩展,封闭修改。"
9.4 一个真实例子:危险命令确认
把 Hooks 用在刀刃上——最常见的场景:
Agent 想执行 bash("rm -rf /tmp/cache")
↓
beforeToolCall 触发:
1. 检查命令是否危险(rm -rf 等)
2. 危险 → 问用户:"确认执行 rm -rf /tmp/cache?(y/N)"
↓
用户拒绝 → 返回"blocked"结果给模型
用户同意 → 放行执行
↓
模型收到"被阻止"的错误结果 → 调整策略🧙 为什么"被阻止"要告诉模型?
fail loud,不静默吞掉。 模型知道自己被拦了、为什么被拦,才能调整策略(换个命令、换个方式)。如果静默忽略,模型会困惑"为什么没效果",可能反复重试同一操作。
9.5 Hooks vs 其他扩展机制
| 是什么 | 什么时候介入 | |
|---|---|---|
| Hooks | 在循环节点插手(拦/改) | 工具执行前后、轮次边界 |
| Tool(第 6 章) | 给 Agent 新增能力 | 模型要调用时 |
| Skill(第 11 章) | 打包的能力包(工具+提示词+知识) | Agent 启动时加载 |
| MCP(第 10 章) | 工具的统一标准 | Agent 调用外部工具时 |
🧙 一句话区分:
- Hooks = 改行为(拦截、修改现有流程)
- Tool = 加能力(让 Agent 会干新事)
- Skill = 打包能力(一组工具+提示词+知识)
- MCP = 统一接口(让工具跨框架共享)
9.6 Hooks 的代价
🧙 Hooks 不是免费的:
- 循环变难预测:挂了 beforeToolCall,模型不知道自己的请求会被改——调试时要想"是不是被 hook 拦了"
- 串多了会乱:多个 hook 逻辑要自己在函数里组织,顺序和覆盖要想清楚
- 可能拖慢:hook 是 async 的(要等用户确认、查远程),慢 hook 会卡住循环
关键:给 hook 设"可中断"机制——用户按了取消,hook 必须响应,不能无限等。
9.7 拦截 vs 订阅:两种介入方式,别混为一谈
"前面讲的 hook,好像都是'拦'?"小a问,"那我想'只看不改'(比如记录日志)呢?"
老z点头:"好问题。Hook 其实有两种介入方式,很多人混着用,结果设计得一团糟。"
🧙 两种介入方式:
拦截式(intercept) 订阅式(subscribe) 干什么 在节点改流程(拦下/改写/决定放不放行) 在节点旁听(观察、记录,不干预) 典型例子 beforeToolCall阻止危险命令记一条"模型调了 bash"的日志 返回值 会影响流程(放行/阻止/改写) 不影响流程(纯观察) 心智模型 门卫(决定谁进谁不进) 摄像头(只记录,不拦人) 设计 Hook 时先问一句:我是要"改流程"还是"看流程"? 想改,用拦截式;只想记录,用订阅式——别把记录逻辑写进拦截 hook 里,也别用订阅式 hook 假装能拦截。
生命周期 hook:不止工具级
🧙 前几节讲的都是"循环内"的 hook。成熟框架还有"生命周期"维度的 hook:
Hook 触发时机 典型用途 onSessionStart会话开始时 加载长期记忆(第 8 章)、初始化上下文 onTurnStart每轮开始时 准备这轮的模型参数 onSessionEnd会话结束时 持久化记忆、清理资源 循环内 hook 管"每一步",生命周期 hook 管"整个会话的起止"。 两者合起来,才覆盖了 Agent 的完整生命周期。
多个 hook 的执行顺序
🧙 挂了多个 hook,顺序怎么定?
- 按注册顺序执行(先挂的先跑)是常见约定——所以挂载顺序本身就是设计
- 拦截式 hook 通常串行(一个决定阻止,后面的就不用跑了)
- 订阅式 hook 可以并行(都只是看,互不影响)
老z类比:"这就像 Express 的中间件——
app.use(...)挂一串,请求按顺序穿过,每个中间件可以选择'放行(next())'或'拦下'。Hook 就是 Agent 循环里的中间件。 读过 Node.js 后端的读者,一秒就懂。"
🐣 小a:"原来 Hook 的本质,是把『循环』拆成了一段段可以插入的节点?"
🧙 "对。Hook 的完整画像:拦截改流程、订阅看流程、生命周期管起止、顺序按注册。 深度源码版见附录 H。"
本章小结
🐣 小a的红绿灯
┌──────── Hooks ────────┐ │ │ │ • Hook = 循环的插手点 │ │ beforeToolCall(前) │ │ afterToolCall(后) │ │ │ │ • 三用途:安全/定制/ │ │ 解耦 │ │ │ │ • 完整家族:工具前后/ │ │ 轮次/调LLM前 │ │ │ │ • 拦截要 fail loud: │ │ 告诉模型"你被拦了" │ │ │ │ • 两种介入: │ │ 拦截(改流程)/ │ │ 订阅(看流程) │ │ │ │ • 生命周期: │ │ onSessionStart/End │ │ │ │ • 代价:难预测/串乱/ │ │ 拖慢 → 要可中断 │ └────────────────────────┘
关键认知:Hooks 让 Agent 的循环"可插手"——不改核心代码就能拦截、定制、解耦。它是安全机制(危险操作拦截)的底层骨架,也是"开放扩展、封闭修改"的体现。设计时分清"拦截改流程、订阅看流程"。深度源码版见附录 H。
课后实验
- 设计拦截:为你的 Agent 设计一个 beforeToolCall——拦截 bash 工具里的危险命令。
- 体会 fail loud:对比"被拦截后模型收到错误提示" vs "被静默忽略",模型行为有何不同。
- 画 Hook 位置图:在循环图上标出 before/afterToolCall、轮次边界、调 LLM 前这几个 hook 的位置。
- 区分两种介入:为同一个节点(如 afterToolCall)分别写一个"拦截式"(修改结果)和一个"订阅式"(记录日志)的 hook,感受两者写法的差异。
下一章:工具能力怎么跨框架共享?小a遇到了工具的"巴别塔危机"。 → 第 10 章 · MCP