Startseite / Artikel

NOVA: Ticket-Filter mit Jev, Python-Regeln und ein LangChain-Agent

Ein Support-Assistent der Stufe 1, der die Analyse des Tickets zusammen mit Jev, die Geschäftsregeln in Python sowie die Erstellung eines Entwurfs durch einen LangChain-Agenten übernimmt.

Verlag: REACTAPP.TOP10 Min. Lesezeit2331 Wörter

Quellen: 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.

Ein Kunde schreibt: „Meine Bestellung ist immer noch nicht angekommen. Es gab bereits drei Nachfragen, und sie muss bis morgen da sein.“ Bevor ein Support-Mitarbeiter eine Antwort verfasst, muss er mehrere Fragen klären. Zu welchem Team gehört der Antrag? Soll er sofort bearbeitet werden? Zeigt der Tonfall echte Frustration? Und vor allem: Was kann man tatsächlich über diese Bestellung überprüfen?

Ein einziger umfangreicher Prompt an ein LLM könnte alles bewältigen. Ein anderer Ansatz teilt die Arbeit in drei Schritte auf: Zuerst liest Jev das Ticket und erstellt eine Analyse, anschließend wendet ein Python-Code die Unternehmensregeln an, und schließlich sucht ein LangChain-Agent mit seinen Werkzeugen nach Informationen und erstellt einen Entwurf.

Das hier beschriebene Projekt heißt NOVA. Es handelt sich um einen ersten Schritt in Richtung eines Assistenten für den Support von Atelier Nova, einer erfundenen Marke, die Bürobedarf verkauft. Der vollständige Code sowie das Notebook befinden sich im GitHub-Repo Langchain-Jev, wobei die folgenden Auszüge nur die zentralen Teile erläutern. Um alles auszuführen, genügt es, das Notebook zu öffnen und die Zellen in der richtigen Reihenfolge auszuführen.

Jev entscheidet, ein anderes Modell formuliert

In dieser Konfiguration trifft Jev, entwickelt von TypeSafe, strukturierte Entscheidungen auf der Grundlage einer in natürlicher Sprache beschriebenen Situation. Es wird von ihm keine Antwort an den Kunden erwartet. Die ihm gestellten Fragen lauten eher so:

  • Welcher Service soll den Ticket bearbeiten?
  • Erfordern die gemeldeten Fakten eine sofortige Bearbeitung?
  • Welchen Grad an Frustration zeigt der Kunde?

Die Antworten kommen in vorgegebenen Feldern, die vom Programm direkt gelesen werden. Das generative LLM greift erst danach ein, um die verfügbaren Informationen abzurufen und einen passenden Text zu erstellen.

Der Vorteil dieser Trennung liegt in der Transparenz der Regeln. Das Ändern der Schwelle, die eine menschliche Überprüfung auslöst, bedeutet lediglich die Anpassung einer Python-Bedingung, ohne den gesamten Prompt des Agents umschreiben zu müssen. Allerdings beweist allein das nichts dafür, dass das Gesamtsystem schneller oder kostengünstiger wird. Nur eine Messung des gesamten Workflows, einschließlich der Aufrufe an Jev und die LLMs, könnte dies bestätigen.

Der Weg eines Tickets, bevor Code geschrieben wird

Hier ist der Ablauf, den eine Nachricht durchläuft.

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

Die Ausführung erfolgt vor dem Prozess der Agentenverarbeitung, sodass sie niemals von einer möglichen Werkzeugwahl durch den LLM abhängt. Anschließend berechnet das Programm eine Orientierung, die aus dem gewählten Service, der Priorität des Tickets sowie der Notwendigkeit einer menschlichen Überprüfung besteht. Diese Orientierung wird im Kontext des Agenten zusammen mit zwei nur zum Lesen bestimmten Werkzeugen bereitgestellt.

NOVA erhält keine Aufgaben: weder das Versenden von Nachrichten noch Rückerstattungen oder Änderungen von Bestellungen. Ihre Rolle besteht darin, die Arbeit der menschlichen Agenten vorzubereiten, und dieser enge Aufgabenbereich ist absichtlich so festgelegt.

Einrichtung der Umgebung

Das Notebook stützt sich insbesondere auf langchain, langchain-core, langchain-openai, langchain-typesafe und python-dotenv. Die Integration von TypeSafe ist auf Version 0.0.1a3 festgelegt, und es ist ratsam, die Versionen der Installationsszelle beizubehalten, um das Beispiel nachzuvollziehen.

Zwei API-Schlüssel sind erforderlich und sollten in einer .env-Datei gespeichert werden:

TYPESAFE_API_KEY=ta_cle_typesafe
OPENAI_API_KEY=ta_cle_openai
JEV_MODEL=jev-latest
OPENAI_MODEL=gpt-4.1-mini

Diese Datei darf auf keinen Fall auf GitHub veröffentlicht werden, und die Schlüssel dürfen nicht in den Ausgaben des Notebooks erscheinen. Nachdem die Umgebungsvariablen geladen wurden, werden die beiden Modelle wie folgt initialisiert:

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,
)

Diese Einstellungen beziehen sich auf den Notebook, und es gibt keine Verpflichtung, dieses LLM an einem anderen Ort zu verwenden. Der Alias jev-latest kann sich ebenfalls ändern. Um Ergebnisse im Laufe der Zeit zu vergleichen, ist es besser, möglichst die genaue Version des Modells beizubehalten.

Drei Primitiven: Choice, Noul und Score

Am einfachsten ist es, Jev zu verstehen, wenn man von den gestellten Fragen ausgeht.

Choice: Ein Service auswählen

Choice wählt eine Kategorie aus denen aus, die definiert werden. Hier gibt es insgesamt vier: Lieferung, Rechnungswesen, Technik und Sonstiges. Jede Kategorie enthält eine Beschreibung, die genau angibt, was in die Klassifizierung aufgenommen werden soll. Ein verspäteter Paket geht in die Kategorie Lieferung, eine umstrittene Zahlung in das Rechnungswesen, ein defektes Gerät in den technischen Support. Die Kategorie Sonstiges umfasst unklare oder außerhalb des Rahmens fallende Anfragen, anstatt sie in einen nicht geeigneten Service zu zwingen.

Noul: eine Ja-oder-Nein-Frage

Noul gibt die Wahrscheinlichkeit für eine positive Antwort an, zwischen 0 und 1. Die gestellte Frage ist, ob die beschriebenen Umstände eine sofortige Behandlung statt einer normalen Bearbeitung erfordern. Die Anweisung besagt, man solle sich nicht allein auf den Ton verlassen, denn ein Kunde kann sehr unzufrieden sein, ohne dass seine Situation eine sofortige Intervention erfordert, während eine ruhige Anfrage durchaus dringend sein kann. Das Ergebnis bleibt eine Schätzung, die aus dem bereitgestellten Kontext abgeleitet wird. Solange zuverlässige Informationen zur Lieferung fehlen, gibt es keine Angabe darüber, ob ein Paket morgen ankommt.

Score: Die Frustration auf einer Skala einordnen

Mit Score werden geordnete Ebenen definiert:

  • Niedriges Niveau: Keine Anzeichen von Unzufriedenheit;
  • Mittleres Niveau: Leichte Verärgerung, zurückhaltend ausgedrückt;
  • Hohes Niveau: Ausprägter Ärger oder sich häufende Beschwerden.

Die verwendete Skala umfasst den Bereich von 0 bis 2, wobei Dezimalwerte möglich sind: Die Antwort ist keine einfache Einordnung als ruhig oder verärgert. Die drei Fragen werden in einer einzigen Definition zusammengefasst.

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.",
            ],
        ),
    }

Jeder Schlüssel (Service, Dringlichkeit, Frustration) ermöglicht es, die entsprechende Antwort im Code zu finden. Was bewertet wird, ergibt sich aus den Anweisungen sowie den damit verbundenen Kriterien.

Ein erstes Ticket zur Analyse

Die im Notebook verwendete Nachricht lautet: AN-1001 wurde immer noch nicht geliefert, es handelt sich um die 3. Nachfrage – es ist bis morgen notwendig, was kann man tun?

Was die Vorbereitung angeht, so assembliert requete_triage die Nachricht sowie ihren Kontext und begrenzt dabei ihre Länge. Nur die letzten drei Nachrichten des Kunden aus der Historie werden an Jev gesendet, gegebenenfalls gekürzt. Der Aufruf sowie das Lesen der Ergebnisse sehen ungefähr so aus.

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)

Der in dem Notizbuch gespeicherte Ausgang lautet:

Service : livraison
Urgence : 0.76
Frustration sur 2 : 1.93

Diese Werte stammen von einer gespeicherten Ausführung und garantieren nichts für zukünftige Aufrufe.

Ein weiteres Feld verdient Aufmerksamkeit: das Vertrauen. Im ersten Beispiel erhält die Lieferbewertung ein Vertrauensniveau von 1,0, was nicht bedeutet, dass das Gerät fehlerfrei ist. Bei TypeSafe ergibt sich dieser Wert aus der Verteilung der Wahrscheinlichkeiten zwischen den möglichen Antworten. Er zeigt an, inwieweit das Modell eine bestimmte Antwort bevorzugt, nicht jedoch, ob diese mit den Fakten übereinstimmt.

Die Geschäftsregeln bleiben im Programm

Vor jeder Nutzung der Ergebnisse isoliert normalisieren_analyse die notwendigen Felder und überprüft insbesondere, ob der Dienst tatsächlich in den Verfahren aufgeführt ist und ob der Frustrationsgrad der vorgesehenen Skala entspricht. Das Ergebnis ist ein leicht lesbares Python-Dictionary. Der Frustrationswert wird nicht in eine andere Skala umgerechnet, und es gibt hier nichts, was beweist, dass die Analyse korrekt ist.

Anschließend werden die Regeln angewendet:

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"],
    }

Die Schwellenwerte sind pädagogischer Natur. Ein echtes Team würde sie anhand einer repräsentativen Stichprobe von Tickets kalibrieren, wobei Fehlrichtungen und bisher übersehene Notfälle erfasst werden. Die bei der vollständigen Testung ermittelte Ausrichtung ist wie folgt:

{
  "service": "livraison",
  "priorite": "normale",
  "revue_humaine": true,
  "raisons": ["forte frustration"]
}

Eine normale Priorität kann somit mit einer menschlichen Überprüfung koexistieren. Die Priorität hängt hier vom Notfallsignal ab, während Frustration eine erneute Überprüfung auslösen kann. Beide Dimensionen werden ausgewählt getrennt behandelt.

Informationen zum Nachschlagen

Die Demonstration stützt sich auf ein einfaches Wörterbuch falscher Bestellungen. AN-1001 bezeichnet dort eine Schreibtischlampe, die sich noch in Transport befindet und drei Tage im Verzug ist. Der Mitarbeiter verfügt über zwei Werkzeuge:

  • consulter_commande(numero) findet eine Bestellung anhand ihrer Referenz;
  • consulter_procedure(service) liest die dem gewählten Service zugehörige Verfahrensanweisung.

Beim Lieferprozess ist es notwendig, den Status zu überprüfen, im Falle einer Verzögerung eine Überprüfung durch das Logistikteam anzubieten und niemals eine fiktive Lieferdatum festzulegen. Bei der Rechnungsstellung erfolgt jede Rückerstattung durch eine menschliche Freigabe. Technisch gesehen muss der Agent das betroffene Produkt, die festgestellte Störung sowie die bereits durchgeführten Tests aufzeichnen.

Der Prompt von NOVA beschreibt seine Aufgabe und legt ein Ausgabeformat fest: Zusammenfassung der Schwierigkeit, überprüfte oder fehlende Informationen, empfohlene nächste Aktion sowie ein Entwurf der Antwort. Doch allein die Angabe im Prompt, dass Rückerstattungen verboten sind, reicht nicht aus, um ein Zahlungstool zu schützen. Die Beschränkung ist daher auch physisch: Kein Rückerstattungstool steht dem Agenten zur Verfügung.

Zusammenstellung des LangChain-Agenten

Die Funktion create_agent erhält das LLM, die Tools sowie einen durch die Analyse von Jev und die berechnete Ausrichtung bereicherten Prompt.

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
        ),
    )

Die Analyse und die Ausrichtung sind daher keine Eigenschaften von create_agent, da die eigene Funktion sie in die Grundanweisungen einbaut. Die Funktion behandeln_ticket koordiniert alles: Sie ruft Jev auf, normalisiert die Analyse, berechnet die Ausrichtung, erstellt den Agenten und startet dessen Schleife mit einer Rekursionsschranke.

Der Code vorgibt die Reihenfolge der Schritte, doch der Aufruf der Abfruchtmittel während der Schleife bleibt dem LLM überlassen. Falls eine Abfrage in der Produktion verpflichtend werden soll, ist es besser, sie in den Workflow aufzunehmen, anstatt sich auf eine einfache Anweisung zu verlassen.

Was die Tests zeigen

Das für die erste Rechnung aufbewahrte Protokoll zeigt, dass der Mitarbeiter zuerst „consulter_commande“ und anschließend „consulter_procedure“ verwendet hat. Er identifiziert die Lampe, stellt fest, dass sie sich im Transport befindet, und notiert die drei Tagesverspätung. Er empfiehlt eine Überprüfung durch die Logistik ohne Festlegung eines erfundenen Lieferdatums und erstellt anschließend einen Entwurf für das Support-Team.

Dieser Entwurf soll noch einmal überprüft werden. Formulierungen wie „Wir werden Ihr Anliegen weiterleiten“ verpflichten das Team zu einer Aktion, die das Programm nicht durchgeführt hat, und eine Empfehlung gilt nicht als tatsächliche Weiterleitung.

Der zweite Versuch bezieht sich auf eine Nachfrage des Kunden nach dem Inhalt der Bestellung. Da dieser die Referenz nicht wiederholt, wird die Historie der ersten Bearbeitung übermittelt.

suivi = traiter_ticket(
    "Quel article contient cette commande ?",
    modele,
    jev,
    historique=dossier["messages"],
)
print(suivi["reponse"])

In der aufgezeichneten Ausgabe wird die Referenz AN-1001 gefunden, ohne dass der Kunde sie wiederholt, und die Antwort bezieht sich auf eine Schreibtischlampe. Es trat keine persistente Speicherung auf – das Programm gab einfach die vorherigen Nachrichten wieder. Für einen anderen Kunden muss man von einem separaten Verlauf ausgehen, da das Mischen der Gespräche die Informationen einer Person einer anderen aussetzen würde.

Vor einer echten Implementierung

Die Demo arbeitet mit fiktiven Daten und beschränkt sich auf Entwürfe. Bevor sie an einen echten Datenträger angeschlossen wird, sind mehrere Schritte erforderlich:

  • die Sortierung anhand annotierter Tickets einschließlich auf Französisch bewerten und anschließend entscheiden, wie zu handeln ist, falls Jev oder das LLM nicht antwortet;
  • überprüfen, wer das Recht hat, welche Befehle zu lesen, und die Gespräche voneinander trennen;
  • Die an die Modelle übermittelten personenbezogenen Daten auf das Unbedingt Notwendige beschränken;
  • nutzbare Aufzeichnungen bewahren, ohne die API-Schlüssel preiszugeben.

Die Einstellung revue_humaine auf true dient hier lediglich als Information für den Agenten. Wenn das Gerät tatsächlich Nachrichten sendet oder sensible Operationen durchführt, muss diese Information einen echten Genehmigungsprozess auslösen.

Das Prinzip, das man sich bereits jetzt merken sollte, ist die Aufgabenteilung. Die strukturierte Analyse stammt von Jev, die Entscheidung über deren Anwendung liegt bei Python, und das LLM übernimmt es, unter Verwendung der entsprechenden Tools die Antwort zu verfassen. Um weiterzukommen, genügt es, verschiedene Fälle hinzuzufügen: Rechnungsaufträge, zweideutige Anfragen oder höfliche, aber dringende Nachrichten. Man prüft die Klassifizierung und stellt sicher, dass die Regeln zur erwarteten Verarbeitung führen, was sowohl die Fähigkeiten des Agenten als auch das Offene zur Verbesserung aufzeigt.

Verwandte Artikel

  • Ein lokaler Support-Agent mit SmolLM3: Think-Modus, Tools und Schutzmechanismen — Der Betrieb eines 3B-Modells auf einem Laptop als Support-Agent: Wann „think“ oder „no_think“ verwendet werden sollte, wie die Aufrufung von Tools funktioniert und die Mechanismen, die Platzhalter-Tool-Aufrufe verhindern.