Определение намерений (intent routing) на Jev: чат-боты и поддержка

Обновлено

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

Идея: сначала понять, потом тратить

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

TypeSafe предлагает ставить Jev перед всеми обработчиками. Модель определяет намерение и сложность одним быстрым запросом, а код отправляет сообщение по нужному пути:

Намерение Обработчик
Статус заказа Код — запрос к базе, без LLM
Вопрос о товаре LLM с контекстом каталога
Возврат или обмен LLM с правилами возврата
Жалоба LLM для урегулирования или оператор — в зависимости от сложности
Непонятно, что хочет клиент Оператор

Шаг 1. Вопросы: намерение и сложность

{
  "model": "jev-latest",
  "state": "Третий раз пишу! Кроссовки пришли на размер меньше, а кнопка обмена в приложении не работает.",
  "questions": {
    "intent": {
      "type": "choice",
      "instructions": "Какое основное намерение у клиента?",
      "criteria": {
        "order_status": "Спрашивает о существующем заказе",
        "product_question": "Спрашивает о товаре до покупки",
        "return_exchange": "Хочет вернуть или обменять товар",
        "complaint": "Недоволен сервисом и хочет решения",
        "other": null
      }
    },
    "complexity": {
      "type": "score",
      "instructions": "Насколько сложно решить этот запрос?",
      "criteria": [
        "Простой поиск или стандартная процедура",
        "Нужно суждение или несколько шагов",
        "Нестандартная ситуация, нужна эскалация"
      ]
    }
  }
}

Вариант other добавили мы: в примере TypeSafe его нет, но документация Choice советует держать такой вариант, если список может не покрыть все входы.

Шаг 2. Маршрутизация в коде

def route_ticket(ticket_id, r):
    intent = r.choices["intent"]
    complexity = r.scores["complexity"]

    if intent.confidence < 0.5 or intent.choice == "other":
        return route_to_human_agent(ticket_id)

    if intent.choice == "order_status":
        return handle_order_status(ticket_id)  # без LLM
    if intent.choice == "product_question":
        return handle_with_llm(ticket_id, PRODUCT_SPECIALIST)
    if intent.choice == "return_exchange":
        return handle_with_llm(ticket_id, RETURNS_SPECIALIST)

    # complaint: сложные или неясные случаи — человеку
    if complexity.score > 1 or complexity.confidence < 0.5:
        return route_to_human_agent(ticket_id)
    return handle_with_llm(ticket_id, COMPLAINT_RESOLUTION)

Одно намерение обрабатывается кодом вообще без LLM, два — разными профильными LLM, а для жалоб сложность решает, справится ли LLM или нужен человек. Обратите внимание на вторую проверку уверенности — по сложности. Если модель не уверена, насколько сложна жалоба, безопаснее считать её сложной.

На трёхуровневой шкале уровни нумеруются 0, 1 и 2, поэтому complexity.score > 1 значит «ближе к нестандартной ситуации». Пороги 0,5 взяты из примера TypeSafe. Свои подбирайте на размеченных диалогах — порядок описан в статье об уверенности Jev.

Контекст диалога

В чат-боте намерение часто понятно только из предыдущих реплик: «а можно сразу на размер больше?» без истории ничего не значит. Передавайте историю в state как объект с именованными полями и указывайте в вопросе, какую часть оценивать:

"state": {
  "history": [
    { "from": "client", "text": "Хочу вернуть кроссовки" },
    { "from": "bot", "text": "Подскажите номер заказа" }
  ],
  "last_message": "А можно сразу обменять на размер больше?"
}

Вопрос тогда звучит так: «Какое намерение у клиента в last_message с учётом history?». TypeSafe рекомендует ссылаться на части state по имени в обратных кавычках и класть в state только то, что нужно для решения: лишние детали отвлекают модель. Поэтому передавайте несколько последних реплик, а не всю переписку за месяц.

Как составить список намерений

  • Начните с реальных сообщений. Выгрузите несколько сотен обращений и разложите их вручную. Так вы увидите намерения, которые есть в потоке, а не те, что кажутся логичными.
  • Разводите похожие варианты. Если модель путает «правила возврата» и «статус возврата», опишите варианты объектами: что входит, что не входит, примеры. В примере TypeSafe с такими описаниями сообщение «Отправил обувь неделю назад, когда вернут деньги?» (в оригинале — на английском) получило return_status с уверенностью 1,0.
  • Не бойтесь длинного списка. Choice принимает до 255 вариантов, и каждый добавляет лишь несколько токенов. Для каталога из сотен намерений используйте два уровня: сначала группа, затем намерение внутри неё.
  • Задавайте уточняющие вопросы сразу. Причину возврата, номер заказа в тексте, просьбу позвать оператора добавьте в тот же запрос — это паттерн fan-out. Код прочитает только те ответы, которые нужны выбранной ветке.

Несколько просьб в одном сообщении

Choice выбирает одно намерение, но распределение показывает и второе. В примере из документации Choice обращение про опоздание посылки, неверный размер и двойное списание получило returns с вероятностью 0,61 и billing — 0,35. Код отдал тикет отделу возвратов, а копию отправил биллингу, потому что его доля превысила порог 0,25.

Если просьбы действительно разные, TypeSafe в демо умного дома поступает так: Noul-вопрос определяет, есть ли в запросе несколько отдельных действий, LLM разбивает его на простые команды, и каждая проходит через Jev отдельно.

Просьба позвать человека

Одну проверку удобнее держать отдельным Noul, а не вариантом в списке намерений: просит ли клиент живого оператора. Это желание не исключает других — клиент может хотеть и обмен, и человека. В документации TypeSafe такой вопрос дал 0,99 на сообщение «Я уже трижды спрашивал, можно просто поговорить с человеком?» и 0,40 на «Вы бот?», где клиент намекает, но прямо не просит (примеры в оригинале — на английском). Для средних значений нужен порог в коде: в примере TypeSafe к оператору уходят сообщения со значением выше 0,9.

Где нужна LLM

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

Jev пригодится и после LLM. В карте сценариев TypeSafe для поддержки есть проверка ответов на соответствие правилам и запросу клиента. Например, Noul «Обещает ли reply возврат, который не предусмотрен refund_policy?» с высоким значением отправляет черновик оператору, а не клиенту.

Если после намерения нужно заполнить аргументы функции, это тоже задача для Choice. В кукбуке Function calling TypeSafe превращает команду «сравни nvda, amd и msft за последние три месяца» (в оригинале — на английском) в вызов compare_returns(symbols=['NVDA', 'AMD', 'MSFT'], window='3mo'). Каждый аргумент с фиксированным набором значений становится отдельным вопросом, и все 54 вопроса уходят одним запросом. Уверенность вызова там считается по самому неуверенному аргументу: одного неверного аргумента достаточно, чтобы испортить результат. Подробнее о таких схемах — в статье о Jev для ИИ-агентов.

Особенности русскоязычного чата

  • Проверяйте точность на русских данных. По данным TypeSafe, основной язык обучения Jev — английский, другие языки модель понимает, но точность может быть ниже. Пороги, подобранные на английских примерах, для русского чата могут не подойти.
  • Ключи вариантов пишите латиницей. Коду проще сравнивать order_status, а модель видит и ключ, и описание. На каком языке описания работают лучше, TypeSafe не уточняет — сравните оба варианта на своих данных.
  • Задержка почти не растёт. Ответ Jev приходит за 70–500 мс, поэтому классификация почти не заметна на фоне генерации LLM или ответа оператора.

Что измерять

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

Когда классификатор стабилен, следующий шаг — каскад Jev → LLM с порогами под цену ошибки каждой ветки. Готовые схемы для поддержки собраны в статье о Jev в поддержке клиентов.

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

Чем Jev удобнее LLM для определения намерений?

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

Что делать, если в сообщении несколько просьб?

Добавьте Noul-вопрос о том, есть ли в сообщении несколько отдельных просьб. Если есть, LLM разбивает сообщение на простые команды, и каждая проходит через Jev отдельно — так устроено демо умного дома TypeSafe.

Работает ли Jev с русскоязычными чатами?

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

Источники