Как создать Telegram-бота с нейросетью через API на Python
Соберём личного текстового бота: вы пишете ему в Telegram, Python отправляет вопрос в VOID API и возвращает ответ модели. В примере есть память последних трёх пар сообщений, команда /reset и лимит 20 обращений к модели за сутки по UTC. Сначала проверим API-ключ отдельным скриптом. Так ошибку подключения к модели можно найти до настройки Telegram. Ниже приведены файлы проекта и результаты проверки через настоящий VOID API.
Подготовьте Python и два ключа
Установите Python 3.11 или новее. Бот будет работать, пока запущен Python-процесс и есть доступ к Telegram API и https://void-api.tech/v1. Для первого запуска достаточно своего компьютера.
В официальном @BotFather отправьте /newbot, задайте название и свободное имя пользователя с окончанием bot. Полученный токен нужен для подключения к Telegram.
В кабинете VOID создайте отдельный API-ключ и проверьте баланс. В этой инструкции используется claude-haiku-4.5 с Chat Completions. Токен BotFather и API-ключ VOID выполняют разные задачи; сохраните их в соответствующие поля .env.
Установите зависимости
Создайте отдельную папку проекта. Сохраните зависимости в requirements.txt. Команды для Linux и macOS приведены под содержимым файла.
В Windows окружение можно создать командой py -m venv .venv и активировать в PowerShell командой .\.venv\Scripts\Activate.ps1. Затем установите зависимости той же командой python -m pip install -r requirements.txt.
requirements.txt
python-telegram-bot==22.8
openai==3.37.0
python-dotenv==1.2.4
Терминал: Linux и macOS
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txtСохраните настройки в .env
Создайте файл .env рядом с Python-файлами. Замените значения в угловых скобках своими ключами. TELEGRAM_ALLOWED_USER_ID пока оставьте равным 0: в этом состоянии никто не сможет отправить запрос к модели через бота.
После первого запуска команда /start покажет ваш числовой Telegram ID. Впишите его в .env и перезапустите программу. Имя пользователя с символом @ здесь не подходит.
Если используете Git, добавьте приведённые исключения в .gitignore. Они оставляют ключи, виртуальное окружение и локальный счётчик за пределами репозитория.
.env: замените значения ключей
TELEGRAM_BOT_TOKEN=<токен от BotFather>
VOID_API_KEY=<API-ключ VOID>
VOID_MODEL=claude-haiku-4.5
TELEGRAM_ALLOWED_USER_ID=0
.gitignore
.env
.venv/
__pycache__/
quota.sqlite3*
Проверьте VOID API до запуска бота
Сохраните check_api.py и выполните python check_api.py в активированном окружении. Скрипт делает один платный запрос к выбранной модели и выводит ответ вместе с количеством токенов. Telegram для этой проверки не нужен.
При проверке 23 октября 2026 года скрипт получил ответ МАЯК, 33 входных и 7 выходных токенов. Ваш результат по числу токенов может отличаться при смене модели или текста запроса.
Если здесь возникла ошибка, сначала проверьте VOID_API_KEY, VOID_MODEL и баланс. Настройка Telegram не исправит отказ со стороны модели.
check_api.py
import os
from pathlib import Path
from dotenv import load_dotenv
from openai import APIError, OpenAI
load_dotenv(Path(__file__).resolve().parent / ".env")
with OpenAI(
api_key=os.environ["VOID_API_KEY"],
base_url="https://void-api.tech/v1",
timeout=60,
max_retries=0,
) as client:
try:
response = client.chat.completions.create(
model=os.environ["VOID_MODEL"],
messages=[{"role": "user", "content": "Ответь только словом МАЯК."}],
max_tokens=32,
)
except APIError as error:
print(f"Ошибка API: {type(error).__name__}, HTTP {getattr(error, 'status_code', None)}")
raise SystemExit(1) from None
if not response.choices or not response.choices[0].message.content:
raise SystemExit("API ответил, но текст отсутствует. Проверьте модель и лимит ответа.")
print(response.choices[0].message.content)
if response.usage:
print(f"input={response.usage.prompt_tokens}, output={response.usage.completion_tokens}")
Команда запуска
python check_api.pyВывод проверенного запуска
МАЯК
input=33, output=7Сохраните код в bot.py
Этот бот обрабатывает личные текстовые сообщения владельца. В группах он не отвечает; изображения и голосовые сообщения в коде не предусмотрены.
Дневной счётчик хранится в quota.sqlite3 рядом с программой и сохраняется после перезапуска. История диалога находится в памяти процесса. Команда /reset очищает историю, сохраняя дневной счётчик.
bot.py
import logging
import os
import sqlite3
from datetime import datetime, timezone
from pathlib import Path
from dotenv import load_dotenv
from openai import APIError, AsyncOpenAI
from telegram import Update
from telegram.ext import Application, CommandHandler, MessageHandler, filters
ROOT = Path(__file__).resolve().parent
load_dotenv(ROOT / ".env")
OWNER_ID = int(os.getenv("TELEGRAM_ALLOWED_USER_ID", "0"))
MODEL = os.environ["VOID_MODEL"]
DAILY_REQUESTS = 20
HISTORY_TURNS = 3
MAX_INPUT_CHARS = 3000
MAX_OUTPUT_TOKENS = 600
logging.basicConfig(level=logging.WARNING)
client = AsyncOpenAI(
api_key=os.environ["VOID_API_KEY"],
base_url="https://void-api.tech/v1",
timeout=60.0,
max_retries=0,
)
db = sqlite3.connect(ROOT / "quota.sqlite3")
db.execute("CREATE TABLE IF NOT EXISTS quota (day TEXT PRIMARY KEY, used INTEGER NOT NULL)")
db.commit()
def reserve_request():
day = datetime.now(timezone.utc).date().isoformat()
with db:
cursor = db.execute(
"INSERT INTO quota(day, used) VALUES (?, 1) "
"ON CONFLICT(day) DO UPDATE SET used = used + 1 WHERE used < ?",
(day, DAILY_REQUESTS),
)
return cursor.rowcount == 1
def keep_history(history, turns):
if turns < 0:
raise ValueError("turns must be non-negative")
if turns == 0:
return []
return history[-2 * turns:]
async def start(update, context):
await update.message.reply_text(
f"Ваш Telegram ID: {update.effective_user.id}.\n"
"Укажите его в TELEGRAM_ALLOWED_USER_ID и перезапустите бота.\n"
"Команда /reset очищает историю диалога."
)
async def reset(update, context):
if update.effective_user.id != OWNER_ID:
return
context.user_data.clear()
await update.message.reply_text("История очищена. Дневной счётчик сохранён.")
async def answer(update, context):
if update.effective_user.id != OWNER_ID:
await update.message.reply_text("Это личный бот. Доступ к модели закрыт.")
return
text = update.message.text.strip()
if not text or len(text) > MAX_INPUT_CHARS:
await update.message.reply_text(f"Отправьте от 1 до {MAX_INPUT_CHARS} символов.")
return
if not reserve_request():
await update.message.reply_text("Дневной лимит исчерпан. Обновление в 00:00 UTC.")
return
history = context.user_data.get("history", [])
user_message = {"role": "user", "content": text}
messages = [
{"role": "system", "content": "Отвечай по-русски, кратко и по существу."},
*history,
user_message,
]
try:
response = await client.chat.completions.create(
model=MODEL,
messages=messages,
max_tokens=MAX_OUTPUT_TOKENS,
)
except APIError as error:
logging.warning(
"Void API: %s, HTTP %s", type(error).__name__, getattr(error, "status_code", None)
)
await update.message.reply_text(
"Не удалось получить ответ. Проверьте ключ, баланс и модель в VOID. "
"Попытка учтена в дневном лимите."
)
return
choice = response.choices[0] if response.choices else None
reply = (choice.message.content or "").strip() if choice else ""
if not reply:
await update.message.reply_text("Модель не вернула текст. Попытка учтена в лимите.")
return
context.user_data["history"] = keep_history(
history + [user_message, {"role": "assistant", "content": reply}],
HISTORY_TURNS,
)
for offset in range(0, len(reply), 2000):
await update.message.reply_text(reply[offset : offset + 2000])
if choice.finish_reason == "length":
await update.message.reply_text("Ответ остановлен по лимиту токенов.")
if response.usage:
print(
f"model={response.model} input={response.usage.prompt_tokens} "
f"output={response.usage.completion_tokens}",
flush=True,
)
async def on_error(update, context):
logging.warning("Ошибка обработчика: %s", type(context.error).__name__)
async def shutdown(application):
await client.close()
db.close()
def build_app():
app = (
Application.builder()
.token(os.environ["TELEGRAM_BOT_TOKEN"])
.concurrent_updates(False)
.post_shutdown(shutdown)
.build()
)
private = filters.ChatType.PRIVATE
app.add_handler(CommandHandler("start", start, filters=private))
app.add_handler(CommandHandler("reset", reset, filters=private))
app.add_handler(MessageHandler(private & filters.TEXT & ~filters.COMMAND, answer))
app.add_error_handler(on_error)
return app
if __name__ == "__main__":
build_app().run_polling(allowed_updates=[Update.MESSAGE], drop_pending_updates=True)
Разрешите доступ и проверьте память
Выполните python bot.py, откройте своего бота в Telegram и отправьте /start. Скопируйте показанный ID в TELEGRAM_ALLOWED_USER_ID. Остановите процесс через Ctrl+C и запустите его повторно.
Отправьте: «Запомни кодовое слово МАЯК. Ответь только этим словом». Затем: «Какое кодовое слово я попросил запомнить? Ответь одним словом». Во втором запросе программа передаст модели предыдущий вопрос и ответ.
Отправьте /reset и задайте новый вопрос. Программа больше не передаст старую историю. Модель при этом может случайно угадать слово; проверяйте очистку по исходящим сообщениям или по новой независимой теме, а не по одному совпадению ответа.
Запускайте один экземпляр бота с этим токеном. Он получает сообщения через long polling, поэтому публичный webhook для примера не нужен. Параметр drop_pending_updates=True пропускает сообщения, накопившиеся за время остановки.
Запуск бота
python bot.pyНастройки памяти и расхода
HISTORY_TURNS задаёт число предыдущих пар «вопрос + ответ». При значении 3 новый запрос содержит до трёх таких пар. Если память не нужна, установите 0 и перезапустите бота. Функция keep_history обрабатывает этот случай отдельно: обычный срез history[-0:] возвращает весь список.
MAX_INPUT_CHARS ограничивает длину нового сообщения в символах. MAX_OUTPUT_TOKENS ограничивает выходной бюджет модели. Символы и токены не равны, поэтому эти значения нельзя складывать для расчёта цены.
DAILY_REQUESTS ограничивает число попыток обращения к модели за сутки по UTC. Программа учитывает попытку до отправки, включая обращения с ошибкой. После сетевого сбоя не всегда известно, успел ли провайдер обработать запрос. Автоматические повторы SDK отключены.
Лимит запросов не задаёт точную сумму расходов. Для оценки смотрите входные и выходные токены и списания в кабинете. Изменение модели требует проверки её тарифа и поддержки Chat Completions и max_tokens.
Настройки в начале bot.py
DAILY_REQUESTS = 20
HISTORY_TURNS = 3
MAX_INPUT_CHARS = 3000
MAX_OUTPUT_TOKENS = 600Что проверено через настоящий API
23 октября 2026 года мы передали обработчику бота два сообщения про кодовое слово. Он обратился к claude-haiku-4.5 через VOID и получил ответы ниже. Число токенов пришло в usage, стоимость взята из истории запросов VOID.
В этой проверке входящие события Telegram и отправка reply_text были заменены локальными заглушками. Она проверяет обработчик, передачу истории и реальные ответы VOID. Доставка сообщений через серверы Telegram в этот тест не входила.
Отдельные локальные проверки подтвердили отказ постороннему пользователю, ограничение длинного ввода, обрезку и отключение истории, /reset, обработку ошибки API, разбиение длинного ответа и сохранение дневного лимита. Это проверки поведения программы, а не измерение качества модели.
Два реальных обращения обработчика к VOID
Сообщение: Запомни кодовое слово МАЯК. Ответь только этим словом.
Ответ модели: МАЯК
Вход: 73 токенов. Выход: 7 токенов.
Списание за запрос: $5.701e-05
Сообщение: Какое кодовое слово я попросил запомнить? Ответь одним словом.
Ответ модели: МАЯК
Вход: 110 токенов. Выход: 7 токенов.
Списание за запрос: $7.81e-05Если бот не отвечает
Не работает /start: проверьте, что процесс запущен, токен BotFather указан правильно и компьютеру доступен Telegram API. Ошибка конфликта получения обновлений может означать, что с тем же токеном работает второй экземпляр.
/start работает, но доступ к модели закрыт: сверьте числовой TELEGRAM_ALLOWED_USER_ID и перезапустите программу после изменения .env.
check_api.py завершается с ошибкой: посмотрите напечатанный класс ошибки и HTTP-статус, проверьте ключ VOID, имя модели и баланс. Убедитесь, что используете API-ключ, а не пароль от кабинета.
При сообщении об исчерпанном дневном лимите проверьте число запросов. Счётчик обновляется в 00:00 UTC. Удаление quota.sqlite3 сбрасывает ограничение, поэтому при переносе на сервер сохраните этот файл.
Если ответ оборвался с уведомлением о лимите токенов, запросите более короткий формат ответа или увеличьте MAX_OUTPUT_TOKENS с учётом расходов.
Подключение и источники
Для запуска создайте ключ VOID и проверьте актуальный тариф модели. Цены и доступность моделей могут измениться после даты проверки статьи.