écrits/tutorial/2026/07
Tutorial27 juil. 2026·30 min

Évaluation d'applications IA en production avec Braintrust et TypeScript

Apprenez à évaluer vos applications IA en production avec Braintrust — lancez des expériences automatisées, suivez les régressions de qualité, gérez les versions de prompts et construisez un pipeline d'évaluation continu avec TypeScript et Next.js.

La lacune que les tests traditionnels ne comblent pas

Les tests unitaires affirment des résultats déterministes comme add(2, 3) === 5. Ce déterminisme s'effondre dès que votre code appelle un modèle de langage. Le même prompt différents jours, avec différentes versions du modèle, ou après une légère reformulation peut produire des sorties qui se notent très différemment en qualité. Les suites de tests classiques ne peuvent pas détecter cette dérive.

Braintrust est une plateforme d'évaluation IA construite précisément pour ce problème. Au lieu d'assertions ponctuelles, vous exécutez des expériences — votre fonction IA face à un jeu de données d'entrées, chaque résultat noté automatiquement et comparé dans le temps. À la fin de ce tutoriel, vous disposerez de :

  • Un runner d'expériences local qui note les sorties LLM par rapport aux vérités terrain
  • Un LLM-as-a-judge pour la qualité en texte libre
  • Un logging de traces en production sans instrumentation supplémentaire
  • Une porte CI/CD qui bloque les déploiements quand la qualité chute

Prérequis

  • Node.js 20 ou plus récent
  • TypeScript 5+
  • Une clé OpenAI API (tout fournisseur compatible fonctionne)
  • Un compte Braintrust — le niveau gratuit couvre ce tutoriel
  • Connaissance de base de TypeScript async et des concepts LLM

Étape 1 : Mise en place du projet

Créez un projet TypeScript vierge ou ajoutez Braintrust à un projet existant.

npm install braintrust autoevals openai zod

Ajoutez vos clés dans .env.local :

BRAINTRUST_API_KEY=your_braintrust_key
OPENAI_API_KEY=your_openai_key
BRAINTRUST_PROJECT_NAME=my-ai-app

Créez un tsconfig.json ciblant ES2022 ou plus récent pour que await de haut niveau fonctionne dans les scripts d'évaluation :

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "outDir": "dist"
  }
}

Étape 2 : Votre première expérience

Une expérience dans Braintrust se compose de trois parties : un jeu de données d'entrées (et des sorties attendues facultatives), une fonction de tâche qui appelle votre IA, et un ou plusieurs scorers qui notent chaque sortie.

Créez lib/eval/summarize.eval.ts :

import { Eval } from "braintrust";
import OpenAI from "openai";
 
const openai = new OpenAI();
 
// La tâche : la fonction IA soumise à évaluation
async function summarize(input: { text: string }): Promise<string> {
  const response = await openai.chat.completions.create({
    model: "gpt-4o-mini",
    messages: [
      {
        role: "system",
        content: "Résumez le texte suivant en deux à trois phrases concises.",
      },
      { role: "user", content: input.text },
    ],
  });
  return response.choices[0].message.content ?? "";
}
 
// Scorer : récompense les résumés concis (moins de 60 mots)
function concisenessScorer(args: { output: string }) {
  const wordCount = args.output.split(/\s+/).length;
  const score = wordCount <= 40 ? 1 : wordCount <= 70 ? 0.7 : 0.3;
  return { name: "conciseness", score };
}
 
// Jeu de données : exemples de référence avec réponses attendues
const dataset = [
  {
    input: {
      text: "Next.js 16 a introduit des améliorations majeures pour l'App Router, notamment des Server Actions plus rapides, un système de cache repensé avec un contrôle explicite pour le développeur, un streaming amélioré pour les grandes données, et un nouveau mode Partial Prerendering qui mélange rendu statique et dynamique sur la même page.",
    },
    expected:
      "Next.js 16 améliore l'App Router avec des Server Actions plus rapides, un cache repensé, un meilleur streaming et un nouveau mode Partial Prerendering.",
  },
];
 
Eval("text-summarization", {
  data: dataset,
  task: summarize,
  scores: [concisenessScorer],
});

Lancez l'expérience :

npx braintrust eval lib/eval/summarize.eval.ts

Braintrust affiche un lien vers votre tableau de bord où vous pouvez inspecter chaque entrée, la sortie produite par votre fonction, la valeur attendue et chaque score. La première exécution devient votre baseline.

Étape 3 : Scoring LLM-as-a-judge

La correspondance de chaînes est fragile pour des sorties en texte libre. Une meilleure approche consiste à utiliser un second appel LLM pour juger la qualité. Le package autoevals fournit des scorers prêts à l'emploi pour les tâches courantes.

import { Factuality, Similarity } from "autoevals";
 
Eval("text-summarization-v2", {
  data: dataset,
  task: summarize,
  scores: [
    // Vérifie la précision factuelle par rapport à la sortie attendue
    Factuality,
    // Mesure la similarité sémantique sur une échelle 0-1
    Similarity,
    // Votre vérification de concision personnalisée
    concisenessScorer,
  ],
});

Factuality utilise un juge LLM avec raisonnement enchaîné pour évaluer si votre sortie introduit des hallucinations par rapport à la réponse attendue. Il détecte des régressions de qualité que la simple comparaison de chaînes rate complètement.

Scorers disponibles dans autoevals :

  • Factuality — détection des hallucinations
  • Similarity — proximité sémantique
  • AnswerCorrectness — pour les tâches de questions-réponses
  • AnswerRelevancy — vérifie si la réponse adresse la question
  • ContextRecall — pour les pipelines RAG
  • Toxicity, Moderation — qualité du contenu

Étape 4 : Modèle de juge LLM personnalisé

Pour un scoring spécifique à votre domaine, définissez votre propre template de juge :

import { LLMClassifierFromTemplate } from "autoevals";
 
const faithfulnessJudge = LLMClassifierFromTemplate({
  name: "faithfulness",
  promptTemplate: `Vous évaluez si un résumé généré par IA est fidèle au texte source.
 
Texte source :
{{input.text}}
 
Résumé :
{{output}}
 
Notez la fidélité :
- "A" — entièrement précis, aucune hallucination
- "B" — majoritairement précis, omissions mineures
- "C" — inexact ou trompeur
 
Répondez avec une seule lettre.`,
  choiceScores: { A: 1, B: 0.6, C: 0 },
  useCoT: false,
});

Les placeholders {{input.text}} et {{output}} sont remplis par Braintrust à l'exécution. useCoT: true demande au juge de raisonner avant de répondre, ce qui améliore la précision sur les tâches complexes au prix de tokens supplémentaires.

Étape 5 : Wrapping du client OpenAI pour le tracing en production

En production, vous souhaitez que chaque appel LLM soit journalisé automatiquement — nom du modèle, prompt, réponse, latence et coût en tokens — sans instrumenter manuellement chaque appel.

import { wrapOpenAI, initLogger } from "braintrust";
import OpenAI from "openai";
 
// Initialisez une seule fois au démarrage de l'application
initLogger({
  projectName: process.env.BRAINTRUST_PROJECT_NAME!,
  apiKey: process.env.BRAINTRUST_API_KEY,
  asyncFlush: true, // non-bloquant pour les environnements serverless
});
 
// Le client wrappé est un remplacement direct du client standard
export const openai = wrapOpenAI(new OpenAI());

Chaque appel fait via openai apparaît désormais dans votre projet Braintrust comme un span journalisé — aucune autre modification nécessaire. Vous pouvez filtrer par modèle, plage de dates, contenu de prompt, percentile de latence ou coût en tokens dans le tableau de bord.

Étape 6 : Spans manuels pour les pipelines complexes

Quand votre pipeline IA effectue plus d'un appel LLM — récupération, reclassement, raisonnement multi-étapes — utilisez traced pour les regrouper :

import { traced } from "braintrust";
 
export async function answerWithContext(question: string, docs: string[]) {
  return traced(
    async (span) => {
      span.log({ input: { question, docCount: docs.length } });
 
      const context = docs.join("\n\n");
 
      const answer = await openai.chat.completions.create({
        model: "gpt-4o",
        messages: [
          {
            role: "system",
            content: `Répondez à la question en utilisant uniquement le contexte fourni.\n\nContexte :\n${context}`,
          },
          { role: "user", content: question },
        ],
      });
 
      const output = answer.choices[0].message.content ?? "";
 
      span.log({
        output,
        metadata: { tokensUsed: answer.usage?.total_tokens },
      });
 
      return output;
    },
    { name: "rag-answer" }
  );
}

La trace résultante dans Braintrust montre l'arbre de spans complet : timing de récupération, détails de l'appel LLM et temps de réponse global en une seule vue.

Étape 7 : Intégration avec App Router de Next.js

Intégrez le client tracé dans votre route API :

// app/api/chat/route.ts
import { NextResponse } from "next/server";
import { traced, initLogger } from "braintrust";
import { openai } from "@/lib/braintrust"; // votre client wrappé
 
initLogger({
  projectName: process.env.BRAINTRUST_PROJECT_NAME!,
  apiKey: process.env.BRAINTRUST_API_KEY,
  asyncFlush: true,
});
 
export async function POST(req: Request) {
  const { message, userId } = (await req.json()) as {
    message: string;
    userId: string;
  };
 
  const response = await traced(
    async (span) => {
      span.log({ input: { message }, metadata: { userId } });
 
      const completion = await openai.chat.completions.create({
        model: "gpt-4o",
        messages: [{ role: "user", content: message }],
      });
 
      const output = completion.choices[0].message.content ?? "";
      span.log({ output });
 
      return output;
    },
    { name: "chat" }
  );
 
  return NextResponse.json({ response });
}

Grâce à asyncFlush: true, la trace est envoyée à Braintrust en arrière-plan après le retour de la réponse — aucune latence supplémentaire perceptible par vos utilisateurs.

Étape 8 : Porte qualité CI/CD

Automatisez les évaluations à chaque pull request qui touche au code IA. Créez .github/workflows/eval.yml :

name: Porte d'évaluation IA
 
on:
  pull_request:
    paths:
      - "lib/ai/**"
      - "lib/eval/**"
      - "prompts/**"
      - "app/api/**"
 
jobs:
  eval:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"
      - run: npm ci
      - name: Lancer les évaluations Braintrust
        env:
          BRAINTRUST_API_KEY: ${{ secrets.BRAINTRUST_API_KEY }}
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
        run: |
          npx braintrust eval lib/eval/*.eval.ts --threshold 0.75

Le flag --threshold 0.75 quitte avec un code non nul si le score moyen descend en dessous de 75%, bloquant le merge. Le seuil est ajustable — augmentez-le à mesure que vos évaluations mûrissent.

Pour GitLab CI, l'étape équivalente dans .gitlab-ci.yml :

eval:
  stage: test
  image: node:20
  only:
    changes:
      - lib/ai/**
      - lib/eval/**
  script:
    - npm ci
    - npx braintrust eval lib/eval/*.eval.ts --threshold 0.75
  variables:
    BRAINTRUST_API_KEY: $BRAINTRUST_API_KEY
    OPENAI_API_KEY: $OPENAI_API_KEY

Étape 9 : Comparaison d'expériences (A/B testing de prompts)

Exécutez le même jeu de données sur deux variantes de prompt et comparez les scores côte à côte :

import { Eval } from "braintrust";
import { Factuality } from "autoevals";
 
const DATASET = [/* vos cas de test */];
 
async function summarizeV1(input: { text: string }) {
  const res = await openai.chat.completions.create({
    model: "gpt-4o-mini",
    messages: [
      { role: "system", content: "Résumez brièvement." },
      { role: "user", content: input.text },
    ],
  });
  return res.choices[0].message.content ?? "";
}
 
async function summarizeV2(input: { text: string }) {
  const res = await openai.chat.completions.create({
    model: "gpt-4o-mini",
    messages: [
      {
        role: "system",
        content:
          "Vous êtes un rédacteur technique. Résumez le texte ci-dessous en deux phrases en préservant tous les faits clés.",
      },
      { role: "user", content: input.text },
    ],
  });
  return res.choices[0].message.content ?? "";
}
 
Eval("summarization-v1", { data: DATASET, task: summarizeV1, scores: [Factuality] });
Eval("summarization-v2", { data: DATASET, task: summarizeV2, scores: [Factuality] });

Ouvrez la vue Comparer dans Braintrust pour voir quelle variante gagne sur chaque scorer, pour chaque exemple individuel. L'ingénierie de prompts devient ainsi un processus fondé sur des preuves plutôt que sur l'intuition.

Étape 10 : Suivi des régressions dans le temps

Braintrust compare automatiquement chaque nouvelle exécution d'expérience à la précédente. Les scores qui baissent apparaissent en rouge ; les améliorations en vert. Pour marquer une expérience comme baseline officielle :

npx braintrust eval lib/eval/summarize.eval.ts --set-baseline

Les exécutions futures référencent cette baseline dans le tableau de bord. Si une mise à jour de modèle dégrade votre qualité de résumé, vous le voyez immédiatement — pas trois sprints plus tard quand un utilisateur le signale.

Dépannage

Les scores n'apparaissent pas dans le tableau de bord : Vérifiez que votre scorer retourne { name: string; score: number }. Les deux champs sont requis ; retourner uniquement le nombre fait tomber silencieusement le score.

Traces manquantes en production : Sur Vercel ou Cloudflare Workers, le processus peut se terminer avant que le flush asynchrone ne soit complet. Ajoutez await logger.flush() à la fin de votre handler pour une livraison garantie.

Trop de tokens consommés lors des évaluations : Utilisez gpt-4o-mini ou claude-haiku-4-5 comme modèle juge pour les tâches de classification simples. Réservez les modèles frontières pour les tâches qui nécessitent vraiment un raisonnement approfondi.

Limites de taux lors des exécutions parallèles : Braintrust exécute les cas du jeu de données en parallèle par défaut. Utilisez un rate limiter ou définissez le flag --concurrency :

npx braintrust eval lib/eval/*.eval.ts --concurrency 3

Prochaines étapes

  • Explorez le serveur MCP de Braintrust pour utiliser Claude ou GPT-4o pour concevoir des jeux de données d'évaluation à partir de descriptions en langage naturel
  • Construisez une boucle d'évaluation en ligne qui échantillonne 1 à 5% des traces de production et les réévalue chaque nuit, avec alerte sur la dérive
  • Utilisez l'API Datasets de Braintrust pour sélectionner des exemples dorés depuis les logs de production et enrichir continuellement votre suite de tests
  • Combinez avec Langfuse pour une observabilité complète du cycle de vie : Braintrust pour les évaluations, Langfuse pour le tracing runtime et la gestion des versions de prompts

Conclusion

Vous avez construit un pipeline complet d'évaluation IA : des expériences qui notent automatiquement les sorties LLM, un juge LLM pour la qualité en texte libre, un tracing de production sans code supplémentaire sur votre client OpenAI, et une porte CI/CD qui bloque les déploiements lors des régressions. Les changements de prompts sont désormais traités avec la même rigueur que les changements de code — et vos utilisateurs bénéficient d'une expérience IA mesurable et en amélioration continue.