多个 AI 在后台搜索、写文件、跑代码——你怎么知道它们都在干嘛?有没有乱来?这就需要 Hooks(钩子) 来监控和控制。

第7课:监控AI的一举一动 —— Hooks 钩子系统

本课目标

多个 AI 在后台搜索、写文件、跑代码——你怎么知道它们都在干嘛?有没有乱来?这就需要 Hooks(钩子) 来监控和控制。


什么是 Hook?

Hook 就是你插在 AI 工作流程中的"监控摄像头"

每当 Claude 要调用一个工具(比如 WebSearch、Write、Bash),系统会在调用前调用后分别"通知"你的 Hook 函数。你可以:

  • 看一眼就放行 — 记录日志
  • 拦截并拒绝 — "不许执行这个命令!"
  • 修改行为 — "你搜这个关键词不对,换一个"
graph TD A["Claude 想搜索 AI 芯片"] --> B["PreToolUse Hook<br/>调用前: 你决定放不放行"] B -- "放行" --> C["执行 WebSearch(AI 芯片)"] C --> D["PostToolUse Hook<br/>调用后: 你看看结果怎么样"] D --> E["结果返回给 Claude"]

最简 Hook:记录日志

python
"""
07_basic_hook.py
最简单的 Hook —— 记录所有工具调用
"""
import anyio
from claude_agent_sdk import (
    ClaudeSDKClient, ClaudeAgentOptions, HookMatcher,
    AssistantMessage, TextBlock
)


# Hook 函数:在工具调用前触发
async def log_tool_call(hook_input, tool_use_id, context):
    """每次 Claude 要用工具时,打印一条日志"""
    tool_name = hook_input["tool_name"]
    tool_input = hook_input["tool_input"]
    print(f"📋 [日志] Claude 正在使用: {tool_name}")
    print(f"         参数: {str(tool_input)[:100]}...")
    return {}  # 返回空字典 = 放行,不干预


# Hook 函数:在工具调用后触发
async def log_tool_result(hook_input, tool_use_id, context):
    """工具执行完后,记录结果"""
    tool_name = hook_input["tool_name"]
    print(f"✅ [完成] {tool_name} 执行完毕")
    return {}


async def main():
    options = ClaudeAgentOptions(
        system_prompt="你是一个助手,会搜索信息。",
        allowed_tools=["WebSearch", "Write"],
        permission_mode="bypassPermissions",
        max_turns=5,
        hooks={
            # PreToolUse: 工具调用前
            "PreToolUse": [
                HookMatcher(
                    matcher=None,  # None 表示匹配所有工具
                    hooks=[log_tool_call]
                ),
            ],
            # PostToolUse: 工具调用后
            "PostToolUse": [
                HookMatcher(
                    matcher=None,
                    hooks=[log_tool_result]
                ),
            ],
        }
    )

    async with ClaudeSDKClient(options=options) as client:
        await client.query("搜索一下最新的 Python 3.13 有什么新特性")
        async for msg in client.receive_response():
            if isinstance(msg, AssistantMessage):
                for block in msg.content:
                    if isinstance(block, TextBlock):
                        print(f"\n💬 {block.text}\n")


anyio.run(main)

运行后你会看到:

python
📋 [日志] Claude 正在使用: WebSearch
         参数: {'query': 'Python 3.13 新特性'}...
✅ [完成] WebSearch 执行完毕
📋 [日志] Claude 正在使用: WebSearch
         参数: {'query': 'Python 3.13 new features 2025'}...
✅ [完成] WebSearch 执行完毕

💬 Python 3.13 带来了以下新特性...

每一步操作都清清楚楚!


Hook 进阶:拦截危险操作

光记录不够,有时候你得拦住 AI 的危险操作:

bash
"""
07_safety_hook.py
安全钩子 —— 拦截危险命令
"""
import anyio
from claude_agent_sdk import (
    ClaudeSDKClient, ClaudeAgentOptions, HookMatcher,
    AssistantMessage, TextBlock
)


async def safety_check(hook_input, tool_use_id, context):
    """检查 Bash 命令,拦截危险操作"""
    tool_name = hook_input["tool_name"]
    tool_input = hook_input["tool_input"]

    if tool_name != "Bash":
        return {}  # 不是 Bash 命令,放行

    command = tool_input.get("command", "")

    # 黑名单:这些命令绝对不能执行
    dangerous_patterns = [
        "rm -rf",       # 删除文件
        "sudo",         # 提权
        "chmod 777",    # 改权限
        "curl | bash",  # 从网上下载并执行脚本
        "DROP TABLE",   # 删数据库表
    ]

    for pattern in dangerous_patterns:
        if pattern in command:
            print(f"🚫 [拦截] 危险命令被阻止: {command}")
            return {
                "hookSpecificOutput": {
                    "hookEventName": "PreToolUse",
                    "permissionDecision": "deny",
                    "permissionDecisionReason": f"安全检查:命令包含危险模式 '{pattern}'",
                }
            }

    print(f"✅ [放行] 安全命令: {command[:60]}...")
    return {}


async def main():
    options = ClaudeAgentOptions(
        allowed_tools=["Bash"],
        permission_mode="bypassPermissions",
        max_turns=5,
        hooks={
            "PreToolUse": [
                HookMatcher(matcher="Bash", hooks=[safety_check]),
                #             ↑ 只针对 Bash 工具触发
            ],
        }
    )

    async with ClaudeSDKClient(options=options) as client:
        # 安全命令 —— 会被放行
        await client.query("用 bash 执行 echo 'hello world'")
        async for msg in client.receive_response():
            if isinstance(msg, AssistantMessage):
                for block in msg.content:
                    if isinstance(block, TextBlock):
                        print(block.text)


anyio.run(main)

关键区别: - return {} → 放行 - return {"hookSpecificOutput": {"permissionDecision": "deny", ...}} → 拦截


HookMatcher 的 matcher 参数

bash
HookMatcher(matcher=None, hooks=[...])     # 匹配所有工具
HookMatcher(matcher="Bash", hooks=[...])   # 只匹配 Bash
HookMatcher(matcher="Write", hooks=[...])  # 只匹配 Write

你可以为不同工具设置不同的 Hook:

bash
hooks = {
    "PreToolUse": [
        HookMatcher(matcher="Bash", hooks=[check_bash_safety]),
        HookMatcher(matcher="Write", hooks=[check_write_path]),
        HookMatcher(matcher=None, hooks=[log_all_tool_calls]),
    ],
}

研究平台的 Hook 实战:子Agent追踪器

先搞懂问题:为什么需要追踪?

想象你是一个公司老板,你让组长安排 3 个员工同时干活: - 研究员去搜资料 - 分析师去跑数据 - 报告员去写文档

你坐在办公室里,怎么知道每个人现在在干嘛?答案是:装一个监控系统,每个人每做一件事都打卡记录

子Agent追踪器就是这个"打卡系统"。

追踪器长什么样?

code
# 简化版的子Agent追踪器
class SubagentTracker:
    def __init__(self):
        self.sessions = {}        # 记录所有子Agent的活动
        self._current_parent_id = None  # 当前正在执行的子Agent

    async def pre_tool_use_hook(self, hook_input, tool_use_id, context):
        """工具调用前:记录是哪个Agent在用什么工具"""
        tool_name = hook_input["tool_name"]

        # 跳过 Task 工具本身(那是组长在派活)
        if tool_name == "Task":
            return {}

        # 找到当前是哪个子Agent在工作
        if self._current_parent_id and self._current_parent_id in self.sessions:
            session = self.sessions[self._current_parent_id]
            agent_id = session["id"]  # 比如 "RESEARCHER-1"
            print(f"[{agent_id}] → {tool_name}")

            # 记录这次工具调用
            session["tool_calls"].append({
                "tool": tool_name,
                "input": hook_input["tool_input"],
                "timestamp": "...",
            })

        return {}

    async def post_tool_use_hook(self, hook_input, tool_use_id, context):
        """工具调用后:记录结果"""
        # 记录输出、错误等信息
        return {}

光有类还不行——完整的接线方式

上面只是定义了一个类,但它还没有被"接入"到 Claude 的工作流里。

在完成接线之前,有一个关键问题要先搞清楚——hook_input 里到底有什么字段?

先搞清楚 hook_input 里有什么

不同的 Hook 事件,hook_input 的内容是不一样的。你记不住没关系,写一个"探测 Hook"把它打印出来就行:

code
async def debug_hook(hook_input, tool_use_id, context):
    """开发时用的调试 Hook:把收到的所有信息都打印出来"""
    print(f"🔍 hook_input 的内容: {hook_input}")
    print(f"🔍 tool_use_id: {tool_use_id}")
    print(f"🔍 context 的 keys: {list(context.keys()) if context else 'None'}")
    return {}

把它挂到任何事件上,一运行就能看到实际传过来了什么数据。比如:

code
# 挂到 PreToolUse 上,你会看到类似这样的输出:
🔍 hook_input 的内容: {
    "tool_name": "WebSearch",
    "tool_input": {"query": "电池技术 2025"}
}

# 挂到 SubagentStart 上,你会看到类似这样的输出:
🔍 hook_input 的内容: {
    "session_id": "abc123",
    "prompt": "搜索电池技术的最新进展..."
}

结论:hook_input 的字段不是你猜的,是 SDK 定义好的。不确定有什么字段?打印一下就知道了。

不同事件的 hook_input 常见字段:

Hook 事件 hook_input 常见字段 说明
PreToolUse tool_name, tool_input 要调用的工具名和参数
PostToolUse tool_name, tool_input, tool_output 多了工具的返回结果
SubagentStart session_id, prompt 子Agent 的会话 ID 和任务指令
SubagentStop session_id 子Agent 的会话 ID

⚠️ 实际的字段名以你的 SDK 版本为准。第一次用某个事件时,永远先用上面的 debug_hook 打印一下,确认字段名再写代码。 这是老手和新手的区别——老手不靠猜,靠打印。

完整的接线代码

知道了 hook_input 里有什么,现在可以写完整代码了:

code
"""
07_subagent_tracker_full.py
完整的子Agent追踪器使用示例
"""
import anyio
from claude_agent_sdk import (
    ClaudeSDKClient, ClaudeAgentOptions, HookMatcher,
    AssistantMessage, TextBlock, ToolUseBlock
)


# ============ 第一步:创建追踪器实例 ============
tracker = SubagentTracker()

# 子Agent计数器,用来生成 RESEARCHER-1、RESEARCHER-2 这样的编号
agent_counter = 0


# ============ 第二步:写额外的 Hook 来追踪子Agent的生命周期 ============

async def on_subagent_start(hook_input, tool_use_id, context):
    """当一个子Agent被组长派出去时触发"""
    global agent_counter
    agent_counter += 1

    # hook_input 里有什么字段?取决于 SDK 版本
    # 第一次不确定的话,先用 debug_hook 打印出来看看
    # 这里假设 SubagentStart 事件会传 session_id 和 prompt
    session_id = hook_input.get("session_id", tool_use_id)
    prompt = hook_input.get("prompt", "未知任务")

    # 给这个子Agent取一个好认的编号
    agent_id = f"AGENT-{agent_counter}"

    # 登记这个子Agent
    tracker.sessions[session_id] = {
        "id": agent_id,
        "task": prompt[:50],   # 只取前50个字符,太长了不好看
        "tool_calls": [],      # 它将来用了哪些工具,都记在这
    }
    tracker._current_parent_id = session_id

    print(f"🚀 Spawning {agent_id}: {prompt[:50]}")
    return {}


async def on_subagent_stop(hook_input, tool_use_id, context):
    """当一个子Agent干完活回来时触发"""
    session_id = hook_input.get("session_id", tool_use_id)

    if session_id in tracker.sessions:
        session = tracker.sessions[session_id]
        count = len(session["tool_calls"])
        print(f"🏁 {session['id']} 完成!共调用了 {count} 次工具")
    return {}


# ============ 第三步:把所有 Hook 注册到 ClaudeAgentOptions ============

async def main():
    options = ClaudeAgentOptions(
        system_prompt="你是研究团队的组长,负责安排子Agent完成研究任务。",
        allowed_tools=["Task"],
        permission_mode="bypassPermissions",
        max_turns=10,

        # 👇 这里是关键:把追踪器的方法挂到 hooks 上
        hooks={
            # 工具调用前 → 追踪器记录"谁在用什么工具"
            "PreToolUse": [
                HookMatcher(
                    matcher=None,  # 匹配所有工具
                    hooks=[tracker.pre_tool_use_hook]
                    #      ^^^^^^^^^^^^^^^^^^^^^^^^^^
                    #      注意:这里传的是实例方法,不是普通函数
                    #      所以 tracker 对象的 self.sessions 能跨调用保持数据
                ),
            ],

            # 工具调用后 → 追踪器记录结果
            "PostToolUse": [
                HookMatcher(
                    matcher=None,
                    hooks=[tracker.post_tool_use_hook]
                ),
            ],

            # 子Agent启动时 → 登记新员工
            "SubagentStart": [
                HookMatcher(
                    matcher=None,
                    hooks=[on_subagent_start]
                ),
            ],

            # 子Agent结束时 → 打印完成报告
            "SubagentStop": [
                HookMatcher(
                    matcher=None,
                    hooks=[on_subagent_stop]
                ),
            ],
        }
    )

    async with ClaudeSDKClient(options=options) as client:
        await client.query("研究一下电动汽车电池技术的最新进展,并生成数据分析报告")

        async for msg in client.receive_response():
            if isinstance(msg, AssistantMessage):
                for block in msg.content:
                    if isinstance(block, TextBlock):
                        print(f"\n💬 {block.text}\n")

    # ============ 最后:打印完整的追踪报告 ============
    print("\n" + "=" * 50)
    print("📊 完整追踪报告:")
    print("=" * 50)
    for session_id, session in tracker.sessions.items():
        print(f"\n👤 {session['id']}(任务:{session['task']})")
        for call in session["tool_calls"]:
            print(f"   └── {call['tool']}")


anyio.run(main)

执行起来是什么效果?

用一张时间线来看整个过程:

code
你说:"研究一下电动汽车电池技术"
  │
  ▼
┌─────────────── 组长 Agent ───────────────┐
│ 组长决定派 3 个子Agent                      │
│                                           │
│  ① 调用 Task 工具 → 派出研究员1             │
│     ┌──── SubagentStart Hook 触发 ────┐   │
│     │ 🚀 Spawning RESEARCHER-1       │   │
│     │ tracker 登记:                   │   │
│     │   sessions["xxx"] = {           │   │
│     │     "id": "RESEARCHER-1",       │   │
│     │     "tool_calls": []            │   │
│     │   }                             │   │
│     └────────────────────────────────┘   │
│                                           │
│  研究员1开始干活:                           │
│     WebSearch("电池技术")                   │
│     ┌──── PreToolUse Hook 触发 ──────┐   │
│     │ [RESEARCHER-1] → WebSearch     │   │
│     │ tool_calls 里多了一条记录        │   │
│     └────────────────────────────────┘   │
│                                           │
│     WebSearch("固态电池 2025")              │
│     ┌──── PreToolUse Hook 触发 ──────┐   │
│     │ [RESEARCHER-1] → WebSearch     │   │
│     └────────────────────────────────┘   │
│                                           │
│     Write("battery_research.md")          │
│     ┌──── PreToolUse Hook 触发 ──────┐   │
│     │ [RESEARCHER-1] → Write         │   │
│     └────────────────────────────────┘   │
│                                           │
│     ┌──── SubagentStop Hook 触发 ────┐   │
│     │ 🏁 RESEARCHER-1 完成!          │   │
│     │    共调用了 3 次工具              │   │
│     └────────────────────────────────┘   │
│                                           │
│  ② 调用 Task 工具 → 派出分析师              │
│     ... 同样的流程 ...                     │
│                                           │
│  ③ 调用 Task 工具 → 派出报告员              │
│     ... 同样的流程 ...                     │
└───────────────────────────────────────────┘
  │
  ▼
最终控制台输出:
bash
🚀 Spawning RESEARCHER-1: battery technology research
[RESEARCHER-1] → WebSearch
[RESEARCHER-1] → WebSearch
[RESEARCHER-1] → Write
🏁 RESEARCHER-1 完成!共调用了 3 次工具

🚀 Spawning DATA-ANALYST-1: generate visualizations
[DATA-ANALYST-1] → Glob
[DATA-ANALYST-1] → Read
[DATA-ANALYST-1] → Bash
[DATA-ANALYST-1] → Write
🏁 DATA-ANALYST-1 完成!共调用了 4 次工具

💬 研究报告已完成...

==================================================
📊 完整追踪报告:
==================================================

👤 RESEARCHER-1(任务:battery technology research)
   └── WebSearch
   └── WebSearch
   └── Write

👤 DATA-ANALYST-1(任务:generate visualizations)
   └── Glob
   └── Read
   └── Bash
   └── Write

关键理解:数据是怎么串起来的?

很多人会困惑:Claude 派了好几个子Agent,Hook 怎么知道"这次 WebSearch 是研究员1的,不是研究员2的"?

秘密在于 tool_use_id 这个参数:

code
async def pre_tool_use_hook(self, hook_input, tool_use_id, context):
    #                                        ^^^^^^^^^^^
    #                                        每次工具调用都有唯一ID

整个流程就像公司的"工牌系统":

code
1. 子Agent被派出时(SubagentStart),拿到一个工牌号(tool_use_id)
   → tracker 用这个工牌号建了一条员工档案

2. 子Agent每次用工具时(PreToolUse),出示工牌号
   → tracker 根据工牌号找到对应的员工档案
   → 在档案里添加一条"用了什么工具"的记录

3. 子Agent干完活了(SubagentStop),交回工牌号
   → tracker 根据工牌号找到档案,打印完成报告

这就是为什么 tracker 要用一个 来写,而不是一个普通函数——因为它需要用 self.sessions 在多次 Hook 调用之间保持记忆。如果用普通函数,每次调用都是"失忆"的,无法把同一个子Agent的多次工具调用串起来。


所有 Hook 事件类型

SDK 支持的 Hook 事件不只有工具调用前后:

事件 什么时候触发 常见用途
PreToolUse 工具调用 安全检查、日志、拦截
PostToolUse 工具调用(成功时) 记录结果、统计
PostToolUseFailure 工具调用失败 错误处理
UserPromptSubmit 用户发送消息时 过滤输入
Stop Agent 停止时 清理资源
SubagentStart 子Agent启动时 追踪子Agent
SubagentStop 子Agent结束时 统计耗时
Notification 系统通知 进度报告

研究平台主要用 PreToolUsePostToolUse,已经够用了。


本课小结

  1. Hook = 监控摄像头,插在 AI 工作流程中
  2. PreToolUse 在工具调用前触发,可以放行或拦截
  3. PostToolUse 在工具调用后触发,用来记录结果
  4. HookMatcher 决定匹配哪些工具
  5. 返回空字典 = 放行,返回 deny 决策 = 拦截
  6. 研究平台用 Hook 追踪每个子Agent的所有活动

课后练习

  1. 写一个 Hook,统计整个对话中总共调用了多少次工具
  2. 写一个 Hook,限制 WebSearch 最多调用 3 次(超过就拦截)
  3. 写一个 PostToolUse Hook,把每次工具调用的结果保存到一个 JSON 文件

沿着当前专题继续,或返回课程目录重新整理阅读顺序。

返回课程目录