第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 | 智能体开始执行前 | 否 | 初始化、记录开始时间 |
PreReasoningEvent | LLM 推理前 | 是 | 修改输入消息、注入提示 |
ReasoningChunkEvent | LLM 流式输出中 | 否 | 显示流式内容 |
PostReasoningEvent | LLM 推理完成后 | 是 | 处理推理结果 |
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 | 技能注入 |
StudioMessageHook | Studio 消息追踪 |
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 系统:
- Hook 接口:
onEvent()方法和priority()优先级 - HookEvent 类型:Pre/Post 事件、流式事件、错误事件
- 事件修改:可修改事件支持改变执行流程
- 优先级机制:控制 Hook 执行顺序
- 内置 Hook:框架提供的常用 Hook 实现
Hook 系统是 AgentScope-Java 的核心扩展机制,掌握它能够实现强大的监控和控制能力。
练习
- 实现一个计时 Hook,记录每次推理和工具调用的耗时
- 实现一个敏感词过滤 Hook,检测并过滤不当内容
- 实现一个限流 Hook,限制工具调用频率
- 组合多个 Hook,观察优先级对执行顺序的影响
上一章:第15章-A2A协议 | 下一章:第17章-Human-in-the-Loop