Hooks 与生命周期
在工具调用前后、会话开始与结束时插入确定性逻辑。
一句话理解:Prompt 里的规则是“请模型遵守”,Hooks 里的规则是“程序保证执行”;凡是不能靠模型自觉的事,就在生命周期的关键节点挂一个 Hook。
设想你在指令里写了“修改订单状态前必须确认退款审批单”。九十九次模型都照做了,第一百次它在长上下文里忘了这条,直接把一个没走审批的订单改成了“已退款”,触发了自动打款。事后复盘,大家会发现问题不在 Prompt 写得不够好,而在于 这条规则本来就不该只写在 Prompt 里。
Claude Code 的官方文档说得很直接:项目说明文件(CLAUDE.md)是上下文而不是强制配置,要想无论模型怎么决定都拦住某个动作,应该用 PreToolUse Hook。这一课讲清楚 Hooks 是什么、有哪些时机、能做什么,以及怎样设计才不会反过来拖垮 Agent。
Hook 是什么:循环上的“插座”
Agent 的运行是一个有明确阶段的生命周期。Hook 就是 Harness 在这些阶段预留的插座:事件发生时,Harness 调用你注册的函数,并根据函数的返回值决定放行、拦截、改写还是补充信息。
Hooks 有两个重要特性:
- 确定性:它是普通代码,一定会在该时刻运行,不受模型“忘没忘”影响。
- 不占上下文:Hook 在你的应用进程里运行,不进入模型的上下文窗口;只有你主动返回的补充信息才会被模型看到。
常用事件与用途
以 Claude Agent SDK 为例(事件名来自其官方文档)。注意 Python 与 TypeScript 两个 SDK 支持的事件并不完全相同,TypeScript 版事件更多,使用前请查对应语言的文档:
| 事件 | 触发时机 | 典型用途 | Python | TypeScript |
|---|---|---|---|---|
PreToolUse |
工具执行前 | 拦截危险操作、改写参数、自动放行只读工具 | 支持 | 支持 |
PostToolUse |
工具成功返回后 | 审计日志、给结果追加提示、脱敏 | 支持 | 支持 |
PostToolUseFailure |
工具执行失败后 | 记录错误、告警 | 支持 | 支持 |
UserPromptSubmit |
用户输入提交时 | 注入当前用户、租户等上下文 | 支持 | 支持 |
PermissionRequest |
需要权限决定时 | 接入自己的审批系统 | 支持 | 支持 |
Stop |
Agent 停止时 | 检查结果、保存状态 | 支持 | 支持 |
SubagentStart / SubagentStop |
子 Agent 开始与结束 | 追踪并行子任务 | 支持 | 支持 |
PreCompact |
上下文压缩前 | 归档完整记录 | 支持 | 支持 |
SessionStart / SessionEnd |
会话开始与结束 | 初始化日志、清理资源 | 不支持 | 支持 |
其他框架也有类似机制,但形态不同:OpenAI Agents SDK 提供 RunHooks 与 AgentHooks,回调包括 on_agent_start、on_llm_start、on_tool_start、on_tool_end、on_handoff 等,主要用于观察;拦截与审批则通过 Guardrails 和工具级的 needs_approval 实现。LangGraph 没有单独的 Hook 概念,你直接在图里加一个校验节点,或在节点中调用 interrupt() 暂停等待人工。
Hook 能返回什么
一个 PreToolUse Hook 可以做出四类决定(字段名来自 Claude Agent SDK 文档):
| 返回 | 效果 | 场景 |
|---|---|---|
| 空对象 | 不干预,按正常权限流程走 | 检查通过 |
permissionDecision: "deny" + 原因 |
阻止执行,模型收到拒绝原因 | 删除生产数据、修改受保护文件 |
permissionDecision: "ask" |
转交给人确认 | 金额超过阈值的退款 |
updatedInput |
改写参数后再执行 | 把写入路径重定向到沙箱目录 |
另外还有 permissionDecision: "defer"(结束本次运行,稍后恢复),以及所有事件通用的 systemMessage(给用户看的提示)和 continue(是否继续运行)。PostToolUse 可以用 additionalContext 在工具结果后追加信息,或用 updatedToolOutput 在模型看到之前替换输出。
多个 Hook 同时生效时,优先级是 deny 高于 defer,defer 高于 ask,ask 高于 allow:只要有一个 Hook 拒绝,操作就会被拦下。
把四类返回和优先级放在一起看:
拒绝时一定要写清原因。原因会作为工具结果交给模型,写得好,模型会换一条合规路径;写得含糊,模型可能换个参数反复尝试。
示例:订单售后 Agent 的两个 Hook
假设订单系统通过 MCP Server 接入(Server 名为 orders),在 Claude Agent SDK 中 MCP 工具的名字形如 mcp__<server>__<action>。我们挂两个 Hook:
- 修改订单状态为“已退款”时,参数里必须带退款审批单号,否则拒绝;
- 所有工具调用成功后写一条审计日志。
import json
from claude_agent_sdk import ClaudeAgentOptions, HookMatcher
async def require_approval_for_refund(input_data, tool_use_id, context):
args = input_data["tool_input"]
if args.get("status") == "已退款" and not args.get("approval_id"):
return {
"hookSpecificOutput": {
"hookEventName": input_data["hook_event_name"],
"permissionDecision": "deny",
"permissionDecisionReason": "改为已退款必须提供退款审批单号 approval_id,请先调用查询退款审批的工具",
}
}
return {} # 放行,继续走正常的权限流程
async def audit_log(input_data, tool_use_id, context):
record = {
"session_id": input_data["session_id"],
"tool": input_data["tool_name"],
"input": input_data["tool_input"],
"tool_use_id": tool_use_id,
}
with open("audit.jsonl", "a", encoding="utf-8") as f:
f.write(json.dumps(record, ensure_ascii=False) + "\n")
return {}
options = ClaudeAgentOptions(
hooks={
"PreToolUse": [
HookMatcher(matcher="mcp__orders__update_order_status", hooks=[require_approval_for_refund])
],
"PostToolUse": [HookMatcher(hooks=[audit_log])], # 不写 matcher:对所有工具生效
}
)
这段代码的价值不在语法,而在分工:“改退款需审批单”是业务硬规则,交给 Hook;“怎么找到审批单”是判断,交给模型。
设计 Hook 的五条原则
- 快:同步 Hook 会阻塞 Agent。纯记录类的副作用(写日志、发通知)可以用异步输出,让 Agent 不必等待;但异步 Hook 无法拦截或改写操作。
- 失败策略要想清楚:校验服务挂了怎么办?安全类 Hook 应该“失败即拒绝”(fail-closed),记录类 Hook 可以“失败即放行”(fail-open)。
- 精准匹配:用 matcher 只匹配需要的工具。对所有工具生效的 Hook 要特别注意性能。
- 拒绝原因可行动:告诉模型“为什么不行、应该怎么做”,而不只是“不允许”。
- Hook 不是万能护栏:它只管经过 Harness 的工具调用。如果工具本身权限过大(例如一个能执行任意 SQL 的工具),Hook 很难拦全,根本解法是 安全的工具设计 与 最小权限。
常见误区
- “有了 Hook 就不用写 Prompt 规则了。” 两者互补:Prompt 让模型一开始就走对路,Hook 兜底。只有 Hook 没有 Prompt,模型会反复撞墙,浪费步数。
- “把复杂业务逻辑都塞进 Hook。” Hook 应该是薄的检查与记录层。复杂的审批流程、跨系统校验,应该做成独立服务或 程序校验 环节。
- “Hook 能看到并修改模型的思考。” Hook 作用于事件(工具调用、提示提交、停止等),不是用来操纵模型推理过程的。
产品经理检查清单
- 列出所有“必须 100% 遵守”的业务规则,逐条确认它由 Prompt、Hook、工具实现还是权限系统保证。
- 每个拒绝型 Hook 的拒绝原因,模型读了能知道下一步怎么做吗?
- 安全类 Hook 在依赖服务不可用时是拒绝还是放行?
- 审计日志记录了谁、在哪个会话、调用了什么、参数是什么吗?是否需要脱敏?
- 是否有 Hook 会让用户等待过久?
练习
为你熟悉的一个 Agent 场景(Coding Agent、SEO 诊断或订单售后)设计一张 Hook 表,至少 4 行,用时约 40 分钟:
| 事件 | 匹配的工具 | 检查或动作 | 返回决定 | 依赖服务挂了怎么办 |
|---|---|---|---|---|
例:PreToolUse |
Bash |
命令含 git push --force |
拒绝并说明原因 | 拒绝 |
如果你会写代码,用 Claude Agent SDK 实现其中一条,并故意触发一次,观察模型收到拒绝原因后怎么调整。
延伸阅读
- Intercept and control agent behavior with hooks:Claude Agent SDK 的完整事件列表、输入输出字段与示例。
- OpenAI Agents SDK:Agents:
RunHooks/AgentHooks的回调说明。 - LangGraph Interrupts:在图中暂停并等待人工输入。
下一步
Hook 是 Harness 最常用的扩展方式之一,但不是唯一一种。下一课「扩展点:MCP、Skills、插件与子 Agent」把所有能挂进 Harness 的扩展方式放在一起比较:各自解决什么、什么时候加载、要付出多少上下文代价。