Aller au contenu principal

Évaluer votre modèle fine-tuné

Mis à jour le 28 juillet 2026

Pourquoi l’évaluation est indispensable

Un fine-tuning réussi ne se mesure pas uniquement à la loss. La vraie question est : votre modèle fine-tuné est-il réellement meilleur que le modèle de base pour votre cas d’usage ? Une courbe qui descend proprement prouve seulement que le modèle a appris à reproduire vos exemples ; elle ne dit rien de son comportement face à une demande qu’il n’a jamais vue. Cette leçon vous montre comment construire un protocole d’évaluation rigoureux, en articulant mesures automatiques, jugement par modèle et relecture humaine.

Construire un jeu de test

Votre jeu de test doit être séparé de vos données d’entraînement ET de validation. La distinction compte : le jeu de validation sert à surveiller l’entraînement et influence donc vos décisions d’hyperparamètres, ce qui le contamine progressivement. Le jeu de test, lui, n’est ouvert qu’une fois le modèle figé. Il représente les situations réelles que votre modèle rencontrera en production, avec les mêmes proportions de cas simples et de cas tordus. Un découpage 80/10/10 sur un corpus mélangé au préalable constitue un point de départ raisonnable.

import json
import random

def preparer_jeux(fichier: str, ratios: tuple = (0.8, 0.1, 0.1)):
    """Sépare les données en train/validation/test."""
    with open(fichier, "r") as f:
        exemples = [json.loads(l) for l in f]

    random.shuffle(exemples)
    n = len(exemples)
    i1 = int(n * ratios[0])
    i2 = int(n * (ratios[0] + ratios[1]))

    jeux = {
        "train": exemples[:i1],
        "validation": exemples[i1:i2],
        "test": exemples[i2:]
    }

    for nom, data in jeux.items():
        with open(f"{nom}.jsonl", "w") as f:
            for ex in data:
                f.write(json.dumps(ex, ensure_ascii=False) + "\n")
        print(f"{nom} : {len(data)} exemples")

    return jeux

preparer_jeux("toutes_les_donnees.jsonl")

Évaluation automatique

Évaluation par métriques

Le principe est simple : pour chaque exemple de test, on retire le message de l’assistant — c’est la réponse attendue, le modèle ne doit pas la voir — on soumet le reste, et on conserve côte à côte l’entrée, la réponse de référence et la réponse produite. Cette structure à trois colonnes est le socle de tout ce qui suit ; l’exploiter en calcul de métriques ou en jugement qualitatif n’est qu’une question de traitement en aval.

from openai import OpenAI

client = OpenAI()

def evaluer_modele(modele: str, exemples_test: list[dict]) -> dict:
    """Évalue un modèle sur un jeu de test."""
    resultats = []

    for ex in exemples_test:
        # Extraire la question (sans la réponse attendue)
        messages_input = [
            m for m in ex["messages"] if m["role"] != "assistant"
        ]
        reponse_attendue = next(
            m["content"] for m in ex["messages"] if m["role"] == "assistant"
        )

        # Obtenir la réponse du modèle
        response = client.responses.create(
            model=modele,
            input=messages_input
        )
        reponse_modele = response.output_text

        resultats.append({
            "input": messages_input[-1]["content"],
            "attendu": reponse_attendue,
            "obtenu": reponse_modele
        })

    return resultats

Évaluation par LLM juge

Une technique puissante consiste à utiliser un modèle performant pour juger la qualité des réponses. La comparaison mot à mot avec la référence est inutilisable dès que plusieurs formulations sont acceptables ; un juge, lui, apprécie le fond. La qualité de son verdict dépend entièrement de la précision de la consigne : quatre critères notés de 1 à 5, une sortie JSON imposée, et un commentaire libre qui vous servira ensuite à comprendre les échecs. Prévoyez le cas où la sortie n’est pas parseable plutôt que de laisser l’exception interrompre la campagne.

def evaluer_avec_juge(resultats: list[dict]) -> list[dict]:
    """Utilise gpt-5.6-sol comme juge pour évaluer les réponses."""
    evaluations = []

    for r in resultats:
        prompt_juge = f"""Évaluez la qualité de la réponse obtenue par rapport à la réponse attendue.

Question : {r['input']}

Réponse attendue : {r['attendu']}

Réponse obtenue : {r['obtenu']}

Critères (notez chacun de 1 à 5) :
1. Exactitude : les informations sont-elles correctes ?
2. Complétude : tous les points importants sont-ils couverts ?
3. Style : le ton et le format sont-ils appropriés ?
4. Concision : la réponse est-elle de longueur appropriée ?

Répondez en JSON : {{"exactitude": N, "completude": N, "style": N, "concision": N, "commentaire": "..."}}"""

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

        try:
            evaluation = json.loads(response.output_text)
            evaluations.append(evaluation)
        except json.JSONDecodeError:
            evaluations.append({"erreur": "Réponse non parseable"})

    return evaluations

Métriques spécialisées

Les deux approches précédentes conviennent à une réponse rédigée. Dès que la sortie de votre modèle a une structure imposée, des mesures plus directes deviennent possibles et vous évitent le détour par un juge.

Pour la classification

Quand la sortie attendue appartient à un ensemble fermé — une catégorie de ticket, un sentiment, un code produit — la correspondance exacte redevient pertinente. Normalisez la casse et les espaces avant de comparer, faute de quoi un « Urgent » face à « urgent » sera compté comme une erreur et faussera votre taux.

def metriques_classification(resultats: list[dict]) -> dict:
    """Calcule précision, rappel et F1 pour une tâche de classification."""
    correct = sum(
        1 for r in resultats
        if r["attendu"].strip().lower() == r["obtenu"].strip().lower()
    )
    total = len(resultats)
    precision = correct / total if total > 0 else 0

    print(f"Précision : {precision:.2%} ({correct}/{total})")
    return {"precision": precision, "correct": correct, "total": total}

Pour le function calling

L’appel de fonction se juge sur deux axes qu’il faut mesurer séparément. Un modèle qui choisit la bonne fonction mais se trompe de paramètre échoue différemment de celui qui appelle carrément le mauvais outil : le premier problème se corrige par des exemples d’extraction, le second par des exemples de discrimination. Un score global unique masquerait cette distinction et vous ferait travailler à l’aveugle.

def metriques_function_calling(resultats: list[dict]) -> dict:
    """Évalue la qualité des appels de fonctions."""
    stats = {"bonne_fonction": 0, "bons_params": 0, "total": 0}

    for r in resultats:
        stats["total"] += 1
        if r.get("fonction_attendue") == r.get("fonction_obtenue"):
            stats["bonne_fonction"] += 1
        if r.get("params_attendus") == r.get("params_obtenus"):
            stats["bons_params"] += 1

    print(f"Bonne fonction : {stats['bonne_fonction']}/{stats['total']}")
    print(f"Bons paramètres : {stats['bons_params']}/{stats['total']}")
    return stats

Évaluation humaine

L’évaluation automatique ne capture pas tout. Un ton légèrement condescendant, une formule qui passe mal auprès de votre clientèle, une réponse techniquement juste mais inutilisable : ces défauts n’apparaissent que sous un œil humain. Le protocole tient en peu de choses. Échantillonnez vingt à cinquante réponses tirées au hasard, jamais les cas que vous avez déjà repérés comme problématiques, sinon vous mesurez votre propre biais de sélection. Définissez ensuite des critères clairs et mesurables, et formalisez-les dans une grille de notation standardisée pour que deux relecteurs jugent bien la même chose.

Faites enfin évaluer par plusieurs personnes et calculez l’accord inter-évaluateurs. Un désaccord marqué ne signale pas forcément un mauvais modèle : le plus souvent, il révèle que votre grille reste ambiguë et qu’il faut la préciser avant de conclure quoi que ce soit.

Grille d’évaluation type

Critère 1 (Mauvais) 3 (Acceptable) 5 (Excellent)
Pertinence Hors sujet Partiellement pertinent Parfaitement ciblé
Ton Inapproprié Correct sans plus Parfaitement aligné
Format Non respecté Partiellement respecté Parfaitement respecté

Rapport d’évaluation

Un dernier réflexe fait la différence entre une évaluation et une décision : consigner les deux familles de résultats dans un rapport unique, assorti d’une recommandation explicite. Le seuil que vous fixez — ici une moyenne humaine de 4 pour passer en production, de 3 pour itérer sur les données — doit être choisi avant de lancer la campagne. Décidé après coup, il s’ajuste toujours au résultat que l’on espérait.

def rapport_evaluation(resultats_auto: dict, resultats_humains: dict):
    """Génère un rapport d'évaluation combiné."""
    print("=" * 60)
    print("RAPPORT D'ÉVALUATION DU MODÈLE FINE-TUNÉ")
    print("=" * 60)

    print(f"\nMétriques automatiques :")
    for k, v in resultats_auto.items():
        print(f"  {k} : {v}")

    print(f"\nÉvaluation humaine :")
    for k, v in resultats_humains.items():
        print(f"  {k} : {v}")

    print(f"\nRecommandation :")
    score_moyen = sum(resultats_humains.values()) / len(resultats_humains)
    if score_moyen >= 4:
        print("  Le modèle est prêt pour la production")
    elif score_moyen >= 3:
        print("  Améliorations nécessaires — itérez sur les données")
    else:
        print("  Résultats insuffisants — revoyez votre approche")

Points clés à retenir

  • Séparez toujours un jeu de test indépendant (ni train, ni validation)
  • Combinez évaluation automatique et évaluation humaine
  • L’évaluation par LLM juge est un bon compromis entre les deux
  • Définissez des critères mesurables avant de commencer
  • Un score de loss bas ne garantit pas la qualité en production