Spring AI Chat Client provides a fluent API for building AI-powered applications with LLMs, supporting advisors, streaming, structured outputs, and conversation memory
Custom advisors allow you to extend the Chat Client with custom behavior for logging, validation, security, and other cross-cutting concerns. This guide covers creating custom advisors from scratch using the advisor interfaces.
import org.springframework.ai.chat.client.advisor.api.Advisor;
import org.springframework.ai.chat.client.advisor.api.CallAdvisor;
import org.springframework.ai.chat.client.advisor.api.StreamAdvisor;
import org.springframework.ai.chat.client.advisor.api.BaseAdvisor;
import org.springframework.ai.chat.client.advisor.api.CallAdvisorChain;
import org.springframework.ai.chat.client.advisor.api.StreamAdvisorChain;
import org.springframework.ai.chat.client.advisor.api.AdvisorChain;
import org.springframework.ai.chat.client.ChatClientRequest;
import org.springframework.ai.chat.client.ChatClientResponse;
import org.springframework.core.Ordered;
import reactor.core.publisher.Flux;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;For synchronous (blocking) request processing, implement CallAdvisor.
interface CallAdvisor extends Advisor {
ChatClientResponse adviseCall(
ChatClientRequest request,
CallAdvisorChain chain
);
}Example - Request Validation Advisor:
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;
public class RequestValidationAdvisor implements CallAdvisor {
private final int maxPromptLength;
public RequestValidationAdvisor(int maxPromptLength) {
this.maxPromptLength = maxPromptLength;
}
@Override
public ChatClientResponse adviseCall(
ChatClientRequest request,
CallAdvisorChain chain
) {
// Extract prompt text
String promptText = request.prompt()
.getInstructions()
.get(0)
.getContent();
// Validate
if (promptText.length() > maxPromptLength) {
throw new IllegalArgumentException(
"Prompt exceeds maximum length of " + maxPromptLength
);
}
// Continue chain
return chain.nextCall(request);
}
@Override
public String getName() {
return "RequestValidationAdvisor";
}
@Override
public int getOrder() {
return 100; // Execute early
}
}For streaming (reactive) request processing, implement StreamAdvisor.
interface StreamAdvisor extends Advisor {
Flux<ChatClientResponse> adviseStream(
ChatClientRequest request,
StreamAdvisorChain chain
);
}Example - Streaming Metrics Advisor:
import org.springframework.ai.chat.client.advisor.api.StreamAdvisor;
import org.springframework.ai.chat.client.advisor.api.StreamAdvisorChain;
import org.springframework.ai.chat.client.ChatClientRequest;
import org.springframework.ai.chat.client.ChatClientResponse;
import reactor.core.publisher.Flux;
import java.util.concurrent.atomic.AtomicInteger;
public class StreamingMetricsAdvisor implements StreamAdvisor {
@Override
public Flux<ChatClientResponse> adviseStream(
ChatClientRequest request,
StreamAdvisorChain chain
) {
AtomicInteger chunkCount = new AtomicInteger(0);
long startTime = System.currentTimeMillis();
return chain.nextStream(request)
.doOnNext(response -> {
chunkCount.incrementAndGet();
})
.doOnComplete(() -> {
long duration = System.currentTimeMillis() - startTime;
System.out.println("Stream completed: " +
chunkCount.get() + " chunks in " +
duration + "ms");
})
.doOnError(error -> {
System.err.println("Stream error: " + error.getMessage());
});
}
@Override
public String getName() {
return "StreamingMetricsAdvisor";
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}For advisors that work with both call and stream modes, implement BaseAdvisor which provides template methods.
interface BaseAdvisor extends CallAdvisor, StreamAdvisor {
ChatClientRequest before(
ChatClientRequest request,
AdvisorChain chain
);
ChatClientResponse after(
ChatClientResponse response,
AdvisorChain chain
);
default Scheduler getScheduler() {
return DEFAULT_SCHEDULER;
}
Scheduler DEFAULT_SCHEDULER = Schedulers.boundedElastic();
}Template Methods:
before() - Pre-processing, modifies request before executionafter() - Post-processing, modifies response after executiongetScheduler() - Provides scheduler for streaming operationsExample - Authentication Advisor:
import org.springframework.ai.chat.client.advisor.api.BaseAdvisor;
import org.springframework.ai.chat.client.advisor.api.AdvisorChain;
import org.springframework.ai.chat.client.ChatClientRequest;
import org.springframework.ai.chat.client.ChatClientResponse;
import org.springframework.ai.chat.model.Prompt;
import org.springframework.ai.chat.model.SystemMessage;
import java.util.ArrayList;
import java.util.List;
public class AuthenticationAdvisor implements BaseAdvisor {
private final AuthService authService;
public AuthenticationAdvisor(AuthService authService) {
this.authService = authService;
}
@Override
public ChatClientRequest before(
ChatClientRequest request,
AdvisorChain chain
) {
// Get user from context
String userId = (String) request.context().get("userId");
if (userId == null) {
throw new SecurityException("User ID required");
}
// Verify authentication
if (!authService.isAuthenticated(userId)) {
throw new SecurityException("User not authenticated");
}
// Add user info to context for downstream advisors
request.context().put("userRole", authService.getUserRole(userId));
request.context().put("authenticated", true);
return request;
}
@Override
public ChatClientResponse after(
ChatClientResponse response,
AdvisorChain chain
) {
// Log successful completion
String userId = (String) response.context().get("userId");
System.out.println("Request completed for user: " + userId);
return response;
}
@Override
public String getName() {
return "AuthenticationAdvisor";
}
@Override
public int getOrder() {
return 50; // Execute very early
}
}Custom advisors can modify requests by creating new immutable request objects.
Example - Adding System Context:
import org.springframework.ai.chat.client.advisor.api.BaseAdvisor;
import org.springframework.ai.chat.client.advisor.api.AdvisorChain;
import org.springframework.ai.chat.client.ChatClientRequest;
import org.springframework.ai.chat.client.ChatClientResponse;
import org.springframework.ai.chat.model.Prompt;
import org.springframework.ai.chat.model.SystemMessage;
import org.springframework.ai.chat.model.Message;
import java.util.ArrayList;
import java.util.List;
public class SystemContextAdvisor implements BaseAdvisor {
private final String systemContext;
public SystemContextAdvisor(String systemContext) {
this.systemContext = systemContext;
}
@Override
public ChatClientRequest before(
ChatClientRequest request,
AdvisorChain chain
) {
// Get existing prompt
Prompt originalPrompt = request.prompt();
// Build new message list with system context
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage(systemContext));
messages.addAll(originalPrompt.getInstructions());
// Create new prompt
Prompt modifiedPrompt = new Prompt(
messages,
originalPrompt.getOptions()
);
// Return modified request
return request.mutate()
.prompt(modifiedPrompt)
.build();
}
@Override
public ChatClientResponse after(
ChatClientResponse response,
AdvisorChain chain
) {
return response;
}
@Override
public String getName() {
return "SystemContextAdvisor";
}
@Override
public int getOrder() {
return 500;
}
}Custom advisors can modify responses after execution.
Example - Content Filtering Advisor:
import org.springframework.ai.chat.client.advisor.api.BaseAdvisor;
import org.springframework.ai.chat.client.advisor.api.AdvisorChain;
import org.springframework.ai.chat.client.ChatClientRequest;
import org.springframework.ai.chat.client.ChatClientResponse;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.model.AssistantMessage;
import java.util.regex.Pattern;
public class ContentFilterAdvisor implements BaseAdvisor {
private final Pattern sensitivePattern;
private final String replacement;
public ContentFilterAdvisor(String regex, String replacement) {
this.sensitivePattern = Pattern.compile(regex);
this.replacement = replacement;
}
@Override
public ChatClientRequest before(
ChatClientRequest request,
AdvisorChain chain
) {
return request;
}
@Override
public ChatClientResponse after(
ChatClientResponse response,
AdvisorChain chain
) {
ChatResponse chatResponse = response.chatResponse();
if (chatResponse == null) {
return response;
}
// Filter content
Generation result = chatResponse.getResult();
String content = result.getOutput().getContent();
String filtered = sensitivePattern.matcher(content)
.replaceAll(replacement);
// Create modified response (simplified)
// In practice, you'd need to reconstruct the full ChatResponse
response.context().put("contentFiltered", !content.equals(filtered));
return response;
}
@Override
public String getName() {
return "ContentFilterAdvisor";
}
@Override
public int getOrder() {
return 9000; // Execute late, after content generation
}
}The request context allows advisors to share data throughout the chain.
Example - Context Sharing:
import org.springframework.ai.chat.client.advisor.api.BaseAdvisor;
import org.springframework.ai.chat.client.advisor.api.AdvisorChain;
import org.springframework.ai.chat.client.ChatClientRequest;
import org.springframework.ai.chat.client.ChatClientResponse;
import java.util.HashMap;
import java.util.Map;
// First advisor: Add metadata
public class MetadataAdvisor implements BaseAdvisor {
@Override
public ChatClientRequest before(
ChatClientRequest request,
AdvisorChain chain
) {
Map<String, Object> metadata = new HashMap<>();
metadata.put("requestId", UUID.randomUUID().toString());
metadata.put("timestamp", System.currentTimeMillis());
metadata.put("environment", "production");
request.context().putAll(metadata);
return request;
}
@Override
public ChatClientResponse after(
ChatClientResponse response,
AdvisorChain chain
) {
return response;
}
@Override
public String getName() {
return "MetadataAdvisor";
}
@Override
public int getOrder() {
return 100;
}
}
// Second advisor: Use metadata
public class AuditAdvisor implements BaseAdvisor {
private final AuditService auditService;
public AuditAdvisor(AuditService auditService) {
this.auditService = auditService;
}
@Override
public ChatClientRequest before(
ChatClientRequest request,
AdvisorChain chain
) {
return request;
}
@Override
public ChatClientResponse after(
ChatClientResponse response,
AdvisorChain chain
) {
// Read metadata from context
String requestId = (String) response.context().get("requestId");
long timestamp = (long) response.context().get("timestamp");
String environment = (String) response.context().get("environment");
// Audit the request
auditService.log(requestId, timestamp, environment);
return response;
}
@Override
public String getName() {
return "AuditAdvisor";
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}Advisors can conditionally execute based on context or request properties.
Example - Feature Flag Advisor:
import org.springframework.ai.chat.client.advisor.api.BaseAdvisor;
import org.springframework.ai.chat.client.advisor.api.AdvisorChain;
import org.springframework.ai.chat.client.ChatClientRequest;
import org.springframework.ai.chat.client.ChatClientResponse;
public class FeatureFlagAdvisor implements BaseAdvisor {
private final String featureName;
private final FeatureFlagService featureFlags;
public FeatureFlagAdvisor(
String featureName,
FeatureFlagService featureFlags
) {
this.featureName = featureName;
this.featureFlags = featureFlags;
}
@Override
public ChatClientRequest before(
ChatClientRequest request,
AdvisorChain chain
) {
// Check feature flag
boolean enabled = featureFlags.isEnabled(featureName);
if (!enabled) {
// Skip this advisor's logic
return request;
}
// Execute feature logic
System.out.println("Feature " + featureName + " is enabled");
request.context().put(featureName + ".enabled", true);
return request;
}
@Override
public ChatClientResponse after(
ChatClientResponse response,
AdvisorChain chain
) {
// Only process if feature was enabled
Boolean enabled = (Boolean) response.context()
.get(featureName + ".enabled");
if (Boolean.TRUE.equals(enabled)) {
System.out.println("Feature " + featureName + " completed");
}
return response;
}
@Override
public String getName() {
return "FeatureFlagAdvisor-" + featureName;
}
@Override
public int getOrder() {
return 1000;
}
}Custom advisors should handle errors appropriately.
Example - Error Recovery Advisor:
import org.springframework.ai.chat.client.advisor.CallAdvisor;
import org.springframework.ai.chat.client.advisor.CallAdvisorChain;
import org.springframework.ai.chat.client.ChatClientRequest;
import org.springframework.ai.chat.client.ChatClientResponse;
public class ErrorRecoveryAdvisor implements CallAdvisor {
private final int maxRetries;
public ErrorRecoveryAdvisor(int maxRetries) {
this.maxRetries = maxRetries;
}
@Override
public ChatClientResponse adviseCall(
ChatClientRequest request,
CallAdvisorChain chain
) {
int attempt = 0;
Exception lastError = null;
while (attempt < maxRetries) {
try {
return chain.nextCall(request);
} catch (Exception e) {
lastError = e;
attempt++;
System.err.println("Attempt " + attempt + " failed: " +
e.getMessage());
if (attempt < maxRetries) {
// Wait before retry
try {
Thread.sleep(1000L * attempt);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException(ie);
}
}
}
}
throw new RuntimeException(
"Failed after " + maxRetries + " attempts",
lastError
);
}
@Override
public String getName() {
return "ErrorRecoveryAdvisor";
}
@Override
public int getOrder() {
return 0; // Execute first to wrap entire chain
}
}Stream advisors can transform streaming responses.
Example - Token Rate Limiter:
import org.springframework.ai.chat.client.advisor.api.StreamAdvisor;
import org.springframework.ai.chat.client.advisor.api.StreamAdvisorChain;
import org.springframework.ai.chat.client.ChatClientRequest;
import org.springframework.ai.chat.client.ChatClientResponse;
import reactor.core.publisher.Flux;
import java.time.Duration;
public class TokenRateLimiterAdvisor implements StreamAdvisor {
private final Duration delayPerToken;
public TokenRateLimiterAdvisor(Duration delayPerToken) {
this.delayPerToken = delayPerToken;
}
@Override
public Flux<ChatClientResponse> adviseStream(
ChatClientRequest request,
StreamAdvisorChain chain
) {
return chain.nextStream(request)
.delayElements(delayPerToken)
.doOnNext(response -> {
System.out.println("Token released");
});
}
@Override
public String getName() {
return "TokenRateLimiterAdvisor";
}
@Override
public int getOrder() {
return 8000;
}
}Custom Advisor with Builder Pattern:
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.api.BaseAdvisor;
import org.springframework.ai.chat.client.advisor.api.AdvisorChain;
import org.springframework.ai.chat.client.ChatClientRequest;
import org.springframework.ai.chat.client.ChatClientResponse;
public class RateLimitAdvisor implements BaseAdvisor {
private final int maxRequestsPerMinute;
private final RateLimiter rateLimiter;
private final int order;
private RateLimitAdvisor(Builder builder) {
this.maxRequestsPerMinute = builder.maxRequestsPerMinute;
this.rateLimiter = new RateLimiter(maxRequestsPerMinute);
this.order = builder.order;
}
public static Builder builder() {
return new Builder();
}
@Override
public ChatClientRequest before(
ChatClientRequest request,
AdvisorChain chain
) {
// Check rate limit
if (!rateLimiter.allowRequest()) {
throw new RateLimitException(
"Rate limit exceeded: " + maxRequestsPerMinute +
" requests per minute"
);
}
return request;
}
@Override
public ChatClientResponse after(
ChatClientResponse response,
AdvisorChain chain
) {
return response;
}
@Override
public String getName() {
return "RateLimitAdvisor";
}
@Override
public int getOrder() {
return order;
}
public static class Builder {
private int maxRequestsPerMinute = 60;
private int order = 100;
public Builder maxRequestsPerMinute(int maxRequestsPerMinute) {
this.maxRequestsPerMinute = maxRequestsPerMinute;
return this;
}
public Builder order(int order) {
this.order = order;
return this;
}
public RateLimitAdvisor build() {
return new RateLimitAdvisor(this);
}
}
}
// Usage
ChatClient client = ChatClient.builder(chatModel)
.defaultAdvisors(
RateLimitAdvisor.builder()
.maxRequestsPerMinute(30)
.order(50)
.build()
)
.build();
String response = client
.prompt("Hello")
.call()
.content();