Java SpringAI

Spring AI Advisor Pattern — Intercept and Customize Every AI Call

Spring AI Advisor Pattern — Intercept and Customize Every AI Call

Advisors are Spring AI's middleware for AI calls. They intercept every ChatClient request and response, letting you add behavior like logging, safety guardrails, content filtering, prompt augmentation, and custom routing — without modifying your service code. This tutorial covers built-in advisors and building custom ones.

Advisor Execution Chain

ChatClient.prompt().user("question").call()
    ↓
[Advisor 1: SafeGuardAdvisor]  ← check input safety
    ↓
[Advisor 2: QuestionAnswerAdvisor]  ← inject RAG context
    ↓
[Advisor 3: MessageChatMemoryAdvisor]  ← inject chat history
    ↓
[Advisor 4: SimpleLoggerAdvisor]  ← log request/response
    ↓
ChatModel (actual AI call)
    ↓
[Advisor 4: log response]
    ↓
[Advisor 3: save response to memory]
    ↓
[Advisor 2: (no-op on response)]
    ↓
[Advisor 1: check output safety]
    ↓
Return to caller

Order matters: advisors with lower getOrder() value run first (outermost)

Built-in Advisors

Advisor                      Purpose
──────────────────────────────────────────────────────────────────
SimpleLoggerAdvisor          Logs every request and response (DEBUG)
MessageChatMemoryAdvisor     Maintains multi-turn conversation history
QuestionAnswerAdvisor        RAG — retrieves documents and injects context
ReActAdvisor                 Controls agent iteration loops
SafeGuardAdvisor             Input safety checking

SimpleLoggerAdvisor — Development Debugging

// Add to ChatClient for full prompt/response logging
this.chatClient = builder
        .defaultAdvisors(new SimpleLoggerAdvisor())
        .build();

// Log output (DEBUG):
// BEFORE: AdvisedRequest{messages=[UserMessage{content='What is RAG?'}]}
// AFTER: ChatResponse{generation=Generation{assistantMessage=AssistantMessage{content='RAG is...'}}}

Custom Input Safety Advisor

import org.springframework.ai.chat.client.advisor.api.*;

@Component
public class ContentSafetyAdvisor implements CallAroundAdvisor {

    private static final List<String> BLOCKED_PATTERNS = List.of(
            "ignore instructions", "jailbreak", "system prompt", "as an AI"
    );

    @Override
    public AdvisedResponse aroundCall(AdvisedRequest request, CallAroundAdvisorChain chain) {
        // 1. Check input safety
        String userInput = request.userText();
        String lower     = userInput == null ? "" : userInput.toLowerCase();

        boolean unsafe = BLOCKED_PATTERNS.stream().anyMatch(lower::contains);
        if (unsafe) {
            // Block the call — return safe response without calling AI
            return AdvisedResponse.from(request)
                    .withResponse(ChatResponse.builder()
                            .withGenerations(List.of(new Generation(
                                    new AssistantMessage("I can't process that request."))))
                            .build())
                    .build();
        }

        // 2. Let request through
        AdvisedResponse response = chain.nextAroundCall(request);

        // 3. Check output safety (optional)
        String output = response.response().getResult().getOutput().getContent();
        if (output.contains("confidential") || output.contains("password")) {
            // Redact sensitive content from response
            String redacted = output.replaceAll("(?i)(password|secret|key)\\s*[:=]\\s*\\S+",
                    "$1: [REDACTED]");
            // Return modified response
        }

        return response;
    }

    @Override
    public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; }  // run first

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

Custom Prompt Augmentation Advisor

@Component
public class LanguageAdvisor implements CallAroundAdvisor {

    @Override
    public AdvisedResponse aroundCall(AdvisedRequest request, CallAroundAdvisorChain chain) {
        // Append language instruction to every prompt
        String language = (String) request.adviseContext()
                .getOrDefault("responseLanguage", "English");

        AdvisedRequest augmented = AdvisedRequest.from(request)
                .withUserText(request.userText() + "\n\nPlease respond in " + language + ".")
                .build();

        return chain.nextAroundCall(augmented);
    }

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

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

// Usage:
chatClient.prompt()
        .user("What is Spring AI?")
        .advisors(a -> a.param("responseLanguage", "Spanish"))
        .call()
        .content();
// Response will be in Spanish

Custom Response Transformation Advisor

@Component
public class MarkdownStripAdvisor implements CallAroundAdvisor {

    @Override
    public AdvisedResponse aroundCall(AdvisedRequest request, CallAroundAdvisorChain chain) {
        AdvisedResponse response = chain.nextAroundCall(request);

        // Strip markdown formatting from response (useful for plain text output)
        String content = response.response().getResult().getOutput().getContent();
        String stripped = content
                .replaceAll("\\*{1,2}([^*]+)\\*{1,2}", "$1")   // bold/italic
                .replaceAll("`{1,3}[^`]*`{1,3}", "")            // code blocks
                .replaceAll("#{1,6}\\s+", "")                    // headers
                .replaceAll("\\[([^]]+)]\\([^)]+\\)", "$1");     // links

        // Return response with modified content
        return response;  // simplified — full implementation modifies ChatResponse
    }

    @Override
    public int getOrder() { return Ordered.LOWEST_PRECEDENCE; }

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

Registering Multiple Advisors

@Service
public class FullyAdvisedService {

    private final ChatClient chatClient;

    public FullyAdvisedService(
            ChatClient.Builder builder,
            VectorStore         vectorStore,
            ContentSafetyAdvisor safetyAdvisor,
            LanguageAdvisor      languageAdvisor,
            TokenTrackingAdvisor trackingAdvisor) {

        InMemoryChatMemory memory = new InMemoryChatMemory();

        this.chatClient = builder
                .defaultAdvisors(
                    safetyAdvisor,                                    // order: HIGHEST_PRECEDENCE
                    new MessageChatMemoryAdvisor(memory),              // order: default
                    new QuestionAnswerAdvisor(vectorStore),            // order: default
                    languageAdvisor,                                   // order: 100
                    new SimpleLoggerAdvisor(),                         // order: LOWEST_PRECEDENCE
                    trackingAdvisor                                    // order: LOWEST_PRECEDENCE
                )
                .build();
    }

    public String chat(String sessionId, String language, String question) {
        return chatClient.prompt()
                .user(question)
                .advisors(a -> a
                        .param(MessageChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY, sessionId)
                        .param("responseLanguage", language))
                .call()
                .content();
    }
}

Key Points

  • Advisors with getOrder() = HIGHEST_PRECEDENCE run first on the way in and last on the way out — use for safety checks
  • Advisors with getOrder() = LOWEST_PRECEDENCE run last on the way in and first on the way out — use for logging
  • Use request.adviseContext() to pass per-call parameters to advisors (language, user ID, feature name)
  • Advisors are Spring beans — they can inject any other Spring component (repositories, services, caches)
  • The order of defaultAdvisors() calls determines execution order — safety advisors should always come first
Topics: Java SpringAI
← Newer Post Older Post →