Aller au contenu principal

Créer des évaluations personnalisées

Mis à jour le 28 juillet 2026

Des évaluations adaptées à votre métier

Les graders génériques ne suffisent pas pour évaluer la qualité dans votre domaine spécifique. Un chatbot médical, un assistant juridique ou un outil de rédaction marketing ont chacun des critères de qualité différents, et ces critères ne sont pas seulement plus nombreux : ils sont hiérarchisés autrement. Une erreur de ton dans une newsletter est un désagrément ; la même approximation dans une notice de posologie est une faute. Un grader générique traite les deux de la même façon, ce qui revient à ne rien évaluer du tout.

D’où la nécessité d’évaluations sur mesure, bâties sur une rubrique explicite qui traduit vos priorités métier en pondérations. C’est cette rubrique que nous allons construire.

Traduire une exigence métier en rubrique

Une rubrique est une liste de critères pondérés dont la somme fait 1. L’exercice le plus utile n’est pas de coder la structure, qui tient en quinze lignes, mais de la remplir : décider que les bénéfices client pèsent 0,25 alors que la longueur ne pèse que 0,10 force une conversation explicite avec les responsables du produit. Ce que vous obtenez à la fin est une définition partagée de « une bonne réponse », que personne ne pourra contester après coup.

from dataclasses import dataclass

@dataclass
class Critere:
    nom: str
    description: str
    poids: float  # 0 à 1, somme des poids = 1

@dataclass
class Rubrique:
    nom: str
    criteres: list[Critere]

    def valider(self) -> bool:
        total = sum(c.poids for c in self.criteres)
        return abs(total - 1.0) < 0.01

# Exemple : rubrique pour un assistant de rédaction marketing
rubrique_marketing = Rubrique(
    nom="Qualité rédaction marketing",
    criteres=[
        Critere("ton", "Le ton est professionnel et engageant", 0.20),
        Critere("cta", "Contient un appel à l'action clair", 0.15),
        Critere("benefices", "Met en avant les bénéfices client", 0.25),
        Critere("longueur", "Respecte la longueur demandée", 0.10),
        Critere("grammaire", "Aucune faute de grammaire ou orthographe", 0.15),
        Critere("marque", "Respecte les guidelines de la marque", 0.15),
    ],
)

La méthode valider mérite d’être appelée en test, pas seulement écrite. Les rubriques évoluent : on ajoute un critère de conformité, on retire un critère devenu inutile, et la somme dérive à 1,15 sans que rien ne signale l’anomalie. Les scores restent alors calculables, simplement faux, et comparés d’une version à l’autre ils vous mèneront à des conclusions erronées.

Le grader traduit ensuite la rubrique en prompt, demande une note de 0 à 5 par critère, puis recompose un score global pondéré. Chaque note s’accompagne d’un commentaire court : c’est ce qui vous permettra, en relisant les mauvais cas, de comprendre si le modèle a mal répondu ou si votre critère était mal formulé.

import openai
import json

client = openai.OpenAI()

def evaluer_multi_criteres(
    question: str,
    reponse: str,
    rubrique: Rubrique,
) -> dict:
    """Évalue une réponse selon une rubrique multi-critères."""

    criteres_texte = "\n".join(
        f"- {c.nom} (poids: {c.poids}): {c.description}"
        for c in rubrique.criteres
    )

    prompt = (
        "Évaluez cette réponse selon les critères suivants.\n"
        "Pour chaque critère, donnez un score de 0 à 5.\n\n"
        f"Question posée : {question}\n"
        f"Réponse à évaluer : {reponse}\n\n"
        f"Critères :\n{criteres_texte}\n\n"
        "Répondez en JSON avec un objet scores contenant pour chaque critère "
        "un score (0-5) et un commentaire court."
    )

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

    try:
        resultat = json.loads(response.output_text)
        scores = resultat.get("scores", {})

        # Calculer le score pondéré
        score_pondere = 0
        for critere in rubrique.criteres:
            if critere.nom in scores:
                score_brut = scores[critere.nom]["score"] / 5.0
                score_pondere += score_brut * critere.poids

        return {
            "score_global": score_pondere,
            "scores_details": scores,
            "rubrique": rubrique.nom,
        }
    except (json.JSONDecodeError, KeyError):
        return {"score_global": 0, "erreur": "Parsing échoué"}

Attention à la boucle de calcul : un critère absent de la réponse du juge est ignoré silencieusement et contribue pour zéro au score pondéré. Une réponse excellente pourra donc plafonner à 0,75 simplement parce que le juge a oublié une clé. Si vos scores semblent bloqués sous un plafond inexpliqué, inspectez scores_details avant de mettre en cause le modèle évalué.

Quand la machine juge mieux qu’un juge

Faire noter du code par un LLM est une facilité coûteuse : le seul verdict qui compte est l’exécution. Le grader ci-dessous concatène le code généré et un test, l’écrit dans un fichier temporaire, l’exécute et compte les réussites. Le score obtenu n’a pas besoin d’être interprété — un test passe ou ne passe pas.

import subprocess
import tempfile

def evaluer_code_python(
    code_genere: str,
    tests: list[str],
) -> dict:
    """Évalue du code Python en l'exécutant contre des tests."""
    resultats_tests = []

    for i, test in enumerate(tests):
        code_complet = f"{code_genere}\n\n{test}"

        with tempfile.NamedTemporaryFile(
            mode="w", suffix=".py", delete=False
        ) as f:
            f.write(code_complet)
            f.flush()

            try:
                resultat = subprocess.run(
                    ["python3", f.name],
                    capture_output=True,
                    text=True,
                    timeout=10,
                )
                succes = resultat.returncode == 0
                resultats_tests.append({
                    "test": i,
                    "succes": succes,
                    "sortie": resultat.stdout[:200],
                    "erreur": resultat.stderr[:200] if not succes else "",
                })
            except subprocess.TimeoutExpired:
                resultats_tests.append({
                    "test": i,
                    "succes": False,
                    "erreur": "Timeout (10s)",
                })

    nb_succes = sum(1 for r in resultats_tests if r["succes"])
    return {
        "score": nb_succes / len(tests) if tests else 0,
        "tests_passes": nb_succes,
        "tests_total": len(tests),
        "details": resultats_tests,
    }

Le timeout=10 n’est pas une précaution de confort : un modèle produit régulièrement des boucles infinies, et sans cette borne votre campagne d’évaluation reste bloquée sur le premier cas défaillant. Rappelez-vous par ailleurs que vous exécutez ici du code que vous n’avez pas écrit — en dehors d’un environnement de développement isolé, cette fonction demande un bac à sable.

Le résumé illustre le cas mixte, où une partie du jugement est mécanique et l’autre non. La longueur se mesure : le ratio calculé pénalise symétriquement un résumé trop court et un résumé trop long, ce qui évite l’astuce du modèle qui recopie le texte source pour ne rien omettre. La fidélité, elle, ne se mesure pas par une règle et revient au juge LLM, avec une question directe sur les informations inventées.

def evaluer_resume(
    texte_original: str,
    resume: str,
    longueur_cible: int = 100,
) -> dict:
    """Évalue la qualité d'un résumé."""
    mots_resume = len(resume.split())
    ratio_longueur = min(
        mots_resume / longueur_cible,
        longueur_cible / max(mots_resume, 1),
    )

    # Fidélité via LLM
    response = client.responses.create(
        model="gpt-5.6-terra",
        input=(
            f"Le résumé suivant est-il fidèle au texte original ? "
            f"Contient-il des informations inventées ?\n\n"
            f"Texte original (extrait) : {texte_original[:2000]}\n\n"
            f"Résumé : {resume}\n\n"
            f"Répondez en JSON avec les clés : fidélité (0-5), "
            f"hallucinations (oui/non), commentaire (court)."
        ),
        temperature=0.0,
    )

    try:
        fidelite = json.loads(response.output_text)
    except json.JSONDecodeError:
        fidelite = {"fidelite": 0, "hallucinations": "inconnu"}

    return {
        "score_longueur": ratio_longueur,
        "score_fidelite": fidelite.get("fidelite", 0) / 5.0,
        "hallucinations": fidelite.get("hallucinations", "inconnu"),
        "mots_resume": mots_resume,
        "longueur_cible": longueur_cible,
    }

Rendre l’évaluation répétable

Une évaluation qu’on relance à la main finit par ne plus être relancée. Le pipeline encapsule la rubrique et conserve l’historique des campagnes, ce qui transforme une mesure ponctuelle en série temporelle : vous voyez que le score moyen est passé de 0,81 à 0,79 après le changement de prompt de mardi, information qu’aucune exécution isolée ne pouvait vous donner.

class PipelineEval:
    """Pipeline d'évaluation réutilisable."""

    def __init__(self, nom: str, rubrique: Rubrique):
        self.nom = nom
        self.rubrique = rubrique
        self.historique: list[dict] = []

    def evaluer_batch(
        self,
        dataset: list[dict],
        modele: str,
        prompt_systeme: str,
    ) -> dict:
        """Évalue un batch complet et retourne les métriques."""
        resultats = []

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

            eval_result = evaluer_multi_criteres(
                cas["input"],
                response.output_text,
                self.rubrique,
            )
            resultats.append(eval_result)

        scores = [r["score_global"] for r in resultats]
        rapport = {
            "pipeline": self.nom,
            "modele": modele,
            "nb_cas": len(dataset),
            "score_moyen": sum(scores) / len(scores),
            "score_min": min(scores),
            "score_max": max(scores),
        }

        self.historique.append(rapport)
        return rapport

Un conseil pour démarrer : construisez votre première rubrique sur vingt cas seulement, faites-la noter en parallèle par le juge LLM et par une personne du métier, puis comparez les deux séries. Les écarts vous diront quels critères sont mal décrits bien avant de vous dire quoi que ce soit sur la qualité du modèle.

Points clés à retenir

  • Créez des rubriques d’évaluation spécifiques à votre domaine métier, avec des poids explicites
  • Vérifiez que la somme des poids reste à 1 quand la rubrique évolue
  • Combinez évaluation automatique (code, longueur) et LLM-as-Judge (qualité)
  • Testez le code généré en l’exécutant, avec un timeout et un environnement isolé
  • Construisez un pipeline réutilisable qui conserve l’historique des campagnes