Aller au contenu principal

Métriques et benchmarks

Mis à jour le 28 juillet 2026

Mesurer la qualité avec des métriques

Au-delà des scores LLM-as-Judge, il existe des métriques quantitatives pour évaluer différents aspects des sorties LLM. Leur intérêt n’est pas d’être plus justes — un juge LLM bien construit voit des choses qu’aucune formule ne verra — mais d’être stables et bon marché. Une similarité cosinus donne exactement le même nombre à chaque exécution, ne coûte presque rien, et peut donc tourner sur des milliers de cas à chaque commit. Vous réservez alors le juge LLM aux dimensions qui l’exigent vraiment.

Nous passerons donc en revue les métriques essentielles avant de les assembler en benchmarks reproductibles.

Comparer le sens plutôt que les mots

Comparer deux textes caractère par caractère est inutilisable dès que la formulation est libre. La similarité sémantique contourne le problème : chaque texte est converti en vecteur par un modèle d’embeddings, et l’on mesure l’angle entre les deux vecteurs. Deux phrases qui disent la même chose avec des mots différents pointent dans la même direction.

import openai
import numpy as np

client = openai.OpenAI()

def similarite_cosinus(vec_a: list[float], vec_b: list[float]) -> float:
    """Calcule la similarité cosinus entre deux vecteurs."""
    a = np.array(vec_a)
    b = np.array(vec_b)
    return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))

def similarite_semantique(texte_a: str, texte_b: str) -> float:
    """Mesure la similarité sémantique entre deux textes."""
    response = client.embeddings.create(
        model="text-embedding-3-large",
        input=[texte_a, texte_b],
    )

    vec_a = response.data[0].embedding
    vec_b = response.data[1].embedding

    return similarite_cosinus(vec_a, vec_b)

# Exemple
score = similarite_semantique(
    "Le chat est sur le tapis.",
    "Un félin se repose sur la moquette.",
)
print(f"Similarité : {score:.3f}")  # ~0.85+

L’exemple est instructif : deux phrases sans aucun mot en commun obtiennent plus de 0,85. C’est la force de la méthode, et aussi son piège. La similarité sémantique ignore la négation et les nuances de degré — « le paiement a été accepté » et « le paiement a été refusé » se ressemblent beaucoup, vectoriellement parlant. Ne l’utilisez donc jamais seule pour valider une réponse dont l’exactitude factuelle compte.

Quand la question est « la réponse répond-elle vraiment à la question posée », aucune formule ne remplace un jugement. Le score de pertinence ci-dessous décompose ce jugement en trois questions distinctes : la réponse est-elle directe, reste-t-elle focalisée, est-elle complète. Cette décomposition vaut mieux qu’une note globale, parce qu’elle vous dit dans quelle direction corriger le prompt — un modèle bavard et un modèle incomplet appellent des corrections opposées.

def score_pertinence(
    question: str,
    reponse: str,
    contexte: str | None = None,
) -> dict:
    """Évalue si la réponse est pertinente par rapport à la question."""
    prompt = (
        "Évaluez la pertinence de cette réponse.\n\n"
        f"Question : {question}\n"
    )
    if contexte:
        prompt += f"Contexte fourni : {contexte[:1000]}\n"
    prompt += (
        f"Réponse : {reponse}\n\n"
        "Critères :\n"
        "1. La réponse répond-elle directement à la question ? (0-5)\n"
        "2. La réponse contient-elle des informations non demandées ? (0-5, 5=aucune)\n"
        "3. La réponse est-elle complète ? (0-5)\n\n"
        "Répondez en JSON avec les clés : directe, focus, complétude."
    )

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

    import json
    try:
        scores = json.loads(response.output_text)
        moyenne = sum(scores.values()) / (len(scores) * 5)
        return {"score": moyenne, "details": scores}
    except (json.JSONDecodeError, TypeError):
        return {"score": 0, "erreur": "Parsing échoué"}

Notez la formulation inversée du deuxième critère, où 5 signifie « aucune information non demandée ». Elle permet d’additionner les trois notes dans le même sens, mais elle est contre-intuitive à la lecture : quand vous ajouterez vos propres critères, gardez cette convention ou vous obtiendrez des moyennes qui récompensent le défaut que vous vouliez pénaliser.

Vérifier que le modèle n’invente pas

Dans une application de type question-réponse sur documents, la métrique décisive n’est pas la qualité rédactionnelle mais la fidélité à la source. Le principe du contrôle est simple : on présente au juge le contexte d’origine et la réponse produite, et on lui demande de classer chaque affirmation selon qu’elle est supportée par le contexte, absente du contexte, ou en contradiction avec lui. La distinction entre ces deux dernières catégories compte, car une affirmation simplement absente peut être une connaissance générale exacte, alors qu’une affirmation contredisant la source est toujours une faute.

def detecter_hallucinations(
    contexte: str,
    reponse: str,
) -> dict:
    """Détecte les affirmations non supportées par le contexte."""
    prompt = (
        "Analysez cette réponse et identifiez toute affirmation "
        "qui n'est PAS supportée par le contexte fourni.\n\n"
        f"Contexte : {contexte[:3000]}\n\n"
        f"Réponse : {reponse}\n\n"
        "Pour chaque affirmation de la réponse, indiquez :\n"
        "- supportee : l'affirmation est dans le contexte\n"
        "- non_supportee : l'affirmation n'est pas dans le contexte\n"
        "- inventee : l'affirmation contredit le contexte\n\n"
        "Répondez en JSON avec la clé affirmations (liste) et "
        "score_fidelite (0 à 1)."
    )

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

    import json
    try:
        return json.loads(response.output_text)
    except json.JSONDecodeError:
        return {"score_fidelite": 0, "erreur": "Parsing échoué"}

La troncature du contexte à 3000 caractères est une limite dont vous devez avoir conscience : si la source dépasse cette taille, des affirmations parfaitement exactes seront comptées comme non supportées, simplement parce que le passage qui les justifie a été coupé. Sur des documents longs, découpez et évaluez par section plutôt que d’accepter un faux positif systématique.

Assembler un benchmark reproductible

Un benchmark n’est pas une métrique de plus : c’est un dispositif qui fige un dataset et un protocole pour que deux exécutions soient comparables. La classe ci-dessous mesure la pertinence, la fidélité quand un contexte est disponible, la latence et le coût, puis conserve chaque résultat daté et étiqueté par version de prompt.

from dataclasses import dataclass, field
import time

@dataclass
class ResultatBenchmark:
    modele: str
    prompt_version: str
    scores: dict
    latence_moyenne: float
    cout_total: float
    date: str = field(default_factory=lambda: time.strftime("%Y-%m-%d"))

class Benchmark:
    """Benchmark réutilisable pour comparer modèles et prompts."""

    def __init__(self, nom: str, dataset: list[dict]):
        self.nom = nom
        self.dataset = dataset
        self.resultats: list[ResultatBenchmark] = []

    def executer(
        self,
        modele: str,
        prompt_systeme: str,
        prompt_version: str,
    ) -> ResultatBenchmark:
        """Exécute le benchmark avec un modèle et prompt donnés."""
        scores_pertinence = []
        scores_hallucination = []
        latences = []
        cout = 0

        for cas in self.dataset:
            debut = time.perf_counter()
            response = client.responses.create(
                model=modele,
                instructions=prompt_systeme,
                input=cas["input"],
            )
            latence = time.perf_counter() - debut
            latences.append(latence)

            # Évaluer
            pertinence = score_pertinence(
                cas["input"], response.output_text
            )
            scores_pertinence.append(pertinence["score"])

            if cas.get("contexte"):
                halluc = detecter_hallucinations(
                    cas["contexte"], response.output_text
                )
                scores_hallucination.append(
                    halluc.get("score_fidelite", 0)
                )

        resultat = ResultatBenchmark(
            modele=modele,
            prompt_version=prompt_version,
            scores={
                "pertinence": sum(scores_pertinence) / len(scores_pertinence),
                "fidelite": (
                    sum(scores_hallucination) / len(scores_hallucination)
                    if scores_hallucination else None
                ),
            },
            latence_moyenne=sum(latences) / len(latences),
            cout_total=cout,
        )

        self.resultats.append(resultat)
        return resultat

    def comparer(self) -> str:
        """Compare tous les résultats du benchmark."""
        lignes = [f"Benchmark: {self.nom}\n"]
        for r in self.resultats:
            lignes.append(
                f"  {r.modele} (prompt {r.prompt_version}) | "
                f"Pertinence: {r.scores['pertinence']:.2f} | "
                f"Latence: {r.latence_moyenne:.2f}s"
            )
        return "\n".join(lignes)

La sortie de comparer place volontairement la pertinence et la latence sur la même ligne, parce que c’est ainsi que la décision se prend réellement. Un modèle qui gagne deux centièmes de pertinence en doublant le temps de réponse n’est pas un meilleur choix pour une application interactive ; il l’est peut-être pour un traitement nocturne. Le tableau ne décide pas à votre place, il vous met l’arbitrage sous les yeux.

Enfin, cout_total reste à zéro dans cette implémentation. Complétez-le à partir des champs d’usage retournés par l’API : une comparaison de modèles qui omet le coût laisse de côté le critère qui tranchera la discussion avec votre direction financière.

Points clés à retenir

  • La similarité sémantique mesure le sens, mais reste aveugle à la négation
  • Décomposez la pertinence en critères distincts plutôt qu’en note globale
  • Détectez les hallucinations en confrontant la réponse au contexte source
  • Créez des benchmarks reproductibles qui mesurent ensemble qualité, latence et coût
  • Combinez métriques automatiques et LLM-as-Judge pour une évaluation complète