20 - Hook系统详解

理解什么是Hook、为什么需要Hook、本项目的9种Hook类型与实战实现

一、什么是Hook(钩子)?

通俗理解

Hook就像生活中的"插队点"——在某个流程执行到特定位置时,允许外部代码"插进来"做点事情。

技术定义

Hook(钩子)是一种观察者模式的应用:系统在关键位置发布事件,注册的监听器(Hook)可以响应这些事件,执行自定义逻辑。

Hook模式的优缺点

  Hook模式的优势:
  ┌──────────────────────────────────────────────────┐
  │ · 解耦: 核心流程不需要知道Hook的存在              │
  │ · 可扩展: 新增功能只需添加Hook,不修改核心代码    │
  │ · 可插拔: Hook可以随时注册/注销                   │
  │ · 顺序控制: 通过Order字段控制执行顺序             │
  └──────────────────────────────────────────────────┘

  Hook模式的代价:
  ┌──────────────────────────────────────────────────┐
  │ · 调试困难: 逻辑分散在多个Hook中,不易追踪       │
  │ · 执行顺序: 多个Hook的顺序可能影响结果            │
  │ · 性能开销: 每个Hook点都有方法调用开销            │
  │ · 错误传播: 一个Hook出错可能影响整个流程          │
  └──────────────────────────────────────────────────┘

  常见Hook框架对比:
  ┌──────────────────────────────────────────────────┐
  │ 框架         Hook类型        特点                 │
  │─────────────────────────────────────────────────│
  │ Git Hooks    pre-commit等    代码提交流程控制     │
  │ React Hooks  useState等      组件状态管理         │
  │ Webpack      tapable         构建流程插件         │
  │ Spring       @PostConstruct  Bean生命周期         │
  │ 本项目       AgentHook       Agent执行生命周期    │
  └──────────────────────────────────────────────────┘

二、本项目的Hook系统设计

核心架构

  Hook系统核心类:

  ┌────────────────────────────────────────────────────────┐
  │                    AgentHook 接口                       │
  │  · supportedTypes()  → 监听哪些Hook类型               │
  │  · getOrder()        → 执行顺序 (越小越先执行)        │
  │  · isAsync()         → 是否异步执行                   │
  │  · onHook(event)     → Hook的实际逻辑                 │
  │                        返回 ALLOW / WARN / BLOCK      │
  └───────────────────────┬────────────────────────────────┘
                          │ @Component 自动注册
                          ▼
  ┌────────────────────────────────────────────────────────┐
  │              AgentHookPublisher (发布器)                │
  │                                                        │
  │  构造函数注入 List<AgentHook> → 按 AgentHookType 分组  │
  │  每组按 order 排序                                      │
  │                                                        │
  │  publish(type, event):                                  │
  │    for each hook in hooks[type]:  ← 按order排序        │
  │      if async → 提交到线程池,不阻塞                   │
  │      else     → 同步调用 onHook()                      │
  │        if result == BLOCK && isBeforeType:              │
  │          → 立即返回,后续Hook和主流程都不执行           │
  └────────────────────────────────────────────────────────┘

  支持类:
  ├── AgentHookType    ← 9种Hook类型枚举
  ├── AgentHookEvent   ← 事件数据(agentId, taskId, request, response...)
  ├── AgentHookResult  ← 返回结果(ALLOW/WARN/BLOCK)
  └── HookContext      ← ConcurrentHashMap, Hook间传递数据

三、9种Hook类型详解

#Hook类型触发时机能阻断?触发位置
1ON_USER_MESSAGE 可阻断用户消息预处理后,传给Agent前ConversationController
2BEFORE_AGENT_EXECUTION 可阻断Agent执行前(熔断/限流之后)AgentTaskOrchestrator
3ON_SYSTEM_PROMPT系统提示词构建后,LLM调用前AbstractLlmAgent
4BEFORE_LLM_CALL 可阻断消息列表组装完成,LLM API调用前AbstractLlmAgent
5AFTER_LLM_CALLLLM调用返回后(仅非ReAct模式)AbstractLlmAgent
6ON_OUTPUT 未使用输出发送给客户端前(预留)暂未实现
7AFTER_AGENT_EXECUTIONAgent执行成功完成后AgentTaskOrchestrator
8ON_AGENT_ERRORAgent执行失败时AgentTaskOrchestrator
9ON_AGENT_TIMEOUTAgent执行超时时AgentTaskOrchestrator

四、Hook在Agent执行流程中的位置

  Agent执行的完整流程 + Hook触发点:

  用户发送消息
       │
       ▼
  ┌─────────────────────────────────────────────┐
  │ ① ON_USER_MESSAGE          ← Hook点 1      │
  │   可以: 修改用户消息内容 / 阻断请求          │
  │   位置: ConversationController.chat()        │
  └──────────────────┬──────────────────────────┘
                     ▼
  ┌─────────────────────────────────────────────┐
  │ AgentTaskOrchestrator.execute()              │
  │   ├── 熔断检查                               │
  │   ├── 限流检查                               │
  │   ├── ② BEFORE_AGENT_EXECUTION ← Hook点 2  │
  │   │   可以: 阻止Agent执行 / 修改请求         │
  │   ├── 创建AgentTask                          │
  │   └── 提交到线程池                           │
  └──────────────────┬──────────────────────────┘
                     ▼
  ┌─────────────────────────────────────────────┐
  │ AbstractLlmAgent.chat()                      │
  │   ├── 构建系统提示词                         │
  │   ├── 安全包装(PromptIsolation)              │
  │   ├── ③ ON_SYSTEM_PROMPT     ← Hook点 3    │
  │   │   可以: 动态修改系统提示词               │
  │   ├── 注入工具描述                           │
  │   ├── 组装消息列表                           │
  │   ├── ④ BEFORE_LLM_CALL      ← Hook点 4    │
  │   │   可以: 修改消息列表 / 阻止LLM调用       │
  │   ├── [LLM API调用]                         │
  │   ├── ⑤ AFTER_LLM_CALL       ← Hook点 5    │
  │   │   可以: 处理LLM响应                     │
  │   └── [ReAct循环 / 流式输出]                │
  └──────────────────┬──────────────────────────┘
                     ▼
  ┌─────────────────────────────────────────────┐
  │ 完成后:                                     │
  │   ├── 成功 → ⑦ AFTER_AGENT_EXECUTION (Hook点7)│
  │   ├── 失败 → ⑧ ON_AGENT_ERROR    (Hook点 8) │
  │   └── 超时 → ⑨ ON_AGENT_TIMEOUT  (Hook点 9) │
  └─────────────────────────────────────────────┘

五、本项目唯一的Hook实现 — SecurityEventHook

项目当前只有一个具体的Hook实现,用于安全审计

SecurityEventHook
文件: hub-api/src/main/java/com/example/agenthub/api/security/SecurityEventHook.java

  ┌──────────────────────────────────────────────────┐
  │ @Component                                        │
  │ public class SecurityEventHook implements AgentHook {│
  │                                                  │
  │   支持的Hook类型:                                 │
  │   ├── ON_USER_MESSAGE   ← 记录用户消息           │
  │   ├── ON_AGENT_ERROR    ← 记录Agent错误          │
  │   └── ON_AGENT_TIMEOUT  ← 记录Agent超时          │
  │                                                  │
  │   执行顺序: 10 (很早执行)                         │
  │   是否异步: true (不阻塞主流程)                   │
  │                                                  │
  │   依赖: AuditLogService (审计日志服务)            │
  │                                                  │
  │   实现逻辑:                                      │
  │   ├── ON_USER_MESSAGE:                           │
  │   │   auditLogService.log(userId, agentId,        │
  │   │       message.substring(0, 200))              │
  │   │   // 截取前200字符记录                        │
  │   │                                              │
  │   ├── ON_AGENT_ERROR:                            │
  │   │   auditLogService.log(userId, agentId,        │
  │   │       "ERROR: " + errorMessage)               │
  │   │                                              │
  │   └── ON_AGENT_TIMEOUT:                          │
  │       auditLogService.log(userId, agentId,        │
  │           "TIMEOUT after " + timeout + "s")       │
  └──────────────────────────────────────────────────┘

五-B、TelemetryHook — 分布式追踪 Hook 实现

基于 Spring AI Alibaba 的 TelemetryHook,为每次 Agent 执行创建 OpenTelemetry 分布式追踪 Span:

TelemetryHook
文件: hub-agent-core/.../hook/TelemetryHook.java

  ┌──────────────────────────────────────────────────┐
  │ @Component                                        │
  │ @ConditionalOnClass(name = "...Tracer")           │
  │ public class TelemetryHook implements AgentHook { │
  │                                                  │
  │   支持的Hook类型:                                 │
  │   ├── BEFORE_AGENT_EXECUTION ← 创建 Span         │
  │   ├── AFTER_AGENT_EXECUTION  ← 标记成功,关闭    │
  │   ├── ON_AGENT_ERROR         ← 标记错误,关闭    │
  │   └── ON_AGENT_TIMEOUT       ← 标记超时,关闭    │
  │                                                  │
  │   执行顺序: 10 (高优先级)                         │
  │   是否异步: false                                 │
  │                                                  │
  │   Span 标签:                                      │
  │   ├── agent.id     ← Agent 唯一标识               │
  │   ├── task.id      ← 任务 ID                     │
  │   ├── conversation.id ← 会话 ID                  │
  │   └── model.id     ← 模型 ID                     │
  │                                                  │
  │   跨生命周期 Span 管理:                           │
  │   ConcurrentHashMap<taskId, Span> activeSpans    │
  │   BEFORE 创建 → AFTER/ERROR/TIMEOUT 关闭         │
  │                                                  │
  │   ON_AGENT_TIMEOUT 特殊处理:                      │
  │   防止超时场景下 Span 泄漏(永不关闭),          │
  │   确保 Span 正确结束并标记 status=timeout         │
  └──────────────────────────────────────────────────┘
@ConditionalOnClass:仅当 classpath 存在 OpenTelemetry Tracer 时自动激活,未引入追踪依赖时零开销。
Hook返回值的三种结果:

  AgentHookResult.ALLOW
  ┌──────────────────────────────────────┐
  │ 继续执行,一切正常                   │
  │ 后续Hook和主流程继续                 │
  └──────────────────────────────────────┘

  AgentHookResult.WARN("警告信息")
  ┌──────────────────────────────────────┐
  │ 记录警告日志                         │
  │ 继续执行,不阻断                     │
  └──────────────────────────────────────┘

  AgentHookResult.BLOCK("拒绝原因")
  ┌──────────────────────────────────────┐
  │ 只有 BEFORE_* 和 ON_USER_MESSAGE     │
  │ 类型的Hook才能生效:                  │
  │                                      │
  │ · 立即停止执行后续Hook               │
  │ · 主流程也停止                       │
  │ · 返回错误响应给用户                 │
  │                                      │
  │ 其他类型的BLOCK被忽略(只记录日志)    │
  └──────────────────────────────────────┘

  阻断流程示例:
  ┌──────────────────────────────────────────────┐
  │ Hook A (order=10): ALLOW ✅                  │
  │ Hook B (order=20): BLOCK("敏感内容") ❌      │
  │ Hook C (order=30): (不执行)                  │
  │ 主流程:         (不执行)                      │
  │ → 用户收到: "请求被拦截: 敏感内容"            │
  └──────────────────────────────────────────────┘

七、Hook间的数据传递 — HookContext

HookContext 预定义的键:

  ┌──────────────────────────────────────────────────┐
  │ HookContext (ConcurrentHashMap)                   │
  │                                                  │
  │ 标准键:                                          │
  │ ├── MODIFIED_MESSAGE     ← ON_USER_MESSAGE用    │
  │ │   Hook写入修改后的消息,Controller读取          │
  │ │                                              │
  │ ├── MODIFIED_SYSTEM_PROMPT ← ON_SYSTEM_PROMPT用 │
  │ │   Hook写入修改后的提示词,Agent读取            │
  │ │                                              │
  │ ├── SKIP_AGENT           ← BEFORE_AGENT用       │
  │ │   Hook设置跳过Agent执行                       │
  │ │                                              │
  │ └── CONFIDENCE_SCORE     ← Pipeline用           │
  │     Pipeline审核者写入置信度分数                 │
  │                                                  │
  │ 使用示例:                                        │
  │ // Hook中修改用户消息                            │
  │ event.getContext().put(                          │
  │   HookContext.MODIFIED_MESSAGE,                  │
  │   sanitizedMessage                               │
  │ );                                              │
  │                                                  │
  │ // Controller中读取修改后的消息                  │
  │ String msg = event.getContext()                 │
  │   .get(HookContext.MODIFIED_MESSAGE);            │
  └──────────────────────────────────────────────────┘

八、如何新增一个Hook(实战指南)

// 示例: 添加一个内容审核Hook
// 文件: hub-api/src/main/java/com/example/agenthub/api/hook/ContentModerationHook.java

@Component
public class ContentModerationHook implements AgentHook {

    @Override
    public AgentHookType[] supportedTypes() {
        // 监听用户消息,在执行前审核内容
        return new AgentHookType[]{ AgentHookType.ON_USER_MESSAGE };
    }

    @Override
    public int getOrder() {
        return 20; // 在SecurityEventHook(10)之后执行
    }

    @Override
    public boolean isAsync() {
        return false; // 同步执行,可以阻断流程
    }

    @Override
    public AgentHookResult onHook(AgentHookEvent event) {
        String message = event.getRequest().getMessage();

        // 检查是否包含违禁词
        if (containsBannedContent(message)) {
            return AgentHookResult.BLOCK("内容包含违禁信息,请求已拦截");
        }

        return AgentHookResult.ALLOW;
    }

    private boolean containsBannedContent(String text) {
        // 实现内容审核逻辑...
        return false;
    }
}

// 就这么简单!@Component注解会自动注册到AgentHookPublisher
// 无需修改任何其他代码

九、Hook vs 其他扩展机制对比

机制触发方式能阻断?适用场景本项目使用
Hook事件驱动是(BEFORE类型)审计、安全、监控SecurityEventHook
Filter请求拦截认证、CORS、编码JwtAuthenticationFilter
Interceptor方法拦截日志、事务Spring AOP
Listener事件监听异步通知、统计DelayedResponseEvent
Plugin动态加载运行时扩展AgentPluginManager

十、PiiDetectionHook 详解(隐私数据检测)

用途与设计目标

PiiDetectionHook 负责在 Agent 管道中自动检测并脱敏个人隐私信息(PII — Personally Identifiable Information)。它在输入侧输出侧各设置一道扫描关卡,确保敏感数据不会泄露给 LLM 或最终用户。

核心原则:宁可误拦不可漏放。PII 一旦进入 LLM,就可能出现在日志、缓存、向量库等多个位置,追溯清除成本极高。

检测模式与正则示例

PII 类型模式特征正则示例说明
身份证号18 位数字 + 校验位\d{17}[\dXx]支持末位 X/x
手机号11 位,1 开头1[3-9]\d{9}覆盖所有运营商号段
银行卡号16-19 位数字\d{16,19}Luhn 校验辅助判定
邮箱地址标准 email 格式[\w.+-]+@[\w-]+\.[\w.]+常见域名
中文姓名2-4 个汉字[一-龥]{2,4}需上下文辅助判断
// PII 正则模式注册
public class PiiPatterns {
    private static final Map<PiiType, Pattern> PATTERNS = Map.of(
        PiiType.ID_CARD,    Pattern.compile("(?<!\\d)(\\d{17}[\\dXx])(?!\\d)"),
        PiiType.PHONE,      Pattern.compile("(?<!\\d)(1[3-9]\\d{9})(?!\\d)"),
        PiiType.BANK_CARD,  Pattern.compile("(?<!\\d)(\\d{16,19})(?!\\d)"),
        PiiType.EMAIL,      Pattern.compile("([\\w.+-]+@[\\w-]+\\.[\\w.]+)"),
        PiiType.NAME,       Pattern.compile("(?<=姓名[::]\\s{0,2})([\\u4e00-\\u9fa5]{2,4})")
    );
}

脱敏策略

策略效果示例适用场景
掩码 (MASK)130****1234 / 3201**********1234保留部分特征,便于用户核对
替换 (REPLACE)[PHONE] / [ID_CARD]完全消除,适合发送给 LLM
删除 (REMOVE)直接移除整段文本高敏感场景
// 脱敏策略应用
public String sanitize(String text, PiiType type, MaskStrategy strategy) {
    return switch (strategy) {
        case MASK    -> applyMask(text, type);     // 130****1234
        case REPLACE -> "[" + type.name() + "]";   // [PHONE]
        case REMOVE  -> "";                         // 删除
    };
}

触发时机

PiiDetectionHook 同时注册了两个 Hook 类型:

  PII 检测双关卡流程:

  User Input                                                    User
     │                                                           ▲
     ▼                                                           │
  ┌─────────────────────┐                              ┌─────────────────────┐
  │ ON_USER_MESSAGE     │                              │ ON_OUTPUT           │
  │ ┌─────────────────┐ │                              │ ┌─────────────────┐ │
  │ │ PII Scan        │ │                              │ │ PII Scan        │ │
  │ │ ├── 身份证检测  │ │                              │ │ ├── 身份证检测  │ │
  │ │ ├── 手机号检测  │ │                              │ │ ├── 手机号检测  │ │
  │ │ ├── 银行卡检测  │ │                              │ │ ├── 银行卡检测  │ │
  │ │ ├── 邮箱检测    │ │                              │ │ ├── 邮箱检测    │ │
  │ │ └── 姓名检测    │ │                              │ │ └── 姓名检测    │ │
  │ └────────┬────────┘ │                              │ └────────┬────────┘ │
  │          ▼          │                              │          ▼          │
  │   Mask / Replace    │                              │   Mask / Replace    │
  └──────────┬──────────┘                              └──────────┬──────────┘
             ▼                                                    │
        Clean Input ──────▶ LLM ──────▶ Raw Output ──────────────┘

十一、SummarizationHook 详解(自动摘要)

用途与触发条件

SummarizationHook 在对话历史 Token 数接近模型上下文窗口极限时,自动压缩历史消息。默认阈值为 80000 tokens,超出后触发摘要流程。

为什么需要自动摘要? LLM 上下文窗口有限(如 GPT-4 128k、Qwen 32k)。长对话不压缩 → Token 超限 → API 报错。手动截断会丢失关键信息,自动摘要是更优解。

摘要算法

  Token 超阈值时的消息处理策略:

  原始消息列表 (超过 80000 tokens):
  ┌──────────────────────────────────────────────────┐
  │ [0] System Prompt          ← 始终保留            │
  │ [1] User: 你好                                   │
  │ [2] Assistant: 你好!                            │
  │ [3] User: 帮我分析数据...   ┐                    │
  │ [4] Assistant: 好的...      │                    │
  │ [5] User: 继续深入分析...   ├── 中间消息 → 摘要  │
  │ [6] Assistant: 分析结果...  │                    │
  │ [7] Tool Call: query_db...  │                    │
  │ [8] Tool Result: {...}      ┘                    │
  │ [9] User: 最新的问题        ┐                    │
  │ [10] Assistant: 最新回复     ├── 最近 N 条保留    │
  │ [11] User: 当前问题          ┘                    │
  └──────────────────────────────────────────────────┘

  压缩后:
  ┌──────────────────────────────────────────────────┐
  │ [0] System Prompt          ← 始终保留            │
  │ [1] Summary: "用户请求数据分析,经过多轮讨论     │
  │     和数据库查询,得出了XXX结论..."  ← LLM摘要   │
  │ [2] User: 最新的问题        ← preserveRecent     │
  │ [3] Assistant: 最新回复      ← preserveRecent     │
  │ [4] User: 当前问题           ← preserveRecent     │
  └──────────────────────────────────────────────────┘

SUMMARIZE_PROMPT 模板

// 摘要 Prompt 模板
private static final String SUMMARIZE_PROMPT = """
    请将以下对话历史压缩为一段简洁的摘要。要求:
    1. 保留所有关键信息:用户意图、重要数据、工具调用结果、中间结论
    2. 保留专有名词、数值、ID 等不可推断的信息
    3. 使用第三人称描述("用户请求了..."、"系统返回了...")
    4. 控制在 500 字以内

    对话历史:
    {messages}
    """;

Fallback 与防重入

@Override
public AgentHookResult onHook(AgentHookEvent event) {
    HookContext ctx = event.getContext();

    // 防重入:同一轮 ReAct 循环中只摘要一次
    if (ctx.containsKey(CTX_SUMMARIZED)) {
        return AgentHookResult.ALLOW;
    }

    int tokenCount = tokenCounter.count(event.getMessages());
    if (tokenCount < tokenThreshold) {
        return AgentHookResult.ALLOW;
    }

    try {
        String summary = chatModel.call(buildSummarizePrompt(event));
        event.getMessages().replaceMiddle(summary, preserveRecentMessages);
        ctx.put(CTX_SUMMARIZED, true);
    } catch (Exception e) {
        // Fallback: chatModel 不可用时,简单截断保留最近 N 条
        log.warn("Summarization failed, falling back to truncation", e);
        event.getMessages().truncateKeepRecent(preserveRecentMessages);
    }
    return AgentHookResult.ALLOW;
}

监控指标

指标名类型说明
hook.summarization.triggeredCounter摘要触发次数
hook.summarization.tokens.beforeGauge摘要前 Token 数
hook.summarization.tokens.afterGauge摘要后 Token 数
hook.summarization.fallbackCounter降级截断次数
hook.summarization.duration_msTimer摘要耗时

十二、HumanInTheLoopHook 详解(人机协同)

三种审批策略

HumanInTheLoopHook 为敏感操作(如工具调用)提供人类审批环节,确保 Agent 不会自动执行高风险动作。

策略行为适用场景
AUTO直接放行,无需审批低风险工具(查询天气、搜索)
APPROVE暂停执行,等待人类审批中高风险工具(发邮件、修改数据)
DENY直接拒绝,不允许执行禁止工具(删除数据库、系统命令)

工具级别粒度配置

# application.yml — 工具审批策略配置
agent:
  human-in-the-loop:
    enabled: true
    default-policy: APPROVE          # 默认策略:需要审批
    timeout: 300s                     # 审批超时时间(默认 300 秒)
    tool-policies:
      weather_query: AUTO             # 天气查询:自动放行
      web_search: AUTO                # 网页搜索:自动放行
      send_email: APPROVE             # 发送邮件:需要审批
      execute_sql: APPROVE            # 执行 SQL:需要审批
      delete_record: DENY             # 删除记录:直接拒绝
      shell_command: DENY             # Shell 命令:直接拒绝

审批流程

  HumanInTheLoop 审批流程:

  Agent ReAct Loop
       │
       ▼ BEFORE_TOOL_CALL
  ┌─────────────────────────────────────────────────────┐
  │ HumanInTheLoopHook.onHook()                         │
  │                                                     │
  │   toolName = event.getToolCall().getName()           │
  │   policy = toolPolicies.getOrDefault(toolName,       │
  │                                      defaultPolicy)  │
  │                                                     │
  │   switch(policy):                                    │
  │   ┌─────────┐  ┌──────────┐  ┌──────────┐          │
  │   │  AUTO   │  │ APPROVE  │  │   DENY   │          │
  │   │ return  │  │ 挂起执行 │  │ return   │          │
  │   │ ALLOW   │  │ 保存状态 │  │ BLOCK    │          │
  │   └────┬────┘  └────┬─────┘  └────┬─────┘          │
  │        │            │              │                 │
  └────────┼────────────┼──────────────┼─────────────────┘
           │            │              │
           ▼            ▼              ▼
      继续执行    ┌──────────┐    拒绝并返回
                  │ 前端审批  │    错误信息
                  │   UI     │
                  │          │
                  │ ✅ 批准  │──────▶ 恢复执行, 从 Checkpoint 继续
                  │ ❌ 拒绝  │──────▶ 返回"操作已被管理员拒绝"
                  │ ⏰ 超时  │──────▶ 返回"审批超时(300s)"
                  └──────────┘

超时与 Checkpoint 机制

// 审批等待核心逻辑
public AgentHookResult waitForApproval(String taskId, String toolName) {
    // 1. 保存当前执行状态到 Checkpoint
    Checkpoint checkpoint = checkpointService.save(taskId);

    // 2. 发送审批请求到前端
    approvalService.requestApproval(ApprovalRequest.builder()
        .taskId(taskId)
        .toolName(toolName)
        .toolArgs(event.getToolCall().getArguments())
        .timeout(timeout)
        .build());

    // 3. 阻塞等待审批结果(带超时)
    ApprovalResult result = approvalService
        .waitForResult(taskId, timeout);

    return switch (result.getStatus()) {
        case APPROVED -> AgentHookResult.ALLOW;
        case REJECTED -> AgentHookResult.BLOCK("操作已被管理员拒绝");
        case TIMEOUT  -> AgentHookResult.BLOCK(
            "审批超时(" + timeout.toSeconds() + "s),操作已取消");
    };
}

十三、Hook 与 Interceptor 协作流程

完整执行流程(Hook + Interceptor 联合视角)

Hook(本项目自定义机制)与 Interceptor(Spring AI Alibaba 拦截器链)在 Agent 执行过程中交替触发,共同构成完整的扩展点体系:

  完整执行流程 — Hook 与 Interceptor 协作:

  Request (用户请求)
     │
     ▼
  ┌──────────────────────────────────────────────────────────────────────┐
  │ ON_USER_MESSAGE (Hook)              ← PII检测、内容审核             │
  └──────────────────┬───────────────────────────────────────────────────┘
                     ▼
  ┌──────────────────────────────────────────────────────────────────────┐
  │ BEFORE_AGENT_EXECUTION (Hook)       ← 遥测Span创建、限流检查       │
  └──────────────────┬───────────────────────────────────────────────────┘
                     ▼
  ╔══════════════════════════════════════════════════════════════════════╗
  ║                    ReAct Loop (循环执行)                             ║
  ║                                                                    ║
  ║  ┌──────────────────────────────────────────────────────────────┐  ║
  ║  │ ON_ITERATION_START (Hook)        ← 摘要检查、Token统计      │  ║
  ║  └──────────────────┬───────────────────────────────────────────┘  ║
  ║                     ▼                                              ║
  ║  ┌──────────────────────────────────────────────────────────────┐  ║
  ║  │ Interceptor.beforeModel()        ← 洋葱模型外层→内层        │  ║
  ║  └──────────────────┬───────────────────────────────────────────┘  ║
  ║                     ▼                                              ║
  ║  ┌──────────────────────────────────────────────────────────────┐  ║
  ║  │ BEFORE_LLM_CALL (Hook)          ← 消息列表最终修改         │  ║
  ║  └──────────────────┬───────────────────────────────────────────┘  ║
  ║                     ▼                                              ║
  ║            ┌─────────────────┐                                     ║
  ║            │    LLM 调用     │                                     ║
  ║            └────────┬────────┘                                     ║
  ║                     ▼                                              ║
  ║  ┌──────────────────────────────────────────────────────────────┐  ║
  ║  │ AFTER_LLM_CALL (Hook)           ← 响应后处理               │  ║
  ║  └──────────────────┬───────────────────────────────────────────┘  ║
  ║                     ▼                                              ║
  ║  ┌──────────────────────────────────────────────────────────────┐  ║
  ║  │ Interceptor.afterModel()         ← 洋葱模型内层→外层        │  ║
  ║  └──────────────────┬───────────────────────────────────────────┘  ║
  ║                     ▼                                              ║
  ║  ┌──────────────────────────────────────────────────────────────┐  ║
  ║  │ BEFORE_TOOL_CALL (Hook)          ← 人机协同审批             │  ║
  ║  └──────────────────┬───────────────────────────────────────────┘  ║
  ║                     ▼                                              ║
  ║  ┌──────────────────────────────────────────────────────────────┐  ║
  ║  │ Interceptor.beforeTool()         ← 工具调用前拦截           │  ║
  ║  └──────────────────┬───────────────────────────────────────────┘  ║
  ║                     ▼                                              ║
  ║            ┌─────────────────┐                                     ║
  ║            │   Tool 执行     │                                     ║
  ║            └────────┬────────┘                                     ║
  ║                     ▼                                              ║
  ║  ┌──────────────────────────────────────────────────────────────┐  ║
  ║  │ Interceptor.afterTool()          ← 工具结果后处理           │  ║
  ║  └──────────────────┬───────────────────────────────────────────┘  ║
  ║                     ▼                                              ║
  ║  ┌──────────────────────────────────────────────────────────────┐  ║
  ║  │ AFTER_TOOL_CALL (Hook)           ← 工具结果审计             │  ║
  ║  └──────────────────┬───────────────────────────────────────────┘  ║
  ║                     ▼                                              ║
  ║  ┌──────────────────────────────────────────────────────────────┐  ║
  ║  │ ON_ITERATION_END (Hook)          ← 循环结束统计             │  ║
  ║  └──────────────────┬───────────────────────────────────────────┘  ║
  ║                     │                                              ║
  ║                     ▼ (判断是否继续循环)                           ║
  ╚══════════════════════════════════════════════════════════════════════╝
                     │
                     ▼
  ┌──────────────────────────────────────────────────────────────────────┐
  │ AFTER_AGENT_EXECUTION (Hook)        ← 遥测Span关闭、结果统计       │
  └──────────────────┬───────────────────────────────────────────────────┘
                     ▼
  ┌──────────────────────────────────────────────────────────────────────┐
  │ ON_OUTPUT (Hook)                    ← PII输出检测、内容过滤         │
  └──────────────────┬───────────────────────────────────────────────────┘
                     ▼
                 Response (返回用户)
关键区别:Hook 是本项目定义的事件驱动扩展点(观察者模式),作用于 Agent 生命周期全局;Interceptor 是 Spring AI Alibaba 框架的洋葱模型拦截器,作用于模型调用和工具调用的局部。两者互补而非互斥。

十四、面试高频问题

Q: Hook和Filter有什么区别?
A: Filter工作在HTTP请求层,拦截所有HTTP请求(如JWT认证、CORS处理),是框架级别的机制。Hook工作在业务逻辑层,只在Agent执行的特定生命周期节点触发(如ON_USER_MESSAGE、BEFORE_LLM_CALL),是业务级别的扩展机制。Hook比Filter更细粒度——它知道当前执行的是哪个Agent、哪个任务,可以访问AgentRequest、SystemPrompt等业务上下文。
Q: 为什么BEFORE类型的Hook可以BLOCK,而AFTER类型不行?
A: BEFORE类型Hook在核心操作执行前触发,此时还没有产生任何副作用,阻断是安全的。AFTER类型Hook在操作已经完成后触发,LLM调用已经发生、Token已经消耗,此时阻断没有意义——事情已经做了。所以AFTER类型的Hook只适合做日志、统计、通知等不阻断的操作。
Q: 异步Hook(isAsync=true)为什么不支持BLOCK?
A: 异步Hook在独立线程池中执行,主流程不会等待它的结果。如果异步Hook返回BLOCK,主流程已经继续执行了,BLOCK无法生效。这是一个有意的设计——异步Hook用于"不影响主流程"的操作(如审计日志),如果需要阻断主流程,必须用同步Hook。
Q: Hook的Order怎么设计?
A: Order越小越先执行。推荐使用10的倍数(10、20、30...),这样后续新增Hook时可以插入中间值(如15)。安全相关的Hook应该Order最小(最先执行),审计Hook可以稍后。本项目的SecurityEventHook使用Order=10,为未来更重要的安全Hook预留了0-9的位置。
Q: Hook 和 AOP 有什么区别?在什么场景下选择 Hook 而不是 AOP?
A: Hook 是事件驱动的观察者模式,解耦更彻底;AOP 基于代理切面,与 Spring 容器耦合。Hook 适合跨组件事件通知、动态注册/注销、异步处理;AOP 适合方法级横切关注点(如事务、日志)。在 Agent 系统中,生命周期事件跨越多个类和模块,Hook 比 AOP 更自然——不需要为每个切入点定义切面表达式,只需声明 supportedTypes()
Q: 如何保证 Hook 执行的幂等性?如果同一个事件被重复发布会怎样?
A: 通过 HookContext 中的状态标记(如 CTX_SUMMARIZED)防重复;设计 Hook 为幂等操作;使用 AtomicBoolean 等 CAS 操作避免并发重入。例如 SummarizationHook 在首次执行后设置标记,后续调用直接返回 ALLOW。对于审计类 Hook,重复写入不影响正确性(幂等天然成立)。
Q: PII 检测如何避免误杀(false positive)?比如正文中的示例电话号码?
A: 多重策略:① 上下文感知——检查周围文本是否在代码块/示例标记内(如 <code>```);② 白名单机制——配置 exclude-patterns 排除已知安全内容;③ 置信度阈值——综合多个信号(正则匹配 + Luhn 校验 + 上下文),置信度 > 0.8 才脱敏;④ 用户可配置例外规则。
Q: SummarizationHook 在 ReAct 循环中每轮都会触发吗?如何避免频繁摘要导致信息丢失?
A: 不会。CTX_SUMMARIZED 标记防止同一轮 ReAct 循环中重复触发;preserveRecentMessages 保留最近 N 条消息不被摘要;只在 Token 超阈值(默认 80000)时触发;摘要 Prompt 明确要求保留关键信息(工具调用结果、中间结论、专有名词和数值)。
Q: HumanInTheLoopHook BLOCK 后,Agent 如何恢复执行?
A: BLOCK 返回后 Graph 执行挂起,当前状态保存到 Checkpoint(包括消息历史、工具调用参数、循环计数器等);前端展示审批 UI(工具名、参数、风险提示);用户确认后通过 API 恢复执行,从 Checkpoint 处继续,不需要重新执行之前的步骤。
Q: AgentHookPublisher 的异步 Hook 如果执行失败,会影响主流程吗?
A: 不会。异步 Hook 提交到专用线程池(hookAsyncExecutor),异常被 catch 并记录日志但不中断管道。这是刻意设计——异步 Hook 适合遥测、审计日志等非关键路径操作。如果业务要求 Hook 失败时阻断主流程,必须使用同步 Hook(isAsync() = false)。
Q: BEFORE_* 类型的 JumpTo 机制是如何工作的?举例说明使用场景。
A: Hook 返回的 AgentHookResult 可携带 JumpTo 指令,Publisher 记录最后一个非 NONE 的跳转目标并最终返回给调度器。场景举例:PiiDetectionHook 检测到用户输入包含恶意注入指令 → 返回 JumpTo("safe_response_node") → Graph 跳过正常 Agent 节点,直接执行安全响应节点返回预设的拒绝消息。
Q: 如果多个 Hook 都返回 stateUpdates 修改同一个 key,如何合并?
A: 通过 KeyStrategy 机制:LAST_WINS(后执行的 Hook 覆盖前者)、MERGE_LIST(列表类型值自动合并)、MERGE_MAP(Map 类型值深度合并)。Hook 可在 getKeyStrategies() 方法中声明每个 key 的合并策略。未声明时默认 LAST_WINS。这确保了多个 Hook 协作修改状态时的行为可预测。