Noqta
  • Accueil
  • Services
  • À propos
  • Écrits
  • Se connecter
écrits/tutorial/2026/05
● Tutorial20 mai 2026·28 min

Observabilité LLM avec Langfuse : Tracer, Déboguer et Améliorer vos Applications IA avec Next.js (2026)

Apprenez à ajouter une observabilité de niveau production à votre application IA Next.js avec Langfuse. Ce tutoriel couvre le traçage LLM, le suivi des sessions utilisateur, la collecte de feedback, la gestion des prompts et l'évaluation automatisée — le tout en TypeScript.

Noqta Team
Noqta Team
Author
·EN · FR · AR

Chaque équipe qui développe des applications alimentées par l'IA finit par se heurter au même mur : l'application fonctionne en développement, mais en production des problèmes surgissent que l'on ne sait pas expliquer. Un utilisateur signale une mauvaise réponse. Les coûts explosent du jour au lendemain. La latence augmente sur certains types d'entrées. Et personne ne sait quelle version du prompt est responsable ni quels utilisateurs sont touchés.

Les outils APM traditionnels mesurent l'infrastructure — CPU, mémoire, durée des requêtes. Ils vous disent qu'une requête a pris 900ms, mais rien sur pourquoi le modèle a donné une mauvaise réponse, quel template de prompt sous-performe, ou comment les coûts en tokens évoluent par segment d'utilisateurs. Vous naviguez à l'aveugle.

Langfuse est la plateforme d'ingénierie LLM open-source de référence, conçue précisément pour combler cette lacune. Elle offre à votre application IA le même niveau de visibilité que Datadog à votre infrastructure : arbres de traces complets, gestion de prompts avec versionnage et A/B testing, boucles de feedback utilisateur, pipelines d'évaluation automatisés, et un tableau de bord d'analyse des coûts — le tout dans un package auto-hébergeable.

Dans ce tutoriel, vous allez construire un chatbot Next.js et l'instrumenter de bout en bout avec Langfuse, du traçage de base à l'évaluation en production.

Prérequis

Avant de commencer, assurez-vous d'avoir :

  • Node.js 20+ installé (node -v pour vérifier)
  • Un projet Next.js 15 (ou créez-en un avec npx create-next-app@latest)
  • Une clé API OpenAI (ou tout autre fournisseur de LLM)
  • Un compte Langfuse gratuit sur cloud.langfuse.com — ou Docker pour l'auto-hébergement
  • Une bonne maîtrise de TypeScript et de l'App Router Next.js

Ce que vous allez construire

À la fin de ce tutoriel, votre chatbot disposera de :

  • Traçage complet des appels LLM — chaque requête IA capturée dans Langfuse avec entrées, sorties, latence et comptage de tokens
  • Spans imbriqués — sous-opérations dans une requête (récupération, reranking, génération) suivies séparément
  • Contexte utilisateur et session — traces regroupées par identifiant utilisateur et session de conversation
  • Feedback utilisateur — interface pouce haut/bas reliée directement aux scores Langfuse
  • Gestion des prompts — prompts récupérés depuis le tableau de bord Langfuse au lieu d'être codés en dur
  • Évaluation automatisée — scoring LLM-as-judge tournant de façon asynchrone après chaque réponse

Pourquoi l'observabilité LLM est incontournable en 2026

Le passage du prototype à la production expose une catégorie de bugs que les outils de monitoring standard ratent complètement :

  • Régressions de prompt — un changement de formulation anodin en apparence dégrade la qualité des réponses pour certains patterns de requêtes
  • Mauvaise utilisation de la fenêtre de contexte — les messages sont tronqués silencieusement lorsque l'historique de conversation dépasse un certain seuil
  • Clusters d'hallucinations — un sous-ensemble d'entrées qui déclenchent systématiquement des réponses fausses présentées avec confiance
  • Anomalies de coût en tokens — une requête particulière consomme 50 fois plus de tokens que prévu
  • Outliers de latence — la latence au p99 est 10 fois supérieure à la médiane à cause d'un modèle ou d'une branche de prompt spécifique

Sans traces, vous ne pouvez réagir qu'après les plaintes des utilisateurs. Avec Langfuse, vous avez une visibilité complète pour détecter ces problèmes avant qu'ils n'affectent la rétention.

Étape 1 : Installer les dépendances

Depuis la racine de votre projet Next.js :

npm install langfuse openai

Créez ou mettez à jour votre .env.local :

# OpenAI
OPENAI_API_KEY=sk-...
 
# Langfuse — depuis les paramètres de votre projet sur cloud.langfuse.com
LANGFUSE_SECRET_KEY=sk-lf-...
LANGFUSE_PUBLIC_KEY=pk-lf-...
LANGFUSE_BASE_URL=https://cloud.langfuse.com

Pour une instance auto-hébergée, remplacez LANGFUSE_BASE_URL par votre propre domaine.

Étape 2 : Initialiser le client Langfuse

Créez un client singleton pour éviter d'ouvrir plusieurs connexions à chaque requête :

// lib/langfuse.ts
import { Langfuse } from "langfuse";
 
export const langfuse = new Langfuse({
  secretKey: process.env.LANGFUSE_SECRET_KEY!,
  publicKey: process.env.LANGFUSE_PUBLIC_KEY!,
  baseUrl: process.env.LANGFUSE_BASE_URL ?? "https://cloud.langfuse.com",
  flushAt: 1,       // envoi immédiat en serverless (pas de processus long)
  flushInterval: 0, // désactiver le flush basé sur un timer
});

Le flush en serverless est critique. Dans un environnement serverless (Vercel, AWS Lambda, Cloudflare Workers), le processus est tué après l'envoi de la réponse — avant que le timer de batch ne se déclenche. Définissez flushAt: 1 et appelez toujours await langfuse.flushAsync() à la fin de chaque handler pour garantir qu'aucun événement ne soit perdu.

Étape 3 : Tracer votre premier appel LLM

Créez une route API simple qui encapsule un appel OpenAI dans une trace Langfuse. Une trace représente une opération logique unique du point de vue de l'utilisateur (par exemple un tour de conversation). À l'intérieur de la trace, vous créez une génération pour enregistrer l'appel LLM avec son modèle, ses entrées, ses sorties et l'utilisation des tokens.

// app/api/chat/route.ts
import { NextRequest, NextResponse } from "next/server";
import OpenAI from "openai";
import { langfuse } from "@/lib/langfuse";
 
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
 
export async function POST(req: NextRequest) {
  const { messages, userId, sessionId } = await req.json();
 
  // 1. Démarrer une trace pour ce tour utilisateur
  const trace = langfuse.trace({
    name: "chat-turn",
    userId: userId ?? "anonymous",
    sessionId: sessionId ?? "default",
    input: { messages },
    tags: ["chat", process.env.NODE_ENV ?? "development"],
  });
 
  // 2. Enregistrer l'appel LLM comme génération dans la trace
  const generation = trace.generation({
    name: "openai-gpt4o",
    model: "gpt-4o",
    modelParameters: {
      temperature: 0.7,
      maxTokens: 1024,
    },
    input: messages,
  });
 
  try {
    const response = await openai.chat.completions.create({
      model: "gpt-4o",
      messages,
      max_tokens: 1024,
      temperature: 0.7,
    });
 
    const content = response.choices[0].message.content ?? "";
 
    // 3. Fermer la génération avec les sorties et l'utilisation des tokens
    generation.end({
      output: content,
      usage: {
        promptTokens: response.usage?.prompt_tokens,
        completionTokens: response.usage?.completion_tokens,
        totalTokens: response.usage?.total_tokens,
      },
    });
 
    trace.update({ output: { content } });
 
    // 4. Flush avant la fin de la fonction serverless
    await langfuse.flushAsync();
 
    return NextResponse.json({
      content,
      traceId: trace.id,
    });
  } catch (error) {
    generation.end({
      level: "ERROR",
      statusMessage: String(error),
    });
    await langfuse.flushAsync();
    throw error;
  }
}

Après une requête, ouvrez le tableau de bord Langfuse et naviguez vers Traces. Vous verrez la trace avec les entrées/sorties complètes, le nom du modèle, le comptage de tokens, la latence et le coût estimé.

Étape 4 : Spans imbriqués pour les pipelines complexes

Les vraies applications IA font souvent plus d'un appel LLM par requête. Un pipeline RAG, par exemple, récupère des documents, les rerankent, construit une fenêtre de contexte, puis génère une réponse. Langfuse vous permet d'imbriquer des spans dans une trace pour rendre tout cela visible.

// app/api/rag-chat/route.ts
import { NextRequest, NextResponse } from "next/server";
import OpenAI from "openai";
import { langfuse } from "@/lib/langfuse";
 
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
 
async function retrieveDocuments(query: string) {
  return [
    { id: "doc-1", content: "Passage pertinent sur le sujet..." },
    { id: "doc-2", content: "Un autre passage pertinent..." },
  ];
}
 
export async function POST(req: NextRequest) {
  const { query, userId, sessionId } = await req.json();
 
  const trace = langfuse.trace({
    name: "rag-chat",
    userId,
    sessionId,
    input: { query },
  });
 
  // Span 1 : récupération des documents
  const retrievalSpan = trace.span({
    name: "vector-retrieval",
    input: { query },
  });
  const documents = await retrieveDocuments(query);
  retrievalSpan.end({
    output: { documentCount: documents.length, documentIds: documents.map((d) => d.id) },
  });
 
  // Span 2 : construction du contexte
  const contextSpan = trace.span({ name: "context-construction" });
  const context = documents.map((d) => d.content).join("\n\n");
  const messages: OpenAI.ChatCompletionMessageParam[] = [
    { role: "system", content: "Répondez uniquement en utilisant le contexte fourni." },
    { role: "user", content: `Contexte:\n${context}\n\nQuestion: ${query}` },
  ];
  contextSpan.end({ output: { tokenEstimate: context.length / 4 } });
 
  // Span 3 : génération de la réponse
  const generation = trace.generation({
    name: "answer-generation",
    model: "gpt-4o",
    input: messages,
  });
 
  const response = await openai.chat.completions.create({
    model: "gpt-4o",
    messages,
  });
 
  const answer = response.choices[0].message.content ?? "";
 
  generation.end({
    output: answer,
    usage: {
      promptTokens: response.usage?.prompt_tokens,
      completionTokens: response.usage?.completion_tokens,
      totalTokens: response.usage?.total_tokens,
    },
  });
 
  trace.update({ output: { answer } });
  await langfuse.flushAsync();
 
  return NextResponse.json({ answer, traceId: trace.id });
}

Dans le tableau de bord Langfuse, la trace apparaît maintenant comme un arbre : la trace racine avec trois enfants (span de récupération, span de contexte, génération). Vous voyez exactement où le temps est passé et comment chaque étape contribue à la réponse finale.

Étape 5 : Collecter le feedback utilisateur

Connecter les pouces haut/bas des utilisateurs à vos traces ferme la boucle entre l'expérience utilisateur et la performance du modèle. Langfuse appelle ces scores — ils peuvent venir des utilisateurs, d'évaluations automatisées ou de réviseurs humains.

Exposez d'abord un endpoint de feedback :

// app/api/feedback/route.ts
import { NextRequest, NextResponse } from "next/server";
import { langfuse } from "@/lib/langfuse";
 
export async function POST(req: NextRequest) {
  const { traceId, value, comment } = await req.json();
 
  // value: 1 pour pouce haut, 0 pour pouce bas
  langfuse.score({
    traceId,
    name: "user-feedback",
    value,
    comment: comment ?? undefined,
    dataType: "BOOLEAN",
  });
 
  await langfuse.flushAsync();
  return NextResponse.json({ ok: true });
}

Puis connectez l'interface utilisateur :

// components/FeedbackButtons.tsx
"use client";
 
import { useState } from "react";
 
interface FeedbackButtonsProps {
  traceId: string;
}
 
export function FeedbackButtons({ traceId }: FeedbackButtonsProps) {
  const [submitted, setSubmitted] = useState<boolean | null>(null);
 
  const sendFeedback = async (value: 0 | 1) => {
    setSubmitted(value === 1);
    await fetch("/api/feedback", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ traceId, value }),
    });
  };
 
  if (submitted !== null) {
    return <p className="text-sm text-gray-500">Merci pour votre retour !</p>;
  }
 
  return (
    <div className="flex gap-2 mt-2">
      <button
        onClick={() => sendFeedback(1)}
        className="px-3 py-1 text-sm border rounded hover:bg-green-50"
      >
        👍 Utile
      </button>
      <button
        onClick={() => sendFeedback(0)}
        className="px-3 py-1 text-sm border rounded hover:bg-red-50"
      >
        👎 Pas utile
      </button>
    </div>
  );
}

Passez le traceId retourné par votre route API à ce composant après chaque réponse IA. Les scores apparaissent immédiatement dans Langfuse, où vous pouvez filtrer les traces par score et corréler les baisses de qualité avec des versions de prompt ou des changements de modèle.

Étape 6 : Gérer les prompts depuis le tableau de bord

Coder les prompts en dur dans le code source signifie que chaque itération nécessite un déploiement. La gestion des prompts de Langfuse vous permet de versionner et faire des A/B tests de prompts depuis le tableau de bord — et de récupérer la version active au runtime sans déploiement.

Créer un prompt dans Langfuse

  1. Dans le tableau de bord Langfuse, allez dans Prompts → New Prompt
  2. Nommez-le chat-system-prompt
  3. Collez votre prompt système en utilisant la syntaxe de variables entre doubles accolades : Vous êtes un assistant utile pour {{company_name}}.
  4. Publiez-le avec le label production

Récupérer et utiliser le prompt

// lib/prompts.ts
import { langfuse } from "@/lib/langfuse";
 
export async function getSystemPrompt(companyName: string): Promise<string> {
  const prompt = await langfuse.getPrompt("chat-system-prompt", undefined, {
    label: "production",
    cacheTtlSeconds: 60,
  });
 
  return prompt.compile({ company_name: companyName });
}

Reliez le prompt à la génération dans votre trace :

const promptObj = await langfuse.getPrompt("chat-system-prompt", undefined, {
  label: "production",
  cacheTtlSeconds: 60,
});
 
const systemPrompt = promptObj.compile({ company_name: "Noqta" });
 
const generation = trace.generation({
  name: "openai-gpt4o",
  model: "gpt-4o",
  input: [
    { role: "system", content: systemPrompt },
    ...messages,
  ],
  prompt: promptObj,
});

Épinglage de version de prompt. Par défaut, getPrompt récupère le label production. Pour une version spécifique (ex. pour le staging), passez le numéro de version comme second argument : langfuse.getPrompt("chat-system-prompt", 3). Utilisez des labels (production, staging, canary) pour gérer le déploiement sans toucher au code.

Étape 7 : Évaluation automatisée

Le feedback utilisateur est précieux mais rare. L'évaluation automatisée vous permet de scorer 100% de vos traces avec des règles simples ou un LLM-as-judge.

Scoring basé sur des règles

// lib/evaluators.ts
import { langfuse } from "@/lib/langfuse";
 
export async function evaluateResponse(
  traceId: string,
  generationId: string,
  output: string,
  expectedKeywords: string[]
): Promise<void> {
  // Score 1 : vérification de la longueur de réponse
  const lengthScore = output.split(" ").length > 20 ? 1 : 0;
 
  langfuse.score({
    traceId,
    observationId: generationId,
    name: "response-length-ok",
    value: lengthScore,
    dataType: "BOOLEAN",
  });
 
  // Score 2 : couverture des mots-clés
  const covered = expectedKeywords.filter((kw) =>
    output.toLowerCase().includes(kw.toLowerCase())
  ).length;
  const coverageScore = expectedKeywords.length > 0
    ? covered / expectedKeywords.length
    : 1;
 
  langfuse.score({
    traceId,
    observationId: generationId,
    name: "keyword-coverage",
    value: coverageScore,
    dataType: "NUMERIC",
    comment: `${covered}/${expectedKeywords.length} mots-clés trouvés`,
  });
 
  await langfuse.flushAsync();
}

LLM-as-Judge

// lib/llm-judge.ts
import OpenAI from "openai";
import { langfuse } from "@/lib/langfuse";
 
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
 
export async function llmJudge(
  traceId: string,
  question: string,
  answer: string
): Promise<void> {
  const judgeTrace = langfuse.trace({ name: "llm-judge", tags: ["eval"] });
 
  const judgeGeneration = judgeTrace.generation({
    name: "judge-call",
    model: "gpt-4o-mini",
    input: [
      {
        role: "user",
        content: `Évaluez la réponse suivante sur une échelle de 0 à 1 pour l'exactitude et l'utilité. Retournez uniquement un objet JSON : {"score": 0.0, "reason": "..."}.
 
Question: ${question}
Réponse: ${answer}`,
      },
    ],
  });
 
  const response = await openai.chat.completions.create({
    model: "gpt-4o-mini",
    messages: [
      {
        role: "user",
        content: `Évaluez la réponse suivante sur une échelle de 0 à 1 pour l'exactitude et l'utilité. Retournez uniquement un objet JSON avec les clés "score" et "reason".
 
Question: ${question}
Réponse: ${answer}`,
      },
    ],
    response_format: { type: "json_object" },
  });
 
  const raw = response.choices[0].message.content ?? '{"score":0,"reason":"erreur de parsing"}';
  const parsed = JSON.parse(raw) as { score: number; reason: string };
 
  judgeGeneration.end({ output: parsed });
 
  langfuse.score({
    traceId,
    name: "llm-judge-quality",
    value: parsed.score,
    dataType: "NUMERIC",
    comment: parsed.reason,
  });
 
  await langfuse.flushAsync();
}

Appelez llmJudge de façon asynchrone (via un job background comme Hatchet ou Trigger.dev) après chaque tour de conversation pour ne pas ajouter de latence au chemin de réponse principal.

Étape 8 : Liste de vérification pour la production

Échantillonnage des traces à fort volume

Si vous traitez des milliers de requêtes par minute, tracer 100% peut être coûteux. Ajoutez une porte d'échantillonnage simple :

export function shouldTrace(sampleRate = 0.1): boolean {
  return Math.random() < sampleRate;
}

Auto-hébergement de Langfuse

Langfuse est sous licence MIT et propose un Docker Compose pour l'auto-hébergement. Idéal pour les exigences de résidence des données dans les marchés européens et MENA :

git clone https://github.com/langfuse/langfuse.git
cd langfuse
cp .env.example .env
# Modifiez .env : définissez NEXTAUTH_SECRET, DATABASE_URL, SALT, etc.
docker compose up -d

Isolation des environnements

Utilisez les Projets Langfuse pour séparer les données de développement, staging et production. Chaque projet a sa propre paire de clés — définissez LANGFUSE_SECRET_KEY et LANGFUSE_PUBLIC_KEY par environnement.

Ne journalisez jamais de données utilisateur sensibles. Les traces Langfuse sont stockées et potentiellement visibles par votre équipe. Avant de passer des messages à trace.input() ou generation.input(), supprimez ou masquez les données PII (noms, e-mails, numéros d'identité) qui ne devraient pas apparaître dans vos outils d'observabilité.

Résolution des problèmes

Les traces n'apparaissent pas dans le tableau de bord

  • Vérifiez que LANGFUSE_SECRET_KEY et LANGFUSE_PUBLIC_KEY sont corrects (ils commencent par sk-lf- et pk-lf-)
  • Assurez-vous que await langfuse.flushAsync() est appelé avant le retour de la fonction
  • Vérifiez que LANGFUSE_BASE_URL correspond exactement à l'URL cloud ou auto-hébergée

Le comptage de tokens affiche zéro

  • Langfuse lit les comptages de tokens depuis l'objet usage que vous passez à generation.end(). Assurez-vous de transmettre response.usage.prompt_tokens, response.usage.completion_tokens et response.usage.total_tokens depuis la réponse OpenAI.

getPrompt retourne une erreur 404

  • Le prompt doit exister dans Langfuse et avoir au moins une version avec le label demandé (production par défaut). Vérifiez la page Prompts dans votre tableau de bord.

Augmentation de la latence due au traçage

  • Les appels SDK Langfuse sont non-bloquants — ils mettent en file d'attente les événements et les flush de façon asynchrone. Le seul travail synchrone est getPrompt (mis en cache après le premier appel). Si vous constatez des augmentations de latence, activez l'option cacheTtlSeconds sur getPrompt.

Prochaines étapes

Avec votre pipeline d'observabilité en place, voici des extensions naturelles :

  • RAG agentique avec Next.js — combinez les traces Langfuse avec un agent de récupération multi-étapes
  • Claude Agent SDK pour TypeScript — ajoutez des traces Langfuse aux workflows d'agents propulsés par Claude
  • Workflows multi-agents n8n — orchestrez plusieurs agents IA et tracez chacun d'eux
  • Datasets Langfuse — constituez des exemples tracés en datasets d'évaluation pour les tests de régression
  • Évaluation en ligne — configurez Langfuse pour exécuter automatiquement votre LLM judge sur chaque nouvelle trace via des webhooks

Conclusion

Vous disposez maintenant d'une couche d'observabilité de niveau production sur votre application IA Next.js. Chaque appel LLM est tracé avec tout son contexte, les utilisateurs peuvent signaler la qualité via des boutons de feedback, les prompts sont versionnés et gérables sans déploiement, et des évaluateurs automatisés notent continuellement la qualité des réponses.

La capacité à voir ce que fait votre IA — pas seulement si elle a réussi ou échoué, mais comment et pourquoi — c'est ce qui distingue les prototypes des produits. Langfuse vous offre cette visibilité avec la flexibilité open-source, que vous la fassiez tourner sur leur cloud ou que vous l'auto-hébergiez dans votre propre infrastructure.

Construisez de l'IA observable, pas des boîtes noires.

● Tags
#langfuse#llm#observability#nextjs#typescript#openai#monitoring#intermediate#28 min de lecture
● Partage
● Une question ?

Discutez de cet article avec un agent Noqta.

Noqta Team
Noqta Team
Author · noqta
Suivre ↗

● À lire ensuite

Construire un Agent IA Autonome avec Agentic RAG et Next.js
● Tutorial

Construire un Agent IA Autonome avec Agentic RAG et Next.js

11 févr. 2026
Construire des agents IA from scratch avec TypeScript : maîtriser le pattern ReAct avec le Vercel AI SDK
● Tutorial

Construire des agents IA from scratch avec TypeScript : maîtriser le pattern ReAct avec le Vercel AI SDK

12 févr. 2026
Guide d'Integration de Chatbot IA : Construire des Interfaces Conversationnelles Intelligentes
● Tutorial

Guide d'Integration de Chatbot IA : Construire des Interfaces Conversationnelles Intelligentes

25 janv. 2026
Noqta
Conditions générales · Politique de Confidentialité
Services
  • Automatisation IA
  • Agents IA
  • Automatisation CX
  • Vibe Coding
  • Gestion de Projet
  • Assurance Qualité
  • Développement Web
  • Intégration API
  • Applications Métier
  • Maintenance
  • Low-Code/No-Code
Liens
  • À propos de nous
  • Comment ça marche?
  • Actualités
  • Tutoriels
  • Blog
  • Contact
  • FAQ
  • Ressources
Régions
  • Arabie Saoudite
  • Émirats Arabes Unis
  • Qatar
  • Bahreïn
  • Oman
  • Libye
  • Tunisie
  • Algérie
  • Maroc
Entreprise
  • Noqta, Tunisie, Tunis, téléphone +216 40 385 594
© Noqta. Tous droits réservés.