Kotlin
March 5

Интегрируем 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)
    }
}

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 не ограничивается представленными возможностями, это обширный проект, к которому регулярно выходят обновления. Получит ли он широкое распространение, неизвестно, но попробовать его в действии как минимум интересно, а, возможно, оно пригодиться не только для пет-проектов.

Исходники проекта вы можете посмотреть в репозитории

Источники

Документация Spring AI
Ollama
Про настройку модели

Источник