Spring AI Chat Client provides a fluent API for building AI-powered applications with LLMs, supporting advisors, streaming, structured outputs, and conversation memory
The Spring AI Chat Client uses an advisor pattern (interceptor chain) to modify requests and responses. Advisors provide a powerful extension point for implementing cross-cutting concerns like logging, memory management, validation, and security.
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.ChatClientRequest;
import org.springframework.ai.chat.client.ChatClientResponse;
import org.springframework.core.Ordered;
import reactor.core.publisher.Flux;
import reactor.core.scheduler.Scheduler;The base interface for all advisors.
interface Advisor extends Ordered {
String getName();
int getOrder();
int DEFAULT_CHAT_MEMORY_PRECEDENCE_ORDER =
Ordered.HIGHEST_PRECEDENCE + 1000;
}Methods:
getName() - Returns the advisor name for identification and logginggetOrder() - Returns the execution order (lower values execute first)Constants:
DEFAULT_CHAT_MEMORY_PRECEDENCE_ORDER - Recommended order for memory advisors (1001)Example:
class MyAdvisor implements CallAdvisor {
@Override
public String getName() {
return "MyCustomAdvisor";
}
@Override
public int getOrder() {
return 100; // Execute early in chain
}
@Override
public ChatClientResponse adviseCall(
ChatClientRequest request,
CallAdvisorChain chain
) {
// Implementation
}
}Advisor interface for synchronous (blocking) call flows.
interface CallAdvisor extends Advisor {
ChatClientResponse adviseCall(
ChatClientRequest request,
CallAdvisorChain chain
);
}Parameters:
request - The current request being processedchain - The chain to invoke the next advisorReturns: Modified or original ChatClientResponse
Example:
class LoggingCallAdvisor implements CallAdvisor {
@Override
public ChatClientResponse adviseCall(
ChatClientRequest request,
CallAdvisorChain chain
) {
System.out.println("Before call: " + request);
ChatClientResponse response = chain.nextCall(request);
System.out.println("After call: " + response);
return response;
}
@Override
public String getName() {
return "LoggingCallAdvisor";
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}Advisor interface for streaming (reactive) flows.
interface StreamAdvisor extends Advisor {
Flux<ChatClientResponse> adviseStream(
ChatClientRequest request,
StreamAdvisorChain chain
);
}Parameters:
request - The current request being processedchain - The chain to invoke the next advisorReturns: Flux<ChatClientResponse> - Reactive stream of responses
Example:
class LoggingStreamAdvisor implements StreamAdvisor {
@Override
public Flux<ChatClientResponse> adviseStream(
ChatClientRequest request,
StreamAdvisorChain chain
) {
System.out.println("Before stream: " + request);
return chain.nextStream(request)
.doOnNext(response ->
System.out.println("Stream chunk: " + response)
)
.doOnComplete(() ->
System.out.println("Stream complete")
);
}
@Override
public String getName() {
return "LoggingStreamAdvisor";
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}Unified interface implementing both CallAdvisor and StreamAdvisor with template methods.
interface BaseAdvisor extends CallAdvisor, StreamAdvisor {
// Template methods
ChatClientRequest before(
ChatClientRequest request,
AdvisorChain chain
);
ChatClientResponse after(
ChatClientResponse response,
AdvisorChain chain
);
Scheduler getScheduler();
Scheduler DEFAULT_SCHEDULER = Schedulers.boundedElastic();
}Template Methods:
before() - Pre-processing logic, modify request before executionafter() - Post-processing logic, modify response after executiongetScheduler() - Get scheduler for streaming operations (default: boundedElastic)Default Implementations:
The interface provides default implementations of adviseCall() and adviseStream() that call the template methods.
Example:
class TimingAdvisor implements BaseAdvisor {
@Override
public ChatClientRequest before(
ChatClientRequest request,
AdvisorChain chain
) {
// Store start time in context
request.context().put("startTime", System.currentTimeMillis());
return request;
}
@Override
public ChatClientResponse after(
ChatClientResponse response,
AdvisorChain chain
) {
long startTime = (long) response.context().get("startTime");
long duration = System.currentTimeMillis() - startTime;
System.out.println("Request took: " + duration + "ms");
return response;
}
@Override
public String getName() {
return "TimingAdvisor";
}
@Override
public int getOrder() {
return 0; // Execute first
}
}Base chain interface providing observability support.
interface AdvisorChain {
ObservationRegistry getObservationRegistry();
}Methods:
getObservationRegistry() - Get observation registry for metrics (default: NOOP)Chain interface for synchronous advisor execution.
interface CallAdvisorChain extends AdvisorChain {
ChatClientResponse nextCall(ChatClientRequest request);
List<CallAdvisor> getCallAdvisors();
CallAdvisorChain copy(CallAdvisor nextAdvisor);
}Methods:
nextCall() - Invoke next advisor in chaingetCallAdvisors() - Get all call advisors in chaincopy() - Create new chain starting after specified advisorChain interface for streaming advisor execution.
interface StreamAdvisorChain extends AdvisorChain {
Flux<ChatClientResponse> nextStream(ChatClientRequest request);
List<StreamAdvisor> getStreamAdvisors();
}Methods:
nextStream() - Invoke next advisor in chain (returns Flux)getStreamAdvisors() - Get all stream advisors in chainCombined chain interface supporting both call and stream advisors.
interface BaseAdvisorChain extends CallAdvisorChain, StreamAdvisorChain {
}Advisors execute in order determined by their getOrder() value. Lower values execute first.
Standard Order Values:
Ordered.HIGHEST_PRECEDENCE (-2147483648) - Absolute firstAdvisor.DEFAULT_CHAT_MEMORY_PRECEDENCE_ORDER (1001) - Memory advisorsOrdered.LOWEST_PRECEDENCE (2147483647) - Absolute lastExample:
ChatClient client = ChatClient.builder(chatModel)
.defaultAdvisors(
SafeGuardAdvisor.builder()
.order(100) // Execute first
.build(),
MessageChatMemoryAdvisor.builder(chatMemory)
.order(Advisor.DEFAULT_CHAT_MEMORY_PRECEDENCE_ORDER)
.build(),
SimpleLoggerAdvisor.builder()
.order(Ordered.LOWEST_PRECEDENCE) // Execute last
.build()
)
.build();Advisors can share data through the request/response context maps.
class ContextSharingAdvisor implements BaseAdvisor {
@Override
public ChatClientRequest before(
ChatClientRequest request,
AdvisorChain chain
) {
// Add data to context
request.context().put("userId", "user-123");
request.context().put("timestamp", System.currentTimeMillis());
return request;
}
@Override
public ChatClientResponse after(
ChatClientResponse response,
AdvisorChain chain
) {
// Read data from context
String userId = (String) response.context().get("userId");
long timestamp = (long) response.context().get("timestamp");
// Context flows through the chain
return response;
}
@Override
public String getName() {
return "ContextSharingAdvisor";
}
@Override
public int getOrder() {
return 0;
}
}Advisors can modify requests before execution.
class RequestModifierAdvisor implements BaseAdvisor {
@Override
public ChatClientRequest before(
ChatClientRequest request,
AdvisorChain chain
) {
// Get existing prompt
Prompt originalPrompt = request.prompt();
// Add system message
List<Message> messages = new ArrayList<>(
originalPrompt.getInstructions()
);
messages.add(0, new SystemMessage("Be concise"));
// Create modified 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 "RequestModifierAdvisor";
}
@Override
public int getOrder() {
return 500;
}
}Advisors can modify responses after execution.
class ResponseModifierAdvisor implements BaseAdvisor {
@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;
}
// Modify response content (example: uppercase)
Generation generation = chatResponse.getResult();
String content = generation.getOutput().getContent();
String modified = content.toUpperCase();
// Create modified response
// (simplified - actual implementation more complex)
return response; // Return modified version
}
@Override
public String getName() {
return "ResponseModifierAdvisor";
}
@Override
public int getOrder() {
return 10000;
}
}Advisors can conditionally execute based on request properties.
class ConditionalAdvisor implements BaseAdvisor {
@Override
public ChatClientRequest before(
ChatClientRequest request,
AdvisorChain chain
) {
// Check if this advisor should execute
boolean enabled = (boolean) request.context()
.getOrDefault("enableConditional", false);
if (!enabled) {
return request; // Skip processing
}
// Execute logic
System.out.println("Conditional advisor executing");
return request;
}
@Override
public ChatClientResponse after(
ChatClientResponse response,
AdvisorChain chain
) {
return response;
}
@Override
public String getName() {
return "ConditionalAdvisor";
}
@Override
public int getOrder() {
return 5000;
}
}
// Usage
chatClient
.prompt()
.user("Hello")
.advisors(spec -> spec
.advisors(new ConditionalAdvisor())
.param("enableConditional", true)
)
.call()
.content();Terminal advisors actually execute the ChatModel call. They are typically last in the chain.
class ChatModelCallAdvisor implements CallAdvisor {
static Builder builder();
interface Builder {
Builder chatModel(ChatModel chatModel);
ChatModelCallAdvisor build();
}
}Executes ChatModel.call() for synchronous requests.
class ChatModelStreamAdvisor implements StreamAdvisor {
static Builder builder();
interface Builder {
Builder chatModel(ChatModel chatModel);
ChatModelStreamAdvisor build();
}
}Executes ChatModel.stream() for streaming requests.
Note: These terminal advisors are automatically added by the ChatClient and typically don't need to be manually configured.
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.*;
import org.springframework.core.Ordered;
// Custom advisor
class AuditAdvisor implements BaseAdvisor {
@Override
public ChatClientRequest before(
ChatClientRequest request,
AdvisorChain chain
) {
System.out.println("Audit: Request started");
request.context().put("auditId", UUID.randomUUID().toString());
return request;
}
@Override
public ChatClientResponse after(
ChatClientResponse response,
AdvisorChain chain
) {
String auditId = (String) response.context().get("auditId");
System.out.println("Audit: Request completed - " + auditId);
return response;
}
@Override
public String getName() {
return "AuditAdvisor";
}
@Override
public int getOrder() {
return 200;
}
}
// Configure client with multiple advisors
ChatClient client = ChatClient.builder(chatModel)
.defaultAdvisors(
SafeGuardAdvisor.builder()
.sensitiveWords(List.of("secret"))
.order(100)
.build(),
new AuditAdvisor(), // order: 200
MessageChatMemoryAdvisor.builder(chatMemory)
.order(Advisor.DEFAULT_CHAT_MEMORY_PRECEDENCE_ORDER) // 1001
.build(),
SimpleLoggerAdvisor.builder()
.order(Ordered.LOWEST_PRECEDENCE)
.build()
)
.build();
// Use client
String response = client
.prompt("Hello")
.call()
.content();