Legan Studio
Все статьи
~ 9 мин чтения

Лайв-чат с оператором в боте MAX: handoff и интеграции

Как сделать в боте MAX переключение на живого оператора: handoff, очередь обращений, рабочее место, интеграция с helpdesk. Архитектура и нюансы.

  • MAX
  • поддержка
  • архитектура

Бот не должен закрывать 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, время до закрытия).

СегментTTRTTResCoverage
Free24 ч72 ч9–21 будни
Paid4 ч24 ч9–21 ежедневно
Enterprise1 ч8 ч24/7
VIP15 мин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 пользователя должен вернуться в обычное состояние — иначе следующее сообщение зависнет в воздухе. Сценарий:

  1. Оператор нажимает «Закрыть тикет».
  2. Backend меняет status = resolved, отправляет пользователю прощальное сообщение и CSAT-опрос.
  3. FSM переключается обратно с with_operator на idle.
  4. Через 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–54.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 через команду бота.