Hooks 与技能 — 生命周期的无限可能

在 Agent 的每个关键时刻插入你的逻辑——这就是无限可能的起点

📝 本章目标

读完本章,你将:

  1. 用三个 Prompt 让 AI 帮你构建钩子系统技能系统
  2. 理解 Agent 生命周期中的关键事件点——在什么时刻能做什么
  3. 掌握四种 Hook 类型和技能包的设计思想

如果你用过 Web 框架,一定对”中间件”不陌生——每个 HTTP 请求进来时,先过一层层中间件:认证、日志、限流、CORS……请求处理完后,再过一层层中间件:压缩、缓存头、审计日志。

Hook 系统就是 AI Agent 的中间件。

想象你的 Agent 是一条流水线:用户说话 → AI 思考 → 调用工具 → 返回结果。在这条流水线的每个接缝处,你都能插入自己的逻辑——启动时加载项目配置、执行命令前做安全检查、修改文件后自动格式化、会话结束时保存记忆。

而”技能”则是把”一段提示词 + 几个工具 + 一个触发条件”打包成可复用的能力模块。就像给 Agent 装上了即插即用的技能包——输入名字就能一键激活。

先动手造出来,再回头理解。

动手:用三个 Prompt 给 Agent 装上扩展能力

确保你跟着前几章做了项目,目录里有 harness/ 代码。如果没有,去 GitHub 仓库 git checkout ch06-memory 获取起点。

打开 Claude Code,确认你在项目根目录,然后跟着走。

Prompt 1:给 Agent 装上钩子机制

复制下面这段话,粘贴到 Claude Code 里:

我想在 AI 工作的关键时刻插入自己的逻辑。
比如每次要执行命令前先跑我的检查脚本,
每次对话结束后自动保存日志。

帮我实现一个钩子机制——定义几个关键事件点,
我能注册自己的处理逻辑,到时候自动触发。

至少支持这些事件:会话开始、会话结束、
发送消息前、收到回复后、执行工具前、执行工具后。

每个钩子可以是一段脚本、一个函数、
或者一段要追加给 AI 的提示词。
钩子要能从配置文件里加载,
这样用户不用改代码就能加自己的钩子。

等 AI 跑完,它会帮你创建钩子系统的代码——事件定义、注册机制、触发逻辑、配置加载。试一下:

$ cat .harness/hooks.json
{
  "hooks": [
    {
      "event": "session_start",
      "type": "shell",
      "command": "echo '会话开始于 $(date)' >> /tmp/harness.log"
    }
  ]
}

$ harness
Harness v0.1.0 — AI Agent Runtime
[hook] session_start: shell command executed

You > 你好
Assistant > 你好!有什么可以帮你的?

You > exit
[hook] session_end: shell command executed

钩子能触发就说明机制跑起来了。但每次都要手写配置太麻烦——下一个 Prompt 把常用套路打包成技能。

Prompt 2:把常用套路做成技能包

帮我把常用套路做成可复用的技能包:
一个技能包含一段提示词和用到的工具,
取个名字,以后输入名字就能一键触发。

技能放在一个专门的目录里,每个技能一个文件,
格式用 JSON 或 YAML 都行。
加一个 /skill 命令列出所有可用技能,
/skill 名字 就能激活对应的技能。

跑完后试一下:

$ ls .harness/skills/
code-review.json  project-overview.json  pre-commit.json

$ harness
You > /skill
Available skills:
  code-review       — 代码审查:分析代码质量和潜在问题
  project-overview  — 项目概览:快速了解项目结构
  pre-commit        — 提交检查:提交前自动检查代码

You > /skill project-overview
[skill] Activated: project-overview
[skill] Loading prompt + tools...
Assistant > 我来看看这个项目的结构...
(AI 自动读取项目文件,输出结构概览)

技能能激活了。现在把钩子和技能结合起来——做几个开箱即用的预置。

Prompt 3:预置几个实用钩子

帮我做几个预置钩子:

1. 启动时自动读项目约定文件让 AI 了解项目背景——
   如果项目根目录有约定文件,会话开始时自动读进来
   作为 AI 的背景知识。

2. AI 修改文件后自动跑格式化——
   如果项目配置了格式化工具,每次 AI 写完文件后
   自动跑一遍格式化,保持代码风格统一。

3. 会话结束时自动提取关键信息存记忆——
   让 AI 总结这次会话的关键决策和发现,
   追加到一个记忆文件里,下次会话启动时自动加载。

这些预置钩子默认启用,用户可以在配置里关掉。
$ harness
[hook] session_start: loaded project conventions
[hook] session_start: loaded 3 memory entries

You > 帮我重构一下入口文件
Assistant > 好的,我来看看...(修改文件)
[hook] post_tool_call: auto-formatted 2 files

You > exit
[hook] session_end: saved 2 memory entries

💡 三个 Prompt 做了什么

  • Prompt 1 建立了骨架——钩子机制跑起来了
  • Prompt 2 加上了复用——常用套路一键触发
  • Prompt 3 给了开箱即用——预置钩子让 Agent 更聪明

现在你手里有了一个可扩展的 Agent。完整代码在 GitHub 仓库,对应 tag ch07-hooks

接下来我们回过头,理解你刚刚构建的东西。


深入理解

生命周期事件

Agent 的一次会话从头到尾,会经历一系列关键时刻。Hook 系统的第一步就是把这些时刻定义清楚——每个时刻就是一个”事件”。

图 7-1:Agent 会话的生命周期事件

  1. SESSION_START → 会话开始,加载配置和记忆
  2. PRE_PROMPT → 用户消息发送前,可修改或拦截
  3. API_CALL → 调用 LLM,发送消息
  4. POST_RESPONSE → 收到 AI 回复后,可审查或修改
  5. PRE_TOOL_CALL → 执行工具前,可检查或拦截
  6. POST_TOOL_CALL → 工具执行后,可处理结果
  7. SESSION_END → 会话结束,保存状态和记忆

这七个事件覆盖了一次会话的完整生命周期。注意中间四个事件(从 PRE_PROMPT 到 POST_TOOL_CALL)在每一轮循环中都会触发——一次会话可能有几十轮循环,这些钩子每轮都跑。

为什么选这七个点?因为它们对应了你最可能想插入逻辑的时刻

  • SESSION_START——加载上下文。你想让 AI 在开口之前就了解项目背景、读取上次的记忆、初始化工具
  • PRE_PROMPT——修改输入。你想在用户消息发送前追加上下文、做内容审查、记录日志
  • POST_RESPONSE——审查输出。你想检查 AI 的回复是否合规、记录用量、触发后续动作
  • PRE_TOOL_CALL——安全门卫。你想在工具执行前检查命令是否安全、参数是否合理
  • POST_TOOL_CALL——后处理。你想在工具执行后格式化代码、验证结果、清理临时文件
  • SESSION_END——善后。你想保存记忆、生成报告、清理资源

每个事件都带上下文参数——比如 PRE_TOOL_CALL 会告诉你工具名、参数、当前对话状态,你的钩子可以读取这些信息做决策。

Hook 的四种类型

不是所有钩子都适合用同一种方式实现。比如”跑一个格式化命令”和”追加一段提示词给 AI”,本质上是两种完全不同的操作。所以我们支持四种 Hook 类型:

表 7-1:四种 Hook 类型对比

类型描述典型场景
Shell Hook执行一条 shell 命令,捕获输出跑格式化工具、执行检查脚本、写日志文件
Prompt Hook把一段文字追加到 AI 的上下文里注入项目约定、加载记忆、补充领域知识
Agent Hook启动一个子 Agent 执行复杂任务生成测试用例、做代码审查、写文档
HTTP Hook发送一个 HTTP 请求到外部服务通知 Slack、触发 CI/CD、记录到监控系统

Shell Hook

最直接的一种——跑一条命令,拿到输出。适合和系统工具集成:

{
  "event": "post_tool_call",
  "type": "shell",
  "command": "npx prettier --write ${file}",
  "condition": "tool_name == 'file_write'"
}

注意 condition 字段:不是每次工具调用后都格式化,只在写文件时才触发。条件表达式让钩子精准命中

Prompt Hook

把一段文字追加到 AI 的系统提示或当前对话里。不执行代码,只改变 AI 的”认知”:

{
  "event": "session_start",
  "type": "prompt",
  "content": "你正在一个 Python 项目中工作。代码风格遵循 PEP 8,使用 type hints,所有公开函数必须有 docstring。"
}

这就是 Prompt 3 里”启动时加载项目约定”的实现方式。

Agent Hook

当钩子逻辑复杂到一条命令搞不定时,启动一个子 Agent:

{
  "event": "session_end",
  "type": "agent",
  "prompt": "总结这次会话的关键决策和发现,用 3-5 个要点概括,保存到记忆文件。",
  "tools": ["file_read", "file_write"]
}

子 Agent 有自己的提示词和工具集,独立执行,执行完把结果返回主流程。这是最强大也是成本最高的 Hook 类型。

HTTP Hook

把事件通知到外部系统:

{
  "event": "session_end",
  "type": "http",
  "url": "https://hooks.slack.com/services/xxx",
  "method": "POST",
  "body": {"text": "Agent 会话结束,执行了 ${tool_count} 次工具调用"}
}

💡 核心概念:Hooks = AI 的中间件

Web 框架的中间件拦截 HTTP 请求/响应,Hook 系统拦截 Agent 的事件流。

相同点:都是在核心流程的关键节点插入自定义逻辑,不修改核心代码。

不同点:中间件通常只处理”请求进、响应出”两个方向。Hook 系统要处理更复杂的生命周期——会话、对话、工具调用三个层级的事件,还要支持异步执行和条件触发。

设计 Hook 系统时记住一条原则:钩子不应该阻塞主流程。Shell Hook 可以设超时,Agent Hook 可以后台执行,HTTP Hook 可以异步发送。只有安全相关的钩子(如 PRE_TOOL_CALL 的权限检查)才值得阻塞等待。

技能系统

钩子是被动触发的——事件来了就跑。技能是主动激活的——用户说”我要用这个能力”,系统加载对应的配置。

一个技能由三部分组成:

图 7-2:技能的三要素

  1. Prompt → 系统提示词
  2. Tools → 工具集合
  3. Trigger → 激活方式

Prompt 定义这个技能的”人设”——比如代码审查技能的 Prompt 告诉 AI”你是一个严格的代码审查专家”。Tools 定义它能用什么工具——代码审查只需要读文件,不需要写文件。Trigger 定义怎么激活——用户输入命令、匹配关键词、或者由钩子自动触发。

激活一个技能时,系统做三件事:

  1. 注入 Prompt——把技能的系统提示词追加到当前上下文
  2. 加载 Tools——把技能专属的工具加入可用工具列表
  3. 修改状态——设置标记,让后续逻辑知道当前处于哪个技能模式

技能的核心实现出奇地简单:

# harness/skills.py — 核心骨架
@dataclass
class Skill:
    name: str
    description: str
    prompt: str
    tools: list[str]
    trigger: str  # "command" | "keyword" | "hook"

def activate_skill(state, skill):
    # 1. 注入系统提示
    state.system += f"\n\n{skill.prompt}"

    # 2. 加载专属工具
    for tool_name in skill.tools:
        if tool_name not in state.active_tools:
            state.active_tools.append(tool_name)

    # 3. 标记当前技能
    state.active_skill = skill.name

就这十几行。技能系统的复杂度不在激活逻辑,而在技能本身的设计——Prompt 写得好不好、工具选得对不对、触发条件合不合理。

技能与钩子的关系

技能和钩子不是二选一,而是经常配合使用

  • 一个钩子可以自动激活一个技能——比如检测到用户在写测试时,自动激活”测试专家”技能
  • 一个技能可以注册自己的钩子——比如”代码审查”技能注册一个 POST_TOOL_CALL 钩子,在 AI 每次读完文件后自动分析

这种组合让 Agent 的行为变得极其灵活。

实战:构建一个代码审查技能

说再多不如做一个。我们来构建一个完整的”代码审查”技能,走一遍从定义到使用的全过程。

先看技能定义文件:

// .harness/skills/code-review.json
{
  "name": "code-review",
  "description": "代码审查:分析代码质量和潜在问题",
  "prompt": "你现在是一个严格的代码审查专家。用户会给你代码或文件路径,你要从以下维度审查:1) 逻辑正确性 2) 错误处理 3) 性能问题 4) 安全隐患 5) 可维护性。每个问题给出严重等级(高/中/低)和修改建议。审查完后给出总评。",
  "tools": ["file_read", "grep_search", "glob_search"],
  "trigger": "command",
  "hooks": [
    {
      "event": "post_response",
      "type": "shell",
      "command": "echo '[review] $(date): reviewed ${file_count} files' >> .harness/review.log"
    }
  ]
}

这个技能包含四个关键设计决策:

  • 只给读权限——审查不需要改文件,不给 file_write 工具可以防止误操作
  • 精确的 Prompt——不是泛泛地说”审查代码”,而是列出具体的审查维度和输出格式
  • 自带钩子——每次审查完自动记日志,方便追踪审查历史
  • 命令触发——用 /skill code-review 主动激活,不是自动触发

实际使用效果:

$ harness
You > /skill code-review
[skill] Activated: code-review
[skill] Loaded: 1 prompt, 3 tools, 1 hook

You > 帮我审查查询引擎的核心循环
Assistant > 我来审查查询引擎的核心代码...

## 审查报告:engine.py

### 问题 1 [高] 无限循环缺少超时保护
query_loop 没有最大轮次限制,如果 AI 持续调用工具
不收敛,会永远循环下去。
建议:增加 MAX_TURNS 限制。

### 问题 2 [中] 异常处理不完整
tool_result 构造失败时会抛出未捕获异常。
建议:在 execute_tool 外层加 try/except。

### 问题 3 [低] 魔法数字
compact 阈值 87000 硬编码在函数里。
建议:提取为配置常量。

总评:核心逻辑清晰,主要风险在健壮性。

[hook] post_response: review log updated

Claude Code 的 26 个事件

我们的简化版定义了 7 个事件。Claude Code 的生产实现定义了 26 个事件,覆盖了 Agent 行为的方方面面。

按类别整理如下:

表 7-2:Claude Code 事件分类(输入事件)

事件触发时机
PrePromptSubmit用户消息提交前
PostPromptSubmit用户消息提交后
PreCompact上下文压缩前
PostCompact上下文压缩后
Stop用户按下停止键

表 7-3:Claude Code 事件分类(工具事件)

事件触发时机
PreToolUse工具执行前——最重要的安全拦截点
PostToolUse工具执行后——适合做后处理
PreToolUse_Bash执行 Bash 命令前
PreToolUse_FileWrite写文件前
PreToolUse_FileRead读文件前
PreToolUse_ListDir列目录前
PreToolUse_Grep搜索前
PostToolUse_Bash执行 Bash 命令后
PostToolUse_FileWrite写文件后

表 7-4:Claude Code 事件分类(输出与生命周期事件)

事件触发时机
PreAPICall调用 LLM API 前
PostAPICallLLM API 返回后
PostResponseAI 完整回复生成后
SubagentStart子 Agent 启动时
SubagentEnd子 Agent 结束时
SessionStart会话开始
SessionEnd会话结束
Notification系统通知触发时
PreModelSwitch模型降级/切换前
PostModelSwitch模型降级/切换后
SkillActivation技能被激活时
ErrorOccurred发生错误时

💡 最重要的 5 个事件

26 个事件不需要全记住。日常使用中,80% 的需求用这 5 个就够了:

  1. SessionStart——加载项目上下文和记忆
  2. PreToolUse——安全检查,拦截危险操作
  3. PostToolUse_FileWrite——写完文件后自动格式化
  4. PostResponse——审查 AI 输出、记录日志
  5. SessionEnd——保存记忆和会话摘要

其余的事件在特殊场景下才用得上。比如 PreModelSwitch 只在你需要控制模型降级行为时才有用,SubagentStart 只在多 Agent 编排时才相关。

事件的传播机制

一个事件可以注册多个钩子,它们按优先级顺序依次执行。如果某个钩子返回”拦截”信号,后续钩子和主流程都会停止——这就是 PRE_TOOL_CALL 能拦截危险操作的原理。

执行顺序遵循三条规则:

  1. 优先级高的先执行——安全检查钩子优先级最高,日志记录最低
  2. 同优先级按注册顺序——先注册的先跑
  3. 拦截即停止——任何钩子返回”拦截”,事件传播立即终止

这和 DOM 事件冒泡、Express 中间件链的设计思想完全一致——责任链模式

异步与超时

钩子执行不应该让用户干等。几条实用规则:

  • Shell Hook 默认 5 秒超时——超时就杀掉进程,记一条警告
  • HTTP Hook 默认异步发送——不等响应,发出去就继续
  • Agent Hook 可以选择后台执行——不阻塞主对话
  • 只有安全相关的 PRE 类钩子值得同步等待

⚠️ 钩子的性能陷阱

每个钩子都有执行开销。如果你在 POST_TOOL_CALL 上注册了一个 3 秒的格式化命令,而一次会话有 50 次工具调用,那就多了 150 秒的等待。

解决方案:用 condition 精确限定触发条件,只在需要时才跑。或者把非关键钩子设为异步执行。


延伸思考

在进入下一章之前,想想这几个问题:

  1. 如果两个钩子互相依赖——A 的输出是 B 的输入——你会怎么处理执行顺序和数据传递?
  2. 技能的 Prompt 注入会增加上下文长度。如果同时激活 5 个技能,上下文可能撑爆——你会怎么设计技能的互斥或优先级机制?
  3. Hook 系统本质上让用户能在 Agent 流程中插入任意代码。这带来了安全风险——恶意钩子可以窃取对话内容、篡改工具结果。你会加什么防护措施?

章节小结

  • 三个 Prompt 构建了一个可扩展的 Agent:钩子机制 → 技能系统 → 预置钩子
  • Agent 生命周期有 7 个关键事件:SESSION_START → PRE_PROMPT → API_CALL → POST_RESPONSE → PRE_TOOL_CALL → POST_TOOL_CALL → SESSION_END
  • 四种 Hook 类型适配不同场景:Shell(执行命令)、Prompt(注入提示词)、Agent(子 Agent)、HTTP(外部通知)
  • 技能 = 系统提示 + 工具集 + 触发方式,一键激活整套能力
  • Claude Code 定义了 26 个事件,日常 80% 的需求用 5 个核心事件就够
  • 钩子设计的关键原则:不阻塞主流程、条件精确触发、安全钩子同步等待
  • 下一章我们集成 MCP——让 Harness 接入外部工具生态
English EN 简体中文 ZH