Интегрируем Kotlin сервис с AI чат-ботом с помощью Spring AI за 5 минут
Чат-боты с генеративным искусственным интеллектом получили широкую известность после релиза 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) } }
@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 не ограничивается представленными возможностями, это обширный проект, к которому регулярно выходят обновления. Получит ли он широкое распространение, неизвестно, но попробовать его в действии как минимум интересно, а, возможно, оно пригодиться не только для пет-проектов.
Исходники проекта вы можете посмотреть в репозитории