一聚教程网:一个值得你收藏的教程网站

最新下载

热门教程

Agent Loop 如何用 Hook 扩展:权限 日志 工具拦截

时间:2026-07-26 18:04:56 编辑:袖梨 来源:一聚教程网

前言

到这一步,Agent 已经能调用工具、维护计划了。

Agent Loop 如何用 Hook 扩展:权限、日志与工具拦截

很多人接下来会很自然地把权限检查、日志记录、输出截断这类“和工具相关的附加处理”塞进 agent_loop()

for tool_call in response.tool_calls:if not check_permission(tool_call):...log_tool(tool_call)output = run_tool(tool_call)if len(output) > 100000:warn(...)

这些逻辑单看都合理,但问题在于:它们并不属于 Loop 的核心职责。

Loop 本来只该做三件事:

1. 调模型2. 有 tool_calls 就执行并回填3. 直到模型不再要工具

一旦权限、日志、审计、截断、补充上下文都开始往里面堆,Loop 很快就会从“主干”变成“杂物间”。

这就是 Hook 要解决的问题。

一句话说:

一、Hook 要解决什么问题

这一篇要解决的问题很直接:

Agent 做动作的时候,能不能被拦、被看见、被约束?

比如模型请求执行一个 bash 命令时,程序常常不只想知道“能不能执行”,还会关心:

  • 这条命令是否危险
  • 这次调用要不要记录日志
  • 输出是不是太长,不能原样塞回上下文
  • 这次任务开始前要不要补一点环境信息

这些逻辑有个共同点:它们不是某一个工具自己的业务逻辑,而是跨越很多工具的公共规则。

也正因为这样,如果把它们分别塞进 run_bashread_filewrite_fileedit_file,代码会越来越散。

二、为什么这些问题更适合交给 Hook

最直接的写法,是在工具函数里自己处理。

比如最小版本里,run_bash 已经做过一点危险命令拦截。这种做法能工作,但继续扩展时会越来越别扭。

第一,策略会重复

如果今天要限制 bash,明天还要限制 write_fileedit_file 写出工作区,那你就得在每个 run_* 函数里各写一遍相似判断。

第二,语义会混在一起

工具函数本来负责“真正执行动作”,但一旦里面既有权限规则、又有日志、又有输出裁剪,它就不再只是执行器,而变成执行器加策略层的混合体。

第三,模型收到的反馈也会不够清楚

如果你在 run_bash 里直接返回一段错误字符串,模型并不一定知道这是“策略拒绝”,还是“命令本身运行失败”。

所以更清楚的分层应该是:

模型请求工具→ 先经过策略层判断→ 再真正执行工具→ 执行完成后再经过观测层处理→ 最后把结果回填给模型

Hook 做的,就是把这层“策略与观测”的公共机制,从工具函数和主循环里抽出来。

三、Hook 如何接入 Agent Loop

这一章不需要先记很多代码,先抓住一个核心:Hook 不是替换 Agent Loop,而是插在 Loop 的几个关键节点上。

3.1 Hook 插在哪几个位置

这份代码里定义了四个触发点:

  • UserPromptSubmit:用户输入刚进入系统时
  • PreToolUse:工具真正执行之前
  • PostToolUse:工具执行完成之后
  • Stop:这一轮准备结束时

可以先把它理解成一句话:

主循环负责往前跑Hook 负责在固定节点插入附加逻辑

先看流程图,会更直观。

3.2 一次工具调用会怎么经过这些 Hook

当 Agent 跑起来时,流程大致会经过下面这些位置:

用户输入 ↓UserPromptSubmit ↓LLM 决定是否调用工具 ↓PreToolUse ↓执行工具 ↓PostToolUse ↓回填结果 ↓若本轮不再调用工具,则进入 Stop

flowchart TDU[用户输入] --> P[UserPromptSubmit]P --> L[LLM 判断是否调用工具]L --> A[PreToolUse]A --> E[执行工具]E --> B[PostToolUse]B --> R[回填结果到 messages]R --> S{本轮还要继续调用工具?}S -- 是 --> LS -- 否 --> T[Stop]

这张图想表达的就是:Loop 仍然是主线,Hook 只是插在主线上的几个检查点。

流程讲完之后,再看 Hook 具体在前后两端分别做什么。

3.3 代码里怎么把这些 Hook 组织起来

在实现上,这份代码用了一个很朴素的注册表:

HOOKS = {"UserPromptSubmit": [],"PreToolUse": [],"PostToolUse": [],"Stop": [],}def register_hook(event: str, callback):HOOKS[event].append(callback)

意思很简单:

  • 先按事件名分出几个桶
  • 每个桶里放这一类 Hook
  • 跑到对应节点时,就把这个桶里的 Hook 依次执行一遍

所以这一版 Hook 示例的落地方式其实就是:先定义触发点,再把权限检查、日志记录、输出截断这些处理挂到对应触发点上,最后由框架按顺序执行。

3.4 Hook 返回结果后,框架怎么处理

HookResult 解决的是:回调执行完之后,框架怎么理解它的结果。

@dataclassclass HookResult:action: Literal["allow", "deny", "ask", "modify"]message: str | None = None

这里最关键的是四种动作语义:

  • allow:明确放行
  • deny:拒绝继续执行
  • ask:先向用户确认
  • modify:修改原本的数据或输出

这比 True / False 更适合真实系统,因为 Agent 的拦截逻辑不只有“让不让过”两种。

3.5 PreToolUsePostToolUse 分别负责什么

在这份实现里,PreToolUsePostToolUse 分别负责两类事情:

PreToolUse 更像“门卫”PostToolUse 更像“审计和整理层”

flowchart LRP[PreToolUse] --> P1[权限检查]P --> P2[危险命令拦截]P --> P3[高风险操作确认]O[PostToolUse] --> O1[日志记录]O --> O2[输出截断]O --> O3[结果整理]

一个负责“先看能不能过”,一个负责“执行完后怎么收拾结果”。

3.6 Hook 为什么先拿到 ToolCallContext

还有一个实现细节是:Hook 不直接操作 SDK 返回的原始对象,而是先把它归一成 ToolCallContext

@dataclassclass ToolCallContext:id: strname: strarguments: dict

这样 Hook 作者只需要关心:

  • 这次调用的是哪个工具
  • 参数里有哪些字段

而不用反复处理对象访问、JSON 解析、异常分支。

四、Hook 在 Agent 架构中的位置

如果把 Agent 的结构拆开看,Loop 负责把流程跑起来,Plan 负责把任务推进下去,Hook 负责在关键节点补上拦截、确认、日志和收尾这些控制逻辑。

也正因为 Hook 专门处理这一层,后面无论是补审批、补审计,还是让不同 Agent 共用同一套工具策略,都会更容易接上去。

小结

Hook 解决的,不是“让 Agent 多一个能力”,而是“让 Agent 在调用工具时保持可控”。

这一篇最值得带走的,是这条工程判断:

热门栏目