Retour aux ressources
// technique · observabilité

Tracer et scorer un agent avec Langfuse

Un agent, c’est plusieurs étapes et plusieurs appels LLM imbriqués. Sans traces, tu débogues à l’aveugle. Voici comment j’instrumente chaque étape, mesure le coût, et note la qualité — automatiquement.

Technique7 juil. 2026~9 min · intermédiaire
LangfuseLangGraphLangChainTypeScriptLLM-as-a-judge
01

Pourquoi un agent est une boîte noire

Un prompt, c’est un appel : entrée, sortie, facile à lire. Un agent, c’est un graphe — comme je le raconte dans d’un prompt à l’agentique : il va chercher du contexte, décide, appelle un ou plusieurs modèles, se relit, boucle. Quand la sortie est mauvaise, la vraie question est : quelle étape a dérapé ?

un prompt

1 appel.

une entrée, une sortie
un log suffit à comprendre
un agent

N appels imbriqués.

plusieurs étapes, des boucles
coût et latence répartis partout
l’erreur peut venir de n’importe où
i

Langfuse est fait pour ça : il enregistre l’arbre complet d’un run — chaque étape, chaque appel LLM, avec tokens, coût et latence — et te laisse y accrocher des notes de qualité.

02

Le vocabulaire, en 4 mots

Avant d’instrumenter, quatre notions suffisent à tout comprendre.

traceun run complet de l’agent, de bout en bout
observationune étape à l’intérieur (un span, ou une génération LLM)
generationun appel LLM : prompt, réponse, tokens, coût, modèle
scoreune note attachée à une trace (0/1, note, texte)
03

Instrumenter en trois lignes

On installe le SDK Langfuse et son intégration LangChain, puis on pose les clés — celles de ton instance.

shellcopier
$ npm i langfuse langfuse-langchain @langchain/langgraph @langchain/openai
.env.localcopier
LANGFUSE_PUBLIC_KEY=pk-lf-...
LANGFUSE_SECRET_KEY=sk-lf-...
LANGFUSE_BASEURL=https://langfuse.mondomaine.com # ton instance auto-hebergee

L’instrumentation tient en un objet : le CallbackHandler de langfuse-langchain passé à graph.invoke relie chaque nœud du graphe — et chaque appel de modèle — à une seule trace. Pas de décorateur à poser partout.

route.tscopier
// app/api/agent/route.ts
import { CallbackHandler } from "langfuse-langchain";
// un handler par run — relie chaque noeud LangGraph a une trace
const handler = new CallbackHandler({ sessionId }); // sessionId : optionnel
const result = await graph.invoke(
{ brief },
{ callbacks: [handler] }, // gather -> clarify -> draft -> review -> apply
);
const traceId = handler.getTraceId(); // pour y accrocher des scores ensuite

Chaque nœud qui appelle un ChatOpenAI apparaît alors comme une génération — prompt, réponse, tokens, coût — sans rien ajouter.

nodes.tscopier
import { ChatOpenAI } from "@langchain/openai";
const model = new ChatOpenAI({ model: "gpt-4o" });
// un noeud du graphe : l'appel ChatOpenAI est trace comme "generation"
// (prompt, reponse, tokens, cout) sans une ligne de plus
async function draft(state: AgentState) {
const res = await model.invoke(buildPrompt(state));
return { draft: res.content };
}

Résultat : dans Langfuse, tu vois l’arbre gather → clarify → draft → review → apply, avec la latence et le coût de chaque nœud. La boucle review ↺ draft apparaît telle quelle.

// le graphe (langgraph)
gather
contexte
clarify
question ?
draft
décider
review
auto-relire
apply
appliquer
review ↺ draft — boucle si insuffisantclarify → human-in-the-loop
Postgres — état durableLangfuse — tracé
04

Scorer : trois niveaux

Une trace te dit ce qui s’est passé. Un score te dit si c’était bon. Trois sources, du moins au plus automatique.

Règle métier et feedback utilisateur d’abord — simples, fiables, gratuits.

scores.tscopier
import { Langfuse } from "langfuse";
const langfuse = new Langfuse();
// 1. score programmatique (regle metier)
langfuse.score({
traceId,
name: "roadmap_valide",
value: plan.milestones.length ? 1 : 0,
comment: "au moins un jalon produit",
});
// 2. feedback utilisateur (pouce) — depuis une route API,
// avec le traceId qu'on a renvoye au front
langfuse.score({
traceId,
name: "user_feedback",
value: 1, // 1 = pouce haut, 0 = pouce bas
dataType: "BOOLEAN",
});

LLM-as-a-judge

Pour la qualité subjective (fidélité, ton, pertinence), un LLM-as-a-judge note la sortie d’un autre. On le trace aussi — un juge qui déraille, ça se débogue comme le reste.

judge.tscopier
import { ChatOpenAI } from "@langchain/openai";
import { z } from "zod";
// un petit modele, en sortie structuree
const judge = new ChatOpenAI({ model: "gpt-4o-mini" }).withStructuredOutput(
z.object({ score: z.number(), reason: z.string() }),
);
async function scoreOutput(question: string, answer: string, traceId: string) {
const v = await judge.invoke(
`Note la reponse de 1 a 5 sur sa fidelite au contexte.
Q: ${question}
R: ${answer}`,
);
langfuse.score({ traceId, name: "fidelite", value: v.score, comment: v.reason });
return v;
}
!

Un juge LLM n’est pas la vérité : calibre-le sur quelques exemples notés à la main avant de lui faire confiance. Et garde-le petit (gpt-4o-mini suffit) — sinon scorer coûte plus cher que produire.

05

Boucler : datasets & experiments

Le vrai gain arrive quand tu rejoues un jeu de cas figé à chaque changement, via les datasets & experiments de Langfuse. Tu figes une trentaine de briefs représentatifs, et avant chaque déploiement tu relances l’agent dessus, noté par le juge. Une régression se voit avant la prod, pas après.

eval.tscopier
// rejouer un jeu de cas fige AVANT de deployer -> pas de regression
const dataset = await langfuse.getDataset("briefs-de-reference");
for (const item of dataset.items) {
const handler = new CallbackHandler();
const output = await graph.invoke(
{ brief: item.input },
{ callbacks: [handler] },
);
await item.link(handler, "v2-review-loop"); // rattache le cas au run
await scoreOutput(item.input, output, handler.getTraceId());
}
await langfuse.flushAsync(); // vide le buffer avant la fin du process
// -> compare v1 vs v2 dans l'onglet Experiments
compare deux versions côte à côte (v1 vs v2) sur les mêmes entrées ;
repère la régression au niveau de l’étape, pas juste du résultat final ;
transforme « ça a l’air mieux » en un chiffre que tu peux suivre.
06

En prod — les pièges

PII : les prompts partent dans Langfuse. Masque les données sensibles, ou héberge Langfuse toi-même pour que rien ne sorte.
Volume : à grande échelle, échantillonne les traces plutôt que tout garder.
Async : le SDK envoie en tâche de fond (batch) — appelle flushAsync() avant la fin d’une route serverless, sinon la fonction gèle avant l’envoi.
i

Garder les traces chez toi règle le point PII d’un coup. C’est exactement ce que couvre mon guide déployer Langfuse sur AWS EC2 — self-hosted, tes données restent tes données.

07

Rattacher les traces au réel : sessions, users, versions

Une trace isolée répond à « ce run a-t-il marché ? ». Mais en prod, la vraie question est « ce run a-t-il marché *pour ce user*, dans *cette conversation*, sur *cette version* de l’agent ? ». Trois attributs suffisent à passer d’un tas de runs anonymes à quelque chose que tu peux filtrer et agréger.

sessionIdregroupe les traces d’une même conversation multi-tours — Langfuse te rejoue l’interaction complète
userIdrattache chaque run à un utilisateur : coût, tokens et feedback se segmentent par personne
version / releasemarque quelle version de l’agent a produit la trace — indispensable pour comparer avant/après un déploiement

Avec le CallbackHandler, rien de neuf à installer : tu passes ces attributs dans le `metadata` de l’invocation, et Langfuse les remonte au niveau de la trace. Les sessions acceptent n’importe quelle chaîne US-ASCII de moins de 200 caractères comme identifiant.

route.tscopier
const handler = new CallbackHandler();
await graph.invoke(
{ brief },
{
callbacks: [handler],
metadata: {
langfuseSessionId: conversationId, // regroupe la conversation / groups the conversation
langfuseUserId: user.id, // segmente par user / segments by user
langfuseTags: ["prod", "agent-v3"],
},
},
);
i

Le user tracking accepte email, username ou tout identifiant stable. Attention : si tu passes un email en clair, il finit dans Langfuse — même précaution PII que pour les prompts (section 06).

Le prompt comme objet versionné, pas comme string en dur

Tant que ton prompt vit dans le code, changer une consigne = redéployer. Le prompt management de Langfuse le sort du code : chaque édition crée une version immuable (1, 2, 3…), et un label mobile (`production`, `staging`) pointe vers la version active. Tu peux rollback en réassignant le label `production` à l’ancienne version, sans toucher au code. Le SDK met le prompt en cache côté client, donc zéro latence ajoutée.

Le gain se combine avec le scoring : quand une génération est liée à sa version de prompt, tu compares les scores du juge *par version*. « La v4 du prompt de review a fait chuter la fidélité de 8 points » devient une phrase que la donnée te dit, pas une intuition.

08

Passer à l’échelle : OTel, échantillonnage, alertes

Trois problèmes apparaissent quand le trafic monte : s’intégrer à une stack d’observabilité existante, ne pas exploser le volume, et être prévenu quand ça part en vrille sans regarder le dashboard toute la journée.

OpenTelemetry : Langfuse comme backend OTLP

Si tu as déjà un collecteur OpenTelemetry, tu n’as pas à choisir. Langfuse est OTel-natif : il expose un endpoint OTLP sur `/api/public/otel/v1/traces` (HTTP, JSON ou protobuf — pas de gRPC pour l’instant). Tu ajoutes Langfuse comme *exporter* de ton collecteur, et les spans émis par n’importe quelle librairie instrumentée OTel se nichent automatiquement dans la bonne trace.

!

Piège classique : les attributs de trace (userId, sessionId, tags, version) doivent être propagés sur *chaque* span pour que le filtrage et l’agrégation marchent côté Langfuse. Un span orphelin sans userId, et il disparaît de tes segmentations.

Échantillonnage : garder 20 % plutôt que tout

À gros volume, tout tracer coûte du stockage et noie le signal. L’échantillonnage est géré côté client : tu poses `LANGFUSE_SAMPLE_RATE` (ou `sampleRate` au constructeur) entre 0 et 1. La décision se prend *au niveau de la trace* — si une trace est retenue, toutes ses observations et scores le sont ; sinon rien ne part. Pas de trace à moitié envoyée.

.env.localcopier
# Garde 20% des traces en prod haut-volume / keep 20% of traces at high volume
LANGFUSE_SAMPLE_RATE=0.2
i

Ma règle : échantillonne le trafic à faible valeur et gros volume, mais garde 100 % des runs qui portent un feedback négatif ou un score de juge sous le seuil — ce sont précisément ceux que tu veux déboguer. Un rate global à 0.2 sur le tout-venant, un handler dédié à 1.0 sur les cas signalés.

Alertes : ne plus fixer le dashboard

Un dashboard, personne ne le regarde à 3 h du matin. Les monitors & alertes déclenchent sur un seuil — coût moyen, p95 de latence, ou un agrégat de qualité — et notifient via Slack, webhook ou un déclencheur GitHub Actions. « p95 latence > 8 s » ou « coût moyen par trace > 0,05 $ » te pinguent avant que l’utilisateur ne râle.

!

Un bémol à connaître : les monitors sont pour l’instant réservés à Langfuse Cloud. En self-hosted, tu récupères les mêmes chiffres via la Metrics API v2 (count, latency, totalCost, totalTokens) et tu branches ton propre système d’alerte dessus.

09

Sources & ressources

01Langfuse — LangChain & LangGraph tracing (CallbackHandler)Le handler qui relie chaque nœud du graphe à une trace, plus le passage de sessionId / userId via metadata.02Langfuse — SessionsRegrouper les traces d’une conversation multi-tours et rejouer l’interaction complète.03Langfuse — Prompt managementSortir les prompts du code : versions immuables, labels production/staging, rollback sans redéploiement.04Langfuse — SamplingLANGFUSE_SAMPLE_RATE, décision au niveau de la trace, pour maîtriser le volume à grande échelle.05Langfuse — OpenTelemetry integrationEndpoint OTLP /api/public/otel : brancher Langfuse comme backend d’une stack OTel existante.06Langfuse — Monitors & alertsAlertes sur coût, latence p95 ou qualité, via Slack, webhook ou GitHub Actions (Cloud).07Langfuse — Metrics APIL’API v2 (count, latency, totalCost, totalTokens) pour brancher ton propre reporting ou alerting en self-hosted.

Une fois l’agent tracé et scoré, tu arrêtes de deviner : tu vois quelle étape coûte, laquelle rate, et si ton dernier changement a amélioré ou cassé les choses. C’est ce qui fait passer un agent de la démo au système de production.