
В этой статье я покажу, как буквально за 15 минут создать собственного Telegram чат-бота на базе ИИ и начать использовать его абсолютно бесплатно.

Пока продолжается работа над основным проектом, который я курирую (ссылка), хочу поделиться приятным опытом экспериментов с микроконтроллером ESP32 на примере платы Lolin Lite.
Не скрою — эта модель мне особенно симпатична благодаря компактности, достаточному числу портов ввода-вывода и хорошему запасу по производительности.
Надеюсь, эта статья окажется полезной и для вас.
Начало
Для реализации проекта потребуется выполнить несколько шагов:
Зарегистрировать бота в Telegram
Создать аккаунт на openrouter.ai
Внести изменения в прошивку
Залить прошивку на микроконтроллер
Регистрация бота в Telegram

Через поиск в Telegram находим @BotFather (без кавычек) и открываем диалог.
Отправляем команду /newbot — после этого BotFather предложит ввести несколько параметров:
Название бота
Укажите публичное имя — то, что будут видеть пользователи (например, SmartHome Assistant).Юзернейм (адрес) бота
Это уникальное имя, которое будет заканчиваться наbot(например, smarthome_helper_bot). Если имя занято, BotFather попросит ввести другое.Получение API-токена
После успешного создания BotFather выдаст токен доступа (API key). Сохраните его — он потребуется позже.Готово
Бот зарегистрирован, можно переходить к следующему шагу.
Регистрация в OpenRouter
Шаги регистрации и получения API-ключа:

OpenRouter — это платформа, которая предоставляет доступ к различным нейросетям, как на платной основе, так и бесплатно. В рамках этой статьи мы будем использовать именно бесплатные возможности.
Переходим на сайт openrouter.ai и регистрируемся. Можно авторизоваться через Google, GitHub или с помощью почты.
После входа открываем: Settings → API Keys → Create API Key.
В поле Name указываем любое название (для себя) и нажимаем Create.
Появится ваш API key — сохраните его, он понадобится далее.
Затем снова переходим в Settings и открываем раздел Training, Logging, & Privacy. Включаем второй переключатель напротив пункта: Enable free endpoints that may publish prompts.
Этап завершён.
Код для микроконтроллера
Далее я выкладываю сам код, который будет необходимо немного настроить:
Скрытый текст
// ESP32 Telegram LLM BOT by RealZel #include <WiFi.h> #include <WiFiClientSecure.h> #include <HTTPClient.h> #include <ArduinoJson.h> #include "time.h" // ====== Настройки ====== const char* ssid = "Название точки доступа Wi-Fi"; const char* password = "Пароль точки доступа Wi-Fi"; const char* telegramBotToken = "Сюда ваш API с Botfather Telegram"; const char* telegramApiBase = "https://api.telegram.org/bot"; const char* LLM_API_URL = "https://openrouter.ai/api/v1/chat/completions"; const char* LLM_MODEL = "deepseek/deepseek-chat-v3.1:free"; const char* LLM_API_KEY = "Сюда ваш API с OpenRouter"; const char* ntpServer = "pool.ntp.org"; const long gmtOffset_sec = 2 * 3600; const int daylightOffset_sec = 0; const unsigned long POLL_INTERVAL_MS = 2000; const size_t TELEGRAM_MSG_LIMIT = 3800; // ====== Persona / системный промпт ====== const char* systemPrompt = "При получении команды /start в своем сообщении сообщай, что ты текстовый помощник и готов ответить на вопросы. Среднее время ответа до 30 секунд.\n" "Не строй таблицы.\n" "Каждый ответ должен умещаться максимум в два сообщения Telegram (~7600 символов).\n" "Если текст длиннее, сокращай, сохраняя ключевую и самую важную информацию.\n" "Делай текст связным и понятным, избегай обрезки важных предложений.\n" "Размер одного сообщения не должен превышать 3800 символов.\n"; // ====== Глобальные переменные ====== WiFiClientSecure client; long lastUpdateId = 0; unsigned long lastPoll = 0; // ====== Хранение предыдущих сообщений ====== String lastUserMessage = ""; String lastBotMessage = ""; // ====== Вспомогательные функции ====== void setupTime() { configTime(gmtOffset_sec, daylightOffset_sec, ntpServer); time_t now = time(nullptr); int tries = 0; while (now < 1600000000 && tries < 20) { delay(500); now = time(nullptr); tries++; } } String httpGet(const String &url) { HTTPClient https; if (url.startsWith("https://")) https.begin(client, url); else https.begin(url); int code = https.GET(); String payload = ""; if (code > 0) payload = https.getString(); https.end(); return payload; } String httpPostJsonWithAuth(const String &url, const String &body, const char* bearer) { HTTPClient https; if (url.startsWith("https://")) https.begin(client, url); else https.begin(url); https.addHeader("Content-Type", "application/json"); if (bearer && strlen(bearer) > 0) { String auth = String("Bearer ") + bearer; https.addHeader("Authorization", auth); } int code = https.POST(body); String payload = ""; if (code > 0) payload = https.getString(); https.end(); return payload; } // ====== Отправка сообщений в Telegram с делением на части ====== void sendTelegramMessage(long chat_id, const String &text, long reply_to_message_id = 0) { size_t start = 0; size_t len = text.length(); while (start < len) { String chunk = text.substring(start, start + TELEGRAM_MSG_LIMIT); start += TELEGRAM_MSG_LIMIT; StaticJsonDocument<2000> doc; doc["chat_id"] = chat_id; doc["text"] = chunk; doc["parse_mode"] = "HTML"; if (reply_to_message_id) doc["reply_to_message_id"] = reply_to_message_id; String body; serializeJson(doc, body); httpPostJsonWithAuth(String(telegramApiBase) + telegramBotToken + "/sendMessage", body, nullptr); } } // ====== Формирование тела запроса к LLM с учетом предыдущих сообщений ====== String buildLLMRequestBody(const String &userMessage) { StaticJsonDocument<8192> req; req["model"] = LLM_MODEL; req["temperature"] = 0.7; req["max_tokens"] = 1200; // ограничиваем, чтобы LLM сам пытался уложиться в 2 сообщения JsonArray messages = req.createNestedArray("messages"); // Системный промпт JsonObject m0 = messages.createNestedObject(); m0["role"] = "system"; m0["content"] = systemPrompt; // Предыдущее сообщение пользователя и ответ бота if (lastUserMessage.length() > 0 && lastBotMessage.length() > 0) { JsonObject prevUser = messages.createNestedObject(); prevUser["role"] = "user"; prevUser["content"] = lastUserMessage; JsonObject prevBot = messages.createNestedObject(); prevBot["role"] = "assistant"; prevBot["content"] = lastBotMessage; } // Текущее сообщение пользователя JsonObject currentUser = messages.createNestedObject(); currentUser["role"] = "user"; currentUser["content"] = userMessage; String out; serializeJson(req, out); return out; } // ====== Универсальный разбор ответа LLM ====== String parseLLMResponse(const String &resp) { if (resp.length() == 0) return "Ответа нет, попробуйте еще раз."; StaticJsonDocument<20000> doc; DeserializationError err = deserializeJson(doc, resp); if (err) return "Ошибка разбора JSON от LLM."; if (doc.containsKey("choices")) { JsonArray choices = doc["choices"].as<JsonArray>(); if (choices.size() > 0) { JsonObject c0 = choices[0].as<JsonObject>(); if (c0.containsKey("message") && c0["message"].containsKey("content")) return String((const char*)c0["message"]["content"]); if (c0.containsKey("text")) return String((const char*)c0["text"]); if (c0.containsKey("content")) return String((const char*)c0["content"]); } } if (doc.containsKey("text")) return String((const char*)doc["text"]); return "LLM вернул неожиданный формат ответа."; } String callLLM(const String &userMessage) { String body = buildLLMRequestBody(userMessage); String resp = httpPostJsonWithAuth(String(LLM_API_URL), body, LLM_API_KEY); String result = parseLLMResponse(resp); return result; } // ====== Фильтры ====== bool containsDangerous(const String &text) { String low = text; low.toLowerCase(); const char* forbids[] = {"убить", nullptr}; for (int i=0; forbids[i]!=nullptr; ++i) if (low.indexOf(forbids[i]) != -1) return true; return false; } bool looksLikeMedicalPrescription(const String &text) { String low = text; low.toLowerCase(); const char* meds[] = {"", nullptr}; for (int i=0; meds[i]!=nullptr; ++i) if (low.indexOf(meds[i]) != -1) return true; return false; } String postFilterResponse(const String &reply) { if (reply.length() == 0) return "Нейросеть молчит, попробуйте позже."; if (looksLikeMedicalPrescription(reply)) return "Не даю медицинских рекомендаций. Обратитесь к врачу."; if (containsDangerous(reply)) return "На такие опасные инструкции не отвечаю."; return reply; } // ====== Telegram Updates ====== void processUpdates() { String url = String(telegramApiBase) + String(telegramBotToken) + "/getUpdates?timeout=5"; if (lastUpdateId) url += "&offset=" + String(lastUpdateId + 1); String resp = httpGet(url); if (resp.length() == 0) return; StaticJsonDocument<20000> doc; if (deserializeJson(doc, resp)) return; if (!doc.containsKey("result")) return; for (JsonVariant update : doc["result"].as<JsonArray>()) { if (update.containsKey("update_id")) lastUpdateId = update["update_id"].as<long>(); if (!update.containsKey("message")) continue; JsonObject msg = update["message"].as<JsonObject>(); long chat_id = msg["chat"]["id"].as<long>(); long message_id = msg["message_id"].as<long>(); if (!msg.containsKey("text")) { sendTelegramMessage(chat_id, "Пишите текст, я не умею обрабатывать медиа.", message_id); continue; } String text = String((const char*)msg["text"]); if (containsDangerous(text)) { sendTelegramMessage(chat_id, "На такие запросы не могу ответить.", message_id); continue; } String hfReply = callLLM(text); String safeReply = postFilterResponse(hfReply); // Сохраняем контекст для следующего запроса lastUserMessage = text; lastBotMessage = safeReply; sendTelegramMessage(chat_id, safeReply, message_id); } } // ====== setup / loop ====== void setup() { Serial.begin(115200); WiFi.begin(ssid, password); int tries = 0; while (WiFi.status() != WL_CONNECTED && tries < 60) { delay(500); Serial.print("."); tries++; } if (WiFi.status() == WL_CONNECTED) { Serial.println("WiFi connected, IP: " + WiFi.localIP().toString()); } else Serial.println("WiFi connection failed"); setupTime(); client.setInsecure(); lastUpdateId = 0; } void loop() { if (millis() - lastPoll >= POLL_INTERVAL_MS) { lastPoll = millis(); processUpdates(); } }
В коде необходимо указать четыре параметра:
SSID и пароль вашей Wi-Fi сети
API-токен Telegram (из BotFather)
API-ключ OpenRouter
Persona / системный промт (необязательно)
Что делает прошивка сейчас:
Принимает сообщения от пользователя и, в соответствии с инструкциями из блока Persona, отправляет их в OpenRouter на модель deepseek-chat-v3.1.
Получает ответ от модели и отправляет его в Telegram.
Автоматически делит длинные ответы на несколько сообщений.
Позволяет менять характер бота: достаточно изменить текст в разделе Persona.
Имеет встроенные фильтры: если сообщение содержит запрещённые слова, бот сразу отправляет уведомление, что не может отвечать на такие запросы.
Запоминает предыдущее сообщение в переписке, поддерживая контекст общения.
При желании можно использовать любую другую LLM на OpenRouter — достаточно изменить значение в строке
LLM_MODEL.

В результате всех шагов у вас будет собственный Telegram-бот, который будет отвечать на сообщения.
Важно понимать, что память ESP32 ограничена, и устройство стабильно обрабатывает запросы только от одного пользователя. Поэтому текущая реализация подходит прежде всего для личных экспериментов и хобби-проектов.
Впрочем, есть и практичные сценарии: вы можете привязать бота к своему каналу и запрограммировать его, например, на публикацию сообщений по расписанию или по событиям.
Вот и всё на сегодня. Надеюсь вам было интересно и полезно.
