У нас уже есть Telegram-бот на python-telegram-bot и работа с ChatGPT API. Соединяем — получаем бота, который отвечает не эхом, а как ассистент с характером и памятью. Это разбор, который потом станет видео недели 3 — тут же код, что будем записывать.
Версии: Python 3.12, python-telegram-bot 21, openai SDK 1.x.
Архитектура: три слоя на пальцах
сообщение в Telegram → хендлер (PTB) → история диалога (в памяти/SQLite)
↓
запрос к ChatGPT (openai SDK)
↓
ответ → (опционально стриминг) → сообщение пользователю
Состояние диалога — отдельная тема: у Telegram-пользователя нет «сессии», каждое сообщение — отдельный запрос к вашему серверу. Значит, историю надо хранить сами: ключ — chat_id, значение — список сообщений.
Базовая версия: память + лимиты
import os
from openai import OpenAI
from telegram import Update
from telegram.ext import ApplicationBuilder, CommandHandler, ContextTypes, MessageHandler, filters
TOKEN = os.environ["BOT_TOKEN"]
openai = OpenAI() # OPENAI_API_KEY и OPENAI_BASE_URL из окружения
MODEL = os.environ.get("OPENAI_MODEL", "gpt-4o-mini")
SYSTEM_PROMPT = (
"Ты — ассистент канала @PythonSimple о Python, Django и AI. "
"Отвечай по-русски, коротко и по делу, с примерами кода, когда уместно."
)
MAX_HISTORY = 10 # сколько сообщений помнит бот
MAX_TOKENS = 600 # потолок ответа — защита бюджета
# Простое хранилище: chat_id -> история. В проде — SQLite или Redis.
histories: dict[int, list[dict]] = {}
async def chat(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
chat_id = update.effective_chat.id
history = histories.setdefault(chat_id, [{"role": "system", "content": SYSTEM_PROMPT}])
history.append({"role": "user", "content": update.message.text})
history[:] = history[-MAX_HISTORY:] # держим окно, иначе счёт растёт с каждым сообщением
response = openai.chat.completions.create(
model=MODEL,
messages=history,
max_tokens=MAX_TOKENS,
temperature=0.4,
)
answer = response.choices[0].message.content
history.append({"role": "assistant", "content": answer})
await update.message.reply_text(answer)
async def reset(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
histories.pop(update.effective_chat.id, None)
await update.message.reply_text("Контекст очищен.")
app = ApplicationBuilder().token(TOKEN).build()
app.add_handler(CommandHandler("reset", reset))
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, chat))
app.run_polling()
Работает, но есть две проблемы, которые сразу вылезут в реальном использовании: долгое «печатает...» и растущая история. Чиним обе.
Стриминг: отвечаем до того, как ответ готов
Telegram заканчивает показывать «печатает...» примерно через 5 секунд, а модель думает дольше. Решение из мира чатов: рисуем черновик и дописываем. В Telegram это редактирование сообщения по мере стрима:
async def chat_streaming(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
chat_id = update.effective_chat.id
history = histories.setdefault(chat_id, [{"role": "system", "content": SYSTEM_PROMPT}])
history.append({"role": "user", "content": update.message.text})
history[:] = history[-MAX_HISTORY:]
placeholder = await update.message.reply_text("…")
answer, chunk_count = "", 0
stream = openai.chat.completions.create(
model=MODEL, messages=history, max_tokens=MAX_TOKENS, temperature=0.4, stream=True,
)
for chunk in stream:
delta = chunk.choices[0].delta.content
if not delta:
continue
answer += delta
chunk_count += 1
if chunk_count % 12 == 0: # Telegram: не чаще ~1 редактирования в секунду
await placeholder.edit_text(answer + " ▍")
await placeholder.edit_text(answer)
history.append({"role": "assistant", "content": answer})
Важная деталь: Telegram ограничивает частоту редактирования сообщения, поэтому обновляем не каждый чанк, а примерно раз в секунду (% 12 при типичной скорости токенов). Используйте либо стриминг, либо обычный вариант из первого примера — смешивать оба хендлера не нужно.
Длинные диалоги: скользящее окно с саммари
Окно в 10 сообщений простое, но бот «забывает» начало разговора. Улучшение: когда история достигает потолка, сжимаем её в саммари — одно системное сообщение вместо десяти:
async def compress_if_needed(history: list[dict]) -> None:
if len(history) < MAX_HISTORY + 6:
return
summary_resp = openai.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": "Сожми диалог в 3–4 предложения: о чём спрашивал пользователь, что отвечали."},
*history[1:], # без старого system-промпта
],
max_tokens=200,
)
summary = summary_resp.choices[0].message.content
history[:] = [history[0], {"role": "system", "content": f"Контекст диалога: {summary}"}]
Саммари стоит один дешёвый запрос и экономит десятки дорогих: выигрыш в цене — с третьего длинного диалога.
Хранение: память → SQLite
Словарь histories живёт, пока работает процесс — упал, перезапустился, и все диалоги обнулились. Минимальная надёжность — SQLite (асинхронно через aiosqlite):
import aiosqlite
DB = "bot.db"
async def load_history(chat_id: int) -> list[dict]:
async with aiosqlite.connect(DB) as db:
row = await (await db.execute(
"SELECT messages FROM history WHERE chat_id = ?", (chat_id,)
)).fetchone()
import json
return json.loads(row[0]) if row else [{"role": "system", "content": SYSTEM_PROMPT}]
async def save_history(chat_id: int, history: list[dict]) -> None:
import json
async with aiosqlite.connect(DB) as db:
await db.execute(
"INSERT INTO history (chat_id, messages) VALUES (?, ?) "
"ON CONFLICT(chat_id) DO UPDATE SET messages = excluded.messages",
(chat_id, json.dumps(history, ensure_ascii=False)),
)
await db.commit()
Схема одна таблица: chat_id INTEGER PRIMARY KEY, messages TEXT. Для личного бота этого хватит с запасом; для публичного — переезд на Postgres без изменения логики.
Деплой и деньги
- Запуск — тот же systemd-юнит, что и для простого бота:
.venv/bin/python bot.py,Restart=always. - Ключ OpenAI — только на сервере. В коде бота ключа нет, утекти может только вместе с сервером.
- Ограничения как страховка:
max_tokensв каждом запросе + счётчик запросов на пользователя в сутки (десятка строк вокругchat()). Один «попробовавший» пользователь не должен стоить вам сотню рублей.
Итог
Умный Telegram-бот — это 80 строк поверх двух готовых кусков: PTB для Telegram, openai SDK для мозгов, плюс три инженерные детали, о которых молчат туториалы: скользящее окно истории, стриминг вместо «печатает...» и жёсткий потолок токенов. Это же ядро, на котором строятся боты для бизнеса — RAG по документам, запись клиентов, поддержка.
Следующий уровень этого ядра — RAG: учим бота отвечать по вашим документам, без него бот будет уверенно выдумывать цены и регламенты вашего бизнеса.