Évaluer un LLM : Métriques, Benchmarks et Tests Pratiques

Évaluer un LLM : Métriques, Benchmarks et Tests Pratiques

Claude, GPT-4o, Mistral, Llama 3.3 — comment choisir le bon modèle de langage pour votre projet métier ? Beaucoup d’équipes font l’erreur de se fier uniquement aux classements génériques, puis découvrent que le modèle “numéro un au benchmark” échoue lamentablement sur leurs données réelles. Une étude de 2024 de l’entreprise Patronus AI révèle que les benchmarks standards prédisent correctement les performances métier dans seulement 42 % des cas. Ce guide vous donne les clés pour évaluer rigoureusement un LLM sur votre cas d’usage spécifique.

Comprendre les Métriques Fondamentales

Perplexité — La Surprise du Modèle

La perplexité mesure à quel point un modèle “surprend” par les séquences de texte. Une perplexité basse = le modèle prédit bien les tokens suivants = meilleure fluidité linguistique. Mais attention : une faible perplexité ne garantit pas la précision factuelle.

import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
import numpy as np

def calculer_perplexite(texte: str, modele_nom: str) -> float:
    tokenizer = AutoTokenizer.from_pretrained(modele_nom)
    model = AutoModelForCausalLM.from_pretrained(modele_nom, torch_dtype=torch.float16)
    model.eval()

    encodings = tokenizer(texte, return_tensors="pt")
    max_length = model.config.max_position_embeddings
    stride = 512

    nlls = []
    seq_len = encodings.input_ids.size(1)

    for i in range(0, seq_len, stride):
        begin_loc = max(i + stride - max_length, 0)
        end_loc = min(i + stride, seq_len)
        trg_len = end_loc - i
        input_ids = encodings.input_ids[:, begin_loc:end_loc]
        target_ids = input_ids.clone()
        target_ids[:, :-trg_len] = -100

        with torch.no_grad():
            outputs = model(input_ids, labels=target_ids)
            nlls.append(outputs.loss)

    ppl = torch.exp(torch.stack(nlls).mean())
    return ppl.item()

# Comparer deux modèles sur votre corpus métier
texte_test = open("corpus_metier.txt").read()[:5000]
ppl_mistral = calculer_perplexite(texte_test, "mistralai/Mistral-7B-v0.3")
print(f"Perplexité Mistral 7B : {ppl_mistral:.1f}")

BLEU et ROUGE — Qualité de la Génération de Texte

BLEU (traduction) et ROUGE (résumé) mesurent le chevauchement entre le texte généré et une référence humaine. Ils restent utiles pour les tâches de génération contrainte (résumé de contrats, extraction structurée).

from rouge_score import rouge_scorer
from nltk.translate.bleu_score import sentence_bleu, SmoothingFunction

def evaluer_generation(reference: str, hypothese: str) -> dict:
    # ROUGE
    scorer = rouge_scorer.RougeScorer(["rouge1", "rouge2", "rougeL"], use_stemmer=True)
    scores_rouge = scorer.score(reference, hypothese)

    # BLEU
    ref_tokens = [reference.split()]
    hyp_tokens = hypothese.split()
    bleu = sentence_bleu(ref_tokens, hyp_tokens,
                         smoothing_function=SmoothingFunction().method1)

    return {
        "bleu": bleu,
        "rouge1_f": scores_rouge["rouge1"].fmeasure,
        "rouge2_f": scores_rouge["rouge2"].fmeasure,
        "rougeL_f": scores_rouge["rougeL"].fmeasure
    }

# Exemple : évaluer des résumés de documents juridiques
reference = "Le contrat stipule une durée de 12 mois, renouvelable tacitement."
hypothese_modele_A = "Le contrat dure un an et se renouvelle automatiquement."
hypothese_modele_B = "Un accord a été signé pour une période déterminée."

scores_A = evaluer_generation(reference, hypothese_modele_A)
scores_B = evaluer_generation(reference, hypothese_modele_B)
print(f"Modèle A — ROUGE-L: {scores_A['rougeL_f']:.3f}")
print(f"Modèle B — ROUGE-L: {scores_B['rougeL_f']:.3f}")

Les Benchmarks Standards — Ce Qu’ils Mesurent Vraiment

MMLU — Connaissances Générales Multi-Domaines

MMLU (Massive Multitask Language Understanding) teste 57 domaines académiques avec des QCM. Un modèle à 85 % sur MMLU maîtrise bien les connaissances factuelles générales — pertinent si votre LLM doit répondre à des questions générales.

Limite : MMLU est en anglais et teste des connaissances “scolaires”. Il prédit mal les performances sur des données métier spécialisées en français.

HumanEval — Génération de Code

HumanEval mesure la capacité à résoudre 164 problèmes de programmation Python. Indispensable si votre application génère du code (Copilot-like, automatisation).

# Exemple de problème HumanEval typique
# Le modèle doit compléter cette fonction

def trier_tableau(lst):
    """
    Trier une liste de tuples (nom, age) d'abord par age croissant,
    puis alphabétiquement pour les égalités.
    >>> trier_tableau([("Alice", 25), ("Bob", 25), ("Charlie", 20)])
    [('Charlie', 20), ('Alice', 25), ('Bob', 25)]
    """
    # Le LLM doit générer cette ligne :
    return sorted(lst, key=lambda x: (x[1], x[0]))

Les meilleurs modèles atteignent 85-92 % sur HumanEval (GPT-4o, Claude 3.5 Sonnet). Mistral 7B plafonne autour de 35-45 %.

MT-Bench — Conversations Multi-Tours

MT-Bench évalue la cohérence sur 8 catégories de questions avec suivi de contexte (80 conversations). C’est le benchmark le plus proche des usages réels de chatbot.

LMSYS Chatbot Arena — L’Évaluation Humaine à Grande Échelle

LMSYS est un tournoi en aveugle où des humains comparent deux réponses anonymes. Le classement Elo résultant est actuellement la meilleure prédiction de préférence humaine générale.

Classement approximatif (fin 2025) :

ModèleMMLUHumanEvalMT-BenchElo Arena
GPT-4o88 %90 %9.0~1300
Claude 3.5 Sonnet88 %92 %9.0~1295
Gemini 1.5 Pro85 %84 %8.9~1280
Llama 3.3 70B86 %80 %8.8~1250
Mistral Large 284 %86 %8.7~1240
Mistral 7B64 %40 %7.3~1100

Construire votre Propre Jeu d’Évaluation Métier

C’est la partie la plus importante et la plus négligée. Les benchmarks génériques ne remplaceront jamais un test sur vos données réelles.

Étape 1 — Définir vos Dimensions d’Évaluation

from dataclasses import dataclass
from typing import Literal

@dataclass
class CritereEvaluation:
    nom: str
    poids: float  # Importance relative (somme = 1.0)
    methode: Literal["llm_juge", "regex", "humain", "score_automatique"]

# Exemple pour un assistant juridique PME
criteres_juridique = [
    CritereEvaluation("exactitude_factuelle", poids=0.35, methode="humain"),
    CritereEvaluation("conformite_droit_francais", poids=0.25, methode="humain"),
    CritereEvaluation("clarte_explication", poids=0.20, methode="llm_juge"),
    CritereEvaluation("completude_reponse", poids=0.15, methode="llm_juge"),
    CritereEvaluation("absence_hallucination", poids=0.05, methode="regex"),
]

Étape 2 — Constituer le Dataset d’Évaluation

Minimum recommandé : 100 à 200 questions avec réponses de référence validées par un expert métier. Couvrez les cas nominaux, les cas limites, et les questions pièges (où le modèle risque d’halluciner).

import json
from pathlib import Path

# Structure d'un exemple d'évaluation
dataset_eval = [
    {
        "id": "legal_001",
        "question": "Quelle est la durée légale de conservation d'une facture en France ?",
        "reponse_reference": "10 ans à partir de la clôture de l'exercice comptable.",
        "mots_cles_obligatoires": ["10 ans", "exercice comptable"],
        "pieges": ["ne pas confondre avec délai fiscal 6 ans ou délai commercial 5 ans"],
        "difficulte": "moyen",
        "domaine": "comptabilite"
    },
    # ... 199 autres exemples
]

Path("eval_dataset.json").write_text(json.dumps(dataset_eval, ensure_ascii=False, indent=2))

Étape 3 — Évaluation Automatique par LLM-Juge

L’évaluation humaine est la plus fiable mais lente. Pour automatiser, utilisez un LLM juge (GPT-4o ou Claude recommandés pour leur cohérence) :

import anthropic

client = anthropic.Anthropic()

def evaluer_reponse_avec_llm(question: str, reference: str, reponse_modele: str) -> dict:
    prompt = f"""Tu es un expert en évaluation de réponses de LLM. Évalue la réponse suivante.

QUESTION : {question}

RÉPONSE DE RÉFÉRENCE : {reference}

RÉPONSE DU MODÈLE ÉVALUÉ : {reponse_modele}

Donne une note de 1 à 5 pour chaque critère et justifie brièvement.
Réponds en JSON avec cette structure :
{{
  "exactitude": {{"note": 1-5, "justification": "..."}},
  "completude": {{"note": 1-5, "justification": "..."}},
  "clarte": {{"note": 1-5, "justification": "..."}},
  "hallucinations": {{"note": 1-5, "justification": "... (5 = aucune hallucination)"}}
}}"""

    response = client.messages.create(
        model="claude-3-5-sonnet-20241022",
        max_tokens=512,
        messages=[{"role": "user", "content": prompt}]
    )

    return json.loads(response.content[0].text)

# Évaluer un modèle sur tout le dataset
def evaluer_modele(modele_nom: str, dataset: list, client_llm) -> dict:
    scores_totaux = []

    for exemple in dataset:
        reponse = client_llm.generer(exemple["question"])
        eval_auto = evaluer_reponse_avec_llm(
            exemple["question"],
            exemple["reponse_reference"],
            reponse
        )
        score_moyen = np.mean([v["note"] for v in eval_auto.values()])
        scores_totaux.append(score_moyen)

    return {
        "modele": modele_nom,
        "score_moyen": np.mean(scores_totaux),
        "score_median": np.median(scores_totaux),
        "ecart_type": np.std(scores_totaux),
        "questions_parfaites": sum(1 for s in scores_totaux if s >= 4.5) / len(scores_totaux)
    }

A/B Testing de Prompts — Optimiser avant de Choisir

Avant de conclure qu’un modèle est supérieur, testez plusieurs formulations de prompt sur le même modèle. Le prompt peut faire varier les performances de 20 à 40 %.

import asyncio
from itertools import product

PROMPTS_SYSTEME = {
    "direct": "Réponds de façon précise et concise.",
    "expert": "Tu es un expert comptable français avec 20 ans d'expérience. Tes réponses sont précises et conformes à la réglementation française en vigueur.",
    "structure": "Réponds en suivant cette structure : 1) Réponse directe 2) Justification légale 3) Points d'attention."
}

async def tester_combinaisons(questions: list, modeles: list, prompts: dict):
    resultats = {}

    for modele, (nom_prompt, prompt_sys) in product(modeles, prompts.items()):
        cle = f"{modele}_{nom_prompt}"
        scores = []

        for q in questions:
            reponse = await generer_reponse_async(modele, prompt_sys, q["question"])
            eval_score = await evaluer_async(q["question"], q["reponse_reference"], reponse)
            scores.append(eval_score)

        resultats[cle] = {"score_moyen": sum(scores) / len(scores)}
        print(f"{cle}: {resultats[cle]['score_moyen']:.2f}/5")

    return resultats

Arbitrage Coût/Qualité — Choisir Pragmatiquement

Matrice de Décision

Cas d’usageModèle recommandéRaison
Questions générales, FAQMistral 7B / Llama 3.1 8BÉconomique, suffisant
Résumé de documentsClaude 3.5 HaikuExcellent rapport qualité/prix
Génération de codeClaude 3.5 Sonnet / GPT-4oMeilleure précision technique
Analyse juridique/médicaleClaude 3.5 SonnetRigueur + moindre hallucination
Traitement massif (batch)Mistral Large 2Équilibre coût/qualité à grande échelle
Données sensibles (on-premise)Llama 3.3 70BDéploiement local possible

Calculer le Coût Total par Requête

# Estimation coût pour 100 000 requêtes/mois
couts_pour_mille_tokens = {
    "gpt-4o": {"input": 0.0025, "output": 0.01},
    "claude-3-5-sonnet": {"input": 0.003, "output": 0.015},
    "mistral-large-2": {"input": 0.002, "output": 0.006},
    "llama-3.3-70b-groq": {"input": 0.0006, "output": 0.0006},
}

def estimer_cout_mensuel(
    modele: str,
    nb_requetes: int,
    tokens_input_moyen: int = 500,
    tokens_output_moyen: int = 200
) -> float:
    tarifs = couts_pour_mille_tokens[modele]
    cout_input = (tokens_input_moyen / 1000) * tarifs["input"] * nb_requetes
    cout_output = (tokens_output_moyen / 1000) * tarifs["output"] * nb_requetes
    return cout_input + cout_output

for modele in couts_pour_mille_tokens:
    cout = estimer_cout_mensuel(modele, 100_000)
    print(f"{modele:30s} : {cout:.0f} €/mois (100k requêtes)")

Conclusion

Choisir un LLM sans l’évaluer sur vos données réelles, c’est comme signer un contrat sans le lire. Les benchmarks génériques donnent une orientation, mais seul un test rigoureux sur vos cas d’usage métier — avec vos données, vos prompts, vos critères de qualité — permet de prendre la bonne décision.

La méthode recommandée : 1) construire un jeu de 100-200 questions avec réponses expertes, 2) tester 3 à 5 modèles candidats, 3) évaluer automatiquement via LLM-juge, 4) valider les cas limites manuellement, 5) intégrer le coût dans la décision finale.

Vous devez choisir un LLM pour un projet métier critique et ne savez pas par où commencer ? Contactez-nous à contact@brio-novia.eu — nous concevons et exécutons des évaluations rigoureuses adaptées à votre contexte.