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_PRECEDENCErun first on the way in and last on the way out — use for safety checks - Advisors with
getOrder() = LOWEST_PRECEDENCErun 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
Comments