Hooks 与技能 — 生命周期的无限可能
在 Agent 的每个关键时刻插入你的逻辑——这就是无限可能的起点
📝 本章目标
读完本章,你将:
- 用三个 Prompt 让 AI 帮你构建钩子系统和技能系统
- 理解 Agent 生命周期中的关键事件点——在什么时刻能做什么
- 掌握四种 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 会话的生命周期事件
- SESSION_START → 会话开始,加载配置和记忆
- PRE_PROMPT → 用户消息发送前,可修改或拦截
- API_CALL → 调用 LLM,发送消息
- POST_RESPONSE → 收到 AI 回复后,可审查或修改
- PRE_TOOL_CALL → 执行工具前,可检查或拦截
- POST_TOOL_CALL → 工具执行后,可处理结果
- 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:技能的三要素
- Prompt → 系统提示词
- Tools → 工具集合
- Trigger → 激活方式
Prompt 定义这个技能的”人设”——比如代码审查技能的 Prompt 告诉 AI”你是一个严格的代码审查专家”。Tools 定义它能用什么工具——代码审查只需要读文件,不需要写文件。Trigger 定义怎么激活——用户输入命令、匹配关键词、或者由钩子自动触发。
激活一个技能时,系统做三件事:
- 注入 Prompt——把技能的系统提示词追加到当前上下文
- 加载 Tools——把技能专属的工具加入可用工具列表
- 修改状态——设置标记,让后续逻辑知道当前处于哪个技能模式
技能的核心实现出奇地简单:
# 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 前 |
| PostAPICall | LLM API 返回后 |
| PostResponse | AI 完整回复生成后 |
| SubagentStart | 子 Agent 启动时 |
| SubagentEnd | 子 Agent 结束时 |
| SessionStart | 会话开始 |
| SessionEnd | 会话结束 |
| Notification | 系统通知触发时 |
| PreModelSwitch | 模型降级/切换前 |
| PostModelSwitch | 模型降级/切换后 |
| SkillActivation | 技能被激活时 |
| ErrorOccurred | 发生错误时 |
💡 最重要的 5 个事件
26 个事件不需要全记住。日常使用中,80% 的需求用这 5 个就够了:
- SessionStart——加载项目上下文和记忆
- PreToolUse——安全检查,拦截危险操作
- PostToolUse_FileWrite——写完文件后自动格式化
- PostResponse——审查 AI 输出、记录日志
- SessionEnd——保存记忆和会话摘要
其余的事件在特殊场景下才用得上。比如 PreModelSwitch 只在你需要控制模型降级行为时才有用,SubagentStart 只在多 Agent 编排时才相关。
事件的传播机制
一个事件可以注册多个钩子,它们按优先级顺序依次执行。如果某个钩子返回”拦截”信号,后续钩子和主流程都会停止——这就是 PRE_TOOL_CALL 能拦截危险操作的原理。
执行顺序遵循三条规则:
- 优先级高的先执行——安全检查钩子优先级最高,日志记录最低
- 同优先级按注册顺序——先注册的先跑
- 拦截即停止——任何钩子返回”拦截”,事件传播立即终止
这和 DOM 事件冒泡、Express 中间件链的设计思想完全一致——责任链模式。
异步与超时
钩子执行不应该让用户干等。几条实用规则:
- Shell Hook 默认 5 秒超时——超时就杀掉进程,记一条警告
- HTTP Hook 默认异步发送——不等响应,发出去就继续
- Agent Hook 可以选择后台执行——不阻塞主对话
- 只有安全相关的 PRE 类钩子值得同步等待
⚠️ 钩子的性能陷阱
每个钩子都有执行开销。如果你在 POST_TOOL_CALL 上注册了一个 3 秒的格式化命令,而一次会话有 50 次工具调用,那就多了 150 秒的等待。
解决方案:用
condition精确限定触发条件,只在需要时才跑。或者把非关键钩子设为异步执行。
延伸思考
在进入下一章之前,想想这几个问题:
- 如果两个钩子互相依赖——A 的输出是 B 的输入——你会怎么处理执行顺序和数据传递?
- 技能的 Prompt 注入会增加上下文长度。如果同时激活 5 个技能,上下文可能撑爆——你会怎么设计技能的互斥或优先级机制?
- 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 接入外部工具生态