Jev в службе поддержки: разбор и маршрутизация обращений

Обновлено

Каждое обращение в поддержку требует решения ещё до ответа — что это за вопрос, насколько он срочный и кто должен им заняться. Показываем, как отдать эту сортировку Jev и оставить людям и LLM только те обращения, где они действительно нужны.

Что Jev делает в поддержке

Jev встаёт на входе очереди и отвечает на вопросы об обращении: о чём оно, насколько сложное и срочное, просит ли клиент живого оператора. По ответам ваш код выбирает путь. Простой запрос закрывает код без LLM, типовой — LLM-специалист, спорный или рискованный — человек. В документации TypeSafe этот паттерн называется intent routing: дорогие ресурсы тратятся только на обращения, которым они действительно нужны. Подробный разбор паттерна — в статье о маршрутизации по намерению.

Путь Что туда попадает Кто отвечает
Код статус заказа, типовые справки интеграция с CRM, шаблон
LLM вопросы о продукте, стандартные возвраты LLM с базой знаний или черновик для оператора
Человек жалобы, угрозы уйти, спорные случаи оператор или менеджер

Схема вопросов

Вопрос Тип Что даёт
intent Choice тему: статус заказа, продукт, возврат, оплата, ошибка, жалоба, другое
complexity Score, 3 уровня стандартная процедура, нужно суждение, нестандартный случай
urgency Score, 3 уровня может подождать, сегодня, немедленно
wants_human Noul клиент просит живого оператора
churn_risk Noul клиент грозит уйти или пойти в суд
refund_requested Noul клиент просит вернуть деньги

Последний вопрос нужен только для обращений об оплате, но задавать его стоит всем. В документации TypeSafe этот приём называется speculative fan-out: все вопросы уходят одним запросом, а код потом берёт нужные ответы. Лишние вопросы почти не увеличивают задержку, а отдельный уточняющий запрос стоил бы ещё одного сетевого обращения. Подробнее — в статье о fan-out в Jev.

В state кладите всё, что нужно для решения: текст обращения, данные клиента, при необходимости — выдержку из правил. Документация TypeSafe советует группировать связанные данные в одном объекте с понятными именами полей.

from typesafe_sdk import TypeSafeClient, Choice, Noul, Score

state = {
    "ticket": {
        "subject": "Списали деньги дважды",
        "messages": [
            "Здравствуйте! За подписку списали оплату два раза. "
            "Если не вернёте до пятницы, отменю подписку."
        ],
    },
    "customer": {"plan": "business", "open_orders": 0},
}

questions = {
    "intent": Choice(
        instructions="Какова основная цель обращения?",
        criteria={
            "order_status": "Узнать статус существующего заказа",
            "product_question": "Вопрос о продукте или тарифе до покупки",
            "return_exchange": "Вернуть или обменять товар",
            "billing": "Списания, счета, оплата подписки",
            "bug": "Ошибка или сбой в работе сервиса",
            "complaint": "Недоволен сервисом и хочет решения",
            "other": None,
        },
    ),
    "complexity": Score(
        instructions="Насколько сложно решить обращение?",
        criteria=[
            "простая справка или стандартная процедура",
            "нужно суждение или несколько шагов",
            "нестандартная ситуация, нужна эскалация",
        ],
    ),
    "urgency": Score(
        instructions="Насколько срочно нужно ответить?",
        criteria=["может подождать", "сегодня", "немедленно"],
    ),
    "wants_human": Noul(instructions="Клиент просит соединить его с живым оператором?"),
    "churn_risk": Noul(instructions="Клиент грозит уйти, отменить подписку или пойти в суд?"),
    "refund_requested": Noul(instructions="Клиент просит вернуть деньги?"),
}

with TypeSafeClient() as client:
    r = client.system_one(state=state, questions=questions)

Маршрутизация по уверенности

def route(r) -> str:
    intent = r.choices["intent"]
    complexity = r.scores["complexity"]  # от 0 до 2

    if intent.confidence < 0.5 or r.nouls["wants_human"].noul >= 0.8:
        return "human"
    if intent.choice == "order_status":
        return "code"  # ответ из CRM без LLM
    if intent.choice == "complaint" and (complexity.score > 1 or complexity.confidence < 0.5):
        return "human"
    if intent.choice == "billing" and r.nouls["refund_requested"].noul >= 0.7:
        return "billing_queue"  # деньги возвращает человек
    return "llm"

Основа взята из примера TypeSafe: при уверенности в теме ниже 0,5 обращение уходит человеку, статус заказа обрабатывает код, а жалобу со сложностью выше 1 или с неуверенной оценкой сложности получает оператор. Остальные правила — наши дополнения. Вопрос churn_risk не меняет маршрут, а влияет на приоритет: высокое значение — повод сразу уведомить менеджера клиента.

Для обращения из примера ответ мог бы выглядеть так (числа условные): тема billing с уверенностью 0,86, refund_requested — 0,93, churn_risk — 0,81, wants_human — 0,12. Код отправит обращение в очередь биллинга, а высокий риск ухода поднимет его в начало очереди. Оценки стоит показать оператору рядом с текстом обращения.

Пороги по цене ошибки

В примере паттерна confidence-gated routing TypeSafe разбирает голосового помощника банка. Общий нижний порог — 0,6: всё, что ниже, уходит в поддержку. Действия с низкой ценой ошибки, например озвучить баланс, выполняются от 0,6. Перевод денег выполняется автоматически только от 0,85, а в промежутке 0,6–0,85 помощник просит подтверждения. В поддержке та же логика может выглядеть так:

Действие Цена ошибки Стартовая политика
Тег и очередь низкая — оператор перекинет автоматически от 0,6
Автоответ из базы знаний средняя — клиент получит не тот ответ автоматически от 0,85, ниже — черновик для оператора
Возврат денег, компенсация высокая только человек, Jev готовит карточку обращения

Числа — отправная точка. Как подобрать их на своих данных, рассказано в статье об уверенности Jev.

Приоритет очереди считайте в коде

Не спрашивайте модель, насколько важно обращение. Спросите о составляющих — срочности, риске ухода — и сложите их с весами в коде. Автор проекта typesafe-triage-guard объясняет выгоду так: изменение веса становится правкой кода с понятными последствиями, а не правкой промпта.

urgency = r.scores["urgency"].score / 2  # приводим к шкале 0..1
churn = r.nouls["churn_risk"].noul
vip = 1.0 if state["customer"]["plan"] == "business" else 0.0
priority = 0.5 * urgency + 0.3 * churn + 0.2 * vip

Подробно этот приём описан в статье о составной оценке.

Как проверить качество

  • Историческая разметка. Возьмите 300–500 закрытых обращений, где известен итоговый отдел. Учтите, что в истории есть ошибки операторов: спорные случаи пересмотрите вручную.
  • Теневой режим. Одну-две недели Jev размечает обращения, но ничего не решает. Сравните её маршруты с решениями операторов по каждому классу.
  • Что мерить. Точность по классам, долю обращений, ушедших к человеку, долю неверных автоматических маршрутов и время до первого ответа.
  • Путаница классов. Если вероятности размазаны между «оплатой» и «возвратом», разведите описания вариантов в criteria.
  • Русский язык. Основной язык обучения Jev — английский, на других языках точность ниже, поэтому мерьте на своих обращениях.

Реальные примеры

Triage Desk. Один запрос на обращение и четыре вопроса: отдел (Choice), срочность (Score), нужен ли человек (Noul) и спам (Noul). По замерам автора, в медиане это около 400 мс и примерно 540 входных токенов на обращение. Дальше обращения расходятся по трём дорожкам: срочные и эскалации — людям, обычные проблемы — LLM, которая готовит черновик ответа для оператора, простые вопросы и спам — в архив или на автоответ из FAQ.

triage-guard. Сначала обращение проходит защитную проверку. Если она блокирует сообщение, классификация не запускается и второй запрос не тратится. Приоритет считается из нескольких оценок с весами в коде, а при низкой уверенности обращение уходит человеку независимо от ответа модели. В опубликованном прогоне на реальной модели сообщение о двойном списании ушло в отдел оплаты с уверенностью 1,0.

Подводные камни

  • Пересекающиеся категории. «Оплата» и «возврат» частично совпадают — опишите границу в criteria явно.
  • Нет варианта «другое». Без него модель выберет ближайшую категорию даже для обращения не по адресу.
  • Больше 255 категорий. В одном Choice их не уместить. Разбейте выбор на два этапа: сначала группа, потом тема внутри группы.
  • Длинные переписки. Отправляйте последние сообщения и нужные поля CRM, а не всю историю: по документации TypeSafe, лишний контекст снижает точность.
  • Суммы и даты. TypeSafe прямо пишет, что Jev — не калькулятор и плохо сравнивает даты. Сколько раз и когда списали деньги, проверяет код по данным биллинга.
  • Ответ пишет не Jev. Для текста нужна LLM или шаблоны. Jev может выбрать шаблон из списка, и это тоже Choice. Примеры кода на Python — в руководстве по SDK.

Когда Jev в поддержке не нужна

Автор обзора на Хабре перечисляет случаи, когда Jev лучше не брать. В поддержке они выглядят так:

  • тему уже выбирает сам клиент в форме обращения — тогда хватит обычных правил;
  • набор тем стабилен и есть тысячи размеченных обращений — дообученный локальный классификатор может оказаться точнее и быстрее;
  • переписку с клиентами нельзя отправлять во внешнее облако;
  • решение сразу запускает необратимое действие, например возврат денег, без проверки человеком.

В остальных случаях основные затраты уходят на то, чтобы написать вопросы и подобрать пороги. Сама модель стоит доли цента на обращение: в Triage Desk это около 540 входных токенов, то есть примерно $0,00002.

Частые вопросы

Может ли Jev отвечать клиентам вместо оператора?

Нет, Jev не генерирует текст. Она может выбрать подходящий шаблон ответа из списка или решить, какому обработчику передать обращение, а сам ответ пишет человек, LLM или готовый шаблон.

Сколько вопросов можно задать одному обращению?

Отдельного лимита на число вопросов в документации нет, ограничивает общий размер запроса. Все вопросы считаются параллельно. В тесте TypeSafe 13 вопросов в одном запросе обошлись в 12 раз дешевле и в 10 раз быстрее, чем 13 отдельных запросов.

Как начать использовать Jev в поддержке?

С теневого режима. Пусть Jev размечает обращения параллельно с текущим процессом, а вы сравниваете её решения с решениями операторов, прежде чем доверить ей маршрутизацию.

Источники