2107 字
约 7 分钟
6
Hooks钩子
无标签

系列:ToyAgent 智能体知识库 24/26

Hooks 钩子:挂上去就生效的能力拦截点

一句话核心:工具执行前后不是裸奔的——宿主在 before / after 两个生命周期点暴露钩子位,审计、风控、脱敏这些横切逻辑从这里挂进去,挂钩即生效、摘钩即消失,工具代码一行不改。

为什么需要

审计、脱敏、风控三类需求有一个共同点:它们横切在所有工具上,而不是某一个工具的私有逻辑。反模式是把这些逻辑写进每个工具的实现里:

  • 发短信工具里写一段"检查内容有没有敏感词",读数据库工具里再写一段"记录谁查了什么"——改一次风控规则要动 N 个工具类,漏一处就是事故;
  • 工具作者的职责本来是"把这件事做好",安全合规却要每个作者都懂、都写对,职责彻底搅浑;
  • 想临时关掉某个审计(比如压测时),只能改代码重新发布。

钩子把这横切逻辑抽出来,挂在宿主的生命周期点上,本质就是工具调用链上的 AOP:切面与目标完全解耦。配套三个组件各管一件事——HookService 管挂载与触发,HookMatcher 管"挂在哪类工具上",OutputMerger 管"多条 after 钩子按什么顺序合并输出"。

核心机制

两个挂载点,能力边界不同:

挂载点 时机 能做什么 典型钩子
before 工具执行前 拦截(DENY)/ 改写参数 / 只记录 审计(全工具记录)、风控(短信含"密码/验证码"即拒)
after 工具返回后 改写输出 / 脱敏 / 追加告警 脱敏(手机号、邮箱打码)

before 能改变"要不要执行、拿什么执行",after 能改变"把什么返回给模型"。两者的数据流向决定了它们天然分工:before 看到的是入参,after 看到的是出参。

钩子本体的最小结构是"名称 + 挂载点 + 匹配规则 + 处理器"四元组,处理器有一个约定好的返回协议:

// handler 返回协议:null=放行;"DENY:原因"=拦截;其他非 null 值=替换内容
record Hook(String name, String phase, List<String> match, Handler handler) {}

// before 风控:只挂在 send_sms 上,命中敏感词即拦截
hooks.add(new Hook("风控-敏感外发", "before", List.of("send_sms"), (tool, payload) ->
    payload.contains("密码") ? "DENY:敏感信息禁止外发" : null));

// after 脱敏:挂在所有工具上,手机号打码
hooks.add(new Hook("脱敏", "after", List.of("*"), (tool, payload) ->
    Pattern.compile("(1[3-9])\\d{9}").matcher(payload)
           .replaceAll("$1*********")));

匹配规则(HookMatcher)是低耦合的关键:每条钩子自己声明挂在哪些工具上,"*" 表示全部;enabled=false 时该钩子整体短路,链路直通。管线的执行语义:

工具调用请求
  → before 链:按注册顺序执行,任何一条返回 DENY → 立即终止,工具不执行
  → 工具执行
  → after 链:按注册顺序依次加工,前一条的输出是后一条的输入(OutputMerger)
  → 最终结果返回给模型

before 链是"一票否决",after 链是"流水线加工"——语义不同,不可混用。

sequenceDiagram
    participant MD as 模型
    participant HS as HookService 管线
    participant TL as 工具
    MD->>HS: 工具调用请求
    HS->>HS: before 链按注册顺序执行
    Note over HS: 审计记录 / 风控判 DENY / 改写参数
    alt 任一钩子返回 DENY
        HS-->>MD: 返回拦截原因 工具未执行
    else 全部放行
        HS->>TL: 携带可能的改写参数执行
        TL-->>HS: 原始输出
        HS->>HS: after 链流水线加工
        Note over HS: 脱敏打码 / 追加告警 / OutputMerger 合并
        HS-->>MD: 返回加工后的最终结果
    end

before 链 → 工具 → after 链 的完整时序:before 一票否决决定执不执行,after 流水线决定返回什么,工具本身对钩子零感知。

举例:对照实验轨迹

同一套工具,钩子开与关跑两遍,差异一目了然:

【钩子开启】

输入:查询:张三
  before 链:审计钩子记录(tool=query_user)→ 放行
  工具执行 → 返回 "张三,手机 13812345678,邮箱 [email protected]"
  after 链:脱敏钩子匹配 "*" → 手机号/邮箱打码
  模型收到:"张三,手机 138*********,邮箱 z***@x.com"
  (模型只见到脱敏后的数据——敏感值从未进入模型上下文)

输入:发送:给 13812345678 发 密码是123456
  before 链:审计记录 → 风控钩子命中 "密码" → "DENY:敏感信息禁止外发"
  管线终止,send_sms 工具根本没执行
  模型收到拦截原因,可以转向引导用户换安全方式

【钩子关闭(enabled=false)】

输入:查询:张三
  链路直通 → 模型收到明文 "13812345678" —— 敏感数据裸奔进模型上下文
  对比出钩子的价值:数据安全不是工具的自觉,是链路的制度

输入:审计日志
  开启时:每次工具调用都有一条 before 记录(谁、何时、调了什么工具)
  关闭时:无记录 —— 审计也是钩子,摘了就没了

这个实验揭示了一个生产级的认知:脱敏挂在 after 而不是让工具自己返回假数据,意味着脱敏策略可以集中管理、随时调整,且对"哪些数据算敏感"的判定升级时不用通知任何工具作者。同理,审计挂在 before 意味着连被拦截的调用也有记录——风控拦截本身就是一条安全事件。

从生产智能体开发视角举一反三,钩子位还能承载:限流熔断(before 统计调用频率)、成本核算(after 累计 token 消耗)、灰度实验(before 按租户改写参数)、PII 合规(after 按 GDPR 规则打码)。

教学版 vs 生产版

维度 教学版 生产版
钩子来源 代码里硬编码 add SPI 插件化,热插拔(与插件机制篇打通)
匹配 工具名列表 + "*" 按工具/租户/参数特征多维匹配,规则中心化下发
审计 内存日志 落 SIEM,结构化事件 + 告警联动
脱敏 单条正则 正则规则库 + NER 模型识别 + 人工审核流程
链路语义 固定顺序、短路策略写死 短路策略、执行顺序可配置,钩子间可见性可控

面试考点

  1. "审计/脱敏这种横切需求在 Agent 里怎么落地?"——答生命周期钩子:before 管拦截与改写入参(风控/审计),after 管改写输出(脱敏/告警);挂钩即生效、摘钩即消失,工具零感知,本质是调用链上的 AOP。
  2. "before 和 after 钩子的能力边界?"——before 看到入参、可一票否决或改参,决定"执不执行";after 看到出参、可改写合并,决定"返回什么";脱敏放 after 的好处是敏感值根本不进模型上下文。
  3. "多条 after 钩子怎么协作?"——按注册顺序的流水线加工,前一条输出作为后一条输入,由 OutputMerger 负责合并语义;before 则是任一 DENY 即短路终止。

生产化路径

  • 钩子声明为 SPI 插件,支持运行时装卸与灰度启用,与插件机制的隔离加载复用同一套基础设施;
  • 审计事件接 SIEM:工具名、参数摘要、拦截原因、耗时全结构化,风控拦截自动触发告警;
  • 脱敏升级为"正则规则库 + NER 识别 + 规则灰度 + 人工审核"的组合,误杀率作为核心指标;
  • 钩子链治理:执行顺序、短路策略、超时兜底可配置;钩子自身故障不应阻断主链路(fail-open/fail-closed 按钩子级别区分);
  • 与审批门禁联动:before 钩子命中的高危操作可升级为"发起人工审批"而非直接 DENY,兼顾安全与可用性。
Hooks钩子
https://clxhxhhr.top/posts/4724/
作者
clxstart
发布于
2026-10-13
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。