目录
- 概述
- 内置实现
-
- 消息压缩
- 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的 区别
| 执行模型 | 责任链模式,作为内联包装器包裹执行点 | 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();
那么整个执行流程如下:
- hook1.beforeAgent()
- hook2.beforeAgent()
- hook3.beforeAgent()
- hook1.beforeModel()
- hook2.beforeModel()
- hook3.beforeModel()
- interceptor1 → interceptor2 → 模型调用
- hook3.afterModel()
- hook2.afterModel()
- hook1.afterModel()
- toolInterceptor1 → toolInterceptor2 → 工具执行
- 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();
}
}
当用户发送包含敏感词的消息时,执行流程如下:
这种模式在内容安全、权限校验等场景中非常实用。拦截器可以在不消耗模型 Token 的情况下提前终止请求。
最佳实践参考
| 消息预处理(注入系统提示、过滤消息) | MessagesModelHook | 简洁的消息列表操作 API |
| 完整状态访问和流程控制 | ModelHook 或 AgentHook | 可访问 OverAllState,支持 jump_to |
| 模型调用的请求/响应修改 | ModelInterceptor | 责任链模式,精细控制 |
| 工具调用的参数验证和日志 | ToolInterceptor | 专注于工具执行层面 |
| 人工审批流程 | HumanInTheLoopHook | 内置实现,开箱即用 |
| 成本控制(限制调用次数) | ModelCallLimitHook | 内置实现,开箱即用 |
| 消息压缩 | SummarizationHook | 内置实现,开箱即用 |
Hooks 在 Agent 执行的关键路径上运行,耗时操作会直接影响用户体验。如果需要执行耗时操作(如调用外部 API、写入数据库),建议异步处理或使用独立的线程池。
Hook 可能被多次调用(如 BEFORE_MODEL 在每次 ReAct 循环中都执行),确保 Hook 的逻辑是幂等的,,这样多次执行不会产生副作用。
在 Interceptor 中抛出异常会中断整个调用链。对于可恢复的错误(如网络超时),建议在 Interceptor 内部处理重试逻辑;对于不可恢复的错误(如参数非法),抛出明确的异常并让上层处理。
小结:本章学习了什么是Hook和Interceptor,以及有哪些内置实现,有哪些使用场景。在内置的满足不了我们时,学会自定义Hook和Interceptor。学会灵活把他们用到我们的应用中。

