NOVA: trier karty z Jevem, reguły Pythona i agent LangChain
Asystent wsparcia poziomu 1, który oddziela analizę zgłoszenia od Jev, zasady biznesowe w Pythonie oraz przygotowanie szkicu przez agenta LangChain.
Klient pisze: „Moja zamówienie wciąż nie dotarło. Było już trzy powiadomienia, a potrzebne jest je jutro”. Zanim agent obsługi klienta napisze odpowiedź, musi rozstrzygnąć kilka kwestii. Do której grupy należy ta prośba? Czy należy ją obsłużyć natychmiast? Czy ton wskazuje na prawdziwe zirytowanie? A przede wszystkim, co można faktycznie sprawdzić dotyczące tego zamówienia?
Jeden duży prompt przekazany modelowi LLM mógłby załatwić wszystko. Inny podejście dzieli pracę na trzy etapy: Jev czyta zgłoszenie i sporządza analizę, kod Python stosuje zasady firmy, a następnie agent LangChain używa swoich narzędzi, by znaleźć informacje i stworzyć szkic odpowiedzi.
Projekt opisany tutaj nazywa się NOVA. Jest to asystent pierwszego poziomu do obsługi Atelier Nova, fikcyjnej marki sprzedającej akcesoria biurowe. Pełny kod oraz notebook znajdują się w repozytorium GitHub Langchain-Jev, a poniższe fragmenty wyjaśniają jedynie najważniejsze części. Aby wszystko uruchomić, wystarczy otworzyć notebook i wykonać komórki w odpowiedniej kolejności.
Jev decyduje, inny model pisze
W tym rozwiązaniu Jev, opracowany przez TypeSafe, podejmuje ustrukturyzowane decyzje na podstawie sytuacji opisanej w języku naturalnym. Nie jest od niego wymagana żadna odpowiedź skierowana do klienta. Pytania, które mu zadaje się, wyglądają raczej w ten sposób:
- Jaki dział powinien przyjąć zgłoszenie?
- Czy zgłoszone fakty wymagają natychmiastowej interwencji?
- Jaki poziom frustracji wyraża klient?
Odpowiedzi przychodzą do specjalnie zdefiniowanych pól, które program odczytuje bezpośrednio. Generatywny LLM interweniuje dopiero później, aby sprawdzić dostępne informacje i przygotować odpowiedni tekst.
Zaletą takiego rozdzielenia jest widoczność zasad. Zmiana progu wyzwalającego ponowną weryfikację przez człowieka oznacza modyfikację warunku w Pythonie, bez konieczności przepisywania całego promptu agenta. Z drugiej strony nic samodzielnie nie potwierdza, że całość staje się szybsza lub tańsza. Tylko pomiar całego procesu pracy, włączając wezwanie do Jeva oraz interakcje z LLM, pozwoliłby to stwierdzić.
Ścieżka ticketu przed napisaniem kodu
Oto ścieżka, którą przebywa wiadomość.
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
Jev jest wykonywany przed procesem obsługi przez agenta, dzięki czemu jego realizacja nigdy nie zależy od ewentualnego wyboru narzędzia dokonanego przez LLM. Następnie program oblicza orientację składającą się z wybranego usługi, priorytetu zgłoszenia oraz konieczności przeglądu przez człowieka. Ta orientacja jest umieszczana w kontekście agenta, obok dwóch narzędzi do jedynie odczytu.
NOVA nie jest odpowiedzialna za żadne działania: ani za wysyłanie wiadomości, ani za zwrot pieniędzy, ani za modyfikację zamówienia. Jej rolą jest przygotowanie pracy ludzkich agentów, a ten wąski zakres jest celowy.
Ustawianie środowiska
Notbook opiera się między innymi na langchain, langchain-core, langchain-openai, langchain-typesafe oraz python-dotenv. Integracja z TypeSafe jest zablokowana na wersji 0.0.1a3, dlatego lepiej zachować wersje pliku instalacyjnego, aby móc odtworzyć przykład.
Wymagane są dwa klucze API, które należy umieścić w pliku .env:
TYPESAFE_API_KEY=ta_cle_typesafe
OPENAI_API_KEY=ta_cle_openai
JEV_MODEL=jev-latest
OPENAI_MODEL=gpt-4.1-mini
Ten plik nie powinien nigdy być publikowany na GitHubie, a klucze nie powinny pojawiać się w wynikach wykonywania notebooka. Po załadowaniu zmiennych środowiskowych oba modele są inicjalizowane w następujący sposób:
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,
)
To są ustawienia notebooka i nie ma żadnego obowiązku używania tego LLM gdzie indziej. Alias jev-latest również może ulegać zmianom. Aby móc porównywać wyniki w czasie, lepiej jest zachować dokładną wersję modelu, gdy to możliwe.
Trzy elementy bazowe: Choice, Noul i Score
Najprostszym sposobem na zrozumienie Jev jest rozpoczęcie od pytań, które mu zadaje się.
Choice: wybór usługi
Choice wybiera kategorię spośród tych, które zostały zdefiniowane. Tutaj jest ich cztery: dostawa, fakturacja, obsługa techniczna i inne. Każda z nich ma opis, który określa, co powinno być uwzględnione w klasyfikacji. Przesłana z opóźnieniem paczka trafia do kategorii dostawa, sporna płatność – do fakturacji, a zepsuty sprzęt – do obsługi technicznej. Kategoria inne obejmuje prośby niejasne lub wykraczające poza zakres, zamiast zmuszać je do umieszczenia w usłudze, która im nie odpowiada.
Noul: pytanie typu tak albo nie
Noul wskazuje prawdopodobieństwo pozytywnej odpowiedzi, w zakresie od 0 do 1. Pytanie brzmi, czy opisane fakty wymagają natychmiastowej interwencji zamiast zwykłego rozpatrzenia sprawy. Instrukcja podkreśla, że nie należy polegać wyłącznie na tonie, ponieważ klient może być bardzo niezadowolony, choć jego sytuacja nie wymaga natychmiastowej pomocy, natomiast spokojna prośba może być wręcz pilna. Wynik pozostaje szacunkiem opartym na dostarczonym kontekście. Nie podaje on, czy paczka dotrze jutro, dopóki brakuje wiarygodnych informacji dotyczących dostawy.
Score: umieszczenie poziomu frustracji na skali
Dzięki Score definiowane są uporządkowane poziomy:
- poziom niski: brak oznak niezadowolenia;
- poziom średni: lekki niepokój, wyrażany powściągliwie;
- poziom wysoki: wyraźna złość lub narastające skargi.
Używana skala obejmuje zakres od 0 do 2, przy czym możliwe są wartości dziesiętne: odpowiedź nie jest prostym oznaczeniem spokoju lub złości. Te trzy pytania zostały połączone w jedną definicję.
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.",
],
),
}
Każdy klucz (usługa, pilność, frustracja) umożliwia znalezienie odpowiedniej odpowiedzi w kodzie. To, co jest oceniane, odbywa się w instrukcjach oraz powiązanych kryteriach.
Pierwszy ticket do analizy
Wiadomość użyta w notatniku brzmi następująco: AN-1001 nadal nie zostało dostarczone, to już trzecie wezwanie, potrzebne jest to do jutra, co można zrobić?
W ramach przygotowań funkcja requete_triage łączy wiadomość z jej kontekstem, ograniczając jednocześnie jej długość. Tylko trzy ostatnie wiadomości klienta z historii trafiają do Jev, w razie potrzeby skracane. Wywołanie oraz odczytanie wyników wyglądają następująco.
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)
Wynik zapisany w notatniku jest następujący:
Service : livraison
Urgence : 0.76
Frustration sur 2 : 1.93
Te wartości pochodzą z zapisanej eksploatacji i nie gwarantują nic w odniesieniu do kolejnych wywołań.
Kolejne pole wymaga uwagi: zaufanie. W pierwszym przykładzie ranking dostaw otrzymuje wartość zaufania 1,0, co nie oznacza, że urządzenie jest nieomylnie poprawne. W TypeSafe ta wartość wynika z rozkładu prawdopodobieństw pomiędzy możliwymi odpowiedziami. Wskazuje ona, w jakim stopniu model faworyzuje daną odpowiedź, a nie to, czy została ona zweryfikowana z faktami.
Zasady biznesowe pozostają w programie
Zanim zostaną wykorzystane wyniki, funkcja normalizować_analizę izoluje niezbędne pola i sprawdza między innymi, czy usługa rzeczywiście występuje w procedurach oraz czy poziom frustracji odpowiada ustalonej skali. Rezultatem jest łatwy do odczytania słownik w Pythonie. Wynik dotyczący frustracji nie jest konwertowany na inną skalę, a nic tutaj nie potwierdza, że analiza jest dokładna.
Następnie stosuje się następujące zasady:
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"],
}
Progi te mają charakter edukacyjny. Prawdziwy zespół dostosowałby je na podstawie reprezentatywnego przykładu zgłoszeń, identyfikując błędy kierowania oraz sytuacje pilne, które pozostały niezauważone. Otrzymana wartość orientacyjna po pełnym teście została zapisana w następujący sposób:
{
"service": "livraison",
"priorite": "normale",
"revue_humaine": true,
"raisons": ["forte frustration"]
}
Zwykła priorytetowość może więc współistnieć z przeglądem przez człowieka. Priorytet zależy tutaj od sygnału awaryjnego, natomiast frustracja może wywołać ponowną weryfikację. Obie te kwestie są traktowane oddzielnie zgodnie z wyborem.
Informacje do sprawdzenia
Demonstracja opiera się na prostym słowniku błędnych zamówień. AN-1001 oznacza w nim lampę biurową, która wciąż jest w transporcie i ma trzydniowy opóźnienie. Pracownik ma do dyspozycji dwa narzędzia:
- consulter_commande(numero) pozwala odnaleźć zamówienie na podstawie jego numeru referencyjnego;
- consulter_procedure(service) odczytuje procedurę związaną z wybraną usługą.
Procedura dostawy wymaga sprawdzania statusu, proponowania weryfikacji przez zespół logistyczny w przypadku opóźnień oraz nigdy nieudawania daty dostawy. W zakresie fakturowania każda zwrotka przechodzi przez ludzką weryfikację. Pod względem technicznym agent musi odnotować daną produkt, stwierdzoną usterkę oraz już przeprowadzone testy.
Instrukcja NOVA opisuje jej zadanie i określa format wyjściowy: streszczenie trudności, sprawdzone lub brakujące informacje, zalecane dalsze kroki oraz szkic odpowiedzi. Jednak samo zapisanie w instrukcji zakazu zwrotów nie wystarcza do ochrony narzędzia płatniczego. Ograniczenie to ma więc również charakter fizyczny: agent nie ma dostępu do żadnych narzędzi do dokonywania zwrotów.
Konfiguracja agenta LangChain
Funkcja create_agent otrzymuje LLM, narzędzia oraz prompt wzbogacony analizą Jev i obliczoną orientacją.
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
),
)
Analiza i orientacja nie są zatem parametrami właściwymi funkcji create_agent, ponieważ ta funkcja wbudowuje je w podstawowe instrukcje. Funkcja traiter_ticket koordynuje cały proces: wywołuje Jev, normalizuje analizę, oblicza orientację, tworzy agenta i uruchamia jego pętlę z określoną granicą rekurencji.
Kod określa kolejność kroków, ale wywołanie narzędzi konsultacyjnych w trakcie pętli pozostaje w gestii LLM. Jeśli konsultacja ma stać się obowiązkowa w środowisku produkcyjnym, lepiej jest włączyć ją do procesu pracy, zamiast polegać jedynie na prostym poleceniu.
To, co pokazują testy
Dziennik przechowywany dla pierwszej faktury pokazuje, że pracownik użył funkcji consulter_commande, a następnie consulter_procedure. Zidentyfikował lampę, stwierdził, że znajduje się w transporcie, i odnotował trzydniowy opóźnienie. Zalecił weryfikację przez dział logistyki bez ustalania fałszywej daty dostawy, a następnie przygotował szkic dla zespołu obsługi klienta.
W tym szkicu prosi się o ponowne przeczytanie. Sformułowanie takie jak „prześlemy państwa sprawę” zobowiązuje zespół do działania, którego program nie wykonał, a zalecenie nie równa się faktycznemu przekazaniu.
Druga próba dotyczy żądania śledzenia, w którym klient pyta, jaki produkt jest zawarty w tej zamówieniu. Nie powtarza on numeru referencyjnego, dlatego przekazywana jest historia pierwszej obsługi.
suivi = traiter_ticket(
"Quel article contient cette commande ?",
modele,
jev,
historique=dossier["messages"],
)
print(suivi["reponse"])
W zapisanej rozmowie znajduje się odniesienie AN-1001, bez konieczności powtarzania go przez klienta, a odpowiedź wskazuje na lampę biurową. Nie pojawiła się żadna trwała pamięć – program po prostu odtworzył poprzednie wiadomości. W przypadku innego klienta trzeba zacząć od odrębnego historii rozmów, ponieważ mieszanie rozmów narażałoby informacje jednej osoby na inną.
Przed prawdziwym wdrożeniem
Demo pracuje na danych fikcyjnych i ogranicza się do szkiców. Zanim podłączy się ją do rzeczywistego urządzenia, konieczne jest przeprowadzenie kilku kroków:
- ocena sortowania na adnotowanych ticketach, w tym w języku francuskim, a następnie decyzja o zachowaniu, które należy przyjąć, jeśli Jev lub LLM nie odpowie;
- sprawdzenie, kto ma prawo czytać które polecenia, oraz oddzielenie rozmów od siebie;
- zmniejszyć do absolutnego minimum dane osobowe przekazywane do modeli;
- przechowywać użyteczne ślady bez ujawniania kluczy API.
Flaga revue_humaine ustawiona na true służy tu jedynie jako informacja przekazywana agentowi. Jeśli urządzenie rzeczywiście wysyła wiadomości lub wykonywa operacje wrażliwe, ta informacja musi uruchomić faktyczny proces zatwierdzenia.
Zasada, którą należy zapamiętać już teraz, to podział zadań. Strukturalna analiza pochodzi od Jev, decyzja o jej zastosowaniu należy do Pythona, a LLM zajmuje się pisaniem odpowiedzi, korzystając z dostępnych narzędzi. Aby posunąć się dalej, wystarczy dodać różnorodne przypadki: faktury, niejednoznaczne prośby, uprzejme, ale pilne wiadomości. Należy sprawdzić sortowanie i upewnić się, że zasady prowadzą do oczekiwanego przetwarzania, co ujawnia zarówno możliwości agenta, jak i to, co wymaga korekty.