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

从零实现 Agent Harness 系列 · 第 03 篇

前面我们已经把 Agent 的最小循环和任务推进方式讲清楚了。

但当 Agent 真开始执行工具后,很快还会碰到另一类问题:权限检查、审计日志、大输出截断,到底该写在哪里?

前言

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

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

python 复制代码
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 本来只该做三件事:

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

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

这就是 Hook 要解决的问题。

一句话说:

Hook 的作用,是把权限检查、日志记录、输出截断这类公共处理,从 agent_loop() 里拿出来,放到它该出现的时机去执行。

一、Hook 要解决什么问题

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

text 复制代码
Agent 做动作的时候,能不能被拦、被看见、被约束?

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

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

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

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

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

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

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

第一,策略会重复

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

第二,语义会混在一起

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

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

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

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

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

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

三、Hook 如何接入 Agent Loop

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

3.1 Hook 插在哪几个位置

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

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

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

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

先看流程图,会更直观。

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

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

text 复制代码
用户输入
   ↓
UserPromptSubmit
   ↓
LLM 决定是否调用工具
   ↓
PreToolUse
   ↓
执行工具
   ↓
PostToolUse
   ↓
回填结果
   ↓
若本轮不再调用工具,则进入 Stop
flowchart TD U[用户输入] --> P[UserPromptSubmit] P --> L[LLM 判断是否调用工具] L --> A[PreToolUse] A --> E[执行工具] E --> B[PostToolUse] B --> R[回填结果到 messages] R --> S{本轮还要继续调用工具?} S -- 是 --> L S -- 否 --> T[Stop]

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

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

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

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

python 复制代码
HOOKS = {
    "UserPromptSubmit": [],
    "PreToolUse": [],
    "PostToolUse": [],
    "Stop": [],
}

def register_hook(event: str, callback):
    HOOKS[event].append(callback)

意思很简单:

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

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

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

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

python 复制代码
@dataclass
class HookResult:
    action: Literal["allow", "deny", "ask", "modify"]
    message: str | None = None

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

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

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

3.5 PreToolUsePostToolUse 分别负责什么

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

text 复制代码
PreToolUse 更像"门卫"
PostToolUse 更像"审计和整理层"
flowchart LR P[PreToolUse] --> P1[权限检查] P --> P2[危险命令拦截] P --> P3[高风险操作确认] O[PostToolUse] --> O1[日志记录] O --> O2[输出截断] O --> O3[结果整理]

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

3.6 Hook 为什么先拿到 ToolCallContext

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

python 复制代码
@dataclass
class ToolCallContext:
    id: str
    name: str
    arguments: dict

这样 Hook 作者只需要关心:

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

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

四、Hook 在 Agent 架构中的位置

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

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

小结

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

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

当一段逻辑会同时作用于很多工具,又不属于主循环核心职责时,它往往就该长成 Hook。

相关推荐
MrZhao4001 小时前
Agent 为什么需要 Skills:别把所有知识都塞进 system prompt
算法
JieE2122 天前
LeetCode 101. 对称二叉树|JS 递归 + 迭代双解法,彻底搞懂镜像判断
javascript·算法
JieE2122 天前
LeetCode 56. 合并区间|超清晰 JS 图解思路,面试高频区间题
javascript·算法·面试
Jack203 天前
HarmonyOS开发中错误处理策略:网络异常统一处理
算法
小小杨树3 天前
读懂色彩:拍照调色不再难
算法·计算机视觉·配色
JieE2123 天前
LeetCode 226. 翻转二叉树|JS 递归超详细拆解,二叉树入门经典题
javascript·算法
JieE2124 天前
LeetCode 104. 二叉树的最大深度|递归思路超详细拆解
javascript·算法
vivo互联网技术4 天前
CVPR 2026 | 全新强化学习框架 BeautyGRPO:重塑真实人像
算法·大模型·cvpr·影像