游乐游手机版
首页/AI教程/文章详情

钩子如何解决智能体循环中条件判断混乱

时间:2026-07-21 18:34
本文继续探讨Agent开发中的关键优化。我们在前一篇文章中为Agent引入了权限检查机制,使其不再盲目地将所有工具请求发送至终端。表面上看,权限检查仅是在工具执行前增加了一次 check_permission(block) 调用。然而,这一改动使 agent_loop() 在不知不觉中承担起了两类职

本文继续探讨Agent开发中的关键优化。我们在前一篇文章中为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 已经能够调用工具,也能够在关键节点插入扩展逻辑。但面对复杂任务时,它仍然可能想到什么就做什么。下一章,我们将为它添加一个待办清单,让它先制定计划,再逐步推进。

来源:https://juejin.cn/post/7664509208797167654
上一篇大模型流式输出实现原理:ReadableStream、Uint8Array与SSE 下一篇我给我的MCP Server做了一次渗透测试,结果吓出一身冷汗
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
TalkVisions实时视频翻译应用,消除语言障碍
AI教程 · 2026-07-25

TalkVisions实时视频翻译应用,消除语言障碍

TalkVisions是一款实时视频翻译应用,能将视频中的口语实时转录为文本并翻译成用户所选语言,以字幕形式叠加在画面上,支持多语言、低延迟,还可保存录制视频,有效消除跨语言沟通障碍。

AI驱动的日历管理工具Ipso
AI教程 · 2026-07-25

AI驱动的日历管理工具Ipso

IpsoAI是一款专为专业人士及助手打造的AI日历管理工具,能够自动协调多方日程、智能草拟邮件,并通过快速安排会议、提供智能建议及自动化工作流程,显著减少琐碎操作,帮助用户高效管理时间、提升工作效率。

Spectate企业级专业高效监控与事故管理一体化平台
AI教程 · 2026-07-25

Spectate企业级专业高效监控与事故管理一体化平台

Spectate是一款高效监控和事故管理工具,能在30秒内检测故障并推送告警。它支持Slack、PagerDuty等主流集成,提供自定义状态页面和全球性能监控。系统自动更新状态并推送修复建议,帮助团队减少沟通成本,快速解决问题。

阿里云通义千问2.5大模型发布 多项能力赶超GPT-4
AI教程 · 2026-07-25

阿里云通义千问2.5大模型发布 多项能力赶超GPT-4

通义千问2 5大模型发布,多项能力宣称赶超GPT-4,中文语境下文本理解、生成、知识问答等表现优异。相比2 1版本,理解提升9%、逻辑推理提升16%、指令遵循提升19%。开源1100亿参数模型超越Llama-3-70B,获评开源最强。已服务超9万家企业,与小米、微博等达成合作。

万知个人AI工作站:一站式智能阅读创作分享平台
AI教程 · 2026-07-25

万知个人AI工作站:一站式智能阅读创作分享平台

万知是集成多种AI能力的个人工作站,支持自然语言交互、文档快速阅读与摘要生成、PPT自动设计与优化,覆盖学术研究、商务报告、写作辅助及日常问答等场景,全方位提升工作效率。