Blog

Jev dans LangChain : un routeur LangGraph à seuil de confiance

Ce guide montre comment confier le routage de vos tickets à Jev, le modèle de décision de TypeSafe AI, dans un graphe LangGraph. Jev décide quand il est sûr. Sinon, votre LLM actuel reprend la main, comme aujourd’hui.

Par Étienne Lescot · 23 septembre 2026

TicketJevconfiance ≥ seuil ? oui : bon servicenon, erreur : LLM LangGraph · journal de chaque décision

Ce que vous allez construire

Un graphe de quatre nœuds qui route un ticket de support vers la facturation, la technique ou le commercial. C’est le même scénario que notre guide n8n, écrit cette fois en Python.

  • ask_jev envoie le ticket à Jev avec deux questions : quel service, et est-ce urgent.
  • is_jev_sure est une arête conditionnelle. Elle compare la confiance de Jev à votre seuil.
  • accept_jev retient le choix de Jev quand la confiance suffit.
  • llm_fallback appelle votre LLM actuel quand Jev hésite ou ne répond pas.
  • log_decision journalise la version du modèle, les probabilités, la confiance et le chemin suivi.

Pourquoi LangGraph pour ce routage

LangGraph est aujourd’hui la façon de router dans l’écosystème LangChain. Depuis LangChain 1.0, les anciennes chaînes de routage comme LLMRouterChain ont rejoint le paquet langchain-classic. Le routage se décrit désormais avec des nœuds et des arêtes conditionnelles.

Ce modèle convient bien à Jev. Le classifieur devient un nœud comme un autre. La règle de seuil devient une arête que tout le monde peut lire. Le reste de votre graphe ne change pas.

Avant de commencer

  • Python 3.10 ou plus récent.
  • Une clé API Jev, créée dans la console TypeSafe, dans la variable TYPESAFE_API_KEY. Les inscriptions à Jev sont suspendues temporairement : vérifiez votre accès.
  • La clé de votre LLM actuel. L’exemple utilise Claude Haiku 4.5 via ANTHROPIC_API_KEY. Remplacez-le par le modèle que vous utilisez déjà.
pip install typesafe-sdk langgraph langchain langchain-anthropic

Le code complet

Le fichier tient en moins de 80 lignes. Copiez-le tel quel, puis lisez les sections suivantes pour l’adapter.

import json
import logging
from typing import Literal, TypedDict

from langchain.chat_models import init_chat_model
from langgraph.graph import END, START, StateGraph
from pydantic import BaseModel
from typesafe_sdk import Choice, Noul, TypeSafeClient, TypeSafeError

JEV_MODEL = "jev-1.13.0"  # pinned: a threshold is calibrated for one model version
THRESHOLD = 0.6  # starting point only, calibrate it on your own tickets
SHADOW = True  # start here: Jev is logged, your current LLM still decides
TEAMS = {"billing": "Payments, invoices, refunds", "technical": "Bugs, outages, integrations",
         "sales": "Pricing, quotes, new accounts"}
QUESTIONS = {
    "department": Choice(instructions="Which team should handle this ticket?", criteria=TEAMS),
    "urgent": Noul(instructions="Does the message express urgency?")}
jev = TypeSafeClient(model=JEV_MODEL)  # reads TYPESAFE_API_KEY, retries 429 and 5xx itself
log = logging.getLogger("ticket_routing")

class Route(BaseModel):
    department: Literal["billing", "technical", "sales"]

# Your current LLM router, unchanged. It only runs when Jev is unsure or unavailable.
llm = init_chat_model("anthropic:claude-haiku-4-5", temperature=0).with_structured_output(Route)

class Ticket(TypedDict, total=False):
    ticket: str
    jev_choice: str
    confidence: float
    probabilities: dict[str, float]
    urgent: float
    model: str
    error: str
    department: str
    decided_by: str

def ask_jev(state: Ticket) -> Ticket:
    try:
        res = jev.system_one(state=state["ticket"], questions=QUESTIONS)
    except TypeSafeError as err:  # 401, 422, or 429/529 once the SDK retries are spent
        return {"error": type(err).__name__, "confidence": 0.0}
    dept = res.choices["department"]
    return {"jev_choice": dept.choice, "confidence": dept.confidence,
            "probabilities": dept.probabilities, "urgent": res.nouls["urgent"].noul,
            "model": res.model}

def is_jev_sure(state: Ticket) -> Literal["accept_jev", "llm_fallback"]:
    return "llm_fallback" if SHADOW or state["confidence"] < THRESHOLD else "accept_jev"

def accept_jev(state: Ticket) -> Ticket:
    return {"department": state["jev_choice"], "decided_by": "jev"}

def llm_fallback(state: Ticket) -> Ticket:
    route = llm.invoke(f"Which team should handle this support ticket?\n\n{state['ticket']}")
    return {"department": route.department, "decided_by": "llm"}

def log_decision(state: Ticket) -> Ticket:
    record = {k: v for k, v in state.items() if k != "ticket"} | {"threshold": THRESHOLD}
    log.info(json.dumps(record))
    return {}  # next: hand the ticket to state["department"]

builder = StateGraph(Ticket)
for node in (ask_jev, accept_jev, llm_fallback, log_decision):
    builder.add_node(node)  # the node name is the function name
builder.add_edge(START, "ask_jev")
builder.add_conditional_edges("ask_jev", is_jev_sure)
builder.add_edge("accept_jev", "log_decision")
builder.add_edge("llm_fallback", "log_decision")
builder.add_edge("log_decision", END)
graph = builder.compile()

if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO)
    text = "Hi, my September invoice shows up twice on my statement. Can you refund the duplicate?"
    print(graph.invoke({"ticket": text})["department"])

1. Le nœud qui appelle Jev

Le client épingle la version du modèle. TypeSafeClient(model="jev-1.13.0") fixe le modèle pour tous les appels. L’alias jev-latest change à chaque nouvelle version, et vos seuils avec lui.

Le nœud pose deux questions typées. Une choice choisit le service parmi trois options décrites. Une noul renvoie la probabilité que le message soit urgent. Les réponses reviennent sous les mêmes noms, dans res.choices et res.nouls.

Le SDK gère déjà les erreurs passagères. Par défaut, il retente deux fois les codes 408, 429 et 5xx, avec un délai croissant. Il respecte l’en-tête Retry-After et s’arrête après 30 secondes au total. Le code 529 de Jev, qui signale une surcharge, entre dans cette plage.

Si l’erreur persiste, le SDK lève une TypeSafeError. Le nœud la capture et renvoie une confiance de 0. Le ticket part alors vers votre LLM, sans interrompre le graphe. Le nom de l’erreur reste dans le journal.

LangGraph propose aussi un retry_policy par nœud. N’empilez pas les deux mécanismes. Trois essais du SDK multipliés par trois essais du nœud font neuf appels pour un seul ticket.

2. L’arête conditionnelle sur la confiance

Toute la décision tient dans is_jev_sure. La fonction lit l’état et renvoie le nom du nœud suivant. add_conditional_edges la branche sur la sortie de ask_jev.

Le type de retour Literal["accept_jev", "llm_fallback"] n’est pas décoratif. LangGraph s’en sert pour connaître les destinations possibles et dessiner le graphe. Vous n’avez pas besoin de lui passer une table de correspondance.

Le seuil de départ est 0,6, la valeur prise dans les exemples de TypeSafe. Ce n’est pas une valeur de production. La section « Choisir le bon seuil » explique comment la mesurer.

3. Le repli vers votre LLM

llm_fallback reprend votre routeur actuel, sans le modifier. L’exemple utilise init_chat_model et une sortie structurée. Le modèle ne peut répondre que par l’un des trois services, grâce au type Literal de la classe Route.

Ce nœud ne tourne que dans trois cas :

  • la confiance de Jev est sous le seuil ;
  • Jev a renvoyé une erreur, même après les nouvelles tentatives ;
  • le mode shadow est actif.

Dans les autres cas, votre LLM n’est pas appelé. C’est là que se font les économies : Jev facture 0,042 $ par million de tokens d’entrée, et ses tokens de sortie sont gratuits.

4. Commencer en mode shadow

Laissez SHADOW = True pendant les premières semaines. Jev répond sur chaque ticket et sa réponse est journalisée. Mais c’est toujours votre LLM qui décide. Vos clients ne voient aucune différence.

Ce journal vous donne, pour chaque ticket, le choix de Jev et celui de votre LLM. Vous pouvez mesurer leur accord avant de confier la moindre décision à Jev. Passez ensuite SHADOW à False pour activer le seuil.

5. Le journal de décision

log_decision écrit une ligne JSON par ticket. Voici celle d’un ticket de facturation traité par Jev, avec les valeurs d’exemple de la documentation :

{"jev_choice": "billing", "confidence": 0.81, "probabilities": {"billing": 0.88, "technical": 0.12, "sales": 0.0}, "urgent": 0.12, "model": "jev-1.13.0", "department": "billing", "decided_by": "jev", "threshold": 0.6}
  • model : la version exacte qui a répondu, renvoyée par l’API.
  • probabilities et confidence : ce qui permet de recalibrer le seuil plus tard.
  • decided_by : le chemin suivi, jev ou llm.
  • threshold : le seuil appliqué ce jour-là.

Le texte du ticket n’est pas journalisé. Envoyez ces lignes vers votre outil habituel. Elles servent à recalibrer, et à répondre à un audit.

Comment nous avons testé ce code

Nous avons exécuté ce fichier tel quel, avec langgraph 1.2.12, langchain 1.4.2 et typesafe-sdk 0.7.1. Le vrai SDK Jev tournait, mais ses appels HTTP partaient vers une fausse API. Elle renvoyait les réponses documentées par TypeSafe. Le SDK accepte pour cela un paramètre transport.

  • Confiance de 0,81 : Jev décide, le LLM n’est pas appelé.
  • Confiance de 0,10 : le graphe passe par le repli.
  • Erreur 429 répétée : trois appels au total, puis le repli.
  • Erreur 401 : pas de nouvelle tentative, repli immédiat.
  • Mode shadow : le LLM décide, le choix de Jev reste dans le journal.

Nous avons aussi exécuté la partie Jev de ce code contre la vraie API le 23/09/2026 : jev-1.13.0 a répondu billing, confiance 1, en 394 ms depuis la France, pour 368 tokens d’entrée.

Choisir le bon seuil

Un seuil ne se devine pas. Il se mesure sur vos données, dans votre langue.

  1. Prenez quelques centaines de tickets passés dont le bon service est connu.
  2. Faites-les passer dans le graphe en mode shadow.
  3. Pour chaque seuil entre 0,5 et 0,95, calculez la part de tickets que Jev traiterait seul, et son taux d’erreur sur cette part.
  4. Retenez le seuil le plus bas dont le taux d’erreur vous convient.

Jev est d’abord entraîné en anglais. TypeSafe recommande de tester sur vos propres contenus avant de l’utiliser dans une autre langue. Le guide n8n détaille la même méthode pas à pas.

Avant la production

  • Données : Jev est hébergé aux États-Unis. Pseudonymisez les données personnelles avant l’envoi, et faites valider le transfert par votre DPO.
  • Version : gardez jev-1.13.0 épinglé. Recalibrez avant de passer à la suivante.
  • Journal : conservez chaque décision, y compris celles du LLM.
  • Décisions sensibles : une confiance élevée ne vaut pas autorisation. Toute issue défavorable à une personne doit passer par un humain.

Vous voulez voir Jev décider avant d’écrire du code ? Essayez la démo avec vos propres exemples de tickets.

Jev et TypeSafe sont des marques de TypeSafe AI, Inc. Cet article est indépendant et n’est pas affilié à TypeSafe AI.