Определение намерений (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 советует проверять модель на своих данных и внимательно следить за уверенностью при маршрутизации.