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.
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_jevenvoie le ticket à Jev avec deux questions : quel service, et est-ce urgent.is_jev_sureest une arête conditionnelle. Elle compare la confiance de Jev à votre seuil.accept_jevretient le choix de Jev quand la confiance suffit.llm_fallbackappelle votre LLM actuel quand Jev hésite ou ne répond pas.log_decisionjournalise 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.probabilitiesetconfidence: ce qui permet de recalibrer le seuil plus tard.decided_by: le chemin suivi,jevoullm.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.
- Prenez quelques centaines de tickets passés dont le bon service est connu.
- Faites-les passer dans le graphe en mode shadow.
- 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.
- 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.