一堆 if 把 Agent Loop 搞乱了:Hooks 究竟解决了什么
前一篇加上权限检查后,Agent 已经不会把每一条工具请求直接送进终端。
表面上看,权限检查只是工具执行前的一次 check_permission(block)。但它带来了一个变化:agent_loop() 开始同时承担两类职责。
第一类是它原本该做的事:驱动模型、工具和消息之间的循环。
第二类是不断冒出来的运行策略:什么操作要拦截、调用时怎样记录、执行后要不要检查结果、结束时要不要收尾。
这两类逻辑混在一起,会造成什么问题呢,我们可以接着往下看。
每加一个能力,我们都得先回答几个问题:
- 它应该插在工具执行前,还是执行后?
- 它失败后,是阻止这次工具调用,还是只记录一条日志?
- 它要不要影响模型接下来看到的工具结果?
- 它和已有检查谁先执行?
例如,日志应该记录被权限系统拒绝的命令吗?文件格式化应该只在 write_file 后执行,还是 edit_file 后也执行?工具输出过大时,是截断结果,还是提醒模型换一种读取方式?
这些问题本来属于扩展策略,却开始挤进核心循环。
于是,agent_loop() 不再只是任务的执行路径,还逐渐变成了权限、日志、通知、统计等规则的汇合点。每次改一个小功能,都有可能影响原有工具调用的顺序和结果。
于是核心循环慢慢变成这样:
def agent_loop(messages):while True:# 调用模型for block in response.content:log_to_file(block)check_permission(block)notify_slack(block)output = execute(block)auto_format(block)check_output_size(output)auto_git_add(block)
代码当然还能跑,但是结构也会变得越来越臃肿。
但每增加一个需求,都要进 agent_loop() 里找位置。时间一长,真正负责模型调用和工具执行的主流程,反而被各种附加逻辑淹没。由此引发一个问题,耦合性越高,越复杂的系统,往往最后就会变成屎山代码。
这一章要解决的,就是这个问题。
一、先把 Agent Loop 的职责说清楚
前面几篇里,Agent Loop 已经做了不少事,但它的主职责其实不多:
- 把消息交给模型;
- 判断模型是否请求工具;
- 找到并执行对应工具;
- 把工具结果放回消息列表;
- 直到模型不再请求工具。
权限、日志、统计、通知这些事情也很重要,只是它们不该和主流程混在一起。
可以把 Agent Loop 看成一条稳定的流水线。
用户输入后、工具执行前、工具执行后、任务结束前,这些都是固定时刻。我们要做的,不是每次有新需求就拆开流水线塞代码,而是在这些时刻预留插口。
Hooks 就是这些插口。
核心循环负责推进任务;Hooks 负责在固定时机插入额外行为。
二、Hooks 挂在什么位置
我们可以把一次 Agent 运行拆成四个事件:
| 事件 | 触发时机 | 这一章里的用途 |
|---|---|---|
UserPromptSubmit | 用户提交输入后、请求模型前 | 记录输入、补充上下文 |
PreToolUse | 工具执行前 | 权限检查、工具日志 |
PostToolUse | 工具执行后 | 检查工具输出 |
Stop | 模型准备结束任务时 | 输出会话统计 |
假设用户输入:
帮我读取 README.md,然后告诉我项目是做什么的。
程序会经过这些节点:
用户提交输入→ UserPromptSubmit→ 模型推理→ PreToolUse→ 执行 read_file→ PostToolUse→ 模型给出答案→ Stop
前一篇的权限检查,也从循环里的硬编码,变成了一个 PreToolUse Hook。
理解了Hook的原理之后,我们很容易把这层关系画出来。
三、实现 Hooks,其实只要一个注册表
Hooks 的核心就是一个字典。
HOOKS = {"UserPromptSubmit": [],"PreToolUse": [],"PostToolUse": [],"Stop": [],}def register_hook(event, callback):HOOKS[event].append(callback)def trigger_hooks(event, *args):for callback in HOOKS[event]:result = callback(*args)if result is not None:return resultreturn None
键是事件名,值是这个事件对应的回调函数列表。
注册 Hook 时,只需要告诉程序:哪个事件发生后,要执行哪个函数。
register_hook("PreToolUse", permission_hook)register_hook("PreToolUse", log_hook)register_hook("PostToolUse", large_output_hook)register_hook("Stop", summary_hook)
这几行分别表示:
- 工具执行前,先检查权限,再记录日志;
- 工具执行后,检查输出是否异常;
- 会话结束前,打印本次调用统计。
以后想加审计日志,不用修改主循环,只要再注册一个 audit_hook。
四、权限检查为什么适合做成 Hook
前一篇中,权限检查直接写在 Agent Loop 里。
现在,权限逻辑被包进 permission_hook(),并注册到 PreToolUse。
它的工作很简单:
命中硬拒绝规则:直接阻止;命中高风险规则:让用户确认;普通操作:返回 None,继续执行。
主循环不再关心具体规则,只关心这次工具调用有没有被拦下:
blocked = trigger_hooks("PreToolUse", block)if blocked:results.append({"type": "tool_result","tool_use_id": block.id,"content": str(blocked),})continuehandler = TOOL_HANDLERS.get(block.name)output = handler(**block.input)
如果 trigger_hooks() 返回 None,工具正常执行。
如果它返回了拒绝原因,例如 Permission denied by deny list,程序不会调用工具,而是把这段内容作为工具结果交还给模型。
模型能知道操作失败了,也能换一种更安全的方案继续处理。
五、同一个事件,可以挂多个 Hook,但顺序本身就是规则
PreToolUse 不是一个单独的函数,而是一条回调链。
这一章里,权限检查和工具日志都挂在工具执行前:
register_hook("PreToolUse", permission_hook)register_hook("PreToolUse", log_hook)
这两行的先后顺序,并不只是代码排版。
它实际定义了一条执行策略:先判断这次操作是否允许;允许后,再把它记为一次正常工具调用;最后,才执行工具。
因此,Agent 想读取 README.md 时,流程会是:
permission_hook→ log_hook→ read_file
但如果模型请求执行 sudo reboot,权限 Hook 会先命中硬拒绝规则,并返回一段拒绝原因。
这时,trigger_hooks() 不会继续往下执行 log_hook,工具本身也不会运行。循环拿到拒绝原因后,会把它包装成工具结果,再交还给模型。
模型看到的不是程序崩掉了,而是一次明确的执行反馈:
Permission denied by deny list
它可以据此放弃这条命令,也可以换一种不需要高权限的方案。
这里的关键不在于“提前 return”这个语法,而在于 Hook 的返回值拥有了控制权:
| Hook 返回值 | 含义 | 主循环接下来做什么 |
|---|---|---|
None | 当前 Hook 不干预 | 继续执行下一个 Hook 或工具 |
非 None | 当前 Hook 拦截操作 | 跳过后续 Hook 和工具执行,将原因返回模型 |
这是一种很轻量的控制协议。
Hook 平时只是旁路逻辑,例如记录日志、统计耗时、检查输出;但在需要时,它也可以把一次工具调用从正常路径中拉出来,改成“拒绝并反馈”。
不过这也带来一个很实际的问题:被拒绝的命令,要不要记录日志?
按当前注册顺序,permission_hook 先运行,拒绝后 log_hook 根本不会触发。日志里只会看到真正通过检查的工具调用。
如果你希望审计所有请求,包括被拒绝的高风险命令,就应该把审计 Hook 放在权限 Hook 前面:
register_hook("PreToolUse", audit_hook)register_hook("PreToolUse", permission_hook)register_hook("PreToolUse", log_hook)
此时三者的职责就变成:
audit_hook:无论允许还是拒绝,都留下记录;permission_hook:决定能否继续;log_hook:只记录已经通过检查、准备执行的操作。
这也是 Hooks 比“在循环里插几行代码”更需要小心的地方。
当 Hook 数量变多后,注册顺序、返回值约定、错误处理方式,都会影响 Agent 的实际行为。它们不是附属细节,而是这套扩展机制的一部分。
六、工具执行后,Hooks 还能做什么
PostToolUse 用在工具真正执行完成之后。
这一章给了一个很简单的例子:如果工具输出过大,就打印提醒。
实际写 Agent 时,这个位置能放的事情很多:
| 场景 | 可以做什么 |
|---|---|
| 工具执行完成 | 记录耗时、记录执行结果 |
| 文件修改完成 | 触发格式化、运行测试 |
| 返回内容过长 | 截断、摘要或写入临时文件 |
| 调用外部服务 | 记录审计日志、脱敏敏感字段 |
这里要克制一点。
Hooks 提供的是扩展位置,不代表所有动作都适合自动执行。
比如每次写文件后都自动 git add,看上去省事,但用户可能根本不希望暂存这次修改。自动化越靠近真实项目状态,越需要明确边界。
七、任务结束前,也可以插入动作
当模型认为任务做完了,不再请求工具时,Agent Loop 就准备结束。
这一章在退出前触发 Stop Hook,用来统计本次会话调用过多少次工具。
终端输出可能像这样:
[HOOK] Stop: session used 3 tool calls
在教学代码里,Stop Hook 甚至可以返回一段新的消息,让 Agent 不要退出,而是再继续一轮。
例如:
测试还没有运行,请先检查测试结果。
不过这个能力不能乱用。
如果 Stop Hook 每次都要求继续,Agent 就可能一直停不下来。真实系统通常需要增加防重复触发和最大轮数限制,不能只依赖一个返回值。
八、直接改循环和使用 Hooks,差别在哪
把前后的组织方式放在一起看,区别会更明显。
| 需求 | 直接写进循环 | 使用 Hooks |
|---|---|---|
| 工具执行前做权限检查 | 改 agent_loop() | 注册 PreToolUse |
| 记录工具调用 | 再改 agent_loop() | 再注册一个 PreToolUse |
| 检查工具输出 | 在执行后插代码 | 注册 PostToolUse |
| 会话结束时打印统计 | 改退出分支 | 注册 Stop |
| 输入前补充上下文 | 改输入处理 | 注册 UserPromptSubmit |
如果项目很小,只有一个固定需求,直接在循环里写逻辑并没有问题。
Hooks 解决的是另一种情况:功能会持续增长,而且这些功能并不属于 Agent Loop 的核心职责。
这时,把扩展逻辑挂到事件上,比不断往循环里加 if 更容易维护。
这里我们可以用一张图片来对比一下两种方式的区别:
九、跑一下这章代码
进入仓库目录后执行:
python s04_hooks/code.py
可以依次试几个任务:
Read the file README.md
观察普通读取时是否出现 Hook 日志。
Create a file called test.txt
观察写文件前后是否经过对应事件。
Delete all temporary files in /tmp
如果模型请求的命令命中了风险规则,权限 Hook 会要求确认,或者直接拒绝。
这里最值得观察的,不是输出内容本身,而是这些额外逻辑都没有继续塞进 agent_loop()。
小结
Hooks 做的事情并不复杂。
它把 Agent 运行过程里的几个固定时刻标出来,让权限、日志、统计、输出检查这些功能,有地方可以挂。
最后,核心循环仍然只需要关注一件事:
接收模型响应→ 触发对应事件→ 执行工具→ 返回结果
当 Agent 后面继续加入更多能力时,这种拆分会越来越有用。
下一篇我们开始了解什么是任务规划。
现在的 Agent 已经会调用工具,也能在关键节点插入扩展逻辑。但面对复杂任务时,它仍然可能想到什么做什么。下一章会给它加一个待办清单,让它先列计划,再逐项推进。
-
07.21
月亮影视大全app如何下载电视剧
-
07.21
炉石兆示萨卡组3月2026一览
-
07.21
炉石打脸法卡组3月2026详情
-
07.21
金铲铲之战16.7b版本更新全部内容详情
-
07.21
江南百景图同乡会馆建造位置介绍
-
07.21
原神冬极白星属性及突破材料介绍
-
-
下载
- |
-
-
下载
- 《行尸走肉第一章》免安装中文汉化硬盘版下载
- 单机|436 MB
- 一款以动作冒险为主题的游戏
-
-
下载
- 《街头霸王X铁拳》免安装中文汉化硬盘版下载
- 单机|111MB
- 一款非常好玩的格斗游戏
-
-
下载
- |
-
-
下载
- 《暗黑破坏神3》免安装繁体中文正式版下载
- 单机|7630 MB
- 一款以角色扮演为主题的游戏
-
-
下载
- 《马克思佩恩3》免安装硬盘版下载
- 单机|27033 MB
- 一款以第三人称射击为主题的游戏