Spring AI Advisors Explained for Java Developers

Building robust AI applications requires more than just sending prompts. You must manage context effectively. Additionally, you need insight into the conversation flow. Spring AI offers a powerful solution for these challenges. This solution is the Advisors API.

This tutorial explores the Advisors API. We will examine how to intercept and modify interactions. Furthermore, we will implement logging and conversational memory in a Spring Boot application.


Setting project.


The Challenge of Stateless Interactions

Large Language Models (LLMs) are incredibly powerful engines. However, they possess a significant limitation. They are stateless by design. The model views every request as a brand-new interaction. It retains no memory of previous questions.

Consequently, you face a disjointed user experience. Imagine introducing yourself to a bot. Then, you ask a follow-up question. The bot suddenly forgets who you are. This creates frustration for users.

Context is King

To fix this, we must provide context. We append this data to every prompt. Generally, this contextual data falls into two categories.

First, there is your proprietary data. The model was not trained on your private business documents. Therefore, you must supply this information dynamically. Even if the model knows the topic, your data takes precedence.

Second, we have conversational history. This includes the back-and-forth dialogue between the user and the AI. You must send this history with every new request. Otherwise, the logic falls apart.

Spring AI Advisors automate this repetitive process. They sit between your code and the AI model. Let’s dive into the implementation.

Understanding the Advisor Architecture

Think of an Advisor as a filter. It functions similarly to a Servlet Filter in standard web development. The Advisor intercepts the request before the model receives it.

Simultaneously, it can modify the response. This happens after the model generates an answer but before your code sees it. This architecture provides a centralized point for logic.

Benefits of this Approach

You keep your business logic clean. You do not need to manually concatenate strings for every prompt. Instead, you configure an Advisor once. It handles the heavy lifting automatically.

Furthermore, this promotes reusability. You can write a custom Advisor for security checks. Then, you can apply it across multiple ChatClient instances.

Configuring the ChatClient

The ChatClient is your primary interface. It offers a fluent API for easy configuration. We use the AdvisorSpec interface to attach our logic.

You can add parameters directly. Alternatively, you can set multiple parameters at once. The flexibility allows for granular control over the AI interaction.

Below is a basic configuration example. We will create a ChatClient bean in a standard Spring configuration class.

package com.example.ai.config;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AiConfig {

    @Bean
    public ChatClient chatClient(ChatClient.Builder builder) {
        return builder
                .defaultSystem("You are a helpful Spring assistant.")
                .build();
    }
}

This sets up the foundation. However, it lacks advisors. We need to enhance it. We will modify the builder to include specific behaviors.

Interactions with Logging

Debugging AI responses can be complex. Often, you need to see precisely what the model received. Did the prompt include the correct context?

Spring AI provides the SimpleLoggerAdvisor. This component automatically logs request and response data. It is an essential tool for monitoring your application.

Implementing the Logger

Let’s update our configuration. We will add the SimpleLoggerAdvisor to the chain.

package com.example.ai.config;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AiConfig {

    @Bean
    public ChatClient chatClient(ChatClient.Builder builder) {
        return builder
                .defaultSystem("You are a helpful Spring assistant.")
                .defaultAdvisors(new SimpleLoggerAdvisor())
                .build();
    }
}

application.properties

logging.level.org.springframework.ai.chat.client.advisor=DEBUG

Now, check your console logs. You will see the raw interaction data. This includes the prompt text and the generated response. Consequently, troubleshooting becomes significantly easier.

Managing Conversational Memory

We discussed the need for history. Now, we will solve it. Spring AI offers the MessageChatMemoryAdvisor.

This advisor manages a conversation store. It reads the history before sending a request. Then, it writes the new response back to the store.

Wiring the Advisor

Next, we attach the advisor to the client. We also specify a conversation ID. This ID tracks specific user sessions.

@Bean
public ChatClient chatClient(ChatClient.Builder builder, ChatMemory chatMemory) {
    return builder
            .defaultSystem("You are a helpful assistant.")
            .defaultAdvisors(
                    MessageChatMemoryAdvisor.builder(chatMemory).build(),
                    new SimpleLoggerAdvisor()                 // Logs interactions
            )
            .build();
}

The system is now stateful. The MessageChatMemoryAdvisor automatically appends previous messages to the prompt.

Runtime Advisor Configuration

Sometimes, you need dynamic behavior. You might not want an advisor on every single call. The AdvisorSpec interface allows runtime configuration.

You can add advisors at the point of interaction. This overrides or adds to the defaults.

Dynamic Implementation Example

Consider a controller method. We want to enable logging only for this specific request.

@RestController
@RequestMapping("/ai")
public class ChatController {

    private final ChatClient chatClient;

    public ChatController(ChatClient chatClient) {
        this.chatClient = chatClient;
    }

    @GetMapping("/chat")
    public String chat(@RequestParam String message) {
        return chatClient.prompt()
                .user(message)
                .advisors(a -> a
                    .param("chat_memory_conversation_id", "user-123")
                    .param("chat_memory_response_size", 100)
                )
                .call()
                .content();
    }
}

Here, we use a lambda expression. We set the conversation ID dynamically. This allows the server to handle multiple users simultaneously.

Advanced Customization Techniques

The standard advisors are influential. Nevertheless, you may need custom logic. You can implement the CallAdvisor interface.

This allows you to create specialized interceptors. For example, you could scrub sensitive data. Or you could enforce strict content-safety policies.

Creating a Custom Advisor

The structure is straightforward. You implement the around method.

import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.advisor.api.CallAdvisor;
import org.springframework.ai.chat.client.advisor.api.CallAdvisorChain;
import org.springframework.ai.chat.client.ChatClientRequest;
import org.springframework.ai.chat.client.ChatClientResponse;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

@Slf4j
public class PiiRedactionAdvisor implements CallAdvisor {

    private static final Pattern EMAIL = Pattern.compile(
            "[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}",
            Pattern.CASE_INSENSITIVE
    );

    private static final Pattern PHONE = Pattern.compile(
            "(?:(?:\\+?1[-.]?)?\\(?([0-9]{3})\\)?[-.]?)?" +
                    "([0-9]{3})[-.]?([0-9]{4})"
    );

    private static final Pattern SSN = Pattern.compile(
            "\\b\\d{3}-\\d{2}-\\d{4}\\b"
    );

    private static final Pattern CREDIT_CARD = Pattern.compile(
            "\\b(?:\\d{4}[-\\s]?){3}\\d{4}\\b"
    );

    private static final Pattern IP_ADDRESS = Pattern.compile(
            "\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b"
    );

    @Override
    public ChatClientResponse adviseCall(ChatClientRequest request, CallAdvisorChain chain) {
        List<Message> redactedMessages = new ArrayList<>();

        for (Message message : request.prompt().getUserMessages()) {
            if (message instanceof UserMessage) {
                String redacted = redactPii(message.getText());
                if(redacted != null) redactedMessages.add(new UserMessage(redacted));

                // Log to verify redaction
                log.info("ORIGINAL: {}", message);
                log.info("REDACTED: {}", redacted);
            } else {
                redactedMessages.add(message);
            }
        }

        Prompt promptWithHistory = new Prompt(redactedMessages);
        ChatClientRequest modifiedRequest = ChatClientRequest.builder()
                .prompt(promptWithHistory)
                .context(Map.copyOf(request.context())) // Carry over existing context/tools
                .build();

        return chain.nextCall(modifiedRequest);
    }

    private String redactPii(String text) {
        if (text == null) return null;

        String result = text;
        result = EMAIL.matcher(result).replaceAll("[EMAIL]");
        result = PHONE.matcher(result).replaceAll("[PHONE]");
        result = SSN.matcher(result).replaceAll("[SSN]");
        result = CREDIT_CARD.matcher(result).replaceAll("[CARD]");
        result = IP_ADDRESS.matcher(result).replaceAll("[IP]");

        return result;
    }

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

    @Override
    public int getOrder() {
        return 0;
    }

}

This grants you total control. You effectively own the pipeline.

Comprehensive test for the PiiRedactionAdvisor

import com.example.spring_ai.config.PiiRedactionAdvisor;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
@Slf4j
class PiiRedactionAdvisorIntegrationTest {

    @Autowired
    private ChatModel chatModel;

    @Test
    void shouldRedactPiiInRealChatClient() {
        // Given
        PiiRedactionAdvisor advisor = new PiiRedactionAdvisor();
        ChatClient chatClient = ChatClient.builder(chatModel)
                .defaultAdvisors(advisor)
                .build();

        // When
        String response = chatClient.prompt()
                .user("My email is [email protected] and phone is 555-1234")
                .call()
                .content();

        log.info(response);
    }
}
ORIGINAL: UserMessage{content='My email is [email protected] and phone is 555-1234', metadata={messageType=USER}, messageType=USER}
REDACTED: My email is [EMAIL] and phone is [PHONE]

Best Practices for Advisors

Advisors add overhead. Therefore, use them purely when necessary. Chaining too many advisors can slow down the response.

Additionally, consider the token limit. Appending massive amounts of history consumes tokens. This increases costs. It also reduces the space available for the actual answer.

Monitoring Token Usage

Constantly monitor your token consumption. The SimpleLoggerAdvisor help here is practical. Watch the logs for the token_usage metrics.

Adjust the memory size if needed. The MessageChatMemoryAdvisor accepts a window size. This limits the number of previous messages that can be sent.


Finally

We have covered significant ground. We explored the stateless nature of AI models. We identified the solution using Spring AI Advisors.

You learned how to configure the ChatClient. We implemented SimpleLoggerAdvisor for visibility. Furthermore, we solved the memory problem with MessageChatMemoryAdvisor.

The Road Ahead

Start experimenting today. Try building a custom advisor. Integrate a vector database for Retrieval Augmented Generation (RAG). The Advisors API is your gateway to creating intelligent, context-aware applications.

Spring AI simplifies the complex. It turns raw models into useful tools. Your journey into AI development is just beginning. Happy coding!

This article was originally published on Medium.