Суть урока
Большинство предпринимателей строят чат-боты так: бот отвечает на FAQ и всё. Это как аэропорт с одной полосой. В этом уроке вы научитесь строить систему с умной маршрутизацией: бот понимает намерение, извлекает ключевые данные, и знает — когда можно ответить самому, а когда немедленно передать живому человеку.
Ключевые концепции
- Intent classification — автоматическое определение намерения пользователя (вопрос, жалоба, покупка, запрос к оператору) с помощью Claude Haiku или правил
- Entity extraction — извлечение конкретных данных из сообщения: имена, суммы, даты, артикулы товаров
- Intent routing — логика направления диалога: правила (ключевые слова) vs AI-классификация vs гибридный подход
- Escalation triggers — набор условий, при которых бот передаёт разговор человеку: злость клиента, прямой запрос, сложный вопрос, крупная сумма
- State management — сохранение контекста разговора между сообщениями через Redis или Supabase
- Multi-channel unification — единая логика маршрутизации для Telegram, WhatsApp и веб-виджета
- Fallback handling — что делать, когда бот не понял намерение: уточнить, предложить варианты или сразу эскалировать
- Метрики качества — escalation rate (% переданных операторам), resolution rate (% решённых ботом), CSAT (оценка от клиента)
Теория
Что такое intent classification и зачем он нужен
Когда клиент пишет "хочу вернуть товар" — это не просто текст. Это намерение (возврат), которое требует конкретного сценария ответа. Когда пишет "вы вообще нормальные?!" — это сигнал тревоги: клиент раздражён, боту отвечать опасно, нужен живой человек.
Intent classification — это автоматическое присвоение метки намерения каждому входящему сообщению. Без этого бот работает как кассир-новичок: не понимает, что от него хотят, и пытается ответить одним шаблоном на всё подряд.
Основные категории намерений для бизнес-бота:
- FAQ — вопросы о ценах, условиях доставки, режиме работы
- Complaint — жалоба, проблема, негатив
- Purchase — интерес к покупке, запрос консультации
- Support — технический вопрос, проблема с заказом
- Human request — явная просьба поговорить с оператором ("хочу к менеджеру")
- Out of scope — нерелевантный запрос, спам
Как работает классификация: три подхода
Подход 1: Правила (keywords)
Самый простой. Словарь ключевых слов для каждого намерения. "верн", "возврат" → возврат. "цена", "сколько стоит" → FAQ о цене. Быстро, дёшево, предсказуемо. Минус — хрупко: "хочу вернуться к вашим услугам" ложно сработает на возврат.
Подход 2: AI-классификация (Claude Haiku)
Передаём сообщение модели с просьбой классифицировать. Точнее, понимает контекст и сарказм. Дороже правил (платишь за токены), чуть медленнее. Зато заметно реже ошибается на живой речи. Цена за сообщение небольшая и считается по токенам: актуальные цены — на странице Актуальное сейчас.
Подход 3: Гибрид (рекомендуется)
Сначала проверяем правила — если совпало с высокой уверенностью, используем правило (дёшево). Если нет — отправляем в Claude Haiku (точно). Это оптимальный баланс скорости и стоимости для нагруженных ботов.
Entity extraction — что скрыто в тексте
Кроме намерения, важно извлечь конкретные данные. Клиент пишет: "хочу заказать 3 коробки к пятнице на адрес Ленина 15". Здесь:
- Количество: 3
- Дедлайн: пятница
- Адрес: Ленина 15
Без entity extraction бот ответит "окей, оформим" и упустит всю информацию. С ним — подтянет данные в CRM, проверит наличие, скажет, успеваем ли к пятнице.
Для entity extraction также используем Claude или regex. Claude лучше справляется с нечёткими формулировками ("послезавтра", "тысяч пять примерно").
Escalation triggers — когда передавать человеку
Это самая важная часть системы. Неправильная эскалация (слишком редкая) — клиент уходит злой. Слишком частая — оператор тонет в простых вопросах.
Умная эскалация работает по нескольким сигналам:
1. Явный запрос на оператора "хочу поговорить с менеджером", "переключи на человека", "call me" — бот немедленно эскалирует без вопросов.
2. Детектор фрустрации Анализируем тон: капслок, восклицательные знаки, слова-маркеры ("кошмар", "ужасно", "никогда больше", "верните деньги"). Claude Haiku возвращает sentiment score. Если > порога — эскалация.
3. Сложный запрос Намерение не распознано 2 раза подряд. Вопрос требует информации из нескольких систем. Юридический или финансовый вопрос (по правилам компании).
4. Высокоценный лид Сумма в запросе > $N. Клиент спрашивает про корпоративный тариф. Упомянул конкурента — нужна личная работа.
5. Системный fallback После 3 неудачных попыток ответить — автоматическая эскалация. Лучше передать человеку, чем раздражать клиента бесконечным "не понял".
State management — память разговора
Для хранения состояния используем Redis (быстро, для активных сессий) или Supabase (персистентно, для истории). Переписка с клиентами содержит персональные данные: храни только нужное, ограничь срок хранения и не отправляй в модель лишнего. В state храним:
{
"session_id": "uuid",
"user_id": "telegram_chat_id",
"history": [...], # последние N сообщений
"current_intent": "complaint",
"entities": {"order_id": "12345"},
"frustration_score": 0.3,
"escalated": False,
"turns_without_resolution": 1
}При каждом новом сообщении обновляем state и принимаем решение о маршрутизации с учётом всей истории, не только последней фразы.
Multi-channel: один движок, много каналов
Логика intent routing не должна быть привязана к Telegram или WhatsApp. Правильная архитектура:
[Telegram] → adapter → [Intent Engine] → action → [Telegram response]
[WhatsApp] → adapter → [Intent Engine] → action → [WhatsApp response]
[Web Widget] → adapter → [Intent Engine] → action → [Web response]Adapter нормализует входящее сообщение в единый формат. Intent Engine работает одинаково для всех. Ответ формируется в формате нужного канала. Это позволяет поддерживать 3 канала с одним кодом.
Альтернативы: Botpress, Dialogflow, Rasa
Botpress — open-source платформа с визуальным построителем диалогов. Хорош для сложных деревьев диалогов, встроенный NLU. Выбирайте, если команда не техническая и нужен visual editor.
Dialogflow (Google) — enterprise-решение с мощным NLU. Хорошо интегрируется с Google Workspace. Дороже, vendor lock-in. Выбирайте для крупных корпоративных проектов.
Rasa — open-source с максимальной гибкостью. Требует ML-экспертизы. Выбирайте, если нужен on-premise и полный контроль над данными.
Claude direct (наш подход) — лучший выбор для solo founders и малых команд. Быстрый старт, низкие затраты, гибкость. Haiku для классификации, Sonnet для сложных ответов.
Мониторинг: три ключевых метрики
Escalation rate — процент разговоров, переданных оператору. Единой нормы нет: она зависит от ниши и сложности вопросов. Если почти все разговоры уходят оператору, бот бесполезен. Если почти никто не уходит, возможно, бот не эскалирует когда нужно.
Resolution rate — процент вопросов, закрытых ботом без оператора. Цель задай по своему пилоту, а не по чужим цифрам. Растёт по мере улучшения базы знаний.
CSAT (Customer Satisfaction Score) — оценка клиента после завершения диалога (1-5). Отслеживайте отдельно для бот-сессий и оператор-сессий. Разрыв покажет, где слабое место.
Дашборд для мониторинга строим в Grafana или через простую Telegram-команду /stats для владельца.
Код: pipeline intent routing на Python + FastAPI
import os
import json
import redis
from fastapi import FastAPI, Request
from anthropic import Anthropic
app = FastAPI()
client = Anthropic()
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
ESCALATION_PHRASES = [
"хочу к менеджеру", "оператор", "живой человек",
"переключи", "call me", "позвоните"
]
FRUSTRATION_WORDS = [
"ужасно", "кошмар", "никогда", "мошенники",
"верните деньги", "обман", "безобразие"
]
def get_session(session_id: str) -> dict:
data = r.get(f"session:{session_id}")
if data:
return json.loads(data)
return {
"history": [],
"frustration_score": 0.0,
"turns_without_resolution": 0,
"escalated": False,
"current_intent": None
}
def save_session(session_id: str, state: dict):
r.setex(f"session:{session_id}", 3600, json.dumps(state))
def check_escalation_rules(message: str, state: dict) -> tuple[bool, str]:
"""Правила эскалации — быстрая проверка без AI."""
msg_lower = message.lower()
# Явный запрос оператора
if any(phrase in msg_lower for phrase in ESCALATION_PHRASES):
return True, "explicit_request"
# Детектор фрустрации по словам
frustration_hit = sum(1 for w in FRUSTRATION_WORDS if w in msg_lower)
if frustration_hit >= 2:
return True, "frustration_detected"
# Накопленная фрустрация по сессии
if state["frustration_score"] > 0.7:
return True, "cumulative_frustration"
# Бот не смог помочь 3 раза подряд
if state["turns_without_resolution"] >= 3:
return True, "repeated_fallback"
return False, ""
def classify_intent(message: str, history: list) -> dict:
"""Claude Haiku классифицирует намерение и извлекает сущности."""
history_text = "\n".join([
f"{m['role']}: {m['content']}" for m in history[-4:]
])
response = client.messages.create(
model="claude-haiku-4-5", # проверь, что модель ещё доступна в API; актуальные модели: страница «Актуальное сейчас»
max_tokens=300,
system="""Ты классификатор намерений для чат-бота интернет-магазина.
Верни JSON с полями:
- intent: одно из [faq_price, faq_delivery, faq_return, complaint, purchase_intent, order_status, out_of_scope]
- confidence: 0.0-1.0
- entities: объект с извлечёнными данными (order_id, amount, date, product)
- sentiment: positive/neutral/negative
- frustration_score: 0.0-1.0
Только JSON, без пояснений.""",
messages=[{
"role": "user",
"content": f"История диалога:\n{history_text}\n\nНовое сообщение: {message}"
}]
)
try:
return json.loads("".join(b.text for b in response.content if b.type == "text"))
except Exception:
return {
"intent": "out_of_scope",
"confidence": 0.0,
"entities": {},
"sentiment": "neutral",
"frustration_score": 0.3
}
def generate_bot_response(intent: str, message: str, entities: dict, history: list) -> str:
"""Генерируем ответ на основе намерения."""
intent_prompts = {
"faq_price": "Ответь на вопрос о цене товара. Если нет конкретного товара — спроси уточнение.",
"faq_delivery": "Ответь о доставке: срок 2-5 дней, бесплатно от 3000 руб.",
"faq_return": "Ответь о возврате: 14 дней без объяснений, нужен чек.",
"complaint": "Прими жалобу с сочувствием. Уточни детали для решения.",
"purchase_intent": "Помоги с покупкой. Уточни детали, предложи добавить в корзину.",
"order_status": "Запроси номер заказа если не указан.",
}
prompt = intent_prompts.get(intent, "Вежливо скажи, что не понял вопрос, и предложи варианты.")
response = client.messages.create(
model="claude-haiku-4-5",
max_tokens=200,
system=f"Ты вежливый помощник интернет-магазина. {prompt}. Отвечай коротко — 1-3 предложения.",
messages=[{"role": "user", "content": message}]
)
return "".join(b.text for b in response.content if b.type == "text")
async def notify_operator(session_id: str, message: str, reason: str, state: dict):
"""Уведомление оператора в Telegram."""
import httpx
bot_token = os.environ.get("TELEGRAM_BOT_TOKEN")
operator_chat_id = os.environ.get("OPERATOR_CHAT_ID")
history_preview = "\n".join([
f"{'👤' if m['role']=='user' else '🤖'} {m['content']}"
for m in state["history"][-3:]
])
text = (
f"🚨 Эскалация | {reason}\n"
f"Сессия: {session_id}\n\n"
f"Последние сообщения:\n{history_preview}\n\n"
f"Последнее: {message}"
)
async with httpx.AsyncClient() as http:
await http.post(
f"https://api.telegram.org/bot{bot_token}/sendMessage",
json={"chat_id": operator_chat_id, "text": text}
)
@app.post("/chat")
async def chat_endpoint(request: Request):
body = await request.json()
session_id = body["session_id"]
message = body["message"]
state = get_session(session_id)
# Быстрая проверка правил эскалации
should_escalate, escalation_reason = check_escalation_rules(message, state)
if should_escalate and not state["escalated"]:
state["escalated"] = True
state["history"].append({"role": "user", "content": message})
save_session(session_id, state)
await notify_operator(session_id, message, escalation_reason, state)
return {
"response": "Понял вас. Переключаю на оператора — он ответит в течение 5 минут.",
"escalated": True,
"reason": escalation_reason
}
# AI-классификация намерения
classification = classify_intent(message, state["history"])
intent = classification.get("intent", "out_of_scope")
confidence = classification.get("confidence", 0.0)
# Обновляем state
state["current_intent"] = intent
state["frustration_score"] = max(
state["frustration_score"],
classification.get("frustration_score", 0.0)
)
# Низкая уверенность — считаем нерешённым
if confidence < 0.5 or intent == "out_of_scope":
state["turns_without_resolution"] += 1
else:
state["turns_without_resolution"] = 0
# Генерируем ответ
bot_response = generate_bot_response(
intent, message,
classification.get("entities", {}),
state["history"]
)
# Сохраняем историю
state["history"].append({"role": "user", "content": message})
state["history"].append({"role": "assistant", "content": bot_response})
state["history"] = state["history"][-20:] # Храним последние 20 сообщений
save_session(session_id, state)
return {
"response": bot_response,
"intent": intent,
"confidence": confidence,
"escalated": False
}Этот код запускается как FastAPI-сервис. Telegram-бот, WhatsApp-адаптер или веб-виджет отправляют POST-запросы на /chat — и получают ответ с intent, confidence и флагом эскалации.
Практика
Шаг 1: Подготовка среды (5 мин)
mkdir chatbot-manager && cd chatbot-manager
python -m venv venv && source venv/bin/activate
pip install fastapi uvicorn anthropic redis httpx python-dotenv
# .env файл
echo "ANTHROPIC_API_KEY=your_key" >> .env
echo "TELEGRAM_BOT_TOKEN=your_bot_token" >> .env
echo "OPERATOR_CHAT_ID=your_chat_id" >> .env
# Запускаем Redis локально (или используем Redis Cloud)
docker run -d -p 6379:6379 redis:alpineШаг 2: Настройте базу знаний для FAQ (10 мин)
Создайте файл knowledge_base.json с ответами на типичные вопросы вашего бизнеса. Структура:
{
"faq_price": "Цены на наши товары начинаются от 500 руб. Актуальный каталог: shop.example.com/catalog",
"faq_delivery": "Доставка 2-5 рабочих дней. Бесплатно при заказе от 3000 руб.",
"faq_return": "Возврат в течение 14 дней. Нужен чек и оригинальная упаковка. Деньги возвращаем за 3-5 дней."
}Замените intent_prompts в коде на реальные ответы из вашей базы знаний.
Шаг 3: Запустите и протестируйте routing (10 мин)
uvicorn main:app --reload --port 8000Тестируйте через curl или Postman:
# Обычный вопрос
curl -X POST http://localhost:8000/chat \
-H "Content-Type: application/json" \
-d '{"session_id": "test1", "message": "сколько стоит доставка?"}'
# Тест эскалации
curl -X POST http://localhost:8000/chat \
-H "Content-Type: application/json" \
-d '{"session_id": "test2", "message": "это УЖАСНО, верните деньги немедленно!!!"}'
# Явный запрос оператора
curl -X POST http://localhost:8000/chat \
-H "Content-Type: application/json" \
-d '{"session_id": "test3", "message": "хочу поговорить с живым человеком"}'Проверьте, что Telegram-уведомление приходит при эскалации.
Шаг 4: Подключите к Telegram-боту (8 мин)
# telegram_adapter.py
from telegram.ext import Application, MessageHandler, filters
import httpx, os
async def handle_message(update, context):
session_id = str(update.effective_chat.id)
message = update.message.text
async with httpx.AsyncClient() as client:
resp = await client.post(
"http://localhost:8000/chat",
json={"session_id": session_id, "message": message}
)
data = resp.json()
await update.message.reply_text(data["response"])
app = Application.builder().token(os.environ["TELEGRAM_BOT_TOKEN"]).build()
app.add_handler(MessageHandler(filters.TEXT, handle_message))
app.run_polling()Шаг 5: Настройте мониторинг (5 мин)
Добавьте эндпоинт /stats для быстрого просмотра метрик из Redis:
@app.get("/stats")
async def get_stats():
keys = r.keys("session:*")
total = len(keys)
escalated = sum(1 for k in keys if json.loads(r.get(k)).get("escalated"))
return {
"total_sessions": total,
"escalated": escalated,
"escalation_rate": f"{escalated/total*100:.1f}%" if total else "0%",
"active_last_hour": total # упрощённо
}Инструменты и ресурсы
| Инструмент | Для чего | Цена |
|---|---|---|
| Claude Haiku | Intent classification, entity extraction | Оплата по токенам, на октябрь 2026: $1 / $5 за миллион токенов (вход / выход) |
| Redis | Хранение state сессий (быстро) | Бесплатно self-hosted, облако по тарифам провайдера |
| Supabase | Персистентная история диалогов | Есть бесплатный уровень с лимитами |
| FastAPI | Backend для webhook-эндпоинтов | Бесплатно open-source |
| Botpress | Визуальный конструктор диалогов (альтернатива) | Есть бесплатный план, тарифы на сайте |
| Dialogflow | Enterprise NLU (когда нужна платформа) | Оплата по использованию, тарифы на сайте Google Cloud |
| Rasa | On-premise, максимальный контроль | Бесплатно open-source |
| Telegram Bot API | Канал + оповещения операторов | Бесплатно |
| Grafana | Дашборд мониторинга метрик | Бесплатно self-hosted |
Рекомендуемый стек для старта: FastAPI + Claude Haiku + Redis + Telegram — собирается быстро и недорого. Расход на модель считай так: два вызова Haiku на сообщение (классификация и ответ) × токены × цена за миллион токенов. Прогони пилот на сотне сообщений и посмотри usage в ответах API.
Ключевые выводы
"Бот без intent routing — это кассир, который отвечает на всё: 'номер три'. Бот с routing — это диспетчер, который знает, куда направить каждый запрос."
"Правило эскалации простое: если сомневаешься — передавай человеку. Лучше потратить время оператора, чем потерять клиента из-за неудачного ботовского ответа."
"Escalation rate — честный KPI вашего бота. Если он зашкаливает, бот не работает. Если почти нулевой, возможно, он эскалирует слишком редко и клиенты уходят молча."
Следующий урок
→ Продуктовая аналитика — PostHog, Mixpanel и умные инсайты
Отметка хранится только в этом браузере и никуда не отправляется. Мой прогресс