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