Бот не должен закрывать 100% запросов — попытка сделать это убивает UX. Зато он отлично фильтрует типовые вопросы и переводит сложные на оператора. В этой статье разберём, как технически устроен handoff (передача диалога человеку) в MAX-боте: триггеры эскалации, очередь приоритетов, двусторонняя связь user↔оператор, интеграции с helpdesk-системами, SLA и метрики. Статья ориентирована на продактов, head of CX и разработчиков, проектирующих поддержку через бот в российском мессенджере MAX.
Когда live-chat действительно нужен
Не каждому боту нужен оператор. Если поток обращений < 50/день и вопросы строго типовые — справится FAQ-бот. Live-chat обоснован, когда выполняется хотя бы одно условие:
- Эскалация после N неудачных ответов AI — пользователь повторил вопрос 3+ раз, confidence модели падает ниже порога 0.6, бот не нашёл интент.
- Sensitive operations — возврат денег, изменение тарифа, удаление аккаунта, юридические вопросы. Здесь риск ошибки бота слишком высокий.
- VIP-клиенты — enterprise-сегмент, premium-подписка, клиенты с LTV выше определённого порога. Они платят за человеческое внимание.
- B2B-кейсы — счета, акты, договоры, индивидуальные условия. Чаще всего вопросы нестандартные и требуют согласования внутри.
- Жалобы и тон агрессии — sentiment-анализ показал негатив, ключевые слова «верните деньги», «суд», «РКН», «жалоба».
Если ни одно из условий не релевантно — не делайте live-chat. Он стоит денег: операторы, helpdesk, обучение, ночные смены.
Архитектура handoff в MAX
Базовая схема двусторонней связи через бота:
Пользователь (MAX)
│
▼
Bot API MAX ←──────────── ответы оператора
│
▼
Backend (FSM + router)
│
├──► AI / FAQ (1-я линия)
│
└──► Очередь tickets (Postgres + Redis)
│
▼
Канал оператора:
- admin-MAX-бот
- Mini App inbox
- helpdesk webhook
Ключевая идея: пользователь всегда общается через свой обычный бот в MAX. Когда FSM переключается в состояние with_operator, бэкенд маршрутизирует входящие сообщения не в AI, а в очередь оператора. Оператор отвечает через свой интерфейс — бэкенд формирует исходящее сообщение через Bot API MAX и доставляет пользователю.
UX-сигналы для эскалации
Триггеры делятся на явные (пользователь сам попросил) и неявные (бот понял по контексту).
Явные:
- Команда
/operatorили кнопка «Соединить с оператором». - Текст «нужен человек», «оператор», «менеджер», «живой».
Неявные:
- Confidence AI меньше 0.6 на двух подряд репликах.
- Один и тот же вопрос задан 3+ раз (детекция по эмбеддингу).
- Sentiment негативный (жалоба, ругательства, восклицательные знаки в сочетании с ключами).
- Ключевые слова: «юрист», «возврат», «чек», «РКН», «суд», «обман».
- Длина диалога превысила 15 реплик без resolution.
Пример простого триггера на Python:
ESCALATION_KEYWORDS = {
"оператор", "человек", "менеджер", "живой",
"юрист", "возврат", "чек", "ркн", "суд", "обман",
}
def should_escalate(state: dict, message: str, confidence: float) -> bool:
text = message.lower()
# Явный запрос
if any(kw in text for kw in ESCALATION_KEYWORDS):
return True
# Низкая уверенность 2 раза подряд
if confidence < 0.6 and state.get("low_confidence_count", 0) >= 1:
return True
# Повторение одного вопроса
if state.get("same_question_count", 0) >= 3:
return True
# Длинный диалог без resolution
if state.get("turns", 0) >= 15 and not state.get("resolved"):
return True
return False
Очередь tickets: схема таблицы
Минимальная схема для Postgres:
CREATE TABLE tickets (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
chat_id BIGINT NOT NULL,
source TEXT NOT NULL DEFAULT 'max',
status TEXT NOT NULL DEFAULT 'new',
priority SMALLINT NOT NULL DEFAULT 5,
segment TEXT,
skill TEXT,
operator_id BIGINT,
subject TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
assigned_at TIMESTAMPTZ,
first_response_at TIMESTAMPTZ,
resolved_at TIMESTAMPTZ,
closed_at TIMESTAMPTZ,
sla_due_at TIMESTAMPTZ,
tags TEXT[],
metadata JSONB
);
CREATE INDEX idx_tickets_status_priority
ON tickets (status, priority DESC, created_at)
WHERE status IN ('new', 'waiting_operator');
CREATE INDEX idx_tickets_operator
ON tickets (operator_id, status)
WHERE status IN ('in_progress', 'waiting_user');
CREATE TABLE ticket_messages (
id BIGSERIAL PRIMARY KEY,
ticket_id BIGINT NOT NULL REFERENCES tickets(id),
author_type TEXT NOT NULL,
author_id BIGINT,
text TEXT NOT NULL,
attachments JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
Поле priority — чем больше, тем выше приоритет. VIP получает 9–10, free — 1–3, paid — 5–7. Поле skill нужно для skills-based routing (например, «биллинг», «технический», «юридический»).
Очередь и распределение между операторами
Три популярных стратегии:
- Round-robin — следующему оператору по кругу. Просто, но не учитывает загрузку.
- Least-loaded — оператору с минимумом активных тикетов. Балансирует нагрузку.
- Skills-based routing — учитывает
skillтикета и матрицу навыков операторов.
Внутри одного приоритета — FIFO. Между приоритетами — priority queue (VIP первыми). Pseudocode выбора следующего тикета для оператора:
def pick_next_ticket(operator_id: int) -> Ticket | None:
op = get_operator(operator_id)
if op.status != "available" or op.active_count >= op.max_concurrent:
return None
return db.execute("""
SELECT * FROM tickets
WHERE status = 'new'
AND (skill IS NULL OR skill = ANY(%s))
ORDER BY priority DESC, created_at ASC
LIMIT 1
FOR UPDATE SKIP LOCKED
""", [op.skills]).fetchone()
FOR UPDATE SKIP LOCKED нужен, чтобы два оператора одновременно не схватили один тикет.
Канал оператора: три варианта
1. Admin-MAX-бот. Команда поддержки сидит в отдельном MAX-боте, получает уведомления о новых тикетах кнопками «Взять в работу», «Передать», «Закрыть». Дёшево, не требует фронтенда. Минус — неудобно обрабатывать поток > 30/день на оператора.
2. Mini App inbox. Веб-интерфейс внутри MAX Mini App: очередь, чат, история, шаблоны, CRM-карточка. Удобно для команд 5+ операторов. Технически — SPA на React/Vue + WebSocket для real-time.
3. Helpdesk-интеграция. Тикеты создаются в Юздеск/OkDesk/Zendesk через API, операторы работают в привычном интерфейсе, ответы синхронизируются обратно через webhook. Подходит, если helpdesk уже внедрён.
Состояния тикета и SLA
Жизненный цикл:
new → assigned → in_progress → waiting_user → resolved → closed
↑ │
└──────────────┘
- new — создан, ожидает оператора.
- assigned — назначен оператору, но он ещё не открыл.
- in_progress — оператор отвечает.
- waiting_user — оператор ответил, ждём пользователя.
- resolved — диалог завершён, бот отправил CSAT-опрос.
- closed — после CSAT или по таймауту.
SLA измеряется по двум метрикам: TTR (time to response, время первого ответа) и TTRes (time to resolution, время до закрытия).
| Сегмент | TTR | TTRes | Coverage |
|---|---|---|---|
| Free | 24 ч | 72 ч | 9–21 будни |
| Paid | 4 ч | 24 ч | 9–21 ежедневно |
| Enterprise | 1 ч | 8 ч | 24/7 |
| VIP | 15 мин | 4 ч | 24/7 |
Двусторонняя связь: route handler
Когда оператор пишет ответ в admin-боте или Mini App, бэкенд должен доставить сообщение пользователю. Пример обработчика на Python (псевдокод поверх Bot API MAX):
async def operator_reply(ticket_id: int, operator_id: int, text: str):
ticket = await db.get_ticket(ticket_id)
if ticket.operator_id != operator_id:
raise PermissionError("not your ticket")
# Сохраняем в историю
await db.insert_message(
ticket_id=ticket_id,
author_type="operator",
author_id=operator_id,
text=text,
)
# Отправляем пользователю через MAX Bot API
await max_api.send_message(
chat_id=ticket.chat_id,
text=f"Оператор: {text}",
)
# Фиксируем first_response_at, если ещё не зафиксирован
if not ticket.first_response_at:
await db.update_ticket(
ticket_id,
first_response_at=now(),
status="waiting_user",
)
И обратное направление — входящее сообщение пользователя, когда тикет уже в работе:
async def user_message_in_ticket(ticket_id: int, text: str):
ticket = await db.get_ticket(ticket_id)
await db.insert_message(
ticket_id=ticket_id,
author_type="user",
author_id=ticket.user_id,
text=text,
)
# Уведомляем оператора в его канале
await notify_operator(
ticket.operator_id,
f"Тикет #{ticket_id}: новое сообщение\n{text}",
)
await db.update_ticket(ticket_id, status="in_progress")
Авто-эскалация при простое
Если оператор не ответил за N минут, тикет нужно перебросить. Cron каждые 30 секунд:
async def reassign_stalled_tickets():
stalled = await db.fetch("""
SELECT id, operator_id, priority
FROM tickets
WHERE status = 'assigned'
AND assigned_at < NOW() - INTERVAL '5 minutes'
AND first_response_at IS NULL
""")
for t in stalled:
next_op = await pick_least_loaded_operator(exclude=[t.operator_id])
if next_op:
await db.update_ticket(
t.id,
operator_id=next_op.id,
assigned_at=now(),
)
await notify_operator(next_op.id, f"Передан тикет #{t.id}")
else:
# Никого свободного — алерт дежурному
await notify_supervisor(t.id)
Контекст для оператора
Оператор должен открыть тикет и сразу видеть всё необходимое:
- Полная история диалога с ботом (включая ответы AI и confidence).
- Профиль пользователя из CRM: имя, телефон, e-mail, сегмент.
- История заказов / подписок / последних обращений.
- Текущий тариф, дата регистрации, LTV.
- Внутренние теги и заметки от других операторов.
Без контекста оператор задаёт пользователю вопросы, на которые бот уже получил ответ — это убивает UX.
Возврат в бот после оператора
Когда оператор закрывает диалог, FSM пользователя должен вернуться в обычное состояние — иначе следующее сообщение зависнет в воздухе. Сценарий:
- Оператор нажимает «Закрыть тикет».
- Backend меняет
status = resolved, отправляет пользователю прощальное сообщение и CSAT-опрос. - FSM переключается обратно с
with_operatorнаidle. - Через 24 ч cron меняет
resolved → closed.
Если пользователь напишет до закрытия CSAT — бот принимает сообщение как ответ на опрос или как новое обращение (зависит от FSM-логики).
Сравнение helpdesk-систем
| Система | API | Цена ₽/опер | Webhook | Подходит для |
|---|---|---|---|---|
| Юздеск | да | от 1 200 | да | СНГ, средний бизнес |
| OkDesk | да | от 1 500 | да | сервисные компании |
| HelpDeskEddy | да | от 990 | да | малый бизнес |
| Zendesk | да | от $55 | да | enterprise, мульти-канал |
| Intercom | да | от $74 | да | SaaS, продуктовые команды |
| Freshdesk | да | от $15 | да | стартапы, бюджетный enterprise |
| Битрикс24 | да | пакетно | частично | если уже Битрикс |
Для небольших команд (1–3 оператора) часто проще обойтись admin-MAX-ботом без helpdesk.
Метрики поддержки
| Метрика | Что показывает | Целевое |
|---|---|---|
| FRT (first response time) | время до первого ответа оператора | по SLA сегмента |
| AHT (average handling time) | среднее время обработки тикета | 8–15 мин |
| Длина очереди | сколько new тикетов ждут | менее 10 |
| Deflection rate | доля диалогов, решённых ботом без оператора | 60–80% |
| CSAT | оценка пользователя 1–5 | 4.3+ |
| FCR (first contact resolution) | решено за один контакт | 70%+ |
| Загрузка оператора | параллельных тикетов в работе | 5–8 |
| Reopen rate | доля тикетов, переоткрытых пользователем | менее 8% |
Deflection rate — главный KPI инвестиций в бота. Если бот не растёт по этой метрике — он не оправдывает себя.
Coverage 24/7 vs одна смена
Стоимость покрытия драматически разная:
- 9–21 будни (one shift) — 1–2 оператора, бюджет от 80 тыс ₽/мес. Подходит для B2B и SMB.
- 9–21 ежедневно (two shifts) — 2–3 оператора + выходные, от 160 тыс ₽/мес.
- 24/7 (three shifts) — 4–6 операторов, ночные смены с надбавкой, от 350 тыс ₽/мес.
Перед запуском 24/7 проверьте реальный поток обращений ночью. Часто 80% ночных тикетов — не срочные, и достаточно автоответа «ответим утром».
Антипаттерны
- Оператор отвечает медленнее бота. Если FRT 30 минут, а бот отвечал за 2 секунды — пользователь злится. Решение: SLA на FRT и автоэскалация.
- Нет уведомлений оператору. Тикет лежит часами, потому что никто не увидел. Решение: push в admin-боте + звуковой сигнал в Mini App.
- Нет SLA по сегментам. VIP ждёт столько же, сколько free. Решение: priority queue + разные таймеры.
- Оператор не видит истории. Спрашивает то, что бот уже выяснил. Решение: контекстная карточка тикета.
- FSM не возвращается в idle. После закрытия тикета бот молчит на новые сообщения. Решение: явный переход FSM при
resolved. - Дубли тикетов. Пользователь нажал «Оператор» дважды — два тикета. Решение: проверка активного
status IN ('new','assigned','in_progress'). - Нет архива переписки. Пользователь просит «дайте копию» — нечего дать. Решение: экспорт в PDF/email через бот.
Итого
Live-chat в MAX-боте — это связка триггеров эскалации (явных и неявных), приоритетной очереди тикетов в Postgres + Redis, маршрутизации между операторами (round-robin / least-loaded / skills-based), двусторонней связи через Bot API MAX, интеграции с CRM и helpdesk, SLA и метрик FRT/AHT/CSAT/deflection. Архитектурно реализуется за 2–4 недели для одной команды и масштабируется до десятков операторов и 24/7-покрытия. Главное — не ставить оператора на типовые вопросы, держать deflection rate выше 60% и не давать тикетам зависать без алерта.
Частые вопросы
Когда боту в MAX действительно нужен лайв-чат с оператором?
Live-chat обоснован, когда выполняется хотя бы одно условие. Эскалация после неудачных ответов AI: пользователь повторил вопрос 3+ раз, confidence модели падает ниже 0.6, бот не нашёл интент. Sensitive operations: возврат денег, изменение тарифа, удаление аккаунта, юридические вопросы. VIP-клиенты: enterprise-сегмент, premium-подписка, клиенты с высоким LTV. B2B-кейсы: счета, акты, договоры, индивидуальные условия. Жалобы и тон агрессии: sentiment-анализ показал негатив, ключевые слова про суд, РКН, возврат. Если ни одно из условий не релевантно и поток меньше 50 обращений в день — достаточно FAQ-бота без оператора.
Какие триггеры эскалации использовать в MAX-боте?
Триггеры делятся на явные и неявные. Явные: команда /operator, кнопка «Соединить с оператором», ключевые слова «нужен человек», «оператор», «менеджер». Неявные: confidence AI меньше 0.6 на двух подряд репликах; повторение одного и того же вопроса 3+ раз (детекция по эмбеддингу); негативный sentiment (жалоба, ругательства); ключевые слова «юрист», «возврат», «чек», «РКН», «суд»; длина диалога превысила 15 реплик без resolution. Все триггеры стоит логировать — это материал для дообучения AI и снижения deflection rate.
Как устроена очередь тикетов в MAX-боте с приоритетами?
Минимальная схема: таблица tickets в Postgres с полями id, user_id, chat_id, status, priority, skill, operator_id, timestamps. Внутри одного приоритета FIFO, между приоритетами priority queue (VIP первыми, например priority 9–10 для VIP, 5–7 для paid, 1–3 для free). Распределение между операторами: round-robin (по кругу), least-loaded (минимум активных тикетов), skills-based routing (по матрице навыков). Выборка следующего тикета — SELECT ... ORDER BY priority DESC, created_at ASC LIMIT 1 FOR UPDATE SKIP LOCKED, чтобы два оператора не схватили один тикет одновременно.
Какой SLA задавать для разных сегментов клиентов в боте MAX?
Стандартная разбивка по TTR и TTRes. Free: TTR 24 часа, TTRes 72 часа, покрытие 9–21 будни. Paid: TTR 4 часа, TTRes 24 часа, покрытие 9–21 ежедневно. Enterprise: TTR 1 час, TTRes 8 часов, покрытие 24/7. VIP: TTR 15 минут, TTRes 4 часа, покрытие 24/7. SLA должен быть автоматизирован: cron каждые 30 секунд проверяет тикеты с истекающим sla_due_at и эскалирует дежурному или next-in-queue. Без авто-эскалации SLA нарушается тихо и видно только постфактум по отчётам.
Где сидит оператор: admin-бот, Mini App или helpdesk?
Три варианта. Admin-MAX-бот: команда поддержки в отдельном боте, получает уведомления и отвечает кнопками. Дёшево, без фронтенда, подходит до 30 тикетов в день на оператора. Mini App inbox: веб-интерфейс внутри MAX (SPA на React/Vue + WebSocket), удобно для команд 5+ операторов с большим потоком. Helpdesk-интеграция: тикеты создаются в Юздеск, OkDesk, Zendesk, Intercom через API, операторы работают в привычном интерфейсе, ответы синхронизируются обратно через webhook. Если helpdesk уже внедрён — берите интеграцию, не дублируйте кабинет.
Какие метрики обязательно мерить в лайв-чате MAX-бота?
Восемь ключевых метрик. FRT (first response time) — время до первого ответа оператора, цель по SLA сегмента. AHT (average handling time) — среднее время обработки тикета, норма 8–15 минут. Длина очереди — сколько new тикетов ждут, норма менее 10. Deflection rate — доля диалогов, решённых ботом без оператора, цель 60–80% (главный KPI инвестиций в бота). CSAT — оценка пользователя 1–5, цель 4.3+. FCR (first contact resolution) — решено за один контакт, цель 70%+. Загрузка оператора — 5–8 параллельных тикетов. Reopen rate — переоткрытые пользователем тикеты, цель менее 8%.
Как избежать главных антипаттернов лайв-чата в боте MAX?
Семь типовых ошибок. Оператор отвечает медленнее бота — лечится SLA на FRT и автоэскалацией. Нет уведомлений оператору — нужен push в admin-боте плюс звуковой сигнал в Mini App. Нет SLA по сегментам — VIP ждёт как free; решение priority queue с разными таймерами. Оператор не видит истории — нужна контекстная карточка тикета с CRM-данными. FSM не возвращается в idle после закрытия — явный переход на статусе resolved. Дубли тикетов при повторном нажатии «Оператор» — проверка активного status IN ('new','assigned','in_progress'). Нет архива переписки — экспорт в PDF/email через команду бота.