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.

The Dominant Programmer
The Dominant Programmer
The Dominant Programmer
Mastering Spring AI Alibaba Agent Hooks and Interceptors: A Complete Guide

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.yml

pom.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,总调用次数: 1

Send 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"
done

Observe 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

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaAI AgentsSpring BootHooksSpring AIAgent FrameworkInterceptors
The Dominant Programmer
Written by

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

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.