Галоўная / Артыкулы

NOVA: сортаванне квітакоў з Jev, правілы Python і агент LangChain

Памочнік апаратнага супарта 1-го рангу, які разлічвае аналіз заявкі з Евам, бізнес-правіллі на Python і падготавленне чэрнавіка агентам LangChain.

Выдавец: REACTAPP.TOP10 хв чытання2331 слоў

Крыніцы: Trier les tickets avec Jev, appliquer ses règles en Python et laisser le LLM préparer la réponse : un tutoriel concret avec NOVA.

Адэн кліент пісае: «Мая замовка яшчэ не прыбыла. Яўся ўжо трохі папяркавання, а яе трэба атрымаць завтра». Перш чым напісаць адказ, агент службы падтрымкі павінен развярнуць калькольнік кількох пытанняў. Калькольніку якой каманды належыць гэта запітанне? Чы трэба яго адрабатваць без чакання? Чы тон выказвае справжню розчароўку? І, галоўнае, што на самай працоўцы можна рэальна пераканаць ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўжо ўж

Праект, які опісаны тут, называецца 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 выбірае катэгорыю з тых, якія былі заданы. Ёсць чатыры такія катэгорыі: доставка, рахункуванне, тэхнічная падтрымка і іншыя. Кожная з іх мае апісанне, якое чытальна паводлівае тое, што павінна включаць тая катэгорыя. Пакет, які запаздзіў, патрапляе пад катэгорыю доставка, спорны платэж — пад катэгорыю рахункуванне, а апарат, які не працуе, — пад катэгорыю тэхнічной падтрымкі. Катэгорыя «іншыя» прымае запіты, якія ёсць неясныя або не падпадаюць пад практычныя правіла, замест таго, каб прымусіць іх пацелавацца ў непадходзячую службу.

Новае: запітанне «так» або «нет»

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 гэта значэнніе вылічваецца на адной з аснов распаду верыятнасцяў між можлівымі адпаведзямі. Яно паказвае, насколькі модэль прагтвае адпаведзь, а не тое, чы рэальнае становішча падтрымлівае гэта адпаведзь.

Бізнес-правілы застаюцца ў програме

Першым крокам пад час вікорыстоўвання рэзультатаў являецца функцыя normalizer_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 займаецца напісаннем адпаведзі даўжынай, спынюючыся на інструментах. Ёсць можлівасць працаваць далей, прыўязаваючы разныя кейсы: квітанцыі, неясныя запыткі, ввічлівыя, але натыснутыя паведамленні. Неабходна адзірваць класыфікацыю і пераканацца, што правілы веду да апэктыўнай обработкі, што паказвае як можымасці агента, так і тое, што застаецца выправіць.

Спадневаная літэратура

  • Агент локальнай падтрымкі з SmolLM3: рэжым раздумаў, інструменты і захоўнікі — Адрабатка 3B-модэлю на ноутбуку як агента падтрымкі: калі вжываць рэжым раздумаў чы no_think, як працюе вызыв інструментаў, і захоўнікі, якія запобегаюць вызывам інструментаў-заместакоў.