10.07概念更新于 2026年9月17日

Hooks 与生命周期

在工具调用前后、会话开始与结束时插入确定性逻辑。

10.07 · 课时讲解

Hooks 与生命周期

凡是不能靠模型自觉的事,就在生命周期的关键节点挂一个 Hook。

1 / 12

一句话理解:Prompt 里的规则是“请模型遵守”,Hooks 里的规则是“程序保证执行”;凡是不能靠模型自觉的事,就在生命周期的关键节点挂一个 Hook。

设想你在指令里写了“修改订单状态前必须确认退款审批单”。九十九次模型都照做了,第一百次它在长上下文里忘了这条,直接把一个没走审批的订单改成了“已退款”,触发了自动打款。事后复盘,大家会发现问题不在 Prompt 写得不够好,而在于 这条规则本来就不该只写在 Prompt 里。

Claude Code 的官方文档说得很直接:项目说明文件(CLAUDE.md)是上下文而不是强制配置,要想无论模型怎么决定都拦住某个动作,应该用 PreToolUse Hook。这一课讲清楚 Hooks 是什么、有哪些时机、能做什么,以及怎样设计才不会反过来拖垮 Agent。

Hook 是什么:循环上的“插座”

Agent 的运行是一个有明确阶段的生命周期。Hook 就是 Harness 在这些阶段预留的插座:事件发生时,Harness 调用你注册的函数,并根据函数的返回值决定放行、拦截、改写还是补充信息。

Hooks 生命周期:从会话开始到会话结束,Harness 在每个阶段预留的 Hook 事件,以及循环中随时可能触发的压缩与子 Agent 事件 会话开始 用户提交输入 模型决定调用工具 工具执行前 需要权限决定 执行工具 工具返回结果 模型给出最终回答 Agent 停止 会话结束 SessionStart 初始化日志与资源 UserPromptSubmit 注入用户上下文 PreToolUse 拦截 · 改参数 · 放行 PermissionRequest 接入审批系统 Stop 检查结果、保存状态 SessionEnd 清理资源 PostToolUse PostToolUseFailure 每轮回到模型 无工具调用 循环中随时可能触发 PreCompact 上下文压缩前 SubagentStart SubagentStop 子 Agent 起止
图 1 · Hooks 生命周期:左侧是一次运行的阶段,右侧是对应的“插座”

Hooks 有两个重要特性:

  1. 确定性:它是普通代码,一定会在该时刻运行,不受模型“忘没忘”影响。
  2. 不占上下文: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 拒绝,操作就会被拦下。

把四类返回和优先级放在一起看:

PreToolUse Hook 的四类返回及其效果,多个 Hook 同时生效时的优先级 模型发起 工具调用请求 PreToolUse 返回什么? {}(空对象) 不干预,走正常权限流程 deny + 原因 阻止执行,原因交给模型 ask 转交给人确认 updatedInput 改写参数后再执行 多个 Hook 同时生效:deny > defer > ask > allow,只要有一个拒绝就拦下
图 2 · PreToolUse 的四种决定;多个 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 的五条原则

  1. 快:同步 Hook 会阻塞 Agent。纯记录类的副作用(写日志、发通知)可以用异步输出,让 Agent 不必等待;但异步 Hook 无法拦截或改写操作。
  2. 失败策略要想清楚:校验服务挂了怎么办?安全类 Hook 应该“失败即拒绝”(fail-closed),记录类 Hook 可以“失败即放行”(fail-open)。
  3. 精准匹配:用 matcher 只匹配需要的工具。对所有工具生效的 Hook 要特别注意性能。
  4. 拒绝原因可行动:告诉模型“为什么不行、应该怎么做”,而不只是“不允许”。
  5. 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 实现其中一条,并故意触发一次,观察模型收到拒绝原因后怎么调整。

延伸阅读

下一步

Hook 是 Harness 最常用的扩展方式之一,但不是唯一一种。下一课「扩展点:MCP、Skills、插件与子 Agent」把所有能挂进 Harness 的扩展方式放在一起比较:各自解决什么、什么时候加载、要付出多少上下文代价。

试试:上下文、MCP、沙箱、Prompt 注入、外链