NOVA: сортировка билетов с Jev, правила Python и агент LangChain
Ассистент поддержки первого уровня, который отвечает за анализ заявки вместе с Jev, настройку бизнес-правил на Python и составление черновика агентом LangChain.
Клиент пишет: «Моя заказная партия так и не прибыла. Уже три раза напоминали, а её нужно получить к завтрашнему дню». Прежде чем написать ответ, сотрудник службы поддержки должен решить несколько вопросов: к какой команде относится запрос? Необходимо ли обработать его немедленно? Выражает ли тон действительное раздражение? И самое главное, что можно действительно проверить относительно этого заказа?
Один большой запрос, переданный в LLM, мог бы решить всё сам. Другой подход предусматривает разделение работы на три этапа: Jev читает заявку и делает анализ, код на Python применяет корпоративные правила, а затем сотрудник LangChain с помощью своих инструментов собирает информацию и составляет черновик.
Проект, описанный здесь, называется NOVA. Это помощник первого уровня для поддержки Atelier Nova — вымышленного бренда, продающего канцелярские принадлежности. Полный код и ноутбук находятся в репозитории GitHub Langchain-Jev, а приведённые ниже фрагменты описывают только ключевые части. Чтобы всё запустить, достаточно открыть ноутбук и выполнить ячейки в указанном порядке.
Jev принимает решение, другая модель пишет
В этой схеме Jev, разработанный TypeSafe, принимает структурированные решения на основе ситуации, описанной на естественном языке. От него не требуется отвечать клиенту. Вопросы, которые ему задают, скорее выглядят так:
- Какой сервис должен принять заявку?
- Требуют ли сообщённые факты немедленной помощи?
- Какой уровень раздражения проявляет клиент?
Ответы поступают в специально определённые поля, которые программа считывает непосредственно. Генеративный LLM вмешивается только позже, чтобы изучить доступную информацию и подготовить соответствующий текст.
Преимущество такого разделения заключается в прозрачности правил. Изменение порога, запускающего повторную проверку человеком, означает модификацию условия на Python без необходимости переписывания всего запроса агента. Однако само по себе это не доказывает, что в целом процесс становится быстрее или дешевле. Только оценка всего рабочего процесса, включая вызовы Jev и LLM, позволит это утверждать.
Путь тикета до написания кода
Вот последовательность действий, которую проходит сообщение.
Message client + historique
↓
Jev : service, urgence, frustration
↓
Python : validation et règles métier
↓
Agent LangChain + LLM
↓
Consultation des commandes et procédures
↓
Dossier pour l’équipe support + brouillon de réponse
Жев выполняется до начала цикла агента, поэтому его выполнение никогда не зависит от возможного выбора инструмента, сделанного большой языковой моделью. Затем программа рассчитывает ориентир, состоящий из выбранного сервиса, приоритета заявки и необходимости человеческого рассмотрения. Этот ориентир добавляется в контекст агента рядом с двумя инструментами для чтения только.
NOVA не выполняет никаких действий: ни отправка сообщений, ни возврат средств, ни изменение заказа. Её роль заключается в подготовке работы человеческих агентов, и этот узкий диапазон действий является преднамеренным.
Настройка среды
В этом ноутбуке используются библиотеки langchain, langchain-core, langchain-openai, langchain-typesafe и python-dotenv. Интеграция с TypeSafe фиксирована на версии 0.0.1a3, поэтому для воспроизведения примера рекомендуется сохранять версии компонентов, использованных при установке.
Требуются два API-ключа, которые следует сохранить в файле .env:
TYPESAFE_API_KEY=ta_cle_typesafe
OPENAI_API_KEY=ta_cle_openai
JEV_MODEL=jev-latest
OPENAI_MODEL=gpt-4.1-mini
Этот файл никогда не должен публиковаться на GitHub, а ключи не должны появляться в результатах выполнения ноутбука. После загрузки переменных среды оба модели инициализируются следующим образом:
jev = TypeSafeClassifier(
model=os.getenv("JEV_MODEL") or "jev-latest",
timeout=30,
)
modele = ChatOpenAI(
model=os.getenv("OPENAI_MODEL") or "gpt-4.1-mini",
timeout=60,
max_retries=1,
)
Эти настройки относятся к ноутбуку, и нет никакой необходимости использовать этот LLM где-либо ещё. Псевдоним jev-latest также может меняться. Чтобы сравнивать результаты со временем, желательно сохранять точную версию модели, когда это возможно.
Три примитива: Choice, Noul и Score
Самый простой способ понять Jev — это исходить из задаваемых ему вопросов.
Choice: выбор сервиса
Choice выбирает категорию из тех, что были определены. Здесь их четыре: доставка, инвойс, техническая поддержка и прочее. Каждая категория сопровождается описанием, которое уточняет, что должно учитываться при классификации. Посылка, прибывшая с опозданием, относится к категории доставка, оспариваемая оплата — к категории инвойс, сломанный прибор — к категории технической поддержки. Категория «прочее» предназначена для запросов, которые неоднозначны или выходят за рамки определенных категорий, чтобы не заставлять их подходить под неподходящий сервис.
Noul: вопрос «да» или «нет»
Noul показывает вероятность положительного ответа, варьирующуюся от 0 до 1. Вопрос заключается в том, требует ли описанная ситуация немедленного вмешательства вместо обычного решения. В инструкции указано, что нельзя полагаться только на тон, поскольку клиент может быть крайне недоволен, но его ситуация не требует немедленных действий, в то время как спокойная просьба может оказаться срочной. Результат представляет собой оценку, основанную на предоставленном контексте. При отсутствии надежной информации о доставке он не указывает, прибудет ли посылка завтра.
Оценка: расположение уровня недовольства по шкале
С помощью функции Оценка определяются упорядоченные уровни:
- низкий уровень: отсутствие признаков недовольства;
- средний уровень: легкое раздражение, выражаемое сдержанно;
- высокий уровень: явное раздражение или множество претензий.
Используемая шкала охватывает диапазон от 0 до 2, при этом возможны десятичные значения: ответ не является просто меткой «спокоен» или «раздражен». Все три вопроса объединены в одно определение.
from langchain_typesafe import Choice, Noul, Score
def questions_triage():
return {
"service": Choice(
instructions=(
"Quel service doit traiter en priorité la dernière demande du client ? "
"Utilise le contexte seulement pour comprendre cette demande."
),
criteria={
"livraison": "Retard, suivi ou réception d'une commande.",
"facturation": "Paiement, facture ou remboursement.",
"technique": "Panne ou utilisation d'un produit.",
"autre": "Demande ambiguë ou sans rapport avec les catégories précédentes.",
},
),
"urgence": Noul(
instructions=(
"Les faits décrits nécessitent-ils une prise en charge immédiate, "
"plutôt qu'un traitement normal ? Ne te fonde pas seulement sur le ton."
)
),
"frustration": Score(
instructions="Quel niveau de frustration le client exprime-t-il ?",
criteria=[
"Le client s'exprime calmement, sans insatisfaction.",
"Le client exprime une insatisfaction tout en restant mesuré.",
"Le client exprime une forte colère ou des réclamations répétées.",
],
),
}
Каждый ключ (услуга, срочность, фрустрация) позволяет найти соответствующий ответ в коде. Оценка происходит на основе инструкций и связанных с ними критериев.
Первый тикет для анализа
Сообщение, используемое в ноутбуке, выглядит следующим образом: AN-1001 до сих пор не доставлен, это уже третья попытка, его нужно получить к завтрашнему дню, что можно сделать?
На этапе подготовки функция requete_triage собирает сообщение и его контекст, ограничивая его длину. Только последние три сообщения клиента из истории отправляются в Jev; при необходимости они обрезаются. Вызов и последующее чтение результатов выглядят примерно так.
reponse_jev = jev.invoke(requete_triage(ticket))
print("Service :", reponse_jev.choices["service"].choice)
print("Urgence :", reponse_jev.nouls["urgence"].noul)
print("Frustration sur 2 :", reponse_jev.scores["frustration"].score)
Результат, записанный в блокноте, выглядит следующим образом:
Service : livraison
Urgence : 0.76
Frustration sur 2 : 1.93
Эти значения взяты из сохранённой экспективации и ничего не гарантируют для последующих вызовов.
Ещё одно поле заслуживает внимания: уровень доверия. В первом примере ранг доставки имеет уровень доверия 1,0, что не означает, что устройство безошибочно. В TypeSafe это значение определяется распределением вероятностей среди возможных ответов. Оно показывает, насколько модель склоняется к тому или иному ответу, а не то, соответствует ли он фактам.
Бизнес-правила остаются в программе
Перед любым использованием результатов функция normaliser_analyse извлекает необходимые поля и проверяет, в частности, что соответствующий сервис действительно включен в процедуры, а уровень фрустрации соответствует установленной шкале. Результатом является легко читаемый словарь на Python. Оценка фрустрации не преобразуется в другую шкалу, и здесь нет ничего, что могло бы подтвердить корректность анализа.
Затем применяются следующие правила:
def orienter_ticket(analyse, seuil_urgence=0.8, seuil_confiance=0.6):
raisons = []
if analyse["urgence"] >= seuil_urgence:
raisons.append("urgence élevée")
if analyse["frustration"] >= 1.5:
raisons.append("forte frustration")
if analyse["confiance_service"] < seuil_confiance:
raisons.append("service incertain")
if analyse["service"] == "autre":
raisons.append("demande à clarifier") return {
"service": analyse["service"],
"priorite": "haute" if analyse["urgence"] >= seuil_urgence else "normale",
"revue_humaine": bool(raisons),
"raisons": raisons or ["traitement courant"],
}
Эти пороги носят учебный характер. Настоящая команда настроила бы их на выборке репрезентативных заявок, выявляя ошибки направления и прошлые срочные случаи, которые не были замечены. Результаты, полученные при полном тестировании, являются следующими:
{
"service": "livraison",
"priorite": "normale",
"revue_humaine": true,
"raisons": ["forte frustration"]
}
Таким образом, обычный приоритет может сосуществовать с ручной проверкой. Приоритет здесь определяется сигналом срочности, в то время как фактор разочарования может спровоцировать повторную проверку. Эти два аспекта обрабатываются отдельно по выбору.
Информация для ознакомления
Демонстрация основана на простом словаре неверных заказов. AN-1001 в нем обозначает офисную лампу, которая всё ещё находится в пути и задерживается на три дня. У оператора есть два инструмента:
- consulter_commande(numero) — находит заказ по его номеру;
- consulter_procedure(service) — читает процедуру, связанную с выбранным сервисом.
При доставке необходимо проверять статус заказа, при возникновении задержек предлагать логистической команде провести проверку и ни в коем случае не устанавливать вымышленные даты доставки. При формировании счетов любая компенсация проходит через проверку человека. С технической точки зрения сотрудник должен указать соответствующий продукт, выявленные неисправности и уже проведенные тесты.
Инструкция NOVA описывает его задачи и определяет формат вывода: краткое описание проблемы, проверенная или отсутствующая информация, рекомендуемые дальнейшие действия и черновик ответа. Однако указание в инструкции о запрете на компенсацию недостаточно для защиты платежного инструмента. Поэтому ограничение также является техническим: сотрудник не имеет доступа к никаким инструментам для выплаты компенсаций.
Сборка агента LangChain
Функция create_agent принимает LLM, инструменты и промпт, дополненный результатами анализа Jev и рассчитанными направлениями.
def creer_agent(modele, analyse, orientation):
contexte = json.dumps(
{"analyse_jev": analyse, "orientation": orientation},
ensure_ascii=False,
)
return create_agent(
model=modele,
tools=[consulter_commande, consulter_procedure],
system_prompt=(
ROLE_NOVA
+ "\nContexte de traitement fourni par le programme :\n"
+ contexte
),
)
Анализ и определение направления не являются параметрами, присущими функции create_agent, поскольку эта пользовательская функция включает их в базовые инструкции. Функция traiter_ticket координирует весь процесс: она вызывает Jev, нормализует результаты анализа, рассчитывает направление действий, создаёт агента и запускает его цикл с ограничением по глубине рекурсии.
Код задаёт порядок выполнения шагов, но вызов инструментов консультаций во время цикла остаётся на усмотрение LLM. Если в реальных условиях использование таких консультаций становится обязательным, лучше включить их в рабочий процесс, чем полагаться лишь на простое указание.
Результаты тестов
В журнале, сохранённом для первой заявки, указано, что сотрудник использовал функции «consulter_commande», затем «consulter_procedure». Он идентифицировал лампу, установил, что она находится в пути, и отметил трёхдневную задержку. Он рекомендовал провести проверку у отдела логистики, не указывая вымышленной даты доставки, а затем подготовил черновик для команды поддержки.
В этом черновике просится его перечитать. Фраза вроде «мы передадим вашу заявку» заставляет команду приступать к действию, которое программа не выполнила, и рекомендация не заменяет фактическую передачу.
Вторая попытка касается запроса на отслеживание, когда клиент спрашивает, какой товар входит в эту заявку. Поскольку он не повторяет номер заказа, передаётся история первой обработки.
suivi = traiter_ticket(
"Quel article contient cette commande ?",
modele,
jev,
historique=dossier["messages"],
)
print(suivi["reponse"])
В записанном ответе встречается ссылка AN-1001 без повторения клиентом, и в ответе указана настольная лампа. Никакая постоянная память не появилась сама по себе: программа просто повторила предыдущие сообщения. Для другого клиента необходимо начинать с отдельной истории, поскольку смешивание разговоров может привести к утечке информации от одного человека к другому.
Перед реальным внедрением
Демо-версия работает с вымышленными данными и ограничивается черновиками. Перед подключением её к реальному носителю необходимо выполнить несколько шагов:
- оценить обработку заявок с примечаниями, включая текст на французском языке, а затем определить дальнейшие действия в случае, если Jev или LLM не отвечают;
- проверить, кто имеет право читать какие команды, и разделить разговоры между пользователями;
- сокращать до минимума личные данные, передаваемые в модели;
- сохранять полезные записи без разглашения ключей API.
Флаг revue_humaine, установленный в значение true, здесь служит лишь информацией для агента. Если устройство действительно отправляет сообщения или выполняет важные операции, эта информация должна запускать реальный процесс утверждения.
Основной принцип, который следует запомнить сейчас, — это разделение задач. Структурированный анализ выполняется инструментом Jev, решение о его применении принимает Python, а LLM занимается составлением ответа с использованием этих инструментов. Чтобы углубиться, достаточно добавить различные случаи: счета-фактуры, неоднозначные запросы, вежливые, но настойчивые сообщения. Необходимо проанализировать классификацию и убедиться, что правила приводят к ожидаемой обработке, что позволяет выявить как возможности агента, так и то, что требует корректировки.
Связанные материалы
- Мини-пайплайн RAG из 90 строк кода на Python: получение данных, их дополнение и генерация — создание системы генерации с дополнением данных на основе эмбеддингов, косинусного сходства и ранжированного списка, а также анализ того, где у упрощенной версии возникают проблемы при масштабировании.