17 июня 2025
Тэги: gradle, Kotlin, Spring AI, Spring Boot, yaml, нейросети, руководство, чат-боты.
Из этой статьи вы узнаете, как написать свой telegram-бот, который взаимодействует с нейросетью. Напишем его на Kotlin, причём таким образом, чтобы с нейросетью можно было вести диалог, т.е. рассмотрим, как сохранять контекст между сообщениями. Для взаимодействия с нейросетью будем использовать фреймворк Spring AI из эксосистемы Spring.
Spring AI при работе с нейросетями обеспечивает принципы проектирования экосистемы Spring, такие как переносимость и модульная конструкция. В Spring AI есть различные провайдеры к популярным нейросетям: DeepSeek, Google Vertex, Groq, Ollama и OpenAI. Работу с последним мы и рассмотрим в данной статье. Однако надо заметить, что многие нейросети поддерживают унифицированный API, поэтому переход с одной нейросети на другую (например, на DeepSeek) благодаря Spring AI ограничится изменением пары параметров в конфиге.
Итак, зайдём на start.spring.io и создадим заготовку проекта. В качестве языка выбираем Kotlin, тип проекта – Gradle Kotlin. Версию Java можно выбрать 21 как последнюю long-term support на текущий момент.
Далее переходим к зависимостям. Нужный нам стартер называется spring-ai-starter-model-openai. Он позволяет настраивать подключение к OpenAI в декларативном стиле. На момент написания статьи он получил первую стабильную версию 1.0.0.
Также хочу отметить, что в качестве альтернативы вместо spring-ai-starter-model-openai вы можете выполнять запросы к нейросетям напрямую с помощью обычного RestClient (если у вас в зависимостях есть spring-boot-starter-web) или WebClient (для spring-boot-starter-webflux). Просто кода вы напишете заметно больше.
В итоге в секции dependencies в файле build.gradle.kts у вас должны быть следующие зависимости:
Для подключения к OpenAI нам потребуется Api-Key, который можно сгенерировать в личном кабинете https://platform.openai.com/api-keys. Однако в настоящее время для работы с OpenAI по API из России нужно каким-то образом пополнить лицевой счёт и подключить VPN. Последнее, кстати, не обязательно, если вы будете разворачивать ваше приложение у хостера с автоматической поддержкой проксирования запросов. Например, dockhost.ru.
Теперь давайте зарегистрируем нового telegram-бота с помощью BotFather. От него нам надо получить botName (который без постфикса «_bot») и токен.
Все эти параметры надо прописать в настройках приложения в файле application.yml. С точки зрения безопасности все чувствительные данные будем подставлять через переменные окружения.
Также, если вы подключаетесь через какой-либо прокси-сервис OpenAI, надо ещё прописать целевой хост в переменной OPEN_AI_BASE_URL. Если же подключаетесь напрямую – этот параметр можно вообще не указывать и будет использовано значение по умолчанию.
Прежде чем написать сервис, следует отметить пару моментов по работе с нейросетью.
В OpenAI (да и во многих других нейросетях с совместимым протоколом) все сообщения могут быть четырёх типов:
spring-ai-starter-model-openai представляет эти сообщения в виде классов SystemMessage, UserMessage, AssistantMessage и ToolResponseMessage с общим интерфейсом Message.
Второй момент заключается в том, что если нейросети отправлять по одному сообщению, то свой ответ она будет формировать только на основании этого сообщения. Так диалог выстроить не удастся. Как же тогда ChatGPT помнит весь контекст беседы, когда мы общаемся с ним через веб-интерфейс? Ответ прост. На самом деле достаточно в каждом запросе отправлять всю переписку – тогда контекст беседы будет сохраняться.
Для этих целей используется бин chatMemory. Для него доступна одна реализация – MessageWindowChatMemory. Она сохраняет последние N сообщений в контексте. Сами сообщения сохраняются с помощью бина ChatMemoryRepository. Он, в свою очередь, имеет реализацию InMemoryChatMemoryRepository, т.е. сообщения хранятся в оперативной памяти и будут утеряны при перезапуске приложения. «Под капотом» используется обычный ConcurrentHashMap.
По умолчанию chatMemory будет хранить в оперативной памяти последние 20 сообщений для каждого диалога. Но это поведение можно изменить, переопределив бин chatMemory в явном виде:
При необходимости Вы можете использовать другую реализацию репозитория и хранить сообщения, например, в БД.
Создадим конфигурационный бин, в котором пропишем общие параметры подключения к OpenAI.
Здесь мы определяем бин chatClient. В параметрах к нему подтягиваем бины ChatClient.Builder и ChatMemory. Через билдер указываем пару общих advisor'ов: PromptChatMemoryAdvisor (для подключения chatMemory) и SimpleLoggerAdvisor (для логирования запроса и ответа при взаимодействии с нейросетью). Чтобы логирование работало корректно, в application.yml надо также прописать следующий уровень логирования:
Вообще есть два способа передачи сообщений из chatMemory: каждый раз аккумулировать их в первом сообщении (PromptChatMemoryAdvisor) или же передавать как есть в виде набора отдельных сообщений (MessageChatMemoryAdvisor). Тут надо выбирать, исходя из ваших потребностей, но PromptChatMemoryAdvisor в логах отображается более компактно, а потому удобнее для отладки:
Здесь мы кстати можем видеть, что помимо обвязки из классов-абстракций, в Spring AI также зашиты типовые промты (правда, на английском).
Теперь создадим обычный спринговый сервис, который будет обращаться к OpenAI. В конструктор мы будем подставлять бин ChatClient, который только что сконфигурировали.
Теперь добавим метод processUserMessage(), который будет принимать сообщение от пользователя и возвращать ответ от нейросети. В качестве параметра также будем передавать chatId – уникальный идентификатор пользователя в telegram.
Внутри метода указываем, что мы хотим получать ответы в виде простого текста (можно также использовать json):
Помимо формата с помощью ChatOptions указываем также целевую модель (OpenAiApi.ChatModel) и «температуру»:
Разные модели тарифицируются по-разному: более дорогие модели выдают более качественный результат и лучше удерживают общий контекст беседы. В этом примере я использую GPT 4.1.
Второй параметр запроса – «температура», которая имеет тип Double и может принимать значения от 0.0 до 2.0. Если вы хотите, чтобы нейросеть выдавала более креативные ответы – выставляйте температуру побольше. Это может иметь смысл если вы попросите ChatGPT придумать, например, название для нового проекта. Если же хотите получить максимально точный ответ, соответствующий правилам предметной области – ставьте температуру в 0. Это полезно для математических вычислений (хотя точность и не гарантирована на 100%).
Ну и, наконец, выполняем сам запрос с помощью chatClient.
Здесь мы указываем системный промт и опции, указанные выше. Также не забываем указывать chatId в качестве параметра CONVERSATION_ID для сохранения контекста беседы. Затем передаём сообщение от пользователя, вызываем метод call() и получаем ответ. Если по каким-то причинам результат мы не получили, то, благодаря элвис-оператору котлина, сообщаем об этом.
Как известно, написание системных промтов – отдельный вид искусства, выходящий за рамки данной статьи. Но я написал примерно следующее:
На этом разработка клиентской части для взаимодействия с OpenAI завершена.
В telegram можно отправлять чат-боту специальные команды. Это фиксированная строка, которая начинается со слеша. Любой telegram-бот должен поддерживать команду «/start», т.к. именно эта команда автоматически выполняется при первом обращении к боту. Однако никто не мешает вызывать её повторно любое количество раз.
В нашем чат-боте эта команда будет очищать chatMemory и начинать новый диалог. Когда захотите поменять тему – просто отправьте чат-боту команду «/start», чтобы он «забыл» всё, о чём вы говорили до этого.
Расширение telegrambotsextensions позволяет добавлять команды независимо друг от друга в виде отдельных бинов.
Подтянем в нашу команду бин ChatMemory и будем сбрасывать историю сообщений для текущего chatId с помощью метода clear().
При выполнении этой команды 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 и все команды в виде
множества Set
В секции init мы регистрируем все подгруженные команды бота с помощью метода registerAll().
Ну а теперь осталось определить ключевой метод processNonCommandUpdate() нашего telegram-бота. Он обрабатывает все сообщения, которые «не-команды», т.е. не начинаются со слеша. Метод получает объект Update.
Для него мы сначала проверяем, содержит ли он сообщение. Если да – можем извлечь из него chatId отправителя. Далее проверяем, является ли сообщение текстом (а не картинкой, стикером, файлом и т.д.). И после этого мы получаем доступ к тексту сообщения, которое отправил пользователь через telegram-бот. Затем отправляем нейросети сообщение от пользователя с помощью метода processUserMessage().
В ответ мы получаем сообщение assistantMessage, которое возвращаем пользователю через telegram с помощью метода execute().
В общем-то это вся бизнес-логика нашего telegram-бота.
Чтобы Spring корректно подключился к telegram и зарегистрировал бота при старте приложения, нам нужно ещё добавить небольшую конфигурацию:
Теперь открываем чат-бот в Telegram и наслаждаемся беседой!
Например, можно сыграть в города с нейросетью:
Здесь очень хорошо демонстрируется сохранение контекста. Кроме того, нейросеть знает правила игры и вежливо поправляет меня, когда я хочу смухлевать.
Помимо игр нейросеть можно также заставить проводить вычисления. Но здесь очень важно выбирать правильные промты и чётко определять нужный формат ответа.
Мы убедились, что экосистема Spring уже сейчас позволяет легко интегрироваться с OpenAI и Telegram. Такая связка из двух популярных платформ открывает поистине широкую область для творчества и решения рутинных задач.
Пример проекта доступен на github и полностью готов к деплою.
А если хотите развернуть приложение в облаке буквально в один клик – воспользуйтесь услугами dockhost.ru. Более подробно читайте в статье Как быстро развернуть Spring Boot в облаке.
Kotlin, Java, Spring, Spring Boot, Spring Data, Spring AI, SQL, PostgreSQL, Oracle, H2, Linux, Hibernate, Collections, Stream API, многопоточность, чат-боты, нейросети, файлы, devops, Docker, Nginx, Apache, maven, gradle, JUnit, YouTube, новости, руководство, ООП, алгоритмы, головоломки, rest, GraphQL, Excel, XML, json, yaml.