位置:首页 > 进阶教程 > Agent Loop中一堆if导致代码混乱,Hooks究竟解决了什么?

Agent Loop中一堆if导致代码混乱,Hooks究竟解决了什么?

时间:2026-07-22  |  作者:夜鞌不睡  |  阅读:0
在前一篇,我们给 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 result
    return 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),
    })
    continue
handler = 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 已经会调用工具,也能在关键节点插入扩展逻辑。但面对复杂任务时,它仍然可能想到什么做什么。下一章会给它加一个待办清单,让它先列计划,再逐项推进。

免责声明:文中图文均来自网络,如有侵权请联系删除,心愿游戏发布此文仅为传递信息,不代表心愿游戏认同其观点或证实其描述。

相关文章

更多

精选合集

更多

大家都在玩

热门话题

大家都在看

更多