Spring AI Chat Client provides a fluent API for building AI-powered applications with LLMs, supporting advisors, streaming, structured outputs, and conversation memory
Spring AI Chat Client provides several pre-built utility advisors for common tasks like logging, validation, security, and tool calling.
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.client.advisor.SafeGuardAdvisor;
import org.springframework.ai.chat.client.advisor.StructuredOutputValidationAdvisor;
import org.springframework.ai.chat.client.advisor.ToolCallAdvisor;
import org.springframework.ai.chat.client.AdvisorParams;
import org.springframework.ai.chat.client.ChatClientRequest;
import org.springframework.ai.chat.client.ChatClientResponse;
import org.springframework.ai.chat.model.ChatResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.core.ParameterizedTypeReference;
import java.util.List;
import java.util.function.Function;Logs request and response information for debugging purposes.
class SimpleLoggerAdvisor implements CallAdvisor, StreamAdvisor {
static Builder builder();
static final Function<ChatClientRequest, String> DEFAULT_REQUEST_TO_STRING;
static final Function<ChatResponse, String> DEFAULT_RESPONSE_TO_STRING;
}interface Builder {
Builder requestToString(
Function<ChatClientRequest, String> requestToString
);
Builder responseToString(
Function<ChatClientResponse, String> responseToString
);
Builder order(int order);
SimpleLoggerAdvisor build();
}import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
ChatClient client = ChatClient.builder(chatModel)
.defaultAdvisors(
SimpleLoggerAdvisor.builder().build()
)
.build();
// Logs will show request and response details
String response = client
.prompt("Hello")
.call()
.content();SimpleLoggerAdvisor logger = SimpleLoggerAdvisor.builder()
.requestToString(request -> {
String userMsg = request.prompt()
.getInstructions()
.stream()
.filter(m -> m instanceof UserMessage)
.map(m -> ((UserMessage) m).getContent())
.findFirst()
.orElse("no message");
return "REQUEST: " + userMsg;
})
.responseToString(response -> {
if (response.chatResponse() == null) {
return "RESPONSE: <streaming>";
}
String content = response.chatResponse()
.getResult()
.getOutput()
.getContent();
return "RESPONSE: " + content.substring(0, Math.min(50, content.length()));
})
.order(Ordered.LOWEST_PRECEDENCE)
.build();ChatClient client = ChatClient.builder(chatModel)
.defaultAdvisors(
SimpleLoggerAdvisor.builder()
.order(Ordered.LOWEST_PRECEDENCE) // Log last to see final request
.build()
)
.build();
// This will log:
// - Before call: request details
// - After call: response details
String answer = client
.prompt("Explain Spring Boot")
.call()
.content();Blocks requests containing sensitive or prohibited words for security.
class SafeGuardAdvisor implements CallAdvisor, StreamAdvisor {
static Builder builder();
}interface Builder {
Builder sensitiveWords(List<String> sensitiveWords);
Builder failureResponse(String failureResponse);
Builder order(int order);
SafeGuardAdvisor build();
}import org.springframework.ai.chat.client.advisor.SafeGuardAdvisor;
SafeGuardAdvisor safeguard = SafeGuardAdvisor.builder()
.sensitiveWords(List.of("password", "secret", "confidential"))
.build();
ChatClient client = ChatClient.builder(chatModel)
.defaultAdvisors(safeguard)
.build();
// This will be blocked
String response = client
.prompt("What is my password?")
.call()
.content();
// Returns: "I cannot assist with that request."SafeGuardAdvisor safeguard = SafeGuardAdvisor.builder()
.sensitiveWords(List.of("hack", "exploit", "crack"))
.failureResponse("That request violates our usage policy.")
.order(100) // Execute early
.build();List<String> securityWords = List.of("password", "secret", "credential");
List<String> complianceWords = List.of("ssn", "credit-card", "bank-account");
List<String> allBlocked = new ArrayList<>();
allBlocked.addAll(securityWords);
allBlocked.addAll(complianceWords);
SafeGuardAdvisor safeguard = SafeGuardAdvisor.builder()
.sensitiveWords(allBlocked)
.failureResponse("Request blocked for security reasons.")
.build();ChatClient secureClient = ChatClient.builder(chatModel)
.defaultAdvisors(
SafeGuardAdvisor.builder()
.sensitiveWords(List.of("password", "api-key", "token"))
.failureResponse("Security policy prevents this request.")
.order(100) // Execute before other advisors
.build()
)
.build();
// Allowed
String ok = secureClient
.prompt("How do I authenticate users?")
.call()
.content();
// Blocked
String blocked = secureClient
.prompt("Show me the API key")
.call()
.content();
// Returns: "Security policy prevents this request."Validates structured JSON output against a schema and retries on validation failure.
class StructuredOutputValidationAdvisor implements CallAdvisor, StreamAdvisor {
static Builder builder();
}interface Builder {
Builder advisorOrder(int order);
Builder outputType(Type outputType);
Builder outputType(TypeRef<?> outputTypeRef);
Builder outputType(TypeReference<?> outputTypeReference);
Builder outputType(
ParameterizedTypeReference<?> outputTypeReference
);
Builder maxRepeatAttempts(int maxRepeatAttempts);
Builder objectMapper(ObjectMapper objectMapper);
StructuredOutputValidationAdvisor build();
}import org.springframework.ai.chat.client.advisor.StructuredOutputValidationAdvisor;
record Person(String name, int age, String email) {}
StructuredOutputValidationAdvisor validator =
StructuredOutputValidationAdvisor.builder()
.outputType(Person.class)
.maxRepeatAttempts(3)
.build();
ChatClient client = ChatClient.builder(chatModel)
.defaultAdvisors(validator)
.build();
// If response is invalid JSON or doesn't match schema, retries automatically
Person person = client
.prompt("Generate a person profile")
.call()
.entity(Person.class);import org.springframework.core.ParameterizedTypeReference;
record Task(String name, String status) {}
StructuredOutputValidationAdvisor validator =
StructuredOutputValidationAdvisor.builder()
.outputType(new ParameterizedTypeReference<List<Task>>() {})
.maxRepeatAttempts(5)
.build();
List<Task> tasks = client
.prompt("Generate 5 tasks")
.call()
.entity(new ParameterizedTypeReference<List<Task>>() {});import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.DeserializationFeature;
ObjectMapper mapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.configure(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, true);
StructuredOutputValidationAdvisor validator =
StructuredOutputValidationAdvisor.builder()
.outputType(MyClass.class)
.objectMapper(mapper)
.maxRepeatAttempts(3)
.build();record WeatherData(
String location,
double temperature,
String conditions,
List<String> forecast
) {}
ChatClient client = ChatClient.builder(chatModel)
.defaultAdvisors(
StructuredOutputValidationAdvisor.builder()
.outputType(WeatherData.class)
.maxRepeatAttempts(3) // Retry up to 3 times
.advisorOrder(5000)
.build()
)
.build();
// Automatically validates and retries if JSON is invalid
WeatherData weather = client
.prompt("Get weather for Paris")
.call()
.entity(WeatherData.class);import org.springframework.ai.chat.client.AdvisorParams;
ChatClient client = ChatClient.builder(chatModel)
.defaultAdvisors(
StructuredOutputValidationAdvisor.builder()
.outputType(MyType.class)
.build()
)
.build();
// Enable native structured output for this request
MyType result = client
.prompt("Generate data")
.advisors(AdvisorParams.ENABLE_NATIVE_STRUCTURED_OUTPUT)
.call()
.entity(MyType.class);Implements the tool calling loop, automatically handling multiple rounds of function calls until completion.
class ToolCallAdvisor implements CallAdvisor, StreamAdvisor {
static Builder<?> builder();
}interface Builder<T extends Builder<T>> {
T toolCallingManager(ToolCallingManager toolCallingManager);
T advisorOrder(int order);
ToolCallAdvisor build();
}import org.springframework.ai.chat.client.advisor.ToolCallAdvisor;
import org.springframework.ai.model.function.FunctionCallback;
FunctionCallback weatherTool = FunctionCallback.builder()
.function("getCurrentWeather", this::getWeather)
.description("Get current weather for a location")
.inputType(WeatherRequest.class)
.build();
ChatClient client = ChatClient.builder(chatModel)
.defaultAdvisors(
ToolCallAdvisor.builder().build()
)
.defaultTools(weatherTool)
.build();
// ToolCallAdvisor automatically handles the tool calling loop
String response = client
.prompt("What's the weather in Paris?")
.call()
.content();FunctionCallback weatherTool = // ... weather tool
FunctionCallback calculatorTool = // ... calculator tool
FunctionCallback searchTool = // ... search tool
ChatClient client = ChatClient.builder(chatModel)
.defaultAdvisors(
ToolCallAdvisor.builder()
.advisorOrder(3000)
.build()
)
.defaultTools(weatherTool, calculatorTool, searchTool)
.build();
// AI can call multiple tools in sequence
String answer = client
.prompt("What's 2+2 and what's the weather in London?")
.call()
.content();ChatClient client = ChatClient.builder(chatModel)
.defaultAdvisors(
ToolCallAdvisor.builder().build()
)
.build();
String response = client
.prompt("Calculate expenses")
.tools(expenseTool)
.call()
.content();record WeatherRequest(String location) {}
record WeatherResponse(String location, double temp, String conditions) {}
public WeatherResponse getWeather(WeatherRequest request) {
// Fetch real weather data
return new WeatherResponse(request.location(), 22.5, "Sunny");
}
FunctionCallback weatherTool = FunctionCallback.builder()
.function("getCurrentWeather", this::getWeather)
.description("Get the current weather for a location")
.inputType(WeatherRequest.class)
.build();
ChatClient client = ChatClient.builder(chatModel)
.defaultAdvisors(
ToolCallAdvisor.builder().build()
)
.defaultTools(weatherTool)
.build();
// The advisor handles:
// 1. Initial request to AI
// 2. AI responds with tool call request
// 3. Execute tool function
// 4. Send tool result back to AI
// 5. AI generates final response
String answer = client
.prompt("What's the weather like in Tokyo and should I bring an umbrella?")
.call()
.content();
System.out.println(answer);
// Output: "The weather in Tokyo is sunny with a temperature of 22.5°C.
// You won't need an umbrella today!"ToolCallAdvisor provides protected hook methods for customization:
protected void doInitializeLoop(
ChatClientRequest request,
AdvisorChain chain
);
protected void doBeforeCall(
ChatClientRequest request,
AdvisorChain chain
);
protected void doAfterCall(
ChatClientResponse response,
AdvisorChain chain
);
protected void doFinalizeLoop(
ChatClientResponse response,
AdvisorChain chain
);Custom Implementation:
class CustomToolCallAdvisor extends ToolCallAdvisor {
@Override
protected void doBeforeCall(
ChatClientRequest request,
AdvisorChain chain
) {
System.out.println("About to make tool call");
}
@Override
protected void doAfterCall(
ChatClientResponse response,
AdvisorChain chain
) {
System.out.println("Tool call completed");
}
}import org.springframework.ai.chat.client.advisor.*;
record Analysis(String summary, List<String> keyPoints) {}
ChatClient client = ChatClient.builder(chatModel)
.defaultAdvisors(
// Security first
SafeGuardAdvisor.builder()
.sensitiveWords(List.of("password", "secret"))
.order(100)
.build(),
// Tool calling
ToolCallAdvisor.builder()
.advisorOrder(3000)
.build(),
// Validation for structured output
StructuredOutputValidationAdvisor.builder()
.outputType(Analysis.class)
.maxRepeatAttempts(3)
.advisorOrder(5000)
.build(),
// Logging last
SimpleLoggerAdvisor.builder()
.order(Ordered.LOWEST_PRECEDENCE)
.build()
)
.defaultTools(researchTool)
.build();
// All advisors work together
Analysis result = client
.prompt("Research topic and create analysis")
.call()
.entity(Analysis.class);Utility class for common advisor operations.
class AdvisorUtils {
static Predicate<ChatClientResponse> onFinishReason();
}Usage:
import org.springframework.ai.chat.client.advisor.AdvisorUtils;
// Check if response has a finish reason (indicates completion)
Predicate<ChatClientResponse> isComplete = AdvisorUtils.onFinishReason();
if (isComplete.test(response)) {
System.out.println("Response is complete");
}Utility for purging media content that exceeds token limits.
class LastMaxTokenSizeContentPurger {
LastMaxTokenSizeContentPurger(
TokenCountEstimator tokenCountEstimator,
int maxTokenSize
);
List<MediaContent> purgeExcess(
List<MediaContent> mediaContents,
int totalSize
);
}Usage:
import org.springframework.ai.chat.client.advisor.LastMaxTokenSizeContentPurger;
import org.springframework.ai.model.TokenCountEstimator;
TokenCountEstimator estimator = // ... get estimator
LastMaxTokenSizeContentPurger purger = new LastMaxTokenSizeContentPurger(
estimator,
4000 // max tokens
);
List<MediaContent> filtered = purger.purgeExcess(mediaList, totalTokens);Provides common advisor parameter constants for configuring advisor behavior at request time.
class AdvisorParams {
static final Consumer<ChatClient.AdvisorSpec> ENABLE_NATIVE_STRUCTURED_OUTPUT;
}Enables native structured output support for the request, bypassing JSON schema-based conversion and using the model's native structured output capabilities.
Usage:
import org.springframework.ai.chat.client.AdvisorParams;
MyType result = chatClient
.prompt("Generate data")
.advisors(AdvisorParams.ENABLE_NATIVE_STRUCTURED_OUTPUT)
.call()
.entity(MyType.class);