Spring AI Integration
Dependencies
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>2.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Choose your model provider — pattern is spring-ai-starter-model-<provider> -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-anthropic</artifactId>
</dependency>
<!-- OR -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<!-- For RAG / vector search -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-pgvector</artifactId>
</dependency>
<!-- QuestionAnswerAdvisor lives here — 2.0 renamed spring-ai-advisors-vector-store -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-vector-store-advisor</artifactId>
</dependency>
</dependencies>
Version pairing matters. Spring Boot 4 requires Spring AI 2.0 (
spring-ai-bom2.0.0); the 1.x line targets Boot 3 only. Starter coordinates followspring-ai-starter-model-<provider>(e.g.-model-anthropic,-model-openai) andspring-ai-starter-vector-store-<store>. Agents trained on pre-1.0 Spring AI emitspring-ai-<x>-spring-boot-starter— those names resolve to nothing in Maven Central. Also gone in 2.0:spring-ai-starter-model-azure-openai(use the OpenAI starter with an Azure base URL instead).
ChatClient — Basic Usage
@Service
@RequiredArgsConstructor
public class DocumentSummaryService {
private final ChatClient chatClient;
public String summarize(String conversationId, String content) {
return chatClient.prompt()
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId))
.user(u -> u.text("Summarize the following document in 3 bullet points:\n\n{content}")
.param("content", content))
.call()
.content();
}
// With system prompt
public String analyzeFinancial(String conversationId, String document, String language) {
return chatClient.prompt()
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId))
.system("You are a financial analyst. Respond in {language}.")
.system(s -> s.param("language", language))
.user(document)
.call()
.content();
}
}
Every call using the configured memory advisor must provide a user- or session-scoped
ChatMemory.CONVERSATION_ID. Never use one shared conversation ID for all users.
ChatClient Bean Configuration
@Configuration
public class AiConfig {
@Bean
public ChatMemory chatMemory() {
// InMemoryChatMemory is long gone. Use MessageWindowChatMemory —
// it caps history to a sliding window and defaults to an in-memory repository.
return MessageWindowChatMemory.builder()
.maxMessages(20)
.build();
}
@Bean
public ChatClient chatClient(ChatClient.Builder builder, ChatMemory chatMemory) {
return builder
.defaultSystem("You are a helpful assistant for an e-commerce platform.")
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(chatMemory).build(), // builder, not new(...)
new SimpleLoggerAdvisor() // logs prompts/responses
)
.build();
}
}
// 2.0: the conversation id is REQUIRED on every call that goes through a memory advisor.
// ChatMemory.DEFAULT_CONVERSATION_ID is removed — omitting the param throws IllegalArgumentException.
public String chat(String sessionId, String message) {
return chatClient.prompt()
.user(message)
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, sessionId))
.call()
.content();
}
Prompt Templates (externalized)
// src/main/resources/prompts/analyze-order.st
// Analyze this order and identify any anomalies:
// Customer: {customer}
// Items: {items}
// Total: {total}
// Flag any unusual patterns.
@Service
public class OrderAnalysisService {
@Value("classpath:prompts/analyze-order.st")
private Resource promptTemplate;
public String analyzeOrder(String conversationId, Order order) {
return chatClient.prompt()
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId))
.user(u -> u.text(promptTemplate)
.param("customer", order.getCustomerEmail())
.param("items", order.getItems().toString())
.param("total", order.getTotal()))
.call()
.content();
}
}
Structured Output
// Define the target record
public record OrderClassification(
String category,
String priority,
List<String> tags,
boolean requiresManualReview
) {}
@Service
public class OrderClassifier {
public OrderClassification classify(String orderDescription) {
return chatClient.prompt()
.user("Classify this order: " + orderDescription)
.call()
.entity(OrderClassification.class); // Spring AI handles JSON parsing
}
}
RAG Pipeline
@Configuration
public class RagConfig {
// No manual VectorStore bean — the spring-ai-starter-vector-store-pgvector
// starter auto-configures one. Just inject it. (The old `new PgVectorStore(...)`
// constructor is removed; if you must build one, use PgVectorStore.builder(...).)
@Bean
public ChatClient ragChatClient(ChatClient.Builder builder, VectorStore vectorStore) {
return builder
.defaultAdvisors(
QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder().topK(5).build()) // builder, not defaults().withTopK()
.build()
)
.build();
}
}
@Service
@RequiredArgsConstructor
public class KnowledgeService {
private final VectorStore vectorStore;
private final ChatClient ragChatClient;
// Ingest documents
public void ingest(List<String> documents) {
List<Document> docs = documents.stream()
.map(content -> new Document(content))
.toList();
vectorStore.add(docs);
}
// Query with RAG
public String ask(String question) {
return ragChatClient.prompt()
.user(question)
.call()
.content();
}
}
Streaming Responses
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> stream(@RequestParam String prompt) {
return chatClient.prompt()
.user(prompt)
.stream()
.content();
}
application.yml
spring:
ai:
anthropic:
api-key: ${ANTHROPIC_API_KEY}
chat:
# 2.0 flattened the properties — the old chat.options.* nesting is dead
model: ${ANTHROPIC_MODEL}
max-tokens: 2048
temperature: 0.7 # 2.0 removed the 0.7 default — set it explicitly if you rely on it
# OR for OpenAI:
openai:
api-key: ${OPENAI_API_KEY}
chat:
model: ${OPENAI_MODEL}
vectorstore:
pgvector:
initialize-schema: true
dimensions: 1536
Gotchas
- Agent uses Spring AI 1.x (
spring-ai-bom1.0.x) on Spring Boot 4 — 1.x targets Boot 3 only; Boot 4 requires Spring AI 2.0 - Agent uses pre-1.0 artifact names (
spring-ai-anthropic-spring-boot-starter) — the pattern isspring-ai-starter-model-anthropic - Agent configures
spring.ai.anthropic.chat.options.model— 2.0 flattened properties; drop the.optionssegment (spring.ai.anthropic.chat.model) - Agent passes built options to
.options(...)— 2.0 takes the builder:.options(AnthropicChatOptions.builder().maxTokens(2048)), no.build() - Agent writes
new MessageChatMemoryAdvisor(new InMemoryChatMemory())— both long removed; useMessageChatMemoryAdvisor.builder(chatMemory)+MessageWindowChatMemory - Agent omits the conversation id on a memory-advisor call — mandatory in 2.0 (
ChatMemory.DEFAULT_CONVERSATION_IDremoved); passa.param(ChatMemory.CONVERSATION_ID, ...)or getIllegalArgumentException - Agent uses
PromptChatMemoryAdvisor— removed in 2.0; useMessageChatMemoryAdvisor - Agent adds
spring-ai-advisors-vector-storeforQuestionAnswerAdvisor— renamed tospring-ai-vector-store-advisorin 2.0 - Agent writes
SearchRequest.defaults().withTopK(n)— useSearchRequest.builder().topK(n).build() - Agent hardcodes API keys — always use environment variables /
${...} - Agent hardcodes provider model IDs - configure them externally because model catalogs change
- Agent builds prompts with string concatenation — use
.param()template variables - Agent puts prompts inline in code — externalize to
src/main/resources/prompts/ - Agent ignores structured output — use
.entity(MyClass.class)instead of parsing manually - Agent uses
.entity(List.class)for a list — generics erase; passnew ParameterizedTypeReference<List<X>>() {} - Agent skips error handling for API calls — wrap in try/catch, handle
NonTransientAiException(don't retry) vsTransientAiException(retry) - Agent uses wrong model string — verify model names against provider docs