第16章 Hook 系统

第16章 Hook 系统

本章目标

  • 理解 Hook 系统的设计理念和作用
  • 掌握各种 HookEvent 类型及其用途
  • 学会实现自定义 Hook 进行监控和干预
  • 了解 Hook 的优先级机制和最佳实践

16.1 Hook 系统概述

16.1.1 什么是 Hook?

Hook(钩子) 是一种事件驱动的拦截机制,允许你在智能体执行的各个关键节点进行监控和干预。通过 Hook,你可以:

  • 监控:记录日志、收集指标、追踪执行过程
  • 修改:改变输入消息、调整工具参数、处理输出结果
  • 控制:实现审批流程、添加安全检查、控制执行流程
┌─────────────────────────────────────────────────────────────┐
│                    Hook 系统架构                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   ┌─────────────────────────────────────────────────────┐   │
│   │                  ReActAgent 执行流程                 │   │
│   │                                                      │   │
│   │   PreCallEvent                                       │   │
│   │        │                                             │   │
│   │        ▼                                             │   │
│   │   ┌─────────────┐                                    │   │
│   │   │  推理阶段   │◀── PreReasoningEvent               │   │
│   │   │  (LLM调用)  │                                    │   │
│   │   └─────────────┘──▶ ReasoningChunkEvent (流式)      │   │
│   │        │           ──▶ PostReasoningEvent            │   │
│   │        ▼                                             │   │
│   │   ┌─────────────┐                                    │   │
│   │   │  行动阶段   │◀── PreActingEvent                  │   │
│   │   │  (工具调用) │                                    │   │
│   │   └─────────────┘──▶ ActingChunkEvent (流式)         │   │
│   │        │           ──▶ PostActingEvent               │   │
│   │        ▼                                             │   │
│   │   PostCallEvent                                      │   │
│   │                                                      │   │
│   │   ErrorEvent(任何阶段出错)                          │   │
│   └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

16.1.2 核心接口

package io.agentscope.core.hook;

import reactor.core.publisher.Mono;

/**
 * Hook 接口
 * 所有 Hook 事件通过 onEvent 方法统一处理
 */
public interface Hook {

    /**
     * 处理 Hook 事件
     *
     * @param event Hook 事件
     * @param <T> 具体的事件类型
     * @return 包含(可能被修改的)事件的 Mono
     */
    <T extends HookEvent> Mono<T> onEvent(T event);

    /**
     * Hook 优先级(值越小优先级越高)
     * 默认值:100
     *
     * 建议的优先级范围:
     * - 0-50: 关键系统 Hook(认证、安全)
     * - 51-100: 高优先级 Hook(验证、预处理)
     * - 101-500: 普通优先级 Hook(业务逻辑)
     * - 501-1000: 低优先级 Hook(日志、指标)
     */
    default int priority() {
        return 100;
    }
}

16.2 HookEvent 类型详解

16.2.1 事件类型总览

// HookEvent 是密封类,只有预定义的事件类型
public abstract sealed class HookEvent
    permits PreCallEvent, PostCallEvent, ReasoningEvent, ActingEvent, ErrorEvent {
    // ...
}
事件类型时机可修改用途
PreCallEvent智能体开始执行前初始化、记录开始时间
PreReasoningEventLLM 推理前修改输入消息、注入提示
ReasoningChunkEventLLM 流式输出中显示流式内容
PostReasoningEventLLM 推理完成后处理推理结果
PreActingEvent工具执行前修改工具参数、权限检查
ActingChunkEvent工具流式输出中显示进度
PostActingEvent工具执行完成后处理工具结果
PostCallEvent智能体执行完成后修改最终响应
ErrorEvent发生错误时错误处理、日志记录

16.2.2 事件详细说明

PreCallEvent - 执行开始

// 获取智能体信息
Agent agent = event.getAgent();
String agentName = agent.getName();
Memory memory = event.getMemory();  // 可能为 null

// 通知事件,不可修改

PreReasoningEvent - 推理前

// 可修改的字段
List<Msg> inputMessages = event.getInputMessages();
// 修改输入消息
event.setInputMessages(modifiedMessages);

// 其他信息
String modelName = event.getModelName();
GenerateOptions options = event.getGenerateOptions();

ReasoningChunkEvent - 推理流式输出

// 获取增量内容
Msg incrementalChunk = event.getIncrementalChunk();
String text = incrementalChunk.getTextContent();

// 获取累积内容
Msg cumulativeResponse = event.getCumulativeResponse();

// 检查是否是最后一块
boolean isLast = event.isLast();

PostReasoningEvent - 推理完成

// 可修改推理结果
Msg response = event.getResponse();
event.setResponse(modifiedResponse);

// 获取使用统计
ChatUsage usage = event.getUsage();

PreActingEvent - 工具执行前

// 可修改工具调用
ToolUseBlock toolUse = event.getToolUse();
String toolName = toolUse.getName();
Map<String, Object> input = toolUse.getInput();

// 修改工具参数
toolUse = toolUse.toBuilder()
    .input(modifiedInput)
    .build();
event.setToolUse(toolUse);

PostActingEvent - 工具执行后

// 可修改工具结果
ToolResultBlock result = event.getToolResult();
ToolUseBlock toolUse = event.getToolUse();

// 修改结果
event.setToolResult(modifiedResult);

PostCallEvent - 执行完成

// 可修改最终响应
Msg response = event.getResponse();
event.setResponse(modifiedResponse);

ErrorEvent - 错误发生

// 获取错误信息
Throwable error = event.getError();
String message = error.getMessage();

// 通知事件,不可修改

16.3 基本使用

16.3.1 创建简单的监控 Hook

import io.agentscope.core.hook.*;
import reactor.core.publisher.Mono;

/**
 * 简单的日志监控 Hook
 */
public class LoggingHook implements Hook {

    @Override
    public <T extends HookEvent> Mono<T> onEvent(T event) {
        // 使用模式匹配处理不同事件
        switch (event) {
            case PreCallEvent e -> {
                System.out.println("[开始] 智能体: " + e.getAgent().getName());
            }
            case PreReasoningEvent e -> {
                System.out.println("[推理] 输入消息数: " + e.getInputMessages().size());
            }
            case ReasoningChunkEvent e -> {
                // 输出流式内容
                String text = e.getIncrementalChunk().getTextContent();
                if (text != null) {
                    System.out.print(text);
                }
            }
            case PreActingEvent e -> {
                System.out.println("\n[工具] 调用: " + e.getToolUse().getName());
            }
            case PostActingEvent e -> {
                System.out.println("[工具] 完成: " + e.getToolUse().getName());
            }
            case PostCallEvent e -> {
                System.out.println("[完成] 响应长度: " + 
                    e.getResponse().getTextContent().length());
            }
            case ErrorEvent e -> {
                System.err.println("[错误] " + e.getError().getMessage());
            }
            default -> {
                // 其他事件忽略
            }
        }
        
        // 返回原事件(未修改)
        return Mono.just(event);
    }

    @Override
    public int priority() {
        return 500;  // 低优先级(日志类)
    }
}

16.3.2 注册 Hook 到智能体

import io.agentscope.core.ReActAgent;
import java.util.List;

// 方式一:构建时添加
ReActAgent agent = ReActAgent.builder()
    .name("MyAgent")
    .model(model)
    .hooks(List.of(
        new LoggingHook(),
        new MetricsHook(),
        new SecurityHook()
    ))
    .build();

// 方式二:运行时添加
agent.addHook(new LoggingHook());

16.4 修改事件数据

16.4.1 修改输入消息

/**
 * 提示注入 Hook
 * 在每次推理前注入额外的系统提示
 */
public class PromptInjectionHook implements Hook {

    private final String additionalPrompt;

    public PromptInjectionHook(String additionalPrompt) {
        this.additionalPrompt = additionalPrompt;
    }

    @Override
    public <T extends HookEvent> Mono<T> onEvent(T event) {
        if (event instanceof PreReasoningEvent e) {
            // 获取原始消息
            List<Msg> messages = new ArrayList<>(e.getInputMessages());
            
            // 添加额外的系统提示
            Msg systemHint = Msg.builder()
                .role(MsgRole.SYSTEM)
                .content(TextBlock.builder()
                    .text(additionalPrompt)
                    .build())
                .build();
            
            messages.add(0, systemHint);  // 添加到开头
            
            // 更新消息列表
            e.setInputMessages(messages);
        }
        
        return Mono.just(event);
    }
}

// 使用示例
Hook thinkingHook = new PromptInjectionHook(
    "请一步步思考,给出详细的推理过程。"
);

16.4.2 修改工具参数

/**
 * 工具参数增强 Hook
 * 自动为工具调用添加公共参数
 */
public class ToolParamEnhancerHook implements Hook {

    private final String userId;
    private final String authToken;

    public ToolParamEnhancerHook(String userId, String authToken) {
        this.userId = userId;
        this.authToken = authToken;
    }

    @Override
    public <T extends HookEvent> Mono<T> onEvent(T event) {
        if (event instanceof PreActingEvent e) {
            ToolUseBlock toolUse = e.getToolUse();
            
            // 创建增强的参数
            Map<String, Object> enhancedInput = new HashMap<>(toolUse.getInput());
            enhancedInput.put("user_id", userId);
            enhancedInput.put("auth_token", authToken);
            
            // 更新工具调用
            ToolUseBlock enhancedToolUse = toolUse.toBuilder()
                .input(enhancedInput)
                .build();
            
            e.setToolUse(enhancedToolUse);
        }
        
        return Mono.just(event);
    }

    @Override
    public int priority() {
        return 10;  // 高优先级(认证类)
    }
}

16.4.3 修改工具结果

/**
 * 结果脱敏 Hook
 * 对工具返回的敏感信息进行脱敏处理
 */
public class SanitizationHook implements Hook {

    @Override
    public <T extends HookEvent> Mono<T> onEvent(T event) {
        if (event instanceof PostActingEvent e) {
            ToolResultBlock result = e.getToolResult();
            
            // 获取结果文本
            String output = result.getOutput().isEmpty() ? "" :
                result.getOutput().get(0).toString();
            
            // 脱敏处理
            String sanitized = sanitize(output);
            
            // 创建新的结果
            ToolResultBlock sanitizedResult = ToolResultBlock.text(sanitized);
            e.setToolResult(sanitizedResult);
        }
        
        return Mono.just(event);
    }

    private String sanitize(String text) {
        // 隐藏手机号
        text = text.replaceAll("1[3-9]\\d{9}", "***");
        // 隐藏邮箱
        text = text.replaceAll("[\\w.-]+@[\\w.-]+\\.[a-zA-Z]{2,}", "***@***");
        // 隐藏身份证号
        text = text.replaceAll("\\d{17}[\\dXx]", "***");
        return text;
    }
}

16.5 完整示例

16.5.1 综合监控 Hook

import io.agentscope.core.hook.*;
import io.agentscope.core.message.Msg;
import io.agentscope.core.message.ToolResultBlock;
import reactor.core.publisher.Mono;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 综合监控 Hook
 * 记录执行时间、调用次数、Token 使用量等
 */
public class ComprehensiveMonitoringHook implements Hook {

    private Instant startTime;
    private final AtomicInteger reasoningCount = new AtomicInteger(0);
    private final AtomicInteger toolCallCount = new AtomicInteger(0);
    private long totalInputTokens = 0;
    private long totalOutputTokens = 0;

    @Override
    public <T extends HookEvent> Mono<T> onEvent(T event) {
        switch (event) {
            case PreCallEvent e -> {
                startTime = Instant.now();
                reasoningCount.set(0);
                toolCallCount.set(0);
                totalInputTokens = 0;
                totalOutputTokens = 0;
                
                System.out.println("=".repeat(50));
                System.out.println("智能体 [" + e.getAgent().getName() + "] 开始执行");
                System.out.println("=".repeat(50));
            }
            
            case PreReasoningEvent e -> {
                int count = reasoningCount.incrementAndGet();
                System.out.println("\n--- 第 " + count + " 轮推理 ---");
                System.out.println("输入消息数: " + e.getInputMessages().size());
            }
            
            case ReasoningChunkEvent e -> {
                // 流式输出
                String text = e.getIncrementalChunk().getTextContent();
                if (text != null && !text.isEmpty()) {
                    System.out.print(text);
                }
            }
            
            case PostReasoningEvent e -> {
                if (e.getUsage() != null) {
                    totalInputTokens += e.getUsage().getPromptTokens();
                    totalOutputTokens += e.getUsage().getCompletionTokens();
                }
            }
            
            case PreActingEvent e -> {
                int count = toolCallCount.incrementAndGet();
                System.out.println("\n\n>>> 工具调用 #" + count);
                System.out.println("工具: " + e.getToolUse().getName());
                System.out.println("参数: " + e.getToolUse().getInput());
            }
            
            case ActingChunkEvent e -> {
                ToolResultBlock chunk = e.getChunk();
                String output = chunk.getOutput().isEmpty() ? "" :
                    chunk.getOutput().get(0).toString();
                System.out.println("  进度: " + output);
            }
            
            case PostActingEvent e -> {
                ToolResultBlock result = e.getToolResult();
                String output = result.getOutput().isEmpty() ? "" :
                    result.getOutput().get(0).toString();
                System.out.println("结果: " + truncate(output, 100));
            }
            
            case PostCallEvent e -> {
                Duration duration = Duration.between(startTime, Instant.now());
                
                System.out.println("\n" + "=".repeat(50));
                System.out.println("执行完成");
                System.out.println("=".repeat(50));
                System.out.println("总耗时: " + duration.toMillis() + "ms");
                System.out.println("推理次数: " + reasoningCount.get());
                System.out.println("工具调用: " + toolCallCount.get());
                System.out.println("Token 使用: " + totalInputTokens + " (输入) + " +
                    totalOutputTokens + " (输出) = " +
                    (totalInputTokens + totalOutputTokens) + " (总计)");
                System.out.println("=".repeat(50) + "\n");
            }
            
            case ErrorEvent e -> {
                System.err.println("\n[错误] " + e.getError().getMessage());
                e.getError().printStackTrace();
            }
            
            default -> {
                // 忽略其他事件
            }
        }
        
        return Mono.just(event);
    }

    @Override
    public int priority() {
        return 500;  // 日志类,低优先级
    }

    private String truncate(String text, int maxLength) {
        if (text.length() <= maxLength) return text;
        return text.substring(0, maxLength) + "...";
    }
}

16.5.2 工具审批 Hook

import io.agentscope.core.hook.*;
import io.agentscope.core.message.ToolResultBlock;
import reactor.core.publisher.Mono;
import java.util.Scanner;
import java.util.Set;

/**
 * 工具审批 Hook
 * 对敏感工具的调用进行人工审批
 */
public class ToolApprovalHook implements Hook {

    private final Set<String> sensitiveTools;
    private final Scanner scanner;

    public ToolApprovalHook(Set<String> sensitiveTools) {
        this.sensitiveTools = sensitiveTools;
        this.scanner = new Scanner(System.in);
    }

    @Override
    public <T extends HookEvent> Mono<T> onEvent(T event) {
        if (event instanceof PreActingEvent e) {
            String toolName = e.getToolUse().getName();
            
            // 检查是否是敏感工具
            if (sensitiveTools.contains(toolName)) {
                System.out.println("\n" + "!".repeat(50));
                System.out.println("敏感工具调用需要审批!");
                System.out.println("工具: " + toolName);
                System.out.println("参数: " + e.getToolUse().getInput());
                System.out.print("是否批准? (y/n): ");
                
                String input = scanner.nextLine().trim().toLowerCase();
                
                if (!input.equals("y") && !input.equals("yes")) {
                    // 拒绝执行:用拒绝消息替换工具调用结果
                    // 注意:这里需要通过修改后续流程来实现
                    System.out.println("已拒绝工具调用");
                    
                    // 可以通过标记或抛出特定异常来阻止执行
                    // 这里示例简化处理
                }
                
                System.out.println("!".repeat(50) + "\n");
            }
        }
        
        return Mono.just(event);
    }

    @Override
    public int priority() {
        return 20;  // 高优先级(安全类)
    }
}

// 使用示例
Hook approvalHook = new ToolApprovalHook(
    Set.of("delete_file", "send_email", "execute_sql")
);

16.6 内置 Hook

AgentScope-Java 提供了一些内置的 Hook:

Hook用途
StreamingHook处理流式输出
StructuredOutputHook处理结构化输出
GenericRAGHook自动 RAG 知识检索
AutoContextHook自动上下文压缩
StaticLongTermMemoryHook长期记忆管理
SkillHook技能注入
StudioMessageHookStudio 消息追踪

16.7 最佳实践

16.7.1 Hook 设计建议

建议说明
单一职责每个 Hook 专注一个功能
合理优先级认证 > 验证 > 业务 > 日志
避免阻塞使用响应式编程,避免阻塞操作
错误处理在 Hook 内处理异常,避免影响主流程
幂等性Hook 应该是幂等的,避免副作用

16.7.2 优先级规划

// 推荐的优先级规划
public class HookPriorities {
    public static final int SECURITY = 10;      // 安全检查
    public static final int AUTH = 20;          // 认证授权
    public static final int VALIDATION = 50;    // 参数验证
    public static final int PREPROCESSING = 80; // 预处理
    public static final int BUSINESS = 100;     // 业务逻辑(默认)
    public static final int POSTPROCESSING = 300; // 后处理
    public static final int LOGGING = 500;      // 日志记录
    public static final int METRICS = 600;      // 指标收集
}

16.8 本章小结

本章介绍了 Hook 系统:

  1. Hook 接口onEvent() 方法和 priority() 优先级
  2. HookEvent 类型:Pre/Post 事件、流式事件、错误事件
  3. 事件修改:可修改事件支持改变执行流程
  4. 优先级机制:控制 Hook 执行顺序
  5. 内置 Hook:框架提供的常用 Hook 实现

Hook 系统是 AgentScope-Java 的核心扩展机制,掌握它能够实现强大的监控和控制能力。


练习

  1. 实现一个计时 Hook,记录每次推理和工具调用的耗时
  2. 实现一个敏感词过滤 Hook,检测并过滤不当内容
  3. 实现一个限流 Hook,限制工具调用频率
  4. 组合多个 Hook,观察优先级对执行顺序的影响

上一章:第15章-A2A协议 | 下一章:第17章-Human-in-the-Loop

← 返回目录