
Чат-боты с генеративным искусственным интеллектом получили широкую известность после релиза ChatGPT в ноябре 2022 года. Сейчас вряд ли найдётся человек из IT, который не слышал про данный инструмент от OpenAI. Именно он вызвал настоящий бум в данной сфере, вынудив конкурентов разрабатывать свои аналоги, чтобы побороться за место на рынке. Таким образом созданная лавина изменений затронула многие языки программирования. Не обошли они и Java-сообщество. Spring Framework, один из наиболее популярных Java фреймворков обзавёлся модулем Spring AI, который обещает упростить разработку приложений с функциями ИИ.
Давайте вместе взглянем на него в деле и опробуем на демо проекте. В данном гайде мы создадим и подключим Kotlin сервис к чат-боту всего за пять минут, используя Spring AI!
Spring AI — это экспериментальный проект, цель которого — упростить разработку приложений с искусственным интеллектом (либо интеграцию с такими приложениями). На момент написания статьи актуальной является версия 0.8.1-SNAPSHOT. В неё входят следующие части:
Embeddings API. В терминах ML эмбеддинг (embedding) означает векторное представление каких-либо данных. Эмбеддинги позволяют преобразовать информацию, которую понимаем мы, в информацию, которую понимает компьютер. В том же NLP они используются, чтобы компьютер мог анализировать и/или преобразовывать текст (переводить, извлекать смысл, перефразировать частично или полностью). Embeddings API позволяет преобразовывать текст в векторы.
Chat Completion API. Данное API используется для взаимодействия с AI чат-ботами. Уже есть клиенты для OpenAI, Ollama, Microsoft Azure, HuggingFace, Google Vertex, Amazon Bedrock.
Function API. У моделей OpenAI есть интересная фича — регистрация функций. Пользователь описывает, какие входные параметры принимает его функция и модель может вывести JSON, который можно в неё передать.Это упрощает составление запросов к модели и парсинг ответов. Модель по контексту будет понимать, что вы хотите и выдавать данные в необходимом формате. Function API позволяет работать с этой фичей — регистрировать функции, описывать условия вызова и т.д.
Image Generation API. API для взаимодействия с моделями, заточенными на генерирование изображений. Есть готовые клиенты для OpenAI (DALL·E) и Stability AI (Stable Diffusion).
Prompts. Промпт (prompt) — входной текст, на основе которого модель генерирует контент. В зависимости от того, с какой моделью вы взаимодействуете, будет менятся и структура промта. В Stable Diffusion, например, есть позитивный промпт (что мы хотим получить), и негативный промпт (что мы не хотим получить), а входной текст чаще всего представляет собой набор ключевых слов (masterpiece, best quality, photo и т.д.). А в ChatGPT промпт представлен как инструкция и ассоциируется с определённой ролью. (подробнее можно почитать тут). Модуль Prompts содержит классы для работы с промптами (шаблоны, интерфейсы, утилиты).
Output Parsers. Получаемые от модели данные нужно поместить в Java классы, для этого можно использовать готовые парсеры или создать свой на основе интерфейса OutputParser.
Vector Databases. Векторая база данных — это база данных, которая заточена под хранение данных в векторном представлении. Она нужна для хранения данных, которые потом будут использоваться AI моделью. Одной из ключевых особенностей векторной базы данных является поиск данных по сходству, когда мы можем найти в базе векторы, наиболее похожие на заданный. Этот поиск играет важную роль в рекомендательных системах, распознавании изображений, языковой обработке. В Spring AI для взаимодействия с векторными базами данных используется интерфейс VectorStore. В настоящий момент есть реализации данного интерфейса для Weaviate, Redis, PineCone, pgvector, Neo4j, Milvus, Chroma, Azure.
ETL Pipeline. ETL расшифровывается как "extract, transform, load" (извлечь, преобразовать, загрузить). Это процесс, который нужен для подготовки данных для BI-систем, датасетов для тренировки моделей ИИ, долгосрочного хранения и пр. Spring AI позволяет создавать ETL пайплайны для чтения данных в "сыром" виде, преобразования их в векторы и загрузка в векторную базу данных.
Generic Model API. Данный модуль включает в себя набор интерфейсов для взаимодействия с AI моделями. Основная цель — упростить и стандартизировать поддержку новых AI моделей, которые будут добавлять в Spring AI.
В рамках данной статьи, используя Spring AI, мы создадим простой Spring сервис для анализа настроения пользователя. Алгоритм его работы достаточно прост:

Для создания проекта будут использованы следующие инструменты:
Kotlin версии 1.9.22.
Gradle версии 8.2.
Spring (в том числе модуль Spring AI).
Ollama (инструмент для запуска языковых моделей). В данном примере я буду использовать модель OpenChat. Она поддерживает русский язык и даёт неплохой результат для моих скромных вычислительных ресурсов.
Postman (для тестирования).
Для начала потребуется создать наш Spring проект. Можно воспользоваться Spring Initializr или сделать это вручную. Не забудьте добавить зависимость для Spring AI, указанную ниже
implementation("org.springframework.ai:spring-ai-ollama-spring-boot-starter:0.8.1-SNAPSHOT")
Далее набросаем базовые классы.
Контроллер:
@RestController
class ChatController(
private val chatService: ChatService
) {
@PostMapping("/chat")
fun sendMessage(@RequestBody message: String): String {
return chatService.exchange(message)
}
}
Сервис:
@Service
class ChatService(
private val client : OllamaApiClient
) {
private val outputParser = BeanOutputParser(SentimentAnalysisResult::class.java)
fun exchange(message: String): String {
return client.sendMessage(message)
}
}
Далее создадим OllamaApiClient. В Spring AI уже есть реализация ChatClient для Ollama, поэтому будем использовать её для взаимодействия с моделью.
@Component
class OllamaApiClient(
properties: OllamaClientProperties
) {
private final var client = OllamaChatClient(OllamaApi(properties.url))
init {
val options = OllamaOptions.create()
options.model = properties.model
}
fun sendMessage(message: String): String {
return client.call(message)
}
}
OllamaClientProperties:
@ConfigurationProperties(prefix = "ai.ollama")
data class OllamaClientProperties(
val url: String,
val model: String
)
Адрес и имя модели указываем в application.yml.
После первоначальной настройки проверяем корректность указанных параметров и отправляем запрос на наш эндпоинт:

Теперь нам нужно попросить чат-бота оценивать настроение нашего сообщения. Для этого нужно добавлять дополнительную информацию к каждому запросу. Сделать это можно с помощью PromptTemplate:
private val promptTemplate = PromptTemplate("""
Какое настроение у следующего текста:'{text}'?
Оно позитивное, негативное или нейтральное?
Укажи степень уверенности с помощью числа от 0.0 до 1.0."
""")
fun exchange(message: String): String {
val prompt = promptTemplateRus.create(mapOf("text" to message))
val generation = client.sendMessage(prompt)
return generation.output.content.toString()
}
В зависимости от модели, которую вы используете, шаблон можно корректировать исходя из получаемых результатов.
Поскольку PromptTemplate возвращает объект типа Prompt, нам нужно изменить и метод sendMessage:
fun sendMessage(prompt: Prompt): Generation {
val response = client.call(prompt)
return response.result
}
Повторяем запрос и получаем такой результат:
Результат неплохой, но его можно сделать лучше. Сейчас наше API отдаёт ответ в виде простого текста. Такой результат придётся парсить, плюс, модель может перефразировать сообщение в зависимости от запроса, что делает это ещё неудобнее для использования. Поэтому настроим форматирование выводимых данных. Модифицируем наш код следующим образом:
Создадим data класс для маппинга данных:
data class SentimentAnalysisResult(
val text: String = "",
val sentiment: String = "",
val confidence: String = ""
)
Также добавим Output Parser чтобы сразу "упаковать" ответ в data класс:
private val promptTemplate = PromptTemplate("""
Какое настроение у следующего текста:'{text}'?
Оно позитивное, негативное или нейтральное?
Укажи степень уверенности с помощью числа от 0.0 до 1.0.{format}"
""")
private val outputParser = BeanOutputParser(SentimentAnalysisResult::class.java)
fun exchange(message: String): SentimentAnalysisResult {
val prompt = promptTemplate.create(mapOf("text" to message, "format" to outputParser.format))
val answer = client.sendMessage(prompt)
return outputParser.parse(answer.output.content)
}
Теперь наш запрос будет иметь следующий вид:
Давайте проверим, что наша программа правильно определяет заданное настроение:
Результат соответствует ожиданиям! Далее, в зависимости от вашего проекта, можно настраивать модель, менять API сервиса или шаблоны для запросов.
В завершающей части хотел бы рассказать про тюнинг модели. OllamaChatClient позволяет настроить широкий спектр параметров модели, вот некоторые из них:
temperature. Параметр креативности модели. Чем выше значение, тем ответы более необычные.
frequencyPenalty. "Наказание" модели за повторения. Меньше значение — больше одинаковых словесных конструкций и выражений, меньше уникальных слов.
presencePenalty. "Наказание" модели за использование одних и тех же токенов в сгенерированном тексте. Чем меньше число, тем чаще могут попадаться одни и те же слова.
topK. У модели есть определенное количество вариантов, как можно продолжить генерируемый текст. Данный параметр ограничивает количество доступных токенов для продолжения. Меньше число — меньше возможных опций.
topP. Ограничение кумулятивной вероятности возможных токенов. Работает схожим образом с topK. Есть набор возможных токенов, которыми модель может продолжить текст. У каждого токена есть определённая вероятность, что он будет следующим. Токены добавляются в некий пул возможных токенов, где их вероятности складываются. Когда вероятность превысит P, больше токены не будут добавляться и модель будет выбирать токен из добавленных.
Из-за того, что сообщение модели подстраивается под указанный формат, нам не сильно важна креативность модели, повторяет ли она одни и те же слова или нет, ведь мы этого не увидим. Единственный параметр, который сильно влияет на выводимые данных — это topP.
Опытным путём было определено, что высокий topP приводит к тому, что модель постоянно меняет уровень уверенности в своём ответе. Для наших целей это нежелательно (иначе проще просто генерировать случайное число самим). Поэтому topP стоит сделать 0.25 или ниже. По остальным параметрам выбор не принципиален и не повлияет на оценку текста (по крайней мере для выбранной мной модели).
Внесём итоговые правки в код.
application.yml
ai:
ollama:
url: "http://localhost:11434"
options:
model: "openchat"
temperature: 0.8f
top_k: 100
top_p: 0.25f
frequency_penalty: 0f
presence_penalty: 0f
Класс конфигурации
@ConfigurationProperties(prefix = "ai.ollama")
data class OllamaClientProperties(
val url: String,
val options: OllamaOptions
)
data class OllamaOptions(
val model: String,
val temperature: Float,
val topK: Int,
val topP: Float,
val frequencyPenalty: Float,
val presencePenalty: Float
)
Конструктор для OllamaApiClient
init {
val options = OllamaOptions.create()
options.model = properties.options.model
options.temperature = properties.options.temperature
options.topK = properties.options.topK
options.topP = properties.options.topP
options.frequencyPenalty = properties.options.frequencyPenalty
options.presencePenalty = properties.options.presencePenalty
client.withDefaultOptions(options)
}
Вот и подошёл к концу гайд. В нём мы рассмотрели базовые компоненты Spring AI, связанные с интеграцией AI чат-бота в ваше приложение. Конечно, Spring AI не ограничивается представленными возможностями, это обширный проект, к которому регулярно выходят обновления. Получит ли он широкое распространение, неизвестно, но попробовать его в действии как минимум интересно, а, возможно, оно пригодиться не только для пет-проектов.
Исходники проекта вы можете посмотреть в репозитории
Источники