Перейти к основному содержанию
VOID
← База кейсов использования ИИ

Как создать 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.

Создать бота в BotFather

Создать API-ключ VOID

Проверить модель и тариф

Установите зависимости

Создайте отдельную папку проекта. Сохраните зависимости в 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

Как мы сократили контекст одного запроса

Что проверено через настоящий 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 и проверьте актуальный тариф модели. Цены и доступность моделей могут измениться после даты проверки статьи.

Создать API-ключ

Модели и цены

Справочник VOID API

Документация Telegram

python-telegram-bot 22.8