您的位置:首页 > 手游攻略 > 一堆 if 把 Agent Loop 搞乱了:Hooks 究竟解决了什么

一堆 if 把 Agent Loop 搞乱了:Hooks 究竟解决了什么

作者:互联网  时间: 2026-07-21 17:35:56  

前一篇加上权限检查后,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 已经做了不少事,但它的主职责其实不多:

  1. 把消息交给模型;
  2. 判断模型是否请求工具;
  3. 找到并执行对应工具;
  4. 把工具结果放回消息列表;
  5. 直到模型不再请求工具。

权限、日志、统计、通知这些事情也很重要,只是它们不该和主流程混在一起。

可以把 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 已经会调用工具,也能在关键节点插入扩展逻辑。但面对复杂任务时,它仍然可能想到什么做什么。下一章会给它加一个待办清单,让它先列计划,再逐项推进。

最新游戏

更多

Copyright©2010-2019. All rights reserved | 波波三国游戏官网|[email protected]

备案编号:湘ICP备2022015115号-4