Aller au contenu principal

Le framework Evals d'OpenAI

Mis à jour le 28 juillet 2026

Pourquoi évaluer systématiquement

Vous ne pouvez pas améliorer ce que vous ne mesurez pas. Quand vous changez un prompt, un modèle ou un paramètre, comment savez-vous que la qualité s’est améliorée ? La méthode spontanée consiste à essayer trois ou quatre exemples, à trouver les réponses meilleures, et à déployer. Elle échoue régulièrement, parce que les exemples testés sont précisément ceux qui posaient problème la semaine dernière : vous mesurez l’amélioration là où vous l’avez cherchée, et vous ne voyez pas les régressions introduites sur les cas qui fonctionnaient déjà.

Les évaluations (evals) transforment cette intuition en mesure objective. OpenAI fournit un framework d’évaluation intégré à la plateforme, accessible sur platform.openai.com dans la section dédiée. Vous y créez des jeux de données de test, définissez des critères d’évaluation, exécutez des évaluations automatiques et comparez les résultats entre modèles et prompts.

Trois notions structurent l’ensemble. Le dataset est un ensemble de cas de test avec entrées et sorties attendues : c’est votre référence, l’équivalent d’une suite de tests unitaires. Le grader est un critère d’évaluation — correspondance exacte, LLM-as-judge, score — qui décide si une réponse donnée est bonne. Le run est une exécution d’évaluation sur un dataset avec un modèle et un prompt donnés ; c’est l’unité que vous comparerez d’une version à l’autre.

Construire un dataset qui vous ressemble

Un cas de test se compose d’une entrée, d’une sortie attendue et de métadonnées. Ces dernières ne sont pas décoratives : ce sont elles qui vous permettront, après coup, de constater qu’un score global de 87 % cache un effondrement sur la catégorie « code » compensé par d’excellents résultats en géographie. Sans catégorie ni difficulté, vous n’avez qu’un chiffre agrégé, et un chiffre agrégé ne dit jamais où intervenir.

import json

# Un dataset est une liste de cas de test
dataset = [
    {
        "input": "Quelle est la capitale de la France ?",
        "expected": "Paris",
        "metadata": {"categorie": "geographie", "difficulte": "facile"},
    },
    {
        "input": "Expliquez le théorème de Bayes en une phrase.",
        "expected": "Le théorème de Bayes permet de calculer la probabilité "
                    "d'un événement en fonction de probabilités conditionnelles "
                    "connues.",
        "metadata": {"categorie": "mathématiques", "difficulte": "moyen"},
    },
    {
        "input": "Écrivez une fonction Python qui inverse une liste.",
        "expected": "def inverser(lst): return lst[::-1]",
        "metadata": {"categorie": "code", "difficulte": "facile"},
    },
]

# Sauvegarder en JSONL
with open("eval_dataset.jsonl", "w") as f:
    for cas in dataset:
        f.write(json.dumps(cas, ensure_ascii=False) + "\n")

Un dataset inventé au bureau évalue votre imagination, pas votre application. Vos utilisateurs formulent leurs demandes autrement que vous : fautes de frappe, questions à moitié posées, contexte implicite. La meilleure source de cas de test reste donc votre propre production, à condition de faire valider les réponses de référence par un humain — sans quoi vous figeriez les erreurs actuelles comme norme.

import openai
import random

client = openai.OpenAI()

def creer_dataset_depuis_logs(
    logs_production: list[dict],
    taille_echantillon: int = 100,
) -> list[dict]:
    """Crée un dataset d'évaluation à partir des logs de production."""
    # Échantillonner
    echantillon = random.sample(
        logs_production,
        min(taille_echantillon, len(logs_production)),
    )

    dataset = []
    for log in echantillon:
        dataset.append({
            "input": log["prompt"],
            "expected": log["reponse_validee"],  # Réponse validée par un humain
            "metadata": {
                "source": "production",
                "date": log["date"],
                "modele_original": log["modele"],
            },
        })

    return dataset

Choisir le bon grader

Le grader à correspondance exacte est le plus sévère et le plus rapide. Il ne convient qu’aux tâches dont la sortie est contrainte : une classification en trois catégories, l’extraction d’un numéro de facture, une réponse par oui ou non. Employé sur du texte libre, il donnerait zéro à une réponse parfaite simplement formulée autrement que la référence.

def grader_exact(reponse: str, attendu: str) -> dict:
    """Évalue si la réponse correspond exactement."""
    score = 1.0 if reponse.strip().lower() == attendu.strip().lower() else 0.0
    return {"score": score, "type": "exact"}

Le grader par inclusion assouplit la contrainte : il vérifie la présence de mots-clés attendus et rend un score partiel. C’est l’outil adapté aux réponses qui doivent couvrir des points obligatoires sans que la formulation importe — une réponse de support qui doit mentionner le délai de rétractation et l’adresse de retour, par exemple. Sa faiblesse est connue : un texte contenant les bons mots dans un ordre absurde obtiendrait un score parfait.

def grader_inclusion(reponse: str, mots_cles: list[str]) -> dict:
    """Vérifie que la réponse contient les mots-clés attendus."""
    reponse_lower = reponse.lower()
    trouves = sum(1 for mot in mots_cles if mot.lower() in reponse_lower)
    score = trouves / len(mots_cles) if mots_cles else 0.0
    return {
        "score": score,
        "trouves": trouves,
        "total": len(mots_cles),
        "type": "inclusion",
    }

Reste le plus puissant : un autre LLM évalue la qualité de la réponse. Le grader LLM-as-Judge apprécie ce qu’aucune règle mécanique ne sait apprécier — la clarté, la complétude, la justesse du ton. Deux détails d’implémentation le rendent utilisable en pratique. temperature=0.0 d’abord : sans cela, le même juge attribuerait des notes différentes à la même réponse d’une exécution à l’autre, et votre benchmark perdrait toute valeur comparative. Le repli sur json.JSONDecodeError ensuite : un juge qui commente au lieu de rendre du JSON ne doit pas faire tomber la campagne entière.

def grader_llm_juge(
    question: str,
    reponse: str,
    attendu: str,
    criteres: str = "exactitude, complétude, clarté",
) -> dict:
    """Utilise un LLM pour évaluer la qualité de la réponse."""
    prompt_juge = f"""Évaluez la qualité de cette réponse sur une échelle de 1 à 5.

Question : {question}
Réponse attendue : {attendu}
Réponse à évaluer : {reponse}

Critères : {criteres}

Répondez UNIQUEMENT avec un JSON :
{{"score": <1-5>, "justification": "<explication courte>"}}"""

    response = client.responses.create(
        model="gpt-5.6-terra",
        input=prompt_juge,
        temperature=0.0,
    )

    import json
    try:
        resultat = json.loads(response.output_text)
        resultat["score_normalise"] = resultat["score"] / 5.0
        return resultat
    except json.JSONDecodeError:
        return {"score": 0, "justification": "Erreur de parsing", "score_normalise": 0}

Assembler l’évaluation complète

L’orchestration ne présente aucune difficulté conceptuelle : on parcourt le dataset, on appelle le modèle avec le prompt système à tester, on applique le grader, on agrège. Le point intéressant est que la fonction de notation arrive en paramètre, ce qui vous laisse rejouer exactement le même dataset avec des critères différents selon la campagne — inclusion pour un contrôle rapide en cours de développement, juge LLM pour la validation avant mise en production.

async def executer_eval(
    dataset: list[dict],
    modele: str,
    prompt_systeme: str,
    grader_fn,
) -> dict:
    """Exécute une évaluation complète sur un dataset."""
    resultats = []

    for cas in dataset:
        response = client.responses.create(
            model=modele,
            instructions=prompt_systeme,
            input=cas["input"],
        )

        score = grader_fn(response.output_text, cas["expected"])
        resultats.append({
            "input": cas["input"],
            "attendu": cas["expected"],
            "obtenu": response.output_text,
            **score,
        })

    # Calculer les métriques agrégées
    scores = [r["score"] for r in resultats if "score" in r]
    return {
        "modele": modele,
        "nb_cas": len(dataset),
        "score_moyen": sum(scores) / len(scores) if scores else 0,
        "score_min": min(scores) if scores else 0,
        "score_max": max(scores) if scores else 0,
        "résultats": resultats,
    }

Le rapport conserve score_min et le détail de chaque cas, et c’est délibéré. La moyenne vous dit si vous avancez ; les cas les plus mal notés vous disent pourquoi. Prenez l’habitude de lire les cinq pires réponses de chaque exécution avant de commenter la moyenne : c’est là, et rarement ailleurs, que se trouve la prochaine correction à apporter.

Points clés à retenir

  • Les evals transforment l’intuition en mesure objective de la qualité
  • Créez vos datasets à partir de cas réels de production, validés par un humain
  • Choisissez le grader selon la tâche : exact pour le contraint, inclusion pour les points obligatoires, LLM-as-Judge pour le nuancé
  • Fixez temperature=0.0 sur le juge pour rendre les scores reproductibles
  • Exécutez les evals avant chaque changement de prompt ou de modèle