Module 9 — Traçage, évaluation et débogage
Un pipeline LangChain qui contient un récupérateur, une mémoire, deux outils et un agent produit, pour une seule question utilisateur, une trace de dix à vingt appels. Sans instrumentation, la moindre régression coûte des heures. Ce module met en place trois choses : le traçage (chaque appel, chaque coût, chaque latence visibles), l'évaluation (un jeu de tests répétable), et la détection de régression (savoir qu'une nouvelle version de consigne casse une catégorie de cas avant les utilisateurs).
Pourquoi le traçage n'est pas un luxe
Trois questions du quotidien qu'un print ne sait pas répondre :
- Cette réponse fausse vient-elle du récupérateur (mauvais passage) ou du modèle (bonne source, mauvaise conclusion) ?
- Pourquoi la latence est-elle passée de 3 à 8 secondes hier soir ?
- Combien coûte, en dollars, un utilisateur type par jour ?
Un traçage bien branché répond aux trois en trente secondes. LangChain expose un système de rappels (callbacks) qui capture chaque nœud d'un Runnable. Deux traceurs principaux : LangSmith (le service hébergé de l'éditeur), et des alternatives ouvertes comme Langfuse, Phoenix Arize ou une intégration OpenTelemetry directe.
Activer LangSmith en trois lignes
import os
os.environ["LANGSMITH_TRACING"] = "true"
os.environ["LANGSMITH_API_KEY"] = "..."
os.environ["LANGSMITH_PROJECT"] = "notes-frais-dev"
# Aucun changement dans le code d'application : les Runnable envoient leurs traces.
chaine_rag.invoke("Le taxi vers un aéroport est-il remboursable ?")
Chaque appel envoie asynchronement, sur un thread de fond, la description de la chaîne, les entrées, les sorties, la latence et les jetons consommés. L'interface web trace l'arbre : RunnableParallel en tête, Retriever à gauche, Passthrough à droite, Prompt puis ChatOpenAI puis StrOutputParser. Chaque nœud affiche son temps, ses jetons, son résultat.
Pour un traceur ouvert, Langfuse s'active avec le même principe : une clé, un import, aucune modification de la logique.
Callbacks personnalisés
Quand la donnée à mesurer n'est pas standard (temps d'un appel base de données, taux de correspondance d'un récupérateur, latence d'un outil externe), un BaseCallbackHandler maison capture les événements :
from langchain_core.callbacks import BaseCallbackHandler
class CoutCumule(BaseCallbackHandler):
def __init__(self):
self.dollars = 0.0
def on_llm_end(self, response, **kwargs):
usage = response.llm_output.get("token_usage", {})
# Tarif indicatif gpt-4o-mini : 0,15$ / 1M jetons entrée, 0,60$ / 1M sortie
self.dollars += usage.get("prompt_tokens", 0) * 0.15e-6
self.dollars += usage.get("completion_tokens", 0) * 0.60e-6
compteur = CoutCumule()
chaine_rag.invoke("Plafond hôtel Paris ?", config={"callbacks": [compteur]})
print(f"Cet appel a coûté {compteur.dollars:.5f} $")
Cet outil de comptage devient un tableau de bord : coût par utilisateur, par jour, par intent.
Le jeu d'évaluation, avant toute optimisation
On n'améliore pas ce qu'on ne mesure pas. Un jeu d'évaluation, pour un assistant de notes de frais, ressemble à un tableau de 30 à 80 exemples : question, réponse attendue, catégorie. On l'écrit à la main, à partir des vrais tickets du support client, en couvrant les cas typiques et les pièges.
jeu = [
{"question": "Plafond hôtel Paris ?", "attendu": "180 EUR TTC par nuit", "categorie": "plafond"},
{"question": "Taxi vers CDG remboursable ?", "attendu": "oui, article 3.1", "categorie": "transport"},
{"question": "Repas d'affaires 4 personnes ?", "attendu": "50 EUR par personne", "categorie": "restauration"},
# ...
]
Ce tableau est la spécification exécutable de l'assistant. Il vit dans le dépôt à côté du code, il est révisé à chaque nouvelle catégorie de question observée en production.
Les évaluateurs
Trois familles d'évaluateurs, choisies selon ce qu'on mesure :
- Exact ou heuristique. Comparer par égalité, contenance de sous-chaîne, expression régulière. Précis pour les extractions structurées, insuffisant pour du texte libre.
- Basé sur un
LLMjuge. Un second modèle reçoit la question, la réponse attendue et la réponse produite, et juge si elles disent la même chose.LangChainfournitload_evaluator("qa")etload_evaluator("labeled_criteria"). Puissant mais coûteux et non déterministe. - Basé sur des références partielles. Vérifier que la réponse cite un article (
re.search(r"article \d+\.\d+")), qu'elle contient un montant, qu'elle mentionne les bons documents dans les sources récupérées.
Pour un jeu de 50 exemples, un mélange fonctionne bien : heuristique sur les cas structurés (montants, articles), juge LLM sur les cas libres, recouvrement de documents pour le récupérateur.
Détecter une régression
Un nouveau prompt système, un changement de modèle (gpt-4o-mini → gpt-4.1-mini), une mise à jour de la politique : chacun peut casser des cas qui marchaient. Le patron est simple :
def evaluer(chaine, jeu):
scores = {"plafond": [], "transport": [], "restauration": []}
for cas in jeu:
reponse = chaine.invoke(cas["question"])
score = 1 if cas["attendu"].lower() in reponse.lower() else 0
scores[cas["categorie"]].append(score)
return {cat: sum(s)/len(s) for cat, s in scores.items() if s}
reference = evaluer(chaine_v1, jeu) # {"plafond": 0.92, "transport": 0.88, ...}
nouveau = evaluer(chaine_v2, jeu) # {"plafond": 0.92, "transport": 0.55, ...}
Une baisse de 33 points sur la catégorie « transport » se voit avant le déploiement. Sans ce jeu, elle se serait vue en un NPS négatif deux semaines plus tard.
LangSmith intègre nativement des jeux d'évaluation et compare deux versions côte à côte, question par question, ce qui accélère l'analyse. Un tableau CSV versionné dans le dépôt fait aussi bien pour commencer.
Le débogage au quotidien
Trois gestes qui tranchent 80 % des mystères :
- Ouvrir la trace du cas défaillant : quel morceau est remonté ? quel a été le prompt final ? qu'a répondu le modèle avant l'analyseur ?
- Rejouer isolément le récupérateur avec la question incriminée pour vérifier qu'il n'est pas la source.
- Baisser la température à 0 temporairement : si le comportement se stabilise, c'était un problème d'inconstance, pas de logique.
Un jeu d'évaluation coûte deux jours à construire, dix minutes par nouvelle version à faire tourner, et évite des soirées entières à chercher pourquoi la production semble « moins bonne » sans qu'on puisse le prouver. Chaque équipe qui a livré un assistant LLM en production a fini par en écrire un ; commencer dès la deuxième version économise le rattrapage.
En résumé
- Le traçage (
LangSmith,Langfuse,Phoenix) capture chaque nœud d'unRunnableavec entrées, sorties, latence et jetons ; il s'active en variables d'environnement, sans toucher au code. - Un
BaseCallbackHandlermaison ajoute les mesures spécifiques (coût cumulé, latence d'un outil, correspondance de récupération) qui alimentent un tableau de bord. - Un jeu d'évaluation de 30 à 80 cas, révisé au fil des retours de production, est la spécification exécutable de l'assistant.
- Trois familles d'évaluateurs — heuristique,
LLMjuge, référence partielle — se combinent selon les cas ; comparer deux versions sur le même jeu avant de déployer détecte les régressions au coût d'une exécution.
Module suivant : assembler tout ce qui précède en un projet livrable — le fil rouge de bout en bout, avec interface, gestion d'erreurs et passage en production.