欢迎光临
我们一直在努力

Spring AI Alibaba(7)Hooks 和 Interceptors

目录

  • 概述
  • 内置实现
    • 消息压缩
    • Human-in-the-Loop(人机协同)
    • 模型调用限制(Model Call Limit)
    • PII 检测(Personally Identifiable Information)
    • 工具重试(Tool Retry)
    • Planning(规划)
    • LLM Tool Selector(LLM 工具选择器)
    • LLM Tool Emulator(LLM 工具模拟器)
    • Context Editing(上下文编辑)
  • 自定义 Hooks
    • MessagesModelHook
    • ModelHook
    • AgentHook
  • 自定义Interceptors
    • ModelInterceptor
    • ToolInterceptor
  • RunnableConfig 跨调用共享数据
  • 执行顺序
  • 示例
  • 最佳实践参考

概述

在构建生产级 AI 应用时,我们可能面临以下一些需求:

  • 日志与监控:记录 Agent 每次推理的输入输出,便于问题排查和性能分析
  • 内容安全:在用户输入进入模型前进行敏感词过滤,在模型输出返回前进行合规审核
  • 成本控制:限制单次对话的模型调用次数,防止无限循环导致费用失控
  • 人工介入:在高风险操作(如删除数据、发送邮件)执行前暂停流程,等待人工审批
  • 上下文增强:在每次模型调用前动态注入系统提示词或用户信息
  • 容错处理:模型调用失败时自动重试,工具执行异常时优雅降级

这些需求如果分散在业务代码中实现,会导致代码臃肿、难以维护。Hooks 和 Interceptors 正是为了解决这类“横切关注点”而设计的。它们将通用逻辑与核心业务逻辑解耦,让我们能够以声明式的方式为 Agent 添加各种增强能力。

是不是和Spring里的切面很像?没错,就是一个思想!

Hooks 和 Interceptors的 区别

维度InterceptorsHooks
执行模型 责任链模式,作为内联包装器包裹执行点 Graph 节点,插入到 StateGraph 中作为独立的执行节点
修改范围 原地修改请求/响应(Request/Response) 修改节点之间的 OverAllState
调用频率 每次模型调用或工具调用都执行 在特定的 HookPosition 执行
对图结构的透明度 对 Graph 结构不可见(透明包装) 对 Graph 结构可见(作为独立节点出现)
典型用途 动态提示词注入、重试逻辑、安全护栏 消息历史操作、人工介入、流程控制

咱可以通俗点理解:Interceptor 像是一个安检通道,每次请求经过时都会被检查、修改,但安检通道本身并不改变“你要去哪”这个路线。Hook 像是高速公路出口,它本身就是一个站点,你可以选择在这里下高速(暂停流程)、绕道(跳转到其他节点)或者继续前行。

核心 Agent 循环流程是调用模型、让其选择要执行的工具,直到不需要调用工具时完成。如下图:

使用了Hooks 和 Interceptors 后,在这些步骤的前后就可以做一些额外的事:

在这里插入图片描述

内置实现

Spring AI Alibaba 提供了一些内置的 Hooks 和 Interceptors 实现,满足我们一些常用的需求:

消息压缩

当接近 token 限制时自动压缩对话历史。

适用场景:

  • 超出上下文窗口的长期对话;
  • 具有大量历史记录的多轮对话;
  • 需要保留完整对话上下文的应用程序。

import com.alibaba.cloud.ai.graph.agent.hook.summarization.SummarizationHook;

// 创建消息压缩 Hook
SummarizationHook summarizationHook = SummarizationHook.builder()
.model(chatModel)
.maxTokensBeforeSummary(4000)
.messagesToKeep(20)
.build();

// 使用
ReactAgent agent = ReactAgent.builder()
.name("my_agent")
.model(chatModel)
.hooks(summarizationHook)
.build();

配置选项:

  • model: 用于生成摘要的 ChatModel;
  • maxTokensBeforeSummary: 触发摘要之前的最大 token 数;
  • messagesToKeep: 摘要后保留的最新消息数。

Human-in-the-Loop(人机协同)

暂停 Agent 执行以获得人工批准、编辑或拒绝工具调用。

适用场景:

  • 需要人工批准的高风险操作(数据库写入、金融交易);
  • 人工监督是强制性的合规工作流程;
  • 长期对话,使用人工反馈引导 Agent。

package com.cys.saa.chapter07.config;

import com.alibaba.cloud.ai.dashscope.api.DashScopeApi;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
import com.alibaba.cloud.ai.graph.agent.ReactAgent;
import com.alibaba.cloud.ai.graph.agent.hook.hip.HumanInTheLoopHook;
import com.alibaba.cloud.ai.graph.agent.hook.hip.ToolConfig;
import com.alibaba.cloud.ai.graph.agent.hook.summarization.SummarizationHook;
import com.alibaba.cloud.ai.graph.checkpoint.savers.redis.RedisSaver;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AgentConfig {

@Value("${spring.ai.dashscope.api-key}")
private String apiKey;

@Bean
public DashScopeApi dashScopeApi() {
return DashScopeApi.builder()
.apiKey(apiKey)
.build();
}

@Bean
public ChatModel chatModel(DashScopeApi dashScopeApi) {
return DashScopeChatModel.builder()
.dashScopeApi(dashScopeApi)
.defaultOptions(DashScopeChatOptions.builder()
.withModel(DashScopeChatModel.DEFAULT_MODEL_NAME)
.withTemperature(0.7)
.withMaxToken(2048)
.build())
.build();
}

@Bean
public ReactAgent dateTimeAgent(ChatModel chatModel, RedisSaver redisSaver) {
String instruction = """
你是一个智能助手。
"""
;

// 创建 Human-in-the-Loop Hook
HumanInTheLoopHook humanReviewHook = HumanInTheLoopHook.builder()
// 使用approvalOn方法添加,第二个参数是ToolConfig对象,用来添加提示语等
.approvalOn("sendEmailTool", ToolConfig.builder().description("请确认发送邮件").build())
// 如果第二个参数传的字符串,方法内自己创建ToolConfig对象,调用重载方法
.approvalOn("deleteDataTool", "请确认删除数据")
.build();

return ReactAgent.builder()
.name("date_time_agent")
.model(chatModel)
.instruction(instruction)
.hooks(humanReviewHook)
// 使用RedisSaver维护中断状态
.saver(redisSaver)
.build();
}
}

注意:Human-in-the-loop Hook 需要 checkpointer 来维护跨中断的状态。上面例子用了 RedisSaver。

approvalOn方法有3个重载方法:

public static class Builder {
private Map<String, ToolConfig> approvalOn = new HashMap();

public Builder approvalOn(String toolName, ToolConfig toolConfig) {
this.approvalOn.put(toolName, toolConfig);
return this;
}

public Builder approvalOn(String toolName, String description) {
ToolConfig config = new ToolConfig();
config.setDescription(description);
this.approvalOn.put(toolName, config);
return this;
}

public Builder approvalOn(Map<String, ToolConfig> approvalOn) {
this.approvalOn.putAll(approvalOn);
return this;
}

public HumanInTheLoopHook build() {
return new HumanInTheLoopHook(this);
}
}

模型调用限制(Model Call Limit)

限制模型调用次数以防止无限循环或过度成本。

适用场景:

  • 防止失控的 Agent 进行太多 API 调用;
  • 在生产部署中强制执行成本控制;
  • 在特定调用预算内测试 Agent 行为。

ReactAgent agent = ReactAgent.builder()
.name("my_agent")
.model(chatModel)
.hooks(ModelCallLimitHook.builder().runLimit(5).build()) // 限制模型调用次数为5次
.saver(new MemorySaver())
.build();

PII 检测(Personally Identifiable Information)

检测和处理对话中的个人身份信息。

适用场景:

  • 具有合规要求的医疗保健和金融应用;
  • 需要清理日志的客户服务 Agent;
  • 任何处理敏感用户数据的应用程序。

import com.alibaba.cloud.ai.graph.agent.hook.pii.PIIDetectionHook;
import com.alibaba.cloud.ai.graph.agent.hook.pii.PIIType;
import com.alibaba.cloud.ai.graph.agent.hook.pii.RedactionStrategy;

// 创建 PII Hook
PIIDetectionHook pii = PIIDetectionHook.builder()
// piiType是检测什么
.piiType(PIIType.EMAIL)
// strategy是具体的策略
.strategy(RedactionStrategy.REDACT)
.applyToInput(true)
.build();

// 使用
ReactAgent agent = ReactAgent.builder()
.name("secure_agent")
.model(chatModel)
.hooks(pii)
.build();

piiType有:

public enum PIIType {
EMAIL, // 检测邮件,是否匹配标准的电子邮箱格式
CREDIT_CARD, // 检测信用卡,匹配主流卡组织(Visa、MasterCard、Amex 等)的卡号格式,
IP, // 检测匹配 IPv4(如 192.168.1.1)和 IPv6 地址。
MAC_ADDRESS, // 检测匹配设备物理网卡地址(如 AA:BB:CC:DD:EE:FF)
URL, // 匹配 HTTP/HTTPS 链接。特别注意:它不仅能识别域名,还会检测 URL 路径和 Query 参数中携带的 token、sessionId 或 userId
CUSTOM; // 允许你传入自定义的正则表达式或检测逻辑(例如中国身份证号 18位、手机号、病历号等)
}

strategy有:

public enum RedactionStrategy {
BLOCK, // 直接阻止包含 PII 的请求或响应流出。
REDACT, // 将敏感数据整体删除,替换为空字符串或占位符(如 [REMOVED]),不保留任何原始字符。
MASK, // 部分隐藏,通常保留前后几位,中间用 * 或 # 填充。这是体验最好的策略,保留了数据的可识别性(如快递单号)。
HASH; // 将原始数据通过哈希算法(如 SHA-256)转换为固定长度的不可逆字符串。
}

工具重试(Tool Retry)

自动重试失败的工具调用,具有可配置的指数退避。

适用场景:

  • 处理外部 API 调用中的瞬态故障;
  • 提高依赖网络的工具的可靠性;
  • 构建优雅处理临时错误的弹性 Agent。

// 创建工具重试拦截器
ToolRetryInterceptor toolRetryInterceptor = ToolRetryInterceptor.builder()
.maxRetries(2)
.onFailure(ToolRetryInterceptor.OnFailureBehavior.RETURN_MESSAGE)
.build();

return ReactAgent.builder()
.name("date_time_agent")
.model(chatModel)
.instruction(instruction)
.interceptors(toolRetryInterceptor)
.build();

Planning(规划)

在执行工具之前强制执行一个规划步骤,以概述 Agent 将要采取的步骤。

适用场景:

  • 需要执行复杂、多步骤任务的 Agent;
  • 通过在执行前显示 Agent 的计划来提高透明度;
  • 通过检查建议的计划来调试错误。

// 创建待办事项拦截器
TodoListInterceptor todoListInterceptor = TodoListInterceptor.builder().build();

return ReactAgent.builder()
.name("date_time_agent")
.model(chatModel)
.instruction(instruction)
.interceptors(todoListInterceptor)
.build();

LLM Tool Selector(LLM 工具选择器)

使用一个 LLM 来决定在多个可用工具之间选择哪个工具。

适用场景:

  • 当多个工具可以实现相似目标时;
  • 需要根据细微的上下文差异进行工具选择;
  • 动态选择最适合特定输入的工具。

// 创建工具选择拦截器
ToolSelectionInterceptor toolSelectionInterceptor = ToolSelectionInterceptor.builder().build();

return ReactAgent.builder()
.name("date_time_agent")
.model(chatModel)
.instruction(instruction)
.interceptors(toolSelectionInterceptor)
.build();

LLM Tool Emulator(LLM 工具模拟器)

在没有实际执行工具的情况下,使用 LLM 模拟工具的输出。

适用场景:

  • 在演示或测试期间模拟 API;
  • 在开发过程中为工具提供占位符行为;
  • 在不产生实际成本或副作用的情况下测试 Agent 逻辑。

// 创建工具模拟拦截器
ToolEmulatorInterceptor toolEmulatorInterceptor = ToolEmulatorInterceptor.builder().model(chatModel).build();

return ReactAgent.builder()
.name("date_time_agent")
.model(chatModel)
.instruction(instruction)
.interceptors(toolEmulatorInterceptor)
.build();

Context Editing(上下文编辑)

在将上下文发送给 LLM 之前对其进行修改,以注入、删除或修改信息。

适用场景:

  • 向 LLM 提供额外的上下文或指令;
  • 从对话历史中删除不相关或冗余的信息;
  • 动态修改上下文以引导 Agent 的行为。

// 创建上下文编辑拦截器
ContextEditingInterceptor contextEditingInterceptor = ContextEditingInterceptor.builder()
.trigger(120000)
.clearAtLeast(60000)
.build();

return ReactAgent.builder()
.name("date_time_agent")
.model(chatModel)
.instruction(instruction)
.interceptors(contextEditingInterceptor)
.build();

自定义 Hooks

当内置 Hooks 无法满足特定业务需求时,我们可以开发自定义 Hook。Spring AI Alibaba 提供了多层次的 Hook 接口,适应不同复杂度的场景。

框架提供了四层 Hook 接口,从简单到复杂依次为:

接口或类状态访问方法签名适用场景
Hook接口 getName(), getHookPositions() 最基础的 Hook 契约
AgentHook抽象类 OverAllState + RunnableConfig beforeAgent(), afterAgent() 在Agent调用前后执行,可操作Agent 级别的状态
ModelHook抽象类 OverAllState + RunnableConfig beforeModel(), afterModel() 在模型调用前后执行,可操作模型级别的状态
MessagesAgentHook抽象类 List<Message> beforeAgent(), afterAgent() 在Agent调用前后执行,专注于消息操作
MessagesModelHook抽象类 List<Message> beforeModel(), afterModel() 在模型调用前后执行,专注于消息操作

该怎么选:

  • 如果只需要操作消息列表(如添加系统提示、过滤敏感词),使用 MessagesModelHook 或 MessagesAgentHook。
  • 如果需要访问完整的 OverAllState 或控制流程跳转(如跳转到 END),实现 ModelHook 或 AgentHook。

只现实上述抽象类还不行,还需要配合注解。Hook 通过 @HookPositions 注解或 getHookPositions() 方法声明在哪些生命周期位置执行。可用的 HookPosition 包括:

  • HookPosition.BEFORE_AGENT:Agent 循环开始前执行(仅一次)
  • HookPosition.AFTER_AGENT:Agent 循环完成后执行(仅一次)
  • HookPosition.BEFORE_MODEL:每次 LLM 调用前执行
  • HookPosition.AFTER_MODEL:每次 LLM 调用后执行

MessagesModelHook

MessagesModelHook 是一个专门用于操作消息列表的 Hook,使用更简单,更推荐。它直接接收和返回消息列表,无需处理复杂的 OverAllState。对于大多数消息操作场景,推荐扩展 MessagesModelHook,它提供了更简洁的 API。

适用场景:

  • 消息修剪、过滤或转换;
  • 添加系统提示或上下文消息;
  • 消息压缩和摘要;
  • 简单的消息操作需求。

MessagesModelHook 和 MessagesAgentHook 的 四个方法,beforeAgent(), afterAgent(),beforeModel(), afterModel()返回值都是 AgentCommand 对象。

AgentCommand 对象诶不有个策略,用来控制消息的处理策略:

  • UpdatePolicy.REPLACE 表示完全替换原有的消息列表
  • UpdatePolicy.APPEND 表示追加到原有消息列表之后

下面看一个消息压缩的Hook示例,消息超过10条,抛弃前面的,保留最新的:

package com.cys.saa.chapter07.hooks;

import com.alibaba.cloud.ai.graph.RunnableConfig;
import com.alibaba.cloud.ai.graph.agent.ReactAgent;
import com.alibaba.cloud.ai.graph.agent.hook.HookPosition;
import com.alibaba.cloud.ai.graph.agent.hook.HookPositions;
import com.alibaba.cloud.ai.graph.agent.hook.messages.AgentCommand;
import com.alibaba.cloud.ai.graph.agent.hook.messages.MessagesModelHook;
import com.alibaba.cloud.ai.graph.agent.hook.messages.UpdatePolicy;
import org.springframework.ai.chat.messages.Message;

import java.util.List;

@HookPositions({HookPosition.BEFORE_MODEL})
public class MessageTrimmingHook extends MessagesModelHook {
private static final int MAX_MESSAGES = 10;

@Override
public String getName() {
return "message_trimming";
}

@Override
public AgentCommand beforeModel(List<Message> previousMessages, RunnableConfig config) {
// 如果消息数量超过限制,只保留最后 MAX_MESSAGES 条消息
if (previousMessages.size() > MAX_MESSAGES) {
List<Message> trimmedMessages = previousMessages.subList(
previousMessages.size() MAX_MESSAGES,
previousMessages.size()
);
// 使用 REPLACE 策略替换所有消息
return new AgentCommand(trimmedMessages, UpdatePolicy.REPLACE);
}
// 如果消息数量未超过限制,返回原始消息(不进行修改)
return new AgentCommand(previousMessages);
}
}

ModelHook

下面实现一个在模型调用前后记录日志的 Hook:

package com.cys.saa.chapter07.hooks;

import com.alibaba.cloud.ai.graph.OverAllState;
import com.alibaba.cloud.ai.graph.RunnableConfig;
import com.alibaba.cloud.ai.graph.agent.hook.ModelHook;
import com.alibaba.cloud.ai.graph.agent.hook.HookPosition;
import com.alibaba.cloud.ai.graph.agent.hook.HookPositions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;

@HookPositions({HookPosition.BEFORE_MODEL, HookPosition.AFTER_MODEL})
public class LoggingHook extends ModelHook {

private static final Logger log = LoggerFactory.getLogger(LoggingHook.class);

@Override
public String getName() {
return "logging_hook";
}

@Override
public CompletableFuture<Map<String, Object>> beforeModel(
OverAllState state,
RunnableConfig config) {

log.info("=== 模型调用开始 ===");
log.info("ThreadId: {}", config.threadId());

// 从状态中读取当前消息数量
state.value("messages").ifPresent(messages ->
log.info("当前消息数: {}", messages)
);

return CompletableFuture.completedFuture(new HashMap<>());
}

@Override
public CompletableFuture<Map<String, Object>> afterModel(
OverAllState state,
RunnableConfig config) {

log.info("=== 模型调用完成 ===");

// 记录模型响应
state.value("last_response").ifPresent(response ->
log.info("模型响应: {}", response)
);

return CompletableFuture.completedFuture(new HashMap<>());
}
}

ModelHook可以操控状态OverAllState,他更强大,也更复杂。下面我也可以使用ModelHook,通过OverAllState间接操控消息没,实现消息压缩:

package com.cys.saa.chapter07.hooks;

import com.alibaba.cloud.ai.graph.agent.hook.ModelHook;
import com.alibaba.cloud.ai.graph.agent.hook.HookPosition;
import com.alibaba.cloud.ai.graph.agent.hook.HookPositions;
import com.alibaba.cloud.ai.graph.OverAllState;
import com.alibaba.cloud.ai.graph.RunnableConfig;
import com.alibaba.cloud.ai.graph.state.ReplaceAllWith;
import org.springframework.ai.chat.messages.Message;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;

@HookPositions({HookPosition.BEFORE_MODEL})
public class AdvancedMessageTrimmingHook extends ModelHook {
private static final int MAX_MESSAGES = 10;
private static final String TRIM_COUNT_KEY = "trim_count";

@Override
public String getName() {
return "advanced_message_trimming";
}

@Override
public CompletableFuture<Map<String, Object>> beforeModel(OverAllState state, RunnableConfig config) {
// 可以访问完整状态
Optional<Object> messagesOpt = state.value("messages");
if (messagesOpt.isEmpty()) {
return CompletableFuture.completedFuture(Map.of());
}

List<Message> messages = (List<Message>) messagesOpt.get();

// 可以访问和更新自定义状态
int trimCount = (Integer) state.value(TRIM_COUNT_KEY).orElse(0);

if (messages.size() > MAX_MESSAGES) {
List<Message> trimmed = messages.subList(
messages.size() MAX_MESSAGES,
messages.size()
);

// 可以同时更新消息和自定义状态
return CompletableFuture.completedFuture(Map.of(
"messages", ReplaceAllWith.of(trimmed),
TRIM_COUNT_KEY, trimCount + 1 // 记录修剪次数
));
}

return CompletableFuture.completedFuture(Map.of());
}
}

AgentHook

在 Agent 整体执行的开始和结束时执行:

package com.cys.saa.chapter07.hooks;

import com.alibaba.cloud.ai.graph.OverAllState;
import com.alibaba.cloud.ai.graph.RunnableConfig;
import com.alibaba.cloud.ai.graph.agent.hook.AgentHook;
import com.alibaba.cloud.ai.graph.agent.hook.HookPosition;
import com.alibaba.cloud.ai.graph.agent.hook.HookPositions;

import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;

@HookPositions({HookPosition.BEFORE_AGENT, HookPosition.AFTER_AGENT})
public class CustomAgentHook extends AgentHook {

@Override
public String getName() {
return "custom_agent_hook";
}

@Override
public CompletableFuture<Map<String, Object>> beforeAgent(OverAllState state, RunnableConfig config) {
System.out.println("Agent 开始执行");
// 可以初始化资源、记录开始时间等
return CompletableFuture.completedFuture(Map.of("start_time", System.currentTimeMillis()));
}

@Override
public CompletableFuture<Map<String, Object>> afterAgent(OverAllState state, RunnableConfig config) {
System.out.println("Agent 执行完成");
// 可以清理资源、计算执行时间等
Optional<Object> startTime = state.value("start_time");
if (startTime.isPresent()) {
long duration = System.currentTimeMillis() (Long) startTime.get();
System.out.println("执行耗时: " + duration + "ms");
}
return CompletableFuture.completedFuture(Map.of());
}
}

自定义Interceptors

当我们要在模型调用或工具调用的请求/响应层面进行精细化控制时,可以自定义 Interceptor。

框架提供了两种Interceptor 类型:

  • ModelInterceptor:拦截对 ChatModel(LLM)的调用,支持请求修改、响应转换和动态工具注入。
  • ToolInterceptor:拦截工具执行调用,用于参数验证、日志记录和错误处理。

两个都基于责任链模式(Chain of Responsibility) 实现,每个 Interceptor 包裹下一个,形成调用链。

使用场景:

  • 根据用户权限动态添加或移除工具;
  • 根据对话上下文临时启用特定工具;
  • 实现工具的动态加载和卸载;
  • 在特定条件下限制可用的工具集。

ModelInterceptor

下面看个示例,自定义一个拦截器,拦截和修改模型请求和响应:

package com.cys.saa.chapter07.hooks;

import com.alibaba.cloud.ai.graph.agent.interceptor.ModelInterceptor;
import com.alibaba.cloud.ai.graph.agent.interceptor.ModelRequest;
import com.alibaba.cloud.ai.graph.agent.interceptor.ModelResponse;
import com.alibaba.cloud.ai.graph.agent.interceptor.ModelCallHandler;

public class LoggingInterceptor extends ModelInterceptor {

@Override
public ModelResponse interceptModel(ModelRequest request, ModelCallHandler handler) {
// 请求前记录
System.out.println("发送请求到模型: " + request.getMessages().size() + " 条消息");

long startTime = System.currentTimeMillis();

// 执行实际调用
ModelResponse response = handler.call(request);

// 响应后记录
long duration = System.currentTimeMillis() startTime;
System.out.println("模型响应耗时: " + duration + "ms");

return response;
}

@Override
public String getName() {
return "LoggingInterceptor";
}
}

ModelInterceptor 还支持在模型调用前动态的管理工具:

  • dynamicToolCallbacks:动态添加工具回调,可以在运行时根据上下文添加新的工具
  • tools:动态筛选工具,指定本次调用可用的工具名称列表。如果为空,则使用所有默认工具

package com.cys.saa.chapter07.hooks;

import com.alibaba.cloud.ai.graph.agent.interceptor.ModelInterceptor;
import com.alibaba.cloud.ai.graph.agent.interceptor.ModelRequest;
import com.alibaba.cloud.ai.graph.agent.interceptor.ModelResponse;
import com.alibaba.cloud.ai.graph.agent.interceptor.ModelCallHandler;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.function.FunctionToolCallback;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class DynamicToolInterceptor extends ModelInterceptor {

// 示例:根据上下文动态创建的工具
private ToolCallback createContextualTool(String context) {
return FunctionToolCallback.builder("contextual_tool", (String input) -> {
return "处理上下文: " + context + ", 输入: " + input;
})
.description("根据上下文动态创建的工具")
.build();
}

@Override
public ModelResponse interceptModel(ModelRequest request, ModelCallHandler handler) {
// 从上下文中获取信息,决定添加哪些工具
Map<String, Object> context = request.getContext();
String userRole = (String) context.getOrDefault("user_role", "default");

// 构建修改后的请求
ModelRequest.Builder builder = ModelRequest.builder(request);

// 示例 1: 动态添加工具回调
List<ToolCallback> dynamicTools = new ArrayList<>();
if ("premium".equals(userRole)) {
// 为高级用户添加额外工具
dynamicTools.add(createContextualTool("premium_feature"));
}
builder.dynamicToolCallbacks(dynamicTools);

// 示例 2: 动态筛选工具(只允许使用指定的工具)
if (shouldRestrictTools(context)) {
// 只允许使用 search 和 calculator 工具
builder.tools(List.of("search", "calculator"));
}
// 如果 tools 为空列表,则使用所有默认工具

ModelRequest modifiedRequest = builder.build();
return handler.call(modifiedRequest);
}

private boolean shouldRestrictTools(Map<String, Object> context) {
// 根据上下文决定是否限制工具
return context.containsKey("restrict_tools");
}

@Override
public String getName() {
return "DynamicToolInterceptor";
}
}

ToolInterceptor

可以拦截和修改工具调用,如下:

package com.cys.saa.chapter07.hooks;

import com.alibaba.cloud.ai.graph.agent.interceptor.ToolInterceptor;
import com.alibaba.cloud.ai.graph.agent.interceptor.ToolCallRequest;
import com.alibaba.cloud.ai.graph.agent.interceptor.ToolCallResponse;
import com.alibaba.cloud.ai.graph.agent.interceptor.ToolCallHandler;

public class ToolMonitoringInterceptor extends ToolInterceptor {

@Override
public ToolCallResponse interceptToolCall(ToolCallRequest request, ToolCallHandler handler) {
String toolName = request.getToolName();
long startTime = System.currentTimeMillis();

System.out.println("执行工具: " + toolName);

try {
ToolCallResponse response = handler.call(request);

long duration = System.currentTimeMillis() startTime;
System.out.println("工具 " + toolName + " 执行成功 (耗时: " + duration + "ms)");

return response;
} catch (Exception e) {
long duration = System.currentTimeMillis() startTime;
System.err.println("工具 " + toolName + " 执行失败 (耗时: " + duration + "ms): " + e.getMessage());

return ToolCallResponse.of(
request.getToolCallId(),
request.getToolName(),
"工具执行失败: " + e.getMessage()
);
}
}

@Override
public String getName() {
return "ToolMonitoringInterceptor";
}
}

RunnableConfig 跨调用共享数据

RunnableConfig 提供了一个 context() 方法,允许我们在同一个执行流程中的多个 Hook调用或多轮模型或工具调用之间共享数据。这对于实现计数器、累积统计信息或跨多次调用维护状态非常有用。

适用场景:

  • 跟踪模型或工具调用次数;
  • 累积性能指标(总耗时、平均响应时间等);
  • 在 before/after Hook 之间传递临时数据;
  • 实现基于计数的限流或断路器。

示例,实现调用计数器:

package com.cys.saa.chapter07.hooks;

import com.alibaba.cloud.ai.graph.agent.hook.ModelHook;
import com.alibaba.cloud.ai.graph.agent.hook.HookPosition;
import com.alibaba.cloud.ai.graph.agent.hook.HookPositions;
import com.alibaba.cloud.ai.graph.RunnableConfig;
import com.alibaba.cloud.ai.graph.OverAllState;
import java.util.concurrent.CompletableFuture;
import java.util.Map;

@HookPositions({HookPosition.BEFORE_MODEL, HookPosition.AFTER_MODEL})
public class ModelCallCounterHook extends ModelHook {

private static final String CALL_COUNT_KEY = "__model_call_count__";
private static final String TOTAL_TIME_KEY = "__total_model_time__";
private static final String START_TIME_KEY = "__call_start_time__";

@Override
public String getName() {
return "model_call_counter";
}

@Override
public CompletableFuture<Map<String, Object>> beforeModel(OverAllState state, RunnableConfig config) {
// 从 context 读取当前计数(如果不存在则默认为 0)
int currentCount = config.context().containsKey(CALL_COUNT_KEY)
? (int) config.context().get(CALL_COUNT_KEY) : 0;

System.out.println("模型调用 #" + (currentCount + 1));

// 记录开始时间
config.context().put(START_TIME_KEY, System.currentTimeMillis());

return CompletableFuture.completedFuture(Map.of());
}

@Override
public CompletableFuture<Map<String, Object>> afterModel(OverAllState state, RunnableConfig config) {
// 读取当前计数并递增
int currentCount = config.context().containsKey(CALL_COUNT_KEY)
? (int) config.context().get(CALL_COUNT_KEY) : 0;
config.context().put(CALL_COUNT_KEY, currentCount + 1);

// 计算本次调用耗时并累加到总耗时
if (config.context().containsKey(START_TIME_KEY)) {
long startTime = (long) config.context().get(START_TIME_KEY);
long duration = System.currentTimeMillis() startTime;

long totalTime = config.context().containsKey(TOTAL_TIME_KEY)
? (long) config.context().get(TOTAL_TIME_KEY) : 0L;
config.context().put(TOTAL_TIME_KEY, totalTime + duration);

// 输出统计信息
int newCount = currentCount + 1;
long newTotalTime = totalTime + duration;
System.out.println("模型调用完成: " + duration + "ms");
System.out.println("累计统计 – 调用次数: " + newCount + ", 总耗时: " + newTotalTime + "ms, 平均: " + (newTotalTime / newCount) + "ms");
}

return CompletableFuture.completedFuture(Map.of());
}
}

示例,实现调用次数限制:

package com.cys.saa.chapter07.hooks;

import com.alibaba.cloud.ai.graph.OverAllState;
import com.alibaba.cloud.ai.graph.RunnableConfig;
import com.alibaba.cloud.ai.graph.agent.hook.ModelHook;
import com.alibaba.cloud.ai.graph.agent.hook.HookPosition;
import com.alibaba.cloud.ai.graph.agent.hook.HookPositions;
import com.alibaba.cloud.ai.graph.agent.hook.JumpTo;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;

import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.util.concurrent.CompletableFuture;

@HookPositions({HookPosition.BEFORE_MODEL, HookPosition.AFTER_MODEL})
public class ModelCallLimiterHook extends ModelHook {

private static final String CALL_COUNT_KEY = "__model_call_count__";
private final int maxCalls;

public ModelCallLimiterHook(int maxCalls) {
this.maxCalls = maxCalls;
}

@Override
public String getName() {
return "model_call_limiter";
}

@Override
public CompletableFuture<Map<String, Object>> beforeModel(OverAllState state, RunnableConfig config) {
// 读取当前调用次数
int callCount = config.context().containsKey(CALL_COUNT_KEY)
? (int) config.context().get(CALL_COUNT_KEY) : 0;

// 检查是否超过限制
if (callCount >= maxCalls) {
System.out.println("达到模型调用次数限制: " + maxCalls);

// 添加终止消息
List<Message> messages = new ArrayList<>(
(List<Message>) state.value("messages").orElse(new ArrayList<>())
);
messages.add(new AssistantMessage(
"已达到模型调用次数限制 (" + callCount + "/" + maxCalls + "),Agent 执行终止。"
));

// 返回更新并跳转到结束
return CompletableFuture.completedFuture(Map.of("messages", messages));
}

return CompletableFuture.completedFuture(Map.of());
}

@Override
public CompletableFuture<Map<String, Object>> afterModel(OverAllState state, RunnableConfig config) {
// 递增计数器
int callCount = config.context().containsKey(CALL_COUNT_KEY)
? (int) config.context().get(CALL_COUNT_KEY) : 0;
config.context().put(CALL_COUNT_KEY, callCount + 1);

return CompletableFuture.completedFuture(Map.of());
}

@Override
public List<JumpTo> canJumpTo() {
return List.of(JumpTo.end);
}
}

执行顺序

当多个 Hooks 或 Interceptors 同时存在时,就涉及到执行顺序了。

如下,假如我们添加了 Hooks 和 Interceptors :

ReactAgent agent = ReactAgent.builder()
.name("my_agent")
.model(chatModel)
.hooks(hook1, hook2, hook3)
.interceptors(interceptor1, interceptor2)
.interceptors(toolInterceptor1, toolInterceptor2)
.build();

那么整个执行流程如下:

  • Before Agent Hooks(按顺序):
    • hook1.beforeAgent()
    • hook2.beforeAgent()
    • hook3.beforeAgent()
  • Agent 循环开始
  • Before Model Hooks(按顺序):
    • hook1.beforeModel()
    • hook2.beforeModel()
    • hook3.beforeModel()
  • Model Interceptors(嵌套调用):
    • interceptor1 → interceptor2 → 模型调用
  • After Model Hooks(逆序):
    • hook3.afterModel()
    • hook2.afterModel()
    • hook1.afterModel()
  • Tool Interceptors(如果有工具调用,嵌套调用):
    • toolInterceptor1 → toolInterceptor2 → 工具执行
  • Agent 循环结束
  • After Agent Hooks(逆序):
    • hook3.afterAgent()
    • hook2.afterAgent()
    • hook1.afterAgent()
  • 关键规则:

    • before_* hooks: 从第一个到最后一个
    • after_* hooks: 从最后一个到第一个(逆序)
    • Interceptors: 嵌套调用(第一个拦截器包装所有其他的)

    当然,还有一种顺序,就是我们可以指定或自定义顺序。

    Hook 通过实现 Prioritized 接口的getOrder方法来控制执行顺序。

    public interface Prioritized {

    int HIGHEST_PRECEDENCE = Integer.MIN_VALUE;

    int LOWEST_PRECEDENCE = Integer.MAX_VALUE;

    int getOrder();
    }

    排序规则是:数值越小,优先级越高,执行越靠前。

    例如:

    @HookPositions(HookPosition.BEFORE_MODEL)
    public class SecurityHook extends MessagesModelHook implements Prioritized {

    @Override
    public int getOrder() {
    return 10; // 优先级较高(数值小)
    }

    @Override
    public String getName() {
    return "security_hook";
    }

    @Override
    public AgentCommand beforeModel(List<Message> previousMessages, RunnableConfig config) {
    // 安全过滤逻辑(优先执行)
    // …
    return new AgentCommand(previousMessages, UpdatePolicy.KEEP);
    }
    }

    优先级建议:

    • HIGHEST_PRECEDENCE(最小整数值):用于核心安全、权限、审核类 Hook
    • 中等优先级(如 0-100):用于业务逻辑类 Hook
    • LOWEST_PRECEDENCE(最大整数值):用于日志、统计、收尾类 Hook

    示例

    通过一个完整的示例,展示如何实现一个内容审核拦截器,在用户输入进入模型前进行敏感词过滤。

    首先创建拦截器:

    package com.cys.saa.chapter07.hooks;

    import com.alibaba.cloud.ai.graph.agent.interceptor.ModelInterceptor;
    import com.alibaba.cloud.ai.graph.agent.interceptor.ModelCallHandler;
    import com.alibaba.cloud.ai.graph.agent.interceptor.ModelRequest;
    import com.alibaba.cloud.ai.graph.agent.interceptor.ModelResponse;
    import org.springframework.ai.chat.messages.AssistantMessage;
    import org.springframework.ai.chat.messages.Message;
    import org.springframework.ai.chat.messages.UserMessage;
    import org.springframework.ai.chat.model.ChatResponse;
    import org.springframework.ai.chat.model.Generation;

    import java.util.List;
    import java.util.Set;

    public class ContentModerationInterceptor extends ModelInterceptor {

    private final Set<String> sensitiveWords;
    private final String moderationMessage;

    public ContentModerationInterceptor(Set<String> sensitiveWords, String moderationMessage) {
    this.sensitiveWords = sensitiveWords;
    this.moderationMessage = moderationMessage;
    }

    @Override
    public String getName() {
    return "contentModerationInterceptor";
    }

    @Override
    public ModelResponse interceptModel(ModelRequest request, ModelCallHandler handler) {
    // 1. 从请求中提取用户消息
    List<Message> messages = request.getMessages();

    // 2. 检查是否包含敏感词
    for (Message message : messages) {
    if (message instanceof UserMessage) {
    String content = message.getText();
    String filteredContent = filterSensitiveWords(content);

    if (!filteredContent.equals(content)) {
    // 3. 发现敏感词,返回拦截响应(不调用模型)
    return ModelResponse.of(new AssistantMessage(moderationMessage));
    }
    }
    }

    // 4. 无敏感词,正常调用模型
    return handler.call(request);
    }

    private String filterSensitiveWords(String content) {
    for (String word : sensitiveWords) {
    if (content.contains(word)) {
    return content.replace(word, "***");
    }
    }
    return content;
    }

    }

    在 Agent 中注册拦截器:

    package com.cys.saa.chapter15.config;

    import com.alibaba.cloud.ai.dashscope.api.DashScopeApi;
    import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
    import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
    import com.alibaba.cloud.ai.graph.agent.ReactAgent;
    import com.cys.saa.chapter15.interceptor.ContentModerationInterceptor;
    import org.springframework.ai.chat.model.ChatModel;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;

    import java.util.Set;

    @Configuration
    public class AgentConfig {

    @Value("${spring.ai.dashscope.api-key}")
    private String apiKey;

    @Bean
    public ChatModel chatModel(DashScopeApi dashScopeApi) {
    return DashScopeChatModel.builder()
    .dashScopeApi(dashScopeApi)
    .defaultOptions(DashScopeChatOptions.builder()
    .withModel(DashScopeChatModel.DEFAULT_MODEL_NAME)
    .withTemperature(0.7)
    .withMaxToken(2048)
    .build())
    .build();
    }

    @Bean
    public ReactAgent moderatedAgent(ChatModel chatModel) {
    // 定义敏感词列表
    Set<String> sensitiveWords = Set.of(
    "暴力", "色情", "赌博", "毒品"
    );

    // 创建内容审核拦截器
    ContentModerationInterceptor moderationInterceptor =
    new ContentModerationInterceptor(
    sensitiveWords,
    "您的输入包含敏感词汇,请修改后重试。"
    );

    return ReactAgent.builder()
    .name("moderated_agent")
    .model(chatModel)
    .instruction("你是一个乐于助人的助手。")
    .interceptors(moderationInterceptor) // 注册拦截器
    .build();
    }
    }

    当用户发送包含敏感词的消息时,执行流程如下:

  • 用户消息进入 Agent
  • ContentModerationInterceptor.interceptModel() 被调用
  • 拦截器检测到敏感词
  • 拦截器直接返回审核消息,不调用模型(handler.call() 未被调用)
  • 用户收到审核提示,模型调用被“短路”
  • 这种模式在内容安全、权限校验等场景中非常实用。拦截器可以在不消耗模型 Token 的情况下提前终止请求。

    最佳实践参考

  • 下面给一些场景和使用参考:
  • 需求场景推荐方案理由
    消息预处理(注入系统提示、过滤消息) MessagesModelHook 简洁的消息列表操作 API
    完整状态访问和流程控制 ModelHook 或 AgentHook 可访问 OverAllState,支持 jump_to
    模型调用的请求/响应修改 ModelInterceptor 责任链模式,精细控制
    工具调用的参数验证和日志 ToolInterceptor 专注于工具执行层面
    人工审批流程 HumanInTheLoopHook 内置实现,开箱即用
    成本控制(限制调用次数) ModelCallLimitHook 内置实现,开箱即用
    消息压缩 SummarizationHook 内置实现,开箱即用
  • 避免在 Hook 中执行耗时操作
  • Hooks 在 Agent 执行的关键路径上运行,耗时操作会直接影响用户体验。如果需要执行耗时操作(如调用外部 API、写入数据库),建议异步处理或使用独立的线程池。

  • 保持 Hook 的幂等性
  • Hook 可能被多次调用(如 BEFORE_MODEL 在每次 ReAct 循环中都执行),确保 Hook 的逻辑是幂等的,,这样多次执行不会产生副作用。

  • Interceptor 中谨慎使用异常
  • 在 Interceptor 中抛出异常会中断整个调用链。对于可恢复的错误(如网络超时),建议在 Interceptor 内部处理重试逻辑;对于不可恢复的错误(如参数非法),抛出明确的异常并让上层处理。

    小结:本章学习了什么是Hook和Interceptor,以及有哪些内置实现,有哪些使用场景。在内置的满足不了我们时,学会自定义Hook和Interceptor。学会灵活把他们用到我们的应用中。

    赞(0)
    未经允许不得转载:171主机测评 » Spring AI Alibaba(7)Hooks 和 Interceptors
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址