Роками вважалося, що 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 — простіший, доки масштабування чи межі команд не виправдають розділення.

Джерела

  1. Spring. Проєкт Spring AI і довідкова документація.
  2. Spring AI. ChatClient API.
  3. Spring AI. Tool calling.
  4. Spring AI. Vector databases.
  5. Документація LangChain4j.
  6. pgvector.