NOVA: сортування квитків з Jev, правила Python та агент LangChain
Асистент підтримки першого рівня, який розділяє аналіз заявки з Jev, бізнес-правила на Python та складання чернетки агентом LangChain.
Клієнт пише: «Моя замовлення досі не надійшло. Вже три рази надсилали нагадування, а його потрібно отримати до завтра». Перш ніж написати відповідь, співробітник служби підтримки має вирішити кілька питань. До якої команди належить це звернення? Чи потрібно обробити його негайно? Чи виражає тон справжнє роздратування? І найголовніше — що саме можна перевірити щодо цього замовлення?
Один великий запит, наданий ШІ, міг би вирішити все. Інший підхід розділяє роботу на три етапи: спочатку 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
Єв виконується до початку обробки замовлення агентом, тож його виконання ніколи не залежить від можливого вибору інструменту з боку LLM. Потім програма обчислює орієнтацію, яка складається з обраного сервісу, пріоритету замовлення та необхідності людського перегляду. Ця орієнтація додається до контексту агента разом із двома інструментами для лише читання.
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 обирає категорію серед тих, які ми визначаємо. Тут їх чотири: доставка, рахування, технічна підтримка та інше. Кожна має опис, який уточнює, що має враховуватися під час класифікації. Посилка, яка запізнилася, потрапляє до категорії доставка, спірна оплата — до рахування, несправний пристрій — до технічної підтримки. Категорія «інше» призначена для сумнівних чи поза межами дії правил запитів, щоб не примушувати їх потрапляти до непідходящого сервісу.
Нове: запитання «так» чи «ні»
Показник «Новий» відображає ймовірність позитивної відповіді в межах від 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: отримання інформації, її доповнення та генерація — створіть пайплайн отримання та доповнення інформації з нуля за допомогою ембеддингів, косинусової схожості та відсортованого списку, а потім перевірте, де «іграшкова» версія припиняє працювати у масштабах.