Spring AI Chat Client provides a fluent API for building AI-powered applications with LLMs, supporting advisors, streaming, structured outputs, and conversation memory
Production-ready patterns and complete applications.
@Service
public class ChatService {
private final ChatClient chatClient;
private final ChatMemory chatMemory;
public ChatService(ChatModel chatModel) {
this.chatMemory = new RedisChatMemory(redisTemplate);
this.chatClient = ChatClient.builder(chatModel)
.defaultSystem("You are a helpful assistant")
.defaultAdvisors(
SafeGuardAdvisor.builder()
.sensitiveWords(List.of("password", "secret"))
.order(100)
.build(),
MessageChatMemoryAdvisor.builder(chatMemory)
.order(1001)
.build(),
SimpleLoggerAdvisor.builder()
.order(Ordered.LOWEST_PRECEDENCE)
.build()
)
.defaultOptions(ChatOptionsBuilder.builder()
.withTemperature(0.7)
.withMaxTokens(1000)
.build())
.observationRegistry(observationRegistry)
.build();
}
public String chat(String userId, String message) {
return chatClient
.prompt()
.user(message)
.advisors(spec -> spec.param("conversationId", userId))
.call()
.content();
}
public Flux<String> chatStream(String userId, String message) {
return chatClient
.prompt()
.user(message)
.advisors(spec -> spec.param("conversationId", userId))
.stream()
.content();
}
}
@RestController
@RequestMapping("/api/chat")
public class ChatController {
private final ChatService chatService;
@PostMapping
public ResponseEntity<String> chat(
@RequestHeader("User-Id") String userId,
@RequestBody ChatRequest request
) {
String response = chatService.chat(userId, request.message());
return ResponseEntity.ok(response);
}
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> chatStream(
@RequestHeader("User-Id") String userId,
@RequestParam String message
) {
return chatService.chatStream(userId, message)
.map(chunk -> ServerSentEvent.builder(chunk).build());
}
}@Service
public class RAGService {
private final ChatClient chatClient;
private final VectorStore vectorStore;
public RAGService(ChatModel chatModel, VectorStore vectorStore) {
this.vectorStore = vectorStore;
ToolCallback searchTool = ToolCallback.builder()
.function("searchKnowledge", this::searchKnowledge)
.description("Search knowledge base for relevant information")
.inputType(SearchRequest.class)
.build();
this.chatClient = ChatClient.builder(chatModel)
.defaultSystem("""
You are a helpful assistant with access to a knowledge base.
Use the searchKnowledge tool to find relevant information.
Always cite sources when using retrieved information.
""")
.defaultAdvisors(
ToolCallAdvisor.builder().build(),
SimpleLoggerAdvisor.builder().build()
)
.defaultTools(searchTool)
.build();
}
public SearchResult searchKnowledge(SearchRequest request) {
List<Document> docs = vectorStore.similaritySearch(
SearchRequest.query(request.query())
.withTopK(5)
.withSimilarityThreshold(0.7)
);
return new SearchResult(
docs.stream()
.map(Document::getContent)
.collect(Collectors.toList())
);
}
public String query(String question) {
return chatClient
.prompt(question)
.call()
.content();
}
}
record SearchRequest(String query) {}
record SearchResult(List<String> documents) {}@Service
public class ContentGenerator {
private final ChatClient chatClient;
public ContentGenerator(ChatModel chatModel) {
this.chatClient = ChatClient.builder(chatModel)
.defaultSystem("You are a professional content writer")
.defaultAdvisors(
StructuredOutputValidationAdvisor.builder()
.outputType(Article.class)
.maxRepeatAttempts(3)
.build()
)
.defaultOptions(ChatOptionsBuilder.builder()
.withTemperature(0.8) // More creative
.withMaxTokens(2000)
.build())
.build();
}
public Article generateArticle(String topic, String style) {
Article article = chatClient
.prompt()
.system(spec -> spec
.text("Write in {style} style")
.param("style", style)
)
.user(spec -> spec
.text("Write an article about {topic}")
.param("topic", topic)
)
.call()
.entity(Article.class);
// Validate
if (article == null || article.content().length() < 100) {
throw new IllegalStateException("Generated article too short");
}
return article;
}
}
record Article(
String title,
String content,
List<String> tags,
String summary
) {}@Service
public class SupportBot {
private final ChatClient chatClient;
private final TicketService ticketService;
public SupportBot(ChatModel chatModel, TicketService ticketService) {
this.ticketService = ticketService;
ToolCallback createTicket = ToolCallback.builder()
.function("createTicket", this::createTicket)
.description("Create a support ticket")
.inputType(TicketRequest.class)
.build();
ToolCallback searchFAQ = ToolCallback.builder()
.function("searchFAQ", this::searchFAQ)
.description("Search FAQ database")
.inputType(FAQSearchRequest.class)
.build();
this.chatClient = ChatClient.builder(chatModel)
.defaultSystem("""
You are a customer support assistant.
- First try to answer from FAQ
- If issue needs human support, create a ticket
- Be empathetic and professional
""")
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(chatMemory).build(),
ToolCallAdvisor.builder().build()
)
.defaultTools(createTicket, searchFAQ)
.build();
}
public String handleCustomerQuery(String customerId, String query) {
return chatClient
.prompt()
.user(query)
.advisors(spec -> spec
.param("conversationId", customerId)
)
.toolContext(Map.of(
"customerId", customerId,
"timestamp", Instant.now()
))
.call()
.content();
}
private TicketResponse createTicket(TicketRequest request) {
return ticketService.create(request);
}
private FAQResult searchFAQ(FAQSearchRequest request) {
return faqService.search(request.query());
}
}@Service
public class CodeReviewAssistant {
private final ChatClient chatClient;
public CodeReviewAssistant(ChatModel chatModel) {
this.chatClient = ChatClient.builder(chatModel)
.defaultSystem("""
You are a senior code reviewer.
Focus on:
- Security vulnerabilities
- Performance issues
- Best practices
- Code maintainability
Provide specific, actionable feedback.
""")
.defaultOptions(ChatOptionsBuilder.builder()
.withTemperature(0.3) // More deterministic
.build())
.build();
}
public CodeReview reviewCode(String code, String language) {
return chatClient
.prompt()
.system(spec -> spec
.text("Review {language} code")
.param("language", language)
)
.user(spec -> spec
.text("""
Review this code:
```{language}
{code}
```
""")
.param("language", language)
.param("code", code)
)
.call()
.entity(CodeReview.class);
}
}
record CodeReview(
String summary,
List<Issue> issues,
List<String> suggestions,
int overallScore
) {}
record Issue(
String severity,
String category,
String description,
String location
) {}@Service
public class DataAnalysisService {
private final ChatClient chatClient;
public DataAnalysisService(ChatModel chatModel) {
this.chatClient = ChatClient.builder(chatModel)
.defaultSystem("You are a data analyst. Provide insights from data.")
.defaultAdvisors(
StructuredOutputValidationAdvisor.builder()
.outputType(Analysis.class)
.maxRepeatAttempts(3)
.build()
)
.build();
}
public Analysis analyzeData(String dataset, String question) {
ResponseEntity<ChatResponse, Analysis> result = chatClient
.prompt()
.user(spec -> spec
.text("""
Analyze this data and answer: {question}
Data:
{dataset}
""")
.param("question", question)
.param("dataset", dataset)
)
.call()
.responseEntity(Analysis.class);
// Log token usage
ChatResponse response = result.getResponse();
if (response != null) {
var usage = response.getMetadata().getUsage();
log.info("Analysis used {} tokens", usage.getTotalTokens());
}
return result.getEntity();
}
}
record Analysis(
String summary,
List<String> keyFindings,
Map<String, Double> metrics,
List<String> recommendations
) {}@Service
public class ResilientChatService {
private final ChatClient chatClient;
private final RetryTemplate retryTemplate;
public ResilientChatService(ChatModel chatModel) {
this.chatClient = ChatClient.create(chatModel);
this.retryTemplate = RetryTemplate.builder()
.maxAttempts(3)
.exponentialBackoff(1000, 2.0, 10000)
.retryOn(ChatModelException.class)
.build();
}
public String chatWithRetry(String message) {
return retryTemplate.execute(context -> {
try {
String response = chatClient
.prompt(message)
.call()
.content();
if (response == null || response.isBlank()) {
throw new IllegalStateException("Empty response");
}
return response;
} catch (Exception e) {
log.warn("Attempt {} failed: {}",
context.getRetryCount() + 1,
e.getMessage()
);
throw e;
}
});
}
public String chatWithFallback(String message) {
try {
return chatClient.prompt(message).call().content();
} catch (Exception e) {
log.error("Chat failed, using fallback", e);
return "I apologize, but I'm having trouble processing your request. Please try again later.";
}
}
}