Роками вважалося, що AI-функції пишуть на Python, а з Java викликають через HTTP. Для багатьох enterprise-команд це означало ще один сервіс, ще один пайплайн деплою і ще одну мову в підтримці. Spring AI це змінює: він приносить доступ до моделей, пошук, виклик інструментів і спостережуваність у модель програмування Spring Boot — з автоконфігурацією, впровадженням залежностей і підтримкою тестування, які Java-команди вже знають. У гайді показуємо, як ми будуємо AI-функції на Spring AI в продакшені, з прикладами на Kotlin (версії на Java майже ідентичні).
Що таке Spring AI
Spring AI — проєкт Spring, що надає переносні абстракції для AI-провайдерів і будівельні блоки LLM-застосунків (довідкова документація):
- ChatClient — fluent API для промптів, системних повідомлень, опцій, стрімінгу й структурованого виводу.
- Абстракції моделей для чату, ембеддингів, зображень і аудіо від різних провайдерів: Anthropic, OpenAI, Azure OpenAI, Amazon Bedrock, Google Vertex AI, Mistral, Ollama та інших.
- Advisors — перехоплювачі навколо викликів моделі для пам'яті, пошуку, логування й запобіжників.
- Абстракція VectorStore з реалізаціями для PGvector, Qdrant, Weaviate, Milvus, Redis, Elasticsearch тощо.
- Виклик інструментів через анотовані методи.
- Спостережуваність через Micrometer, а також помічники для оцінювання й підтримка MCP-клієнта і сервера.
Цінність не в магії, а в узгодженості. Перехід від одного провайдера моделей до іншого чи від хмарної моделі до власної через Ollama або vLLM стає зміною конфігурації.
Налаштування
Використовуйте BOM Spring AI і стартер для свого провайдера та векторного сховища:
// build.gradle.kts
dependencies {
implementation(platform("org.springframework.ai:spring-ai-bom:1.0.0")) // беріть актуальний реліз
implementation("org.springframework.ai:spring-ai-starter-model-anthropic")
implementation("org.springframework.ai:spring-ai-starter-vector-store-pgvector")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-actuator")
}
# application.yml
spring:
ai:
anthropic:
api-key: ${ANTHROPIC_API_KEY}
chat:
options:
model: ${CHAT_MODEL}
max-tokens: 1024
vectorstore:
pgvector:
initialize-schema: false # схемою керуйте через Flyway/Liquibase
dimensions: 1024
index-type: HNSW
distance-type: COSINE_DISTANCE
Тримайте назву моделі в конфігурації, щоб змінювати модель для різних середовищ без змін коду; див. як обрати LLM.
Основи ChatClient
@RestController
@RequestMapping("/api/assistant")
class AssistantController(builder: ChatClient.Builder) {
private val chat = builder
.defaultSystem(
"""
You are an internal assistant for ACME's sales team.
Answer in the user's language. If you are not sure, say so.
""".trimIndent(),
)
.build()
@PostMapping("/ask")
fun ask(@RequestBody req: AskRequest): String =
chat.prompt()
.user(req.question)
.call()
.content() ?: ""
}
ChatClient.Builder автоматично конфігурується для провайдера в classpath. Створюйте окремий клієнт для кожного сценарію з власними налаштуваннями за замовчуванням — системним промптом, advisors, інструментами, — а не один глобальний. Продакшен-промпти заслуговують тієї самої ретельності, що й код; див. промпт-інженерія для розробників.
Структурований вивід у data-класи Kotlin
Spring AI вміє відображати відповіді безпосередньо в типізовані об'єкти, генеруючи інструкції щодо формату та JSON-схему з цільового класу:
data class LeadQualification(
val companyName: String?,
val budgetEur: Int?,
val timeline: Timeline,
val fitScore: Int, // 1..5
val reasoning: String,
)
enum class Timeline { NOW, THIS_QUARTER, LATER, UNKNOWN }
fun qualify(emailText: String): LeadQualification =
chat.prompt()
.user { it.text("Qualify this inbound lead email:\n<email>{email}</email>").param("email", emailText) }
.call()
.entity(LeadQualification::class.java)
?: error("Model returned no result")
Використовуйте nullable-поля й значення enum UNKNOWN для інформації, якої може не бути, і перевіряйте бізнес-правила після відображення (Bean Validation добре для цього підходить). Якщо провайдер підтримує нативний вивід з обмеженням схемою, вмикайте його. Загальні принципи — у статті структурований вивід LLM.
RAG з advisors і pgvector
Advisors обгортають виклики моделі додатковою поведінкою. Два найкорисніші — пам'ять чату й пошук:
@Configuration
class AssistantConfig {
@Bean
fun supportChat(builder: ChatClient.Builder, vectorStore: VectorStore, chatMemory: ChatMemory): ChatClient =
builder
.defaultSystem(SUPPORT_SYSTEM_PROMPT)
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(chatMemory).build(),
QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder().topK(6).similarityThreshold(0.5).build())
.build(),
)
.build()
}
@Service
class SupportAssistant(private val supportChat: ChatClient) {
fun answer(userId: Long, conversationId: String, question: String): String =
supportChat.prompt()
.user(question)
.advisors { it.param(ChatMemory.CONVERSATION_ID, "$userId:$conversationId") }
.call()
.content() ?: ""
}
Завантаження даних використовує ETL-подібні DocumentReader, TextSplitter і VectorStore.add() зі Spring AI. Для продакшен-якості виходьте за межі налаштувань за замовчуванням: чанкінг з урахуванням структури, метадані для фільтрації за орендарем і рівнем доступу, гібридний пошук, переранжування. Саме ці рішення важать найбільше; див. RAG для бізнесу, ембеддинги та чанкінг і порівняння векторних баз даних. PGvector тримає вектори в тому самому PostgreSQL, який ви вже бекапите й моніторите.
Контроль доступу: застосовуйте фільтри метаданих на основі автентифікованого користувача в SearchRequest (filter expression) і ніколи не покладайтеся на промпт, щоб приховати документи.
Виклик інструментів
Позначте методи анотацією @Tool і передайте екземпляри в промпт:
class OrderTools(private val orders: OrderService, private val customerId: Long) {
@Tool(description = "Get status, items and delivery estimate of one order of the current customer.")
fun getOrderStatus(
@ToolParam(description = "Order number, digits only, e.g. 482193") orderNumber: String,
): OrderStatusDto =
orders.findForCustomer(customerId, orderNumber)
?.toDto()
?: OrderStatusDto.notFound(orderNumber)
}
fun answerWithTools(customerId: Long, question: String): String =
chat.prompt()
.user(question)
.tools(OrderTools(orderService, customerId))
.call()
.content() ?: ""
Ідентифікатор клієнта береться з контексту безпеки, а не від моделі. Повертайте компактні DTO замість JPA-сутностей — ліниві зв'язки й сутності на 40 полів марнують токени й розкривають зайві дані. Принципи дизайну інструментів — у статті проєктування інструментів для LLM-агентів, модель безпеки — у статті безпека AI-агентів.
Щоб відкрити ваші Spring-сервіси зовнішнім AI-клієнтам — Claude Desktop, асистентам IDE, іншим агентам, — Spring AI також підтримує MCP-сервери; див. MCP для бізнесу.
Стрімінг
Для чат-інтерфейсів стрімте токени як server-sent events:
@GetMapping("/stream", produces = [MediaType.TEXT_EVENT_STREAM_VALUE])
fun stream(@RequestParam q: String): Flux<String> =
chat.prompt().user(q).stream().content()
Це працює і в Spring MVC, і у WebFlux. Перевірте, що reverse proxy не буферизує відповідь. На фронтенді підійде будь-який SSE-клієнт; для React/Next.js див. AI-чат на Next.js.
Конкурентність: блокувальні виклики й віртуальні потоки
Виклики LLM — це повільний I/O: секунди, а не мілісекунди. У класичних серверах «потік на запит» сплеск AI-запитів може вичерпати пул потоків. На Java 21+ зі Spring Boot 3.2+ увімкнення віртуальних потоків (spring.threads.virtual.enabled=true) дозволяє масштабувати блокувальний код із call() без переписування на реактивний стиль. Стежте за pinning у synchronized-блоках і за лімітами downstream-ресурсів; наш продакшен-досвід — у статті віртуальні потоки у Spring Boot.
Також додайте таймаути, повторні спроби з backoff для відповідей 429/5xx і фолбеки між провайдерами — допомагають Spring Retry і налаштування повторів на рівні провайдера в Spring AI; див. надійність LLM API.
Спостережуваність
Spring AI генерує observations Micrometer для викликів ChatClient, запитів до моделі, advisors, викликів інструментів і операцій векторного сховища, включно з використанням токенів. Зі Spring Boot Actuator і експортером OpenTelemetry вони з'являються як метрики й траси поруч з рештою застосунку:
- Відстежуйте використання токенів на ендпоінт і на орендаря для контролю витрат; див. оптимізація витрат на LLM.
- Трасуйте RAG-запити наскрізно: затримку пошуку, кількість документів, затримку моделі.
- Свідомо ставтеся до логування вмісту промптів і відповідей — з міркувань приватності воно часто вимкнене за замовчуванням; вмикайте лише там, де дозволяє політика.
Ширший підхід описаний у статтях спостережуваність LLM і OpenTelemetry.
Тестування
- Юніт-тести: мокайте
ChatModelабо обгорніть використання ChatClient власним інтерфейсом для детермінованих тестів навколишньої логіки, інструментів і перевірок прав. - Інтеграційні тести: Testcontainers для PostgreSQL з pgvector і, якщо хостите самі, Ollama з малою моделлю.
- Оцінювання: Spring AI містить оцінювачі на кшталт перевірки релевантності й фактів, що використовують модель як суддю. Розглядайте їх як частину набору evals з еталонним датасетом, що запускається в CI на зміни промптів чи моделей; див. evals для LLM.
Spring AI чи LangChain4j
LangChain4j — інший основний варіант для JVM. Обидва надійні:
| Аспект | Spring AI | LangChain4j |
|---|---|---|
| Стиль інтеграції | Автоконфігурація Spring Boot, ідіоми Spring | Незалежний від фреймворку; інтеграції для Spring, Quarkus, Micronaut |
| Високорівневий API | ChatClient + advisors | AI Services (анотовані інтерфейси) |
| Спостережуваність | Вбудований Micrometer | Listeners; інтеграції різняться |
| Екосистема | Частина портфеля Spring | Велика спільнота, багато інтеграцій |
Для команд, що вже на Spring Boot, Spring AI — природний вибір за замовчуванням. Для Quarkus чи бібліотек, незалежних від фреймворку, краще підходить LangChain4j.
FAQ
Чи готовий Spring AI до продакшену? Так, з релізу 1.0 GA у 2025 році. Як і з будь-якою бібліотекою, що швидко розвивається, фіксуйте версії, читайте release notes і тримайте інтеграційні тести.
Чи можна використовувати Spring AI із self-hosted моделями? Так, через стартер Ollama або стартер OpenAI, спрямований на OpenAI-сумісний сервер на кшталт vLLM.
Чи працює він з корутинами Kotlin?
Блокувальний API чудово працює на віртуальних потоках; стрімінговий API повертає Reactor Flux, який перетворюється на Kotlin Flow через asFlow().
Чи виносити AI-логіку в окремий мікросервіс? Не за замовчуванням. Модуль усередині наявного сервісу — наприклад, в архітектурі Spring Modulith — простіший, доки масштабування чи межі команд не виправдають розділення.
Джерела
- Spring. Проєкт Spring AI і довідкова документація.
- Spring AI. ChatClient API.
- Spring AI. Tool calling.
- Spring AI. Vector databases.
- Документація LangChain4j.
- pgvector.