Mastering Spring AI Alibaba Agent Hooks and Interceptors: A Complete Guide
This guide explains the extension mechanism of Spring AI Alibaba 1.1.2.0, compares built‑in Hooks and Interceptors, provides full code examples for creating custom components, shows project structure, testing procedures, and best‑practice tips for fine‑grained control of AI agents.
Introduction
Spring AI Alibaba Agent Framework offers powerful extension points that let developers inject custom logic at any stage of an Agent's execution. The extension mechanism consists of two components—Hooks and Interceptors—used for monitoring, modifying, and controlling Agent behavior such as logging, message trimming, PII detection, and human‑in‑the‑loop collaboration.
Core Concept Comparison
Target : Hooks operate on specific Agent execution nodes (start/end, before/after model calls). Interceptors operate on model request/response and tool calls.
Typical Use Cases : Hooks handle message trimming, compression, PII detection, and human‑in‑the‑loop scenarios. Interceptors handle logging, retry, dynamic tool management, and context editing.
Execution Mode : Hooks run asynchronously and return CompletableFuture. Interceptors run synchronously and return results directly.
Priority : Hooks execute before Interceptors during the model call phase; Interceptors are nested around the model invocation.
Built‑in Hooks
SummarizationHook – automatically compresses conversation history when approaching token limits.
SummarizationHook hook = SummarizationHook.builder()
.model(chatModel)
.maxTokensBeforeSummary(4000)
.messagesToKeep(20)
.build();HumanInTheLoopHook – pauses on specific tool calls for manual approval or editing.
HumanInTheLoopHook hook = HumanInTheLoopHook.builder()
.approvalOn("sendEmailTool", ToolConfig.builder()
.description("请确认是否发送该邮件。")
.build())
.build();ModelCallLimitHook – limits the number of model calls to prevent infinite loops.
ModelCallLimitHook hook = ModelCallLimitHook.builder()
.runLimit(5)
.build();PIIDetectionHook – detects and redacts sensitive information such as email addresses.
PIIDetectionHook hook = PIIDetectionHook.builder()
.piiType(PIIType.EMAIL)
.strategy(RedactionStrategy.REDACT)
.applyToInput(true)
.build();Built‑in Interceptors
ToolRetryInterceptor – automatically retries failed tool calls.
ToolRetryInterceptor interceptor = ToolRetryInterceptor.builder()
.maxRetries(2)
.onFailure(OnFailureBehavior.RETURN_MESSAGE)
.build();TodoListInterceptor – forces a planning step before tool execution.
TodoListInterceptor interceptor = TodoListInterceptor.builder().build();ToolSelectionInterceptor – lets the LLM choose the most appropriate tool among many.
ToolSelectionInterceptor interceptor = ToolSelectionInterceptor.builder().build();ToolEmulatorInterceptor – simulates tool output with an LLM, avoiding real execution.
ToolEmulatorInterceptor interceptor = ToolEmulatorInterceptor.builder()
.model(chatModel)
.build();ContextEditingInterceptor – edits the context before sending it to the LLM (e.g., clearing old messages).
ContextEditingInterceptor interceptor = ContextEditingInterceptor.builder()
.trigger(120000) // trigger threshold (ms)
.clearAtLeast(60000)
.build();Custom Hook Development
Several custom Hook examples illustrate how to extend the framework.
MessagesModelHook (recommended) – operates on the message list before the model runs.
@HookPositions({HookPosition.BEFORE_MODEL})
public class ContextEnhancementHook extends MessagesModelHook {
@Override
public String getName() { return "context_enhancement"; }
@Override
public AgentCommand beforeModel(List<Message> previousMessages, RunnableConfig config) {
List<Message> enhanced = new ArrayList<>();
enhanced.add(new SystemMessage("你是一个专业的AI助手,请提供准确、有帮助的回答。"));
enhanced.addAll(previousMessages);
return new AgentCommand(enhanced, UpdatePolicy.REPLACE);
}
}ModelHook (flexible) – provides full access to the overall state and can run both before and after the model.
@HookPositions({HookPosition.BEFORE_MODEL, HookPosition.AFTER_MODEL})
public class LoggingModelHook extends ModelHook {
@Override
public CompletableFuture<Map<String, Object>> beforeModel(OverAllState state, RunnableConfig config) {
int size = ((List<?>) state.value("messages").orElse(List.of())).size();
log.info(">>> 调用模型,消息数: {}", size);
config.context().put("__start__", System.currentTimeMillis());
return CompletableFuture.completedFuture(Map.of());
}
@Override
public CompletableFuture<Map<String, Object>> afterModel(OverAllState state, RunnableConfig config) {
long duration = System.currentTimeMillis() - (long) config.context().get("__start__");
log.info("<<< 调用完成,耗时 {} ms", duration);
return CompletableFuture.completedFuture(Map.of());
}
}AgentHook (agent‑level) – runs before and after the whole Agent execution, useful for global statistics.
@HookPositions({HookPosition.BEFORE_AGENT, HookPosition.AFTER_AGENT})
public class AgentMonitoringHook extends AgentHook {
private static final String COUNT_KEY = "__agent_call_count__";
@Override
public CompletableFuture<Map<String, Object>> beforeAgent(OverAllState state, RunnableConfig config) {
int count = (int) config.context().getOrDefault(COUNT_KEY, 0) + 1;
config.context().put(COUNT_KEY, count);
config.context().put("__agent_start__", System.currentTimeMillis());
log.info(">>> Agent 开始 (第{}次)", count);
return CompletableFuture.completedFuture(Map.of());
}
@Override
public CompletableFuture<Map<String, Object>> afterAgent(OverAllState state, RunnableConfig config) {
long duration = System.currentTimeMillis() - (long) config.context().get("__agent_start__");
int count = (int) config.context().get(COUNT_KEY);
log.info("<<< Agent 完成,本次耗时 {} ms,总调用次数: {}", duration, count);
return CompletableFuture.completedFuture(Map.of());
}
}MessageTrimmingHook – limits the number of retained messages to a fixed maximum.
@HookPositions({HookPosition.BEFORE_MODEL})
public class MessageTrimmingHook extends MessagesModelHook {
private static final int MAX = 5;
@Override
public AgentCommand beforeModel(List<Message> previousMessages, RunnableConfig config) {
if (previousMessages.size() <= MAX) {
return new AgentCommand(previousMessages);
}
List<Message> trimmed = previousMessages.subList(previousMessages.size() - MAX, previousMessages.size());
return new AgentCommand(trimmed, UpdatePolicy.REPLACE);
}
}Custom Interceptor Development
A logging interceptor demonstrates how to intercept model calls.
public class LoggingInterceptor extends ModelInterceptor {
@Override
public ModelResponse interceptModel(ModelRequest request, ModelCallHandler handler) {
log.debug("模型请求含 {} 条消息", request.getMessages().size());
long start = System.currentTimeMillis();
ModelResponse response = handler.call(request);
log.debug("响应耗时 {} ms", System.currentTimeMillis() - start);
return response;
}
@Override
public String getName() { return "LoggingInterceptor"; }
}Execution Order
Before Agent Hooks (in addition order)
Agent main loop starts
Before Model Hooks (in addition order)
Model Interceptors (nested, in addition order)
Model call
After Model Hooks (reverse order)
After Agent Hooks (reverse order)
Full Example Project
Project Structure
spring-ai-hooks-demo/
├── pom.xml
├── src/main/
│ ├── java/com/example/ai/
│ │ ├── SpringAiHooksDemoApplication.java
│ │ ├── config/AgentConfig.java
│ │ ├── controller/AgentController.java
│ │ ├── service/AgentService.java
│ │ ├── hook/ContextEnhancementHook.java
│ │ ├── hook/MessageTrimmingHook.java
│ │ ├── hook/LoggingModelHook.java
│ │ └── hook/AgentMonitoringHook.java
│ │ └── interceptor/LoggingInterceptor.java
│ └── resources/application.ymlpom.xml Highlights
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
</parent>
<groupId>com.example.ai</groupId>
<artifactId>spring-ai-hooks-demo</artifactId>
<version>1.0.0</version>
<properties>
<java.version>17</java.version>
<spring-ai-alibaba.version>1.1.2.0</spring-ai-alibaba.version>
<jackson.version>2.17.2</jackson.version>
</properties>
<dependencyManagement>…</dependencyManagement>
</project>Key Java Files
SpringAiHooksDemoApplication.java
package com.example.ai;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringAiHooksDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringAiHooksDemoApplication.class, args);
}
}AgentConfig.java (bean definition)
package com.example.ai.config;
import com.alibaba.cloud.ai.graph.agent.ReactAgent;
import com.alibaba.cloud.ai.graph.checkpoint.savers.MemorySaver;
import com.example.ai.hook.*;
import com.example.ai.interceptor.LoggingInterceptor;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AgentConfig {
@Bean
public ReactAgent reactAgent(ChatModel chatModel) {
return ReactAgent.builder()
.name("hook_demo_agent")
.model(chatModel)
.saver(new MemorySaver())
.hooks(new AgentMonitoringHook(), new ContextEnhancementHook(), new MessageTrimmingHook(), new LoggingModelHook())
.interceptors(new LoggingInterceptor())
.build();
}
}AgentService.java
package com.example.ai.service;
import com.alibaba.cloud.ai.graph.RunnableConfig;
import com.alibaba.cloud.ai.graph.agent.ReactAgent;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.stereotype.Service;
@Service
public class AgentService {
private final ReactAgent reactAgent;
public AgentService(ReactAgent reactAgent) { this.reactAgent = reactAgent; }
public String chat(String userMessage, String sessionId) {
RunnableConfig config = RunnableConfig.builder()
.threadId(sessionId)
.build();
AssistantMessage response = reactAgent.call(userMessage, config);
return response.getText();
}
}AgentController.java
package com.example.ai.controller;
import com.example.ai.service.AgentService;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/api/agent")
public class AgentController {
private final AgentService agentService;
public AgentController(AgentService agentService) { this.agentService = agentService; }
@PostMapping("/chat")
public Map<String, Object> chat(@RequestParam String message,
@RequestParam(defaultValue = "default") String sessionId) {
return Map.of(
"success", true,
"response", agentService.chat(message, sessionId),
"sessionId", sessionId);
}
}Testing and Verification
Manual Test Steps
Start the application : Ensure the DASHSCOPE_API_KEY environment variable is set, then run mvn spring-boot:run.
Send the first message (e.g.,
curl -X POST "http://localhost:885/api/agent/chat?message=你好&sessionId=test") and verify logs such as:
>>> Agent 开始执行 (第 1 次调用)
>>> 准备调用模型,当前消息数: 1
<<< 模型调用完成,耗时 XXX ms
<<< Agent 执行完成,本次耗时 XXX ms,累计耗时 XXX ms,总调用次数: 1Send more than six messages to test the trimming hook:
for i in {1..7}; do
curl -X POST "http://localhost:885/api/agent/chat?message=消息${i}&sessionId=test"
doneObserve that the current message count never exceeds 5, confirming MessageTrimmingHook works.
Session isolation test : Use two different sessionId values and verify each session retains its own memory.
curl -X POST "http://localhost:885/api/agent/chat?message=我叫张三&sessionId=A"
curl -X POST "http://localhost:885/api/agent/chat?message=我叫李四&sessionId=B"
curl -X POST "http://localhost:885/api/agent/chat?message=我叫什么名字?&sessionId=A"
curl -X POST "http://localhost:885/api/agent/chat?message=我叫什么名字?&sessionId=B"Expected: session A returns “张三”, session B returns “李四”.
Check Interceptor logs (DEBUG level) : Logs should contain messages like “模型请求含 X 条消息” and “响应耗时 X ms”.
Validate system‑prompt injection : Query “你觉得你是一个什么样的助手?” and ensure the response includes keywords such as “专业” and “帮助”, proving ContextEnhancementHook injected the system prompt.
Common Issues and Troubleshooting
Hook logs not appearing – cause: logging level not set to debug. Solution: set appropriate levels in application.yml.
Message count always 1 – cause: MemorySaver not configured. Solution: verify .saver(new MemorySaver()) is present in AgentConfig.
Trimming ineffective – cause: Hook not added or order incorrect. Solution: ensure MessageTrimmingHook is included in the .hooks() list.
Call count not incrementing – cause: inconsistent context key. Solution: use the same key (e.g., __agent_call_count__) for all reads/writes.
Conclusion
Hooks allow insertion of logic at key Agent stages (start/end, before/after model) for tasks such as monitoring, message processing, and human‑in‑the‑loop collaboration.
Interceptors provide finer‑grained interception of model requests/responses and tool calls, suitable for retry, logging, and dynamic tool management. MessagesModelHook is the preferred base for message‑level processing, while ModelHook gives full state access and AgentHook enables global statistics. RunnableConfig.context() is the shared data bridge between Hooks; careful key naming avoids conflicts.
Combining multiple Hooks and Interceptors enables precise control of Agent behavior without modifying core framework code.
References
Spring AI Alibaba official documentation
DashScope model integration guide
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
The Dominant Programmer
Resources and tutorials for programmers' advanced learning journey. Advanced tracks in Java, Python, and C#. Blog: https://blog.csdn.net/badao_liumang_qizhi
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
