Мир Enterprise-разработки на Java/Kotlin и мир нейронных сетей кажутся параллельными вселенными. С одной стороны - статическая типизация, многопоточность, Spring-контейнеры, а с другой - Python-скрипты, тензорные операции и эксперименты в Jupyter Notebook. Между ними - пропасть, через которую многие команды не решаются перешагнуть.

Однако необходимость строить этот мост возникает всё чаще. Заказчик хочет «искусственный интеллект» в новом фиче, аналитики мечтают о реализации чат-бота  с преферансом и барышнями, а менеджеры слышали, что конкуренты уже всё автоматизировали. Как же совместить надежность и структуру JVM-проекта с гибкостью и мощью AI? В этой статье постараемся разобраться какие инструменты для этого есть на данный момент и как с ними работать.

Use-Cases

Давайте сперва попробуем разобраться с типовыми сценариями применения AI, которые сейчас чаще всего заказчики предлагают реализовать:

  • Сложные агенты с планированием и выполнением

  • Многошаговые рабочие процессы с состоянием

  • Диалоговые системы с памятью

  • Обработка с человеческим в вмешательством

  • Сложная маршрутизация и условная логика

Список можно продолжать и дальше, всё зависит от конкретной сферы применения. Но прежде чем открывать IDE, давайте ответим на простые, но критически важные вопросы:

  • Из каких элементов строится AI система?

  • Как мы будем с этим общаться из Java/Kotlin кода?

Составные элементы

Системы с использованием AI не являются какой-то «магией», как можно было бы себе представить разбираясь во многих маркетинговых материалах. Так же как и в Enterprise решениях есть уже сложившиеся элементы, с которыми нужно уметь работать:

  1. LLM (Большая языков��я модель) — ядро системы, выполняющее функции рассуждения и генерации. Именно эта компонента анализирует запросы, создает текст и принимает решения о вызове внешних инструментов.

  2. Memory (Память) — механизм для сохранения контекста между взаимодействиями. Позволяет агенту оперировать историей диалога и долгосрочными данными, выходящими за рамки одного промпта.

  3. Embeddings (Векторные представления) — технология трансформации текста (или иных данных) в числовые векторы. Это позволяет оценивать семантическое сходство и находить информацию не по точному совпадению, а по смысловой близости.

  4. Vector DB (Векторная база данных) — специализированное хранилище, оптимизированное для эффективного сохранения векторных эмбеддингов и выполнения над ними операций поиска ближайших соседей.

  5. State Graph (Граф состояний/потока) — структура, определяющая возможные пути выполнения задачи. Позволяет агенту планировать последовательность действий и осознанно переходить между этапами работы.

  6. Tools (Инструменты) — набор внешних сервисов и API, доступных агенту. Эти «инструменты» расширяют его возможности, позволяя взаимодействовать с реальными системами: выполнять запросы к БД, вызывать API и т.д.

  7. Docling (Парсер документов) — конвертер, задача которого — извлекать структурированный текст и смысл из файлов сложных форматов (например, PDF, DOCX), приводя их к виду, пригодному для обработки языковыми моделями.

  8. RAG (Извлечение + Генерация) — сквозной паттерн, объединяющий поиск и генерацию. Его суть: сначала извлекается релевантная информация из внешних источников, затем этот контекст передается в LLM для формирования точного и обоснованного ответа.

Пример построения AI-системы
Пример построения AI-системы

Общение с AI компонентами из Java/Kotlin

Теория это здорово, но что у нас есть в мире Java/Kotlin, чтобы со всем этим работать? Экосистема выглядит уже достаточно зрелой, чтобы можно было не изобретать свой велосипед. Давайте рассмотрим наиболее популярные решения, которые уже есть на данный момент:

  1. OpenAI Java API Library (https://github.com/openai/openai-java) - официальная библиотека для работы с OpenAI API. Предоставляет клиент для взаимодействия по REST API

  2. Spring AI (https://spring.io/projects/spring-ai) - реализация библиотеки взаимодействия c AI экосистемой от команды разработки Spring фреймворка. Реализует такой функционал как:

    - взаимодействие с AI в режиме чата

    - работа с эмбеддингами

    - работа с моделями AI генерирующие изображения

    - работа с моделями AI генерирующие аудио

    - работа с моделями для выявления оскорбительного или конфиденциального контента

    - расширенная регенерация ответа от модели AI (RAG)

    - работа с векторными базами данными

  3. Langchain4j (https://github.com/langchain4j/langchain4j) - это библиотека, которая была разработана на волне хайпа вокруг ChatGPT и в чью основу были заложены концепции из LangChain, Haystack, LlamaIndex и другие идее сообщества. Библиотека является достаточно низкоуровневым решением для работы с экосистемой AI.

  4. Koog - это фреймворк с открытым исходным кодом от JetBrains для создания агентов искусственного интеллекта с использованием Kotlin DSL

Возможно не совсем корректно ставить все эти решения в один список, так как они в разной степени реализуют функционал. Дальше мы рассмотрим конкретные примеры реализации компонентов на примере работы с библиотекой Langchain4j.

LLM

LLM является «мозгом» системы и позволяет генерировать текст ответа на отправленный пользовательский запрос или же принимать решение, какой сторонний сервис вызывать, чтобы получить дополнительную информацию или же выполнить необходимое действие.

Но перед тем как начать писать код, давай те развернём локально какую-то LLM, чтобы это можно было проверить работоспособность и заодно и разберёмся как это можно сделать. Для этого предлагаю использовать Ollama (https://ollama.com) - это инструмент с открытым исходным кодом для локального запуска, настройки и управления большими языковыми моделями. Скачиваем его и устанавливаем. Дальше переходим в консоль и выполняем следующее:

# Скачать модель
ollama pull deepseek-r1:latest

# Запустить
ollama run deepseek-r1:latest

Другие модели, которые можно попробовать можно найти в сообществе huggingface (https://huggingface.co)

Перейдём к написанию кода и приведём пример обращения к только что развернутой LLM DeepSeek-R1. Здесь и далее примеры будем приводить на Kotlin:

import dev.langchain4j.model.ollama.OllamaChatModel

fun main() {
    val model = OllamaChatModel.builder()
        .baseUrl("http://localhost:11434")
        .modelName("deepseek-r1:latest")
        .temperature(0.1) // для более детерминированных ответов
        .topP(0.9)
        .numPredict(256) // ограничение длины ответа
        .build()

    val answer = model.chat("What is your name?")
    println(answer) //Hi there! 😊 I'm DeepSeek-R1, your helpful AI assistant. You can also call me 小深 (if you prefer something a bit more friendly and Chinese-style 😄). How can I help you
}

Memory

По умолчанию LLM является stateless системой и каждый раз при отправке в неё запроса она ничего не знает о предыдущей истории переписки. Для хранения состояния необходимо предоставить подходящий механизм. В библиотеке Langchain4j для этого есть два класса TokenWindowChatMemory и MessageWindowChatMemory отличающиеся способом указания максимального размера.

import dev.langchain4j.memory.chat.MessageWindowChatMemory
import dev.langchain4j.model.ollama.OllamaChatModel
import dev.langchain4j.service.AiServices

internal interface Assistant {
    fun chat(message: String): String
}

fun main() {
    val model = OllamaChatModel.builder()
        .baseUrl("http://localhost:11434")
        .modelName("deepseek-r1:latest")
        .temperature(0.1) // для более детерминированных ответов
        .topP(0.9)
        .numPredict(256) // ограничение длины ответа
        .build()

    val chatMemory = MessageWindowChatMemory.withMaxMessages(10)

    val assistant = AiServices.builder(Assistant::class.java)
        .chatModel(model)
        .chatMemory(chatMemory)
        .build()

    val answer = assistant.chat("Hello! My name is Sergey.")
    println(answer) // Hello Sergey! 😊 It's nice to meet you. How can I assist you today?

    val answerWithName = assistant.chat("What is my name?")
    println(answerWithName) // Your name is Sergey! 😊
}

Embeddings

При работе LLM преобразует входящие запросы в своё внутреннее представление. Этим представлением является в вектора в многомерном пространстве, которое называется embedding. Это нужно для поиска по смыслу, в противовес поиску по ключевым словам. Текст с похожим смыслом имеет близкие вектора.

Для того, чтобы попробовать продемонстрировать работу с этим представлением установим модель nomic-embed-text, выполнив в консоли следующие команды:

ollama pull nomic-embed-text:latest
ollama run nomic-embed-text:latest " "

После этого мы сможем запросить у LLM внутреннее представление для запроса:

import dev.langchain4j.model.ollama.OllamaEmbeddingModel

fun main() {
    val embeddingModel = OllamaEmbeddingModel.builder()
        .baseUrl("http://localhost:11434")
        .modelName("nomic-embed-text:latest")
        .build()

    val response = embeddingModel.embed("Hello, how are you?")
    println(response) //Response { content = Embedding { vector = [-0.049178958, -0.06575274, -0.14865492, 0.0038396032, 0.042202424, 0.008886771, 0.0011453873, -0.006525421, 0.003989601...
}

Vector DB

Если LLM - это «мозг», способный рассуждать, то векторная база данных - это его долговременная, структурированная память с интеллектуальным поиском. В отличие от традиционных SQL/NoSQL-хранилищ, которые ищут по ключам или точным совпадениям, векторные БД оптимизированы для одной фундаментальной операции: поиска k ближайших соседей (k-NN) в многомерном векторном пространстве.

Именно эта особенность делает их незаменимыми в современных AI-архитектурах:

  • Расширение контекста (RAG): Мгновенная доставка релевантных документов в промпт LLM

  • Семантический поиск: Поиск продуктов, статей или ответов по смыслу, а не по ключевым словам

  • Кластеризация и рекомендации: Группировка схожего контента и поиск аналогов

  • Память агентов: Структурированное хранение истории взаимодействий для извлечения прошлого контекста

Без этого компонента ваш агент будет ограничен лишь общей эрудицией модели, не имея доступа к специфичным, свежим или приватным данным.

Вот список наиболее популярных векторных баз данных с краткой классификацией:

Название

Тип / Модель

Ключевая особенность

Идеальный use-case

Лицензия / Модель

Qdrant

Отдельный векторный движок

Высокая производительность, богатый API, написан на Rust. Есть облачная версия (Qdrant Cloud)

Высоконагруженные production-системы с низкой latency, RAG-системы

Open Source (Apache 2.0) / Cloud

Weaviate

Векторно-гибридная база

Гибридный поиск (векторный + ключевые слова). Встроенные модули для генерации векторов

Сложный семантический поиск в сочетании с фильтрацией по метаданным

Open Source (BSD-3) / Cloud

Chroma

Встроенная / Лёгкая

Простота и удобство для разработки, легко запустить локально. Отличный выбор для прототипов

Быстрый старт, эксперименты, локальная разработка RAG

Open Source (Apache 2.0)

Pinecone

Облачный сервис (SaaS)

Полностью управляемый сервис. Нет необходимости настраивать инфраструктуру

Команды, которые хотят сфокусироваться только на логике приложения, а не на инфраструктуре

Проприетарная / Только Cloud

Milvus

Распределённая система

Создана для масштабирования на огромные объёмы данных (миллиарды векторов)

Big Data в ML, крупные корпоративные хранилища эмбеддингов

Open Source (Apache 2.0) / Cloud

Давайте попробуем локально развернуть Qdrant и попробуем с ним поработать. Легко установить его можно в докере следующей командой:

docker run -d -p 6333:6333 -p 6334:6334 qdrant/qdrant:latest

Дальше подключаемся к векторной базе данных, добавляем в неё несколько эмбеддингов и пробуем осуществлять поиск по смыслу текста. В качестве результата выводим степень релевантности и сам найденный текст:

import dev.langchain4j.data.segment.TextSegment
import dev.langchain4j.model.ollama.OllamaEmbeddingModel
import dev.langchain4j.store.embedding.EmbeddingSearchRequest
import dev.langchain4j.store.embedding.qdrant.QdrantEmbeddingStore
import io.qdrant.client.QdrantClient
import io.qdrant.client.QdrantGrpcClient
import io.qdrant.client.grpc.Collections
import java.util.*

private const val qdrantHost = "localhost"
private const val qdrantPort = 6334
private val collectionName = "langchain4j-${UUID.randomUUID()}"
private val distance = Collections.Distance.Cosine
private const val dimension = 768L

fun main() {
    val embeddingModel = OllamaEmbeddingModel.builder()
        .baseUrl("http://localhost:11434")
        .modelName("nomic-embed-text:latest")
        .build()

    val embeddingStore = QdrantEmbeddingStore.builder()
            .host(qdrantHost)
            .port(qdrantPort)
            .collectionName(collectionName)
            .build()
    
    val grpcClient = QdrantGrpcClient.newBuilder(qdrantHost, qdrantPort, false).build()
    val client = QdrantClient(grpcClient)

    val vectorParams = Collections.VectorParams.newBuilder().setDistance(distance).setSize(dimension).build()
    client.createCollectionAsync(collectionName, vectorParams).get()

    embeddingStore.addEmbedding("Installation Guide for Debian 13", embeddingModel)
    embeddingStore.addEmbedding("Debian 13 Setup Manual", embeddingModel)
    embeddingStore.addEmbedding("Traditional Russian Olivier Salad Recipe", embeddingModel)
    embeddingStore.addEmbedding("Procedure for Installing Debian 13", embeddingModel)

    val queryEmbedding = embeddingModel.embed("How to Install Debian 13?").content()
    val embeddingSearchRequest = EmbeddingSearchRequest.builder()
        .queryEmbedding(queryEmbedding)
        .maxResults(1)
        .build()
    val matches = embeddingStore.search(embeddingSearchRequest).matches()
    val embeddingMatch = matches[0]

    println(embeddingMatch.score()) //0.9637723605018205
    println(embeddingMatch.embedded()!!.text()) //Debian 13 Setup Manual
}

private fun QdrantEmbeddingStore.addEmbedding(
    text: String,
    embeddingModel: OllamaEmbeddingModel,
) {
    val segment = TextSegment.from(text)
    val embedding = embeddingModel.embed(segment).content()
    this.add(embedding, segment)
}

State Graph

Стандартный диалог с ChatGPT - это линейный промптинг: один запрос → один ответ. Для сложных задач мы часто интуитивно выстраиваем цепочки: «Сначала сделай это, потом то, а затем на основе всего вышесказанного...» Именно эту интуицию формализуют и усиливают графы состояний.

Они превращают линейную цепочку в ветвящийся, управляемый процесс, где:

  • Есть чёткие этапы (состояния), которые можно тестировать и улучшать по отдельности.

  • Можно внедрять человеческий контроль (human-in-the-loop) на ключевых этапах.

  • Агент может «возвращаться» на предыдущие шаги при ошибках или новых данных.

  • Визуализация потока становится тривиальной задачей, понятной всей команде — от продакта до тестировщика.

Из наиболее популярных библиотек для работы с графом состояний в мире Java/Kotlin является LangGraph4j (https://github.com/langgraph4j/langgraph4j), созданная по образцу Python-библиотеки LangGraph.

В общем виде работа с LangGraph4j выглядит следующим образом: описываем структуру графа, задаём для каждого узла графа обработчики, компилируем граф, и запускаем на выполнение. Вот простой пример:

import org.bsc.langgraph4j.StateGraph
import org.bsc.langgraph4j.StateGraph.END
import org.bsc.langgraph4j.StateGraph.START
import org.bsc.langgraph4j.action.AsyncNodeAction.node_async
import org.bsc.langgraph4j.action.NodeAction
import org.bsc.langgraph4j.state.AgentState
import org.bsc.langgraph4j.state.Channels


class GreeterNode : NodeAction<SimpleState> {
    override fun apply(state: SimpleState): Map<String, Any> {
        println("GreeterNode executing. Current messages: " + state.messages())
        return mapOf(SimpleState.MESSAGES_KEY to "Hello from GreeterNode!")
    }
}

class ResponderNode : NodeAction<SimpleState> {
    override fun apply(state: SimpleState): Map<String, Any> {
        println("ResponderNode executing. Current messages: " + state.messages())
        return mapOf(SimpleState.MESSAGES_KEY to "Hello from ResponderNode!")
    }
}

class SimpleState(initData: Map<String, Any>) : AgentState(initData) {
    fun messages() = this.value<List<String>>(MESSAGES_KEY).orElse(emptyList())

    companion object {
        const val MESSAGES_KEY = "messages"
        val SCHEMA = mapOf(MESSAGES_KEY to Channels.appender { emptyList<String>() })
    }
}

fun main() {
    val greeterNode = GreeterNode()
    val responderNode = ResponderNode()

    // Определение структуры графа
    val stateGraph =
        StateGraph(SimpleState.SCHEMA) { initData -> SimpleState(initData) }
            .addNode("greeter", node_async(greeterNode))
            .addNode("responder", node_async(responderNode)) // Определение этапов
            .addEdge(START, "greeter") // Старт с ноды greeter
            .addEdge("greeter", "responder")
            .addEdge("responder", END)

    // Компиляция графа
    val compiledGraph = stateGraph.compile()

    // Запуск графа
    val graphInputs = mapOf(SimpleState.MESSAGES_KEY to "Let's, begin!")
    compiledGraph.stream(graphInputs).forEach { item ->
        println(item)
    }
}

У LangGraph4j есть интеграция с LangChain4j, которая позволяет задать обработчик для тестирования выполнения этапа:

class TestTool {
    @Tool("tool for test AI agent executor")
    fun execTest(@P("test message") message: String?): String {
        return "test tool ('$message') executed with result 'OK'"
    }

    @Tool("return current number of system thread allocated by application")
    fun threadCount(): Int {
        return Thread.getAllStackTraces().size
    }
}

А инициализация LLM и запуск выполнения графа:

val model = OllamaChatModel.builder()
        .modelName("deepseek-r1:latest")
        .baseUrl("http://localhost:11434")
        .supportedCapabilities(Capability.RESPONSE_FORMAT_JSON_SCHEMA)
        .logRequests(true)
        .logResponses(true)
        .maxRetries(2)
        .temperature(0.0)
        .build()

    val agent = AgentExecutor.builder()
        .chatModel(model)
        .toolsFromObject(TestTool())
        .build()
        .compile()

    agent.stream(mapOf("messages" to "perform test twice and return number of current active threads"))
        .forEach { item -> println(item) }

Docling

AI-модель, особенно LLM, работает с текстом. Чистым, структурированным, машиночитаемым текстом. Но в реальном мире 80% корпоративных знаний «заперты» в неудобных форматах: сканы договоров в PDF, презентации в PPTX, отчёты в DOCX и даже старые сканы в формате изображений. Если скормить такой файл языковой модели напрямую, в лучшем случае вы получите ошибку, в худшем — модель попытается «прочесть» двоичный код как текст, породив бессмыслицу.

 Парсер документов (Document Parser или «Docling») — это критически важный шлюз между хаосом реальных данных и аккуратным миром AI. Его задача — не просто извлечь сырые байты из файла, а интеллектуально преобразовать документ в семантически обогащённый текст: сохранить структуру (заголовки, списки, таблицы), вытащить текст из-под картинок (OCR), отбросить мусор (колонтитулы, номера страниц) и в итоге выдать чистый markdown или JSON, который станет качественным «топливом» для вашего AI-конвейера.

 Для экосистемы Java/Kotlin реализовано Api - Docling Java (https://github.com/docling-project/docling-java)

 Пример его использования ниже:

val doclingServeApi = DoclingServeApi.builder()
        .baseUrl("http://localhost:8000")
        .logRequests()
        .logResponses()
        .prettyPrint()
        .build()

    val request = ConvertDocumentRequest.builder()
        .source(
            HttpSource.builder()
                .url(URI.create("https://arxiv.org/pdf/2408.09869"))
                .build()
        )
        .build()

    val response = doclingServeApi.convertSource(request)
    println(response.getDocument().getMarkdownContent())

RAG

RAG (Retrieval-Augmented Generation) - Извлечение-Усиленная Генерация - это архитектурный паттерн для построения AI-систем, который комбинирует поиск (retrieval) информации из внешних источников знаний с генерацией (generation) ответов языковой моделью (LLM).

Вместо того чтобы полагаться только на внутренние знания LLM (которые могут быть устаревшими, неточными или слишком общими), RAG-система сначала ищет актуальные и релевантные данные в вашей собственной базе (документы, база знаний, CRM), а затем «подсказывает» эти данные LLM в качестве контекста для генерации точного и обоснованного ответа.

Ниже приведён пример реализации чата с LLM обогащённого дополнительной документацией:

import dev.langchain4j.data.document.Document
import dev.langchain4j.data.document.DocumentParser
import dev.langchain4j.data.document.loader.FileSystemDocumentLoader.loadDocument
import dev.langchain4j.data.document.parser.TextDocumentParser
import dev.langchain4j.data.document.splitter.DocumentSplitters
import dev.langchain4j.data.embedding.Embedding
import dev.langchain4j.data.segment.TextSegment
import dev.langchain4j.memory.ChatMemory
import dev.langchain4j.memory.chat.MessageWindowChatMemory
import dev.langchain4j.model.ollama.OllamaChatModel
import dev.langchain4j.model.ollama.OllamaEmbeddingModel
import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever
import dev.langchain4j.service.AiServices
import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore
import org.slf4j.LoggerFactory
import java.nio.file.Path
import java.util.*
import kotlin.io.path.Path

interface Assistant {
    fun answer(query: String): String
}

fun main() {
    // Создадим ассистента, знающего о дополнительном документе
    val assistant = createAssistant(Path("documents/Ubuntu_manual.txt"))

    // Начнём чат с ассистентом. Мы можем задавать вопросы вида:
    // - How install Ubuntu?
    // - How work with packages?
    assistant.startConversationWith()
}

private fun createAssistant(documentPath: Path): Assistant {
    // Создадим модель для чата
    val chatModel = OllamaChatModel.builder()
        .modelName("deepseek-r1:latest")
        .baseUrl("http://localhost:11434")
        .build()

    // Загрузим документ, который мы хотим использовать для RAG
    // Здесь загружаем только один документ, но можем загрузить произвольное количество
    val documentParser: DocumentParser = TextDocumentParser()
    val document: Document = loadDocument(documentPath, documentParser)

    // Теперь необходимо разбить документ на небольшие сегменты(чанки)
    // Это позволит загружать только релевантные сегменты в LLM
    // К примеру если пользователь спрашивает как работать с пакетами в Ubuntu, то мы можем отправить в LLM сегменты
    // касающиеся только этой темы
    val splitter = DocumentSplitters.recursive(300, 0)
    val segments = splitter.split(document)

    //Преобразуем серменты в эмбединги
    val embeddingModel = OllamaEmbeddingModel.builder()
        .baseUrl("http://localhost:11434")
        .modelName("nomic-embed-text:latest")
        .build()
    val embeddings: List<Embedding> = embeddingModel.embedAll(segments).content()

    // Сохранить эмбединги в хранилище
    val embeddingStore = InMemoryEmbeddingStore<TextSegment>()
    embeddingStore.addAll(embeddings, segments)

    // Средство получения контента отвечает за получение релевантного сегмента
    val contentRetriever = EmbeddingStoreContentRetriever.builder()
        .embeddingStore(embeddingStore)
        .embeddingModel(embeddingModel)
        .maxResults(2) // на каждой итерации получаем 2 актуальных сегмента
        .minScore(0.5) // Степень релевантности сегментов
        .build()


    // Используем память чата для хранения истории
    val chatMemory: ChatMemory = MessageWindowChatMemory.withMaxMessages(10)

    // Заключительный этап создания сервиса
    return AiServices.builder(Assistant::class.java)
        .chatModel(chatModel)
        .contentRetriever(contentRetriever)
        .chatMemory(chatMemory)
        .build()
}

fun Assistant.startConversationWith() {
    val log = LoggerFactory.getLogger(Assistant::class.java)
    Scanner(System.`in`).use { scanner ->
        while (true) {
            log.info("User: ")
            val userQuery = scanner.nextLine()

            if ("exit".equals(userQuery, ignoreCase = true)) {
                break
            }

            val agentAnswer = this.answer(userQuery)
            log.info("Assistant: " + agentAnswer)
        }
    }
}

Заключение

Первоначальный миф о AI как о чёрном ящике, в который «закидываешь задачу и получаешь гениальное решение», окончательно развеивается при ближайшем рассмотрении. На самом деле, вы строите архитектуру, где каждый компонент решает свою конкретную задачу:

  • LLM - не волшебник, а мощный, но ограниченный процессор текста

  • RAG - не самоцель, а паттерн доставки контекста, требующий настройки чанкирования и поиска

  • Векторная БД - не хранилище «интеллекта», а специализированный индекс для семантического поиска

  • Графы состояний - не про искусственный разум, а про управление бизнес-процессом

Ваша Java/Kotlin-экосистема — не препятствие, а фундамент. Вы можете:

  • Использовать проверенные практики DI, конфигурации, логирования

  • Интегрировать AI-компоненты как обычные клиенты (HTTP, gRPC) или библиотеки

  • Применять существующие паттерны отказоустойчивости, кэширования, балансировки

  • Строить гибридные системы, где AI решает только то, что действительно нужно

AI перестаёт быть экзотикой и становится ещё одним инструментом в арсенале разработчика. Сложность не в «волшебстве нейросетей», а в грамотной интеграции разнородных компонентов - задаче.