一堆 if 把 Agent Loop 写乱了:Hooks 到底解决了什么?

本文是「从零理解 Claude Code:20 个 Agent Harness 机制」系列的第 4 篇。

源码仓库:shareAI-lab/learn-claude-code

本文基于开源仓库学习整理,具体实现以仓库代码为准。

前一篇加上权限检查后,Agent 已经不会把每一条工具请求直接送进终端。

表面上看,权限检查只是工具执行前的一次 check_permission(block)。但它带来了一个变化:agent_loop() 开始同时承担两类职责。

第一类是它原本该做的事:驱动模型、工具和消息之间的循环。

第二类是不断冒出来的运行策略:什么操作要拦截、调用时怎样记录、执行后要不要检查结果、结束时要不要收尾。

这两类逻辑混在一起,会造成什么问题呢,我们可以接着往下看。

每加一个能力,我们都得先回答几个问题:

  • 它应该插在工具执行前,还是执行后?
  • 它失败后,是阻止这次工具调用,还是只记录一条日志?
  • 它要不要影响模型接下来看到的工具结果?
  • 它和已有检查谁先执行?

例如,日志应该记录被权限系统拒绝的命令吗?文件格式化应该只在 write_file 后执行,还是 edit_file 后也执行?工具输出过大时,是截断结果,还是提醒模型换一种读取方式?

这些问题本来属于扩展策略,却开始挤进核心循环。

于是,agent_loop() 不再只是任务的执行路径,还逐渐变成了权限、日志、通知、统计等规则的汇合点。每次改一个小功能,都有可能影响原有工具调用的顺序和结果。

于是核心循环慢慢变成这样:

python 复制代码
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 就是这些插口。

text 复制代码
核心循环负责推进任务;
Hooks 负责在固定时机插入额外行为。

二、Hooks 挂在什么位置

我们可以把一次 Agent 运行拆成四个事件:

事件 触发时机 这一章里的用途
UserPromptSubmit 用户提交输入后、请求模型前 记录输入、补充上下文
PreToolUse 工具执行前 权限检查、工具日志
PostToolUse 工具执行后 检查工具输出
Stop 模型准备结束任务时 输出会话统计

假设用户输入:

text 复制代码
帮我读取 README.md,然后告诉我项目是做什么的。

程序会经过这些节点:

text 复制代码
用户提交输入
→ UserPromptSubmit
→ 模型推理
→ PreToolUse
→ 执行 read_file
→ PostToolUse
→ 模型给出答案
→ Stop

前一篇的权限检查,也从循环里的硬编码,变成了一个 PreToolUse Hook。

理解了Hook的原理之后,我们很容易把这层关系画出来。

三、实现 Hooks,其实只要一个注册表

Hooks 的核心就是一个字典。

python 复制代码
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 时,只需要告诉程序:哪个事件发生后,要执行哪个函数。

python 复制代码
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

它的工作很简单:

text 复制代码
命中硬拒绝规则:直接阻止;
命中高风险规则:让用户确认;
普通操作:返回 None,继续执行。

主循环不再关心具体规则,只关心这次工具调用有没有被拦下:

python 复制代码
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 不是一个单独的函数,而是一条回调链。

这一章里,权限检查和工具日志都挂在工具执行前:

python 复制代码
register_hook("PreToolUse", permission_hook)
register_hook("PreToolUse", log_hook)

这两行的先后顺序,并不只是代码排版。

它实际定义了一条执行策略:先判断这次操作是否允许;允许后,再把它记为一次正常工具调用;最后,才执行工具。

因此,Agent 想读取 README.md 时,流程会是:

text 复制代码
permission_hook
→ log_hook
→ read_file

但如果模型请求执行 sudo reboot,权限 Hook 会先命中硬拒绝规则,并返回一段拒绝原因。

这时,trigger_hooks() 不会继续往下执行 log_hook,工具本身也不会运行。循环拿到拒绝原因后,会把它包装成工具结果,再交还给模型。

模型看到的不是程序崩掉了,而是一次明确的执行反馈:

text 复制代码
Permission denied by deny list

它可以据此放弃这条命令,也可以换一种不需要高权限的方案。

这里的关键不在于"提前 return"这个语法,而在于 Hook 的返回值拥有了控制权:

Hook 返回值 含义 主循环接下来做什么
None 当前 Hook 不干预 继续执行下一个 Hook 或工具
None 当前 Hook 拦截操作 跳过后续 Hook 和工具执行,将原因返回模型

这是一种很轻量的控制协议。

Hook 平时只是旁路逻辑,例如记录日志、统计耗时、检查输出;但在需要时,它也可以把一次工具调用从正常路径中拉出来,改成"拒绝并反馈"。

不过这也带来一个很实际的问题:被拒绝的命令,要不要记录日志?

按当前注册顺序,permission_hook 先运行,拒绝后 log_hook 根本不会触发。日志里只会看到真正通过检查的工具调用。

如果你希望审计所有请求,包括被拒绝的高风险命令,就应该把审计 Hook 放在权限 Hook 前面:

python 复制代码
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,用来统计本次会话调用过多少次工具。

终端输出可能像这样:

text 复制代码
[HOOK] Stop: session used 3 tool calls

在教学代码里,Stop Hook 甚至可以返回一段新的消息,让 Agent 不要退出,而是再继续一轮。

例如:

text 复制代码
测试还没有运行,请先检查测试结果。

不过这个能力不能乱用。

如果 Stop Hook 每次都要求继续,Agent 就可能一直停不下来。真实系统通常需要增加防重复触发和最大轮数限制,不能只依赖一个返回值。

八、直接改循环和使用 Hooks,差别在哪

把前后的组织方式放在一起看,区别会更明显。

需求 直接写进循环 使用 Hooks
工具执行前做权限检查 agent_loop() 注册 PreToolUse
记录工具调用 再改 agent_loop() 再注册一个 PreToolUse
检查工具输出 在执行后插代码 注册 PostToolUse
会话结束时打印统计 改退出分支 注册 Stop
输入前补充上下文 改输入处理 注册 UserPromptSubmit

如果项目很小,只有一个固定需求,直接在循环里写逻辑并没有问题。

Hooks 解决的是另一种情况:功能会持续增长,而且这些功能并不属于 Agent Loop 的核心职责。

这时,把扩展逻辑挂到事件上,比不断往循环里加 if 更容易维护。

这里我们可以用一张图片来对比一下两种方式的区别:

九、跑一下这章代码

进入仓库目录后执行:

bash 复制代码
python s04_hooks/code.py

可以依次试几个任务:

text 复制代码
Read the file README.md

观察普通读取时是否出现 Hook 日志。

text 复制代码
Create a file called test.txt

观察写文件前后是否经过对应事件。

text 复制代码
Delete all temporary files in /tmp

如果模型请求的命令命中了风险规则,权限 Hook 会要求确认,或者直接拒绝。

这里最值得观察的,不是输出内容本身,而是这些额外逻辑都没有继续塞进 agent_loop()

小结

Hooks 做的事情并不复杂。

它把 Agent 运行过程里的几个固定时刻标出来,让权限、日志、统计、输出检查这些功能,有地方可以挂。

最后,核心循环仍然只需要关注一件事:

text 复制代码
接收模型响应
→ 触发对应事件
→ 执行工具
→ 返回结果

当 Agent 后面继续加入更多能力时,这种拆分会越来越有用。

下一篇我们开始了解什么是任务规划。

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

相关推荐
m沐沐8 小时前
【深度学习】深入理解长短期记忆网络 LSTM
人工智能·pytorch·深度学习·神经网络·lstm
xiancai_xianyu8 小时前
企业本体语义:设备保养与供应商评估,为什么需要统一的语义模型?
大数据·人工智能
中微极客8 小时前
多模态AI实战:GPT-4o/Gemini/Claude 3对比与工程落地
人工智能
武子康8 小时前
GitHub Actions `pull_request_target` 与 Pwn Request:高权限工作流里的 Fork 代码执行风险(2026)
人工智能·github·ai编程
laboratory agent开发8 小时前
AI agent多知识源并存时,一致性为何总出偏差
大数据·人工智能
markvivv8 小时前
智能问数 Multi-Agent Harness 架构设计与实践
人工智能·harness
Jiamiren8 小时前
WEEX Labs 周度观察:AI 基础设施的“权力游戏”与硬核变现时代
人工智能·游戏
是上好佳佳佳呀8 小时前
【机器学习|DAY02】数据划分与特征工程
人工智能·机器学习
Database_Cool_9 小时前
AI Agent 应用数据库选型:阿里云 PolarDB-X 高并发分布式数据底座
数据库·人工智能·阿里云