Хабр, привет! Сегодня мы попробуем сделать свой ИИ с телеграм ботом для возможности простого общения с ней. Сразу оговорюсь, мы не будем в очередной раз использовать открытый API ChatGPT или новомодного Deepseek. Мы развернем свой полноценный ИИ локально и сынтегрируем его с телеграм ботом.
LLM модель

Первое, что нам нужно сделать – это запустить LLM локально. Задача не такая простая как кажется – современные LLM модели имеют нетривиальные алгоритмы взаимодействия с видеокартой, токенизации текста и т.д. Процесс функционаирования уже обученной LLM называется инференсом. Основные характеристики инференса – это точность и скорость. Подробно останавливаться на них мы не буем, важно понимать саму концепцию. Для реализации такого инференса на чистой java была разработана библиотека Jlama – максимально простой движок инференса для LLM, написанной на голой java без тяжеловесных фрэймворков. В этом и заключается главное отличие Jlama от имеющихся альтернатив.
По факту Jlama дает возможность обслуживать LLM в java окружении напрямую, то есть в той же jvm, где работает наше приложение. В таком подходе есть ряд плюсов: прежде всего это возможность разработчика гибко влиять на функциональность своего приложения с LLM. У Jlama есть одна особенность – для оптимизации движок использует Vector API, а это значит, что какое-то время придется включать preview фичи.
Список доступных моделей перечислен в документации к проекту:
Gemma & Gemma 2 Models
Llama & Llama2 & Llama3 Models
Mistral & Mixtral Models
Qwen2 Models
IBM Granite Models
GPT-2 Models
BERT Models
BPE Tokenizers
WordPiece Tokenizers
Как видите некоторые модели порядком устарели. Я выберу Llama 3 – неплохо обученная модель от Meta. Пока что модель будет воспринимать только английский язык.
Добавляем в проект необходимые зависимости:
implementation("com.github.tjake:jlama-core:$jlamaVersion") implementation("com.github.tjake:jlama-native:$jlamaVersion:linux-x86_64")
Есть три доступные версии jlama-native: linux-x86_64, macos-x86_64/aarch_64 и windows-x86_64. Выбирайте свою версию в соответствии с вашей ОС, я запускал на ubuntu, поэтому версия linux-x86_64.
Так библиотека очень проста, весь код умещается в одном небольшом классе ArtificialIntelligenceModel:
@Component public class ArtificialIntelligenceModel { @Value("${llm.model-name}") private String modelName; @Value("${llm.working-directory}") private String workingDirectory; @Value("${llm.temperature}") private String temperature; @Value("${llm.ntokens}") private String ntokens; public String ask(String prompt) throws IOException { File localModelPath = new Downloader(workingDirectory, modelName).huggingFaceModel(); try (AbstractModel model = ModelSupport.loadModel(localModelPath, DType.F32, DType.I8);) { PromptContext ctx; if (model.promptSupport().isPresent()) { ctx = model.promptSupport() .get() .builder() .addSystemMessage("You are a helpful chat bot who writes short responses.") .addUserMessage(prompt) .build(); } else { ctx = PromptContext.of(prompt); } Generator.Response response = model.generate(UUID.randomUUID(), ctx, Float.parseFloat(temperature), Integer.parseInt(ntokens), (s, f) -> { } ); return response.responseText; } } }
Это вся наша модель, довольно компактно. В application.yaml указываем переменную llm.model-name, у нас это будет tjake/Llama-3.2-1B-Instruct-JQ4. Эта модель будет скачана с помощью объекта Downloader и сохранена в директорию llm.working-directory:
File localModelPath = new Downloader(workingDirectory, modelName).huggingFaceModel();
Скачанная модель будет загружена в переменную model:
AbstractModel model = ModelSupport.loadModel(localModelPath, DType.F32, DType.I8);
Теперь нам нужно создать наш промт и отправить его модели. Для этого нам нужно сгенерировать PromptContext с определенными параметрами. Помимо самого текста мы должны указать температуру и количество токенов.
Теперь нам нужно создать наш промт и отправить его модели. Для этого нам нужно сгенерировать PromptContext с определенными параметрами. Помимо самого текста мы должны указать температуру и количество токенов.
Температура — это числовое значение (часто устанавливаемое между 0 и 1, но иногда и выше), которое регулирует распределение вероятностей следующего слова. Другими словами – температура влияет на креативность текста: чем меньше значение, тем меньше будет и креативность полученного результата. В качестве примера выставим значение температуры в 0.
Остался последний важный параметр – это количество токенов. Токен — это набор текстовых символов. LLM разбивают текст не на слова, а на токены. Слишком большие значения ntokens локально лучше не выставлять, чтобы сильно не грузить наши вычислительные ресурсы. Поставим 256.
В итоге получаем PromptContext:
PromptContext ctx; if (model.promptSupport().isPresent()) { ctx = model.promptSupport() .get() .builder() .addSystemMessage("You are a helpful chat bot who writes short responses.") .addUserMessage(prompt) .build(); } else { ctx = PromptContext.of(prompt); }
И получение ответа от модели:
Generator.Response response = model.generate(UUID.randomUUID(), ctx, Float.parseFloat(temperature), Integer.parseInt(ntokens), (s, f) -> { }
Итоговый результат доступен в поле responseText.
Телеграм бот
Для начала создадим бота с помощью всем известного BotFather.

Тут все достаточно просто, вызываем /newbot и вводим нужное нам имя нашего бота. В моем примере это FranticticticChatBot. После создания бота не забываем скопировать API ключ (Use this token to access the HTTP API).
Сам бот также очень простой:
@Slf4j @Component @RequiredArgsConstructor public class AiChatBot implements SpringLongPollingBot, LongPollingSingleThreadUpdateConsumer { private static final String START = "/start"; @Value("${bot.token}") private String token; private TelegramClient telegramClient; private final ArtificialIntelligenceModel model; @PostConstruct private void init() { telegramClient = new OkHttpTelegramClient(getBotToken()); } @Override public String getBotToken() { return this.token; } @Override public LongPollingUpdateConsumer getUpdatesConsumer() { return this; } @Override public void consume(Update update) { if (update.hasMessage() && update.getMessage().hasText()) { long chatId = update.getMessage().getChatId(); var text = update.getMessage().getText(); if(!text.equals(START)) { try { var answer = model.ask(text); SendMessage message = SendMessage.builder() .chatId(chatId) .text(answer) .build(); telegramClient.execute(message); } catch (TelegramApiException | IOException e) { log.error("Error {}", e.getMessage()); } } } } @AfterBotRegistration public void afterRegistration(BotSession botSession) { log.info("Registered bot running state is: " + botSession.isRunning()); } }
Нам потребуются две зависимости:
implementation("org.telegram:telegrambots-springboot-longpolling-starter:$telegramVersion") implementation("org.telegram:telegrambots-client:$telegramVersion")
В bot.token прописываем полученный при создании бота токен. В качестве клиента будем использовать OkHttpTelegramClient. В методы consume первое, что надо сделать – это получить текстовое сообщение из чата если оно есть:
var text = update.getMessage().getText();
Для получения ответа от модели достаточно вызвать ме��од ask с полученным из чата текстом:
var answer = model.ask(text);
Ответ от нашего ИИ заворачиваем в объект SendMessage и возвращаем обратно в чат:
telegramClient.execute(message);
Тестируем
Проверяем работу нашего бота с ИИ. Например, отправим ему вопрос: «What is java?». Получаем результат:

Ответ получим небыстро, так как модель требует определённого времени на работу своих алгоритмов. Кроме того, не стоит забывать, что качество ответа будет зависеть как от самой модели, так и от ее параметров – в основном это температура и количество токенов. Если попробовать поиграться с этими значениями, то можно получить разные результаты.
Пример кода доступен тут. В ближайшее время я попробую развернуть бота в Yandex Cloud.
Подписывайтесь на мой телеграм-канал. Буду рад любым вашим комментариям
