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

很多人接下来会很自然地把权限检查、日志记录、输出截断这类“和工具相关的附加处理”塞进 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 要解决的问题。
一句话说:
这一篇要解决的问题很直接:
Agent 做动作的时候,能不能被拦、被看见、被约束?
比如模型请求执行一个 bash 命令时,程序常常不只想知道“能不能执行”,还会关心:
这些逻辑有个共同点:它们不是某一个工具自己的业务逻辑,而是跨越很多工具的公共规则。
也正因为这样,如果把它们分别塞进 run_bash、read_file、write_file、edit_file,代码会越来越散。
最直接的写法,是在工具函数里自己处理。
比如最小版本里,run_bash 已经做过一点危险命令拦截。这种做法能工作,但继续扩展时会越来越别扭。
第一,策略会重复
如果今天要限制 bash,明天还要限制 write_file、edit_file 写出工作区,那你就得在每个 run_* 函数里各写一遍相似判断。
第二,语义会混在一起
工具函数本来负责“真正执行动作”,但一旦里面既有权限规则、又有日志、又有输出裁剪,它就不再只是执行器,而变成执行器加策略层的混合体。
第三,模型收到的反馈也会不够清楚
如果你在 run_bash 里直接返回一段错误字符串,模型并不一定知道这是“策略拒绝”,还是“命令本身运行失败”。
所以更清楚的分层应该是:
模型请求工具→ 先经过策略层判断→ 再真正执行工具→ 执行完成后再经过观测层处理→ 最后把结果回填给模型
Hook 做的,就是把这层“策略与观测”的公共机制,从工具函数和主循环里抽出来。
这一章不需要先记很多代码,先抓住一个核心:Hook 不是替换 Agent Loop,而是插在 Loop 的几个关键节点上。
这份代码里定义了四个触发点:
UserPromptSubmit:用户输入刚进入系统时PreToolUse:工具真正执行之前PostToolUse:工具执行完成之后Stop:这一轮准备结束时可以先把它理解成一句话:
主循环负责往前跑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 具体在前后两端分别做什么。
在实现上,这份代码用了一个很朴素的注册表:
HOOKS = {"UserPromptSubmit": [],"PreToolUse": [],"PostToolUse": [],"Stop": [],}def register_hook(event: str, callback):HOOKS[event].append(callback)
意思很简单:
所以这一版 Hook 示例的落地方式其实就是:先定义触发点,再把权限检查、日志记录、输出截断这些处理挂到对应触发点上,最后由框架按顺序执行。
HookResult 解决的是:回调执行完之后,框架怎么理解它的结果。
class HookResult:action: Literal["allow", "deny", "ask", "modify"]message: str | None = None
这里最关键的是四种动作语义:
allow:明确放行deny:拒绝继续执行ask:先向用户确认modify:修改原本的数据或输出这比 True / False 更适合真实系统,因为 Agent 的拦截逻辑不只有“让不让过”两种。
PreToolUse 和 PostToolUse 分别负责什么在这份实现里,PreToolUse 和 PostToolUse 分别负责两类事情:
PreToolUse 更像“门卫”PostToolUse 更像“审计和整理层”
flowchart LRP[PreToolUse] --> P1[权限检查]P --> P2[危险命令拦截]P --> P3[高风险操作确认]O[PostToolUse] --> O1[日志记录]O --> O2[输出截断]O --> O3[结果整理]
一个负责“先看能不能过”,一个负责“执行完后怎么收拾结果”。
ToolCallContext还有一个实现细节是:Hook 不直接操作 SDK 返回的原始对象,而是先把它归一成 ToolCallContext。
class ToolCallContext:id: strname: strarguments: dict
这样 Hook 作者只需要关心:
而不用反复处理对象访问、JSON 解析、异常分支。
如果把 Agent 的结构拆开看,Loop 负责把流程跑起来,Plan 负责把任务推进下去,Hook 负责在关键节点补上拦截、确认、日志和收尾这些控制逻辑。
也正因为 Hook 专门处理这一层,后面无论是补审批、补审计,还是让不同 Agent 共用同一套工具策略,都会更容易接上去。
Hook 解决的,不是“让 Agent 多一个能力”,而是“让 Agent 在调用工具时保持可控”。
这一篇最值得带走的,是这条工程判断: