2 марта 2025
Тэги: gradle, Kotlin, Spring Boot, yaml, нейросети, руководство, чат-боты.
Из этой статьи вы узнаете, как написать свой telegram-бот, который взаимодействует с нейросетью. Напишем его на Kotlin, причём таким образом, чтобы с нейросетью можно было вести диалог, т.е. рассмотрим, как сохранять контекст между сообщениями. Для взаимодействия с нейросетью будем использовать фреймворк Spring AI из эксосистемы Spring.
Spring AI при работе с нейросетями обеспечивает принципы проектирования экосистемы Spring, такие как переносимость и модульная конструкция. В Spring AI есть различные провайдеры к популярным нейросетям: DeepSeek, Google Vertex, Groq, Ollama и ChatGPT. Работу с последним мы и рассмотрим в данной статье. Однако надо заметить, что многие нейросети поддерживают унифицированный API, поэтому переход с одной нейросети на другую (например, на DeepSeek) благодаря Spring AI ограничится изменением пары параметров в конфиге.
Итак, зайдём на start.spring.io и создадим заготовку проекта. В качестве языка выбираем Kotlin, тип проекта – Gradle Kotlin. Версию Java можно выбрать 21 как последнюю long-term support на текущий момент.
Далее переходим к зависимостям. Нужный нам стартер называется spring-ai-openai-spring-boot-starter. Он позволяет настраивать подключение к ChatGPT в декларативном стиле. На момент написания статьи он ещё не имеет релизной версии, но скорее всего она появится в ближайшее время.
Также хочу отметить, что вместо spring-ai-openai-spring-boot-starter вы можете выполнять запросы к нейросетям с помощью обычного RestClient (если у вас в зависимостях есть spring-boot-starter-web) или WebClient (для spring-boot-starter-webflux). Просто кода вы напишете чуть больше.
В итоге в секции dependencies в файле build.gradle.kts у вас должны быть следующие зависимости:
Для подключения к ChatGPT нам потребуется Api-Key, который можно сгенерировать в личном кабинете https://platform.openai.com/api-keys. Однако в настоящее время для работы с ChatGPT по API нужно каким-то образом пополнить лицевой счёт и подключить VPN. Последнее, кстати, не обязательно, если вы будете разворачивать ваше приложение у хостера с автоматической поддержкой проксирования запросов. Например, dockhost.ru.
Теперь давайте зарегистрируем нового telegram-бота с помощью BotFather. От него нам надо получить botName (который без постфикса «_bot») и токен.
Все эти параметры надо прописать в настройках приложения в файле application.yml. С точки зрения безопасности все чувствительные данные будем подставлять через переменные окружения.
Также, если вы подключаетесь через какой-либо прокси-сервис ChatGPT, надо ещё прописать целевой хост в переменной CHATGPT_BASE_URL. Если же подключаетесь напрямую – этот параметр можно вообще не указывать и будет использовано значение по умолчанию.
Прежде чем написать сервис, следует отметить пару моментов по работе с нейросетью.
В ChatGPT (да и во многих других нейросетях) все сообщения могут быть трёх типов:
spring-ai-openai-spring-boot-starter представляет эти сообщения в виде классов SystemMessage, UserMessage и AssistantMessage с общим интерфейсом Message.
Второй момент заключается в том, что если нейросети отправлять по одному сообщению, то свой ответ она будет формировать только на основании этого сообщения. Так диалог выстроить не удастся. Тогда как ChatGPT помнит весь контекст беседы, когда мы общаемся с ним через веб-интерфейс? Ответ прост. На самом деле достаточно в каждом запросе отправлять всю переписку – тогда контекст будет сохраняться.
Теперь создадим обычный спринговый сервис, который будет обращаться к ChatGPT. В конструктор ему мы будем подставлять OpenAiChatModel – этот бин как раз предназначен для отправки запросов в нейросеть.
Теперь добавим метод sendMessages(), который будет отправлять всю историю нашей переписки в виде списка объектов Message.
Реализация этого метода довольно проста. Вызываем метод call() у бина chatModel. В параметрах передаём ему объект Prompt, в который подставляем список наших сообщений и с помощью OpenAiChatOptions.builder() указываем ряд параметров запроса:
В параметрах мы указываем целевую модель. Разные модели тарифицируются по-разному: более дорогие модели выдают более качественный результат и лучше удерживают общий контекст беседы. В этом примере я использую GPT 4.0.
Второй параметр запроса – «температура», которая имеет тип Double и может принимать значения от 0.0 до 2.0. Если вы хотите, чтобы нейросеть выдавала более креативные ответы – выставляйте температуру побольше. Это может иметь смысл если вы попросите ChatGPT придумать, например, название для нового проекта. Если же хотите получить максимально точный ответ, соответствующий правилам предметной области – ставьте температуру в 0. Это полезно для математических вычислений (хотя точность и не гарантирована на 100%).
Наконец, третий параметр запроса позволяет указать формат ответа. Здесь я указываю формат «простой текст», но можно также попросить выдавать ответы в json.
После вызова метода call() мы получим список объектов типа Generation. Каждый из них содержит поле text – это и есть текстовый ответ нейросети. По умолчанию список будет содержать всегда 1 элемент.
Если по каким-то причинам результат мы не получили, то, благодаря элвис-оператору котлина, возвращаем пустой список.
На этом разработка клиентской части ChatGPT завершена.
Нам нужно где-то сохранять все сообщения текущего диалога, причём отдельно для каждого пользователя telegram.
В идеале нужно создать две таблицы в БД:
- dialog, который будет связан с chatId (идентификатор пользователя в Telegram, имеет тип Long)
- message, каждая запись в которой будет связана с dialog отношением «один-ко-многим» через поле dialog_id.
Но взаимодействие с БД выходит за рамки данного гайда, поэтому не будем усложнять и обойдёмся без неё.
Вместо этого мы воспользуемся встроенным в Spring механизмом кеширования. В кеше будем накапливать все сообщения в разрезе текущего chatId, а при старте нового диалога будем сбрасывать этот кеш.
Реализация компонента MessageCache довольно тривиальна. Она содержит три метода для чтения, обновления и сброса кеша соответственно:
Аннотация @Cacheable содержит параметры cacheNames (имя кеша) и key (ключ, по которому извлекаются данные из кеша). У всех трёх методов одинаковые имя кеша и ключа (chatId).
Метод getOrInitMessages() вызывается в том случае, если в кеше ещё нет сущности с указанным chatId. Это происходит при начале нового диалога, поэтому возвращаем пустой список. Последующие вызовы этого же метода будут возвращать данные из кеша по указанному ключу и реальный вызов метода выполняться не будет.
Аннотация @CachePut обновляет содержимое кеша. Мы будем передавать туда каждый раз полный актуальный набор сообщений из диалога. И эти же сообщения возвращаем.
Таким образом, поочерёдный вызов методов с аннотациями @Cacheable и @CachePut обеспечивает нам накопление сообщений для пользователя.
Аннотация @CacheEvict удаляет все сообщения с указанным ключом. Метод с такой аннотацией будем вызывать при начале новой беседы с нейросетью.
В telegram можно отправлять чат-боту специальные команды. Это фиксированная строка, которая начинается со слеша. Любой telegram-бот должен поддерживать команду «/start», т.к. именно эта команда автоматически выполняется при первом обращении к боту. Однако никто не мешает вызывать её повторно любое количество раз.
В нашем чат-боте эта команда будет начинать новый диалог. То есть когда захотите поменять тему – просто отправьте чат-боту команду «/start», чтобы он «забыл» всё, о чём вы говорили до этого.
Расширение telegrambotsextensions добавляет к стандартному компоненту возможность добавлять команды независимо друг от друга в виде отдельных бинов. Подтянем в нашу команду бин MessageCache и будем сбрасывать историю сообщений.
При выполнении этой команды telegram-бот отправляет пользователю сообщение «Начинаем диалог!» с помощью absSender.execute(). Создание сообщения происходит через утилитарный метод createMessage():
Здесь создаём стандартный объект SendMessage из telegrambots-spring-boot-starter и передаём ему chatId и текст сообщения. Также активируем формат сообщений markdown и отключаем превью гиперссылок (это по желанию, чтобы не загромождать переписку).
Взаимодействие с серверами telegram возможно в двух режимах: webhook и long-polling.
В режиме webhook вы разворачиваете полноценное веб-приложение, к которому будет обращаться сам telegram. Однако обязательным требованием является наличие публичного домена и сертификата https.
В случае с long-polling общедоступная точка доступа не требуется, т.к. приложение само делает запрос к серверу telegram и ждёт ответа до тех пор, пока не произойдёт какое-то событие, связанное с ботом (например, получение сообщения от пользователя). В таком режим вы можете поднимать telegram-бота откуда угодно, в том числе локально на вашем устройстве в целях отладки.
С точки зрения простоты мы будем использовать long-polling. Для этого создадим новый сервис и унаследуем его от класса TelegramLongPollingCommandBot. Также в этот сервис подтянем созданные ранее бины OpenAiService, MessageCache и все команды в виде множества Set
В секции init мы регистрируем все подгруженные команды бота с помощью метода registerAll().
Ну а теперь осталось определить ключевой метод processNonCommandUpdate() нашего telegram-бота. Он обрабатывает все сообщения, которые «не-команды», т.е. не начинаются со слеша. Метод получает объект Update. Для него мы сначала проверяем, содержит ли он сообщение. Если да – можем извлечь из него chatId отправителя. Далее проверяем, является ли сообщение текстом (а не картинкой, стикером, файлом и т.д.). И после этого мы получаем доступ к тексту сообщения, которое отправил пользователь через telegram-бот.
При получении сообщения от пользователя мы извлекаем другие сообщения из кеша, чтобы «вспомнить» контекст диалога. При первом обращении диалог будет пустой, т.к. в кеше ничего нет. Добавляем к сообщениям из кеша новое сообщение от пользователя как объект UserMessage. И затем этот список отправляем в ChatGPT.
В ответ мы получаем список сообщений AssistantMessage. В нашем случае там будет всегда один элемент. Добавляем эти сообщения к списку сообщений, обновляем кеш и отправляем пользователю через telegram только последние сообщения от нейросети.
В общем-то это вся бизнес-логика нашего telegram-бота.
Чтобы Spring корректно подключился к telegram и зарегистрировал бота при старте приложения, нам нужно ещё добавить небольшую конфигурацию:
А чтобы включить механизм-кеширования, нужно добавить аннотацию @EnableCaching к main-классу приложения (который помечен @SpringBootApplication):
Если этого не сделать, ошибки при старте вы не получите, но кеширование работать не будет.
Теперь открываем чат-бот в Telegram и наслаждаемся беседой!
Например, можно сыграть в города с нейросетью:
Здесь очень хорошо демонстрируется сохранение контекста. Кроме того, нейросеть знает правила игры и вежливо поправляет меня, когда я хочу смухлевать.
Помимо игр нейросеть можно также заставить проводить вычисления. Но здесь очень важно выбирать правильные промты и чётко определять нужный формат ответа.
Мы убедились, что экосистема Spring уже сейчас позволяет легко интегрироваться с ChatGPT и Telegram. Такая связка из двух популярных платформ открывает вам поистине широкую область для творчества и для решения рутинных задач. Однако стандартный компонент для работы с нейросетями явно не поспевает, т.к. новые нейросети появляются чуть ли не каждый месяц, а компонент до сих пор не имеет стабильной версии. Будем надеяться, в ближайшее время она появится.
Пример проекта доступен на github и полностью готов к деплою.
А если хотите развернуть приложение в облаке буквально в один клик – воспользуйтесь услугами dockhost.ru. Более подробно читайте в статье Как быстро развернуть Spring Boot в облаке.
Kotlin, Java, Spring, Spring Boot, Spring Data, SQL, PostgreSQL, Oracle, H2, Linux, Hibernate, Collections, Stream API, многопоточность, чат-боты, нейросети, файлы, devops, Docker, Nginx, Apache, maven, gradle, JUnit, YouTube, новости, руководство, ООП, алгоритмы, головоломки, rest, GraphQL, Excel, XML, json, yaml.