Accueil / Articles / Une évaluation de modèles de langage grand public sans dépendances, avec un juge en qui on peut vraiment avoir confiance

Une évaluation de modèles de langage grand public sans dépendances, avec un juge en qui on peut vraiment avoir confiance

Créez une petite évaluation de LLM à partir de journaux réels, de vérifications de code et d’un juge basé sur un seul critère, puis calibrez ce juge par rapport aux étiquettes fournies par des humains afin que ses scores aient une signification.

2147 mots

Les tests unitaires fonctionnent parce que du code déterministe donne toujours la même réponse : on vérifie qu’une fonction renvoie quatre, et c’est bien ce qu’elle fait à chaque fois. La sortie d’un modèle de langage change les formulations à chaque appel, il n’y a donc pas de chaîne de caractères attendue à comparer, et les équipes répondent souvent en adoptant une plateforme d’évaluation qui utilise un autre modèle pour noter les résultats. C’est avec ce modèle de notation que la plupart des évaluations cessent silencieusement de mesurer quoi que ce soit, car personne n’a confirmé qu’il était en accord avec un jugement humain. Ce guide permet de créer une évaluation complète en Python simple en quelques heures, et consacre sa section la plus importante à la calibration du juge afin que le chiffre obtenu puisse être justifié.

Les trois composants de toute évaluation

En mettant de côté les outils, une évaluation se compose de trois éléments, aucun desquels n’a besoin d’une bibliothèque :

  • Un ensemble de données d’entrée, de préférence issues d’une utilisation réelle plutôt que créées artificiellement.
  • Un moyen de générer une sortie pour chaque entrée, qui n’est rien d’autre que votre système en cours de test.
  • Une fonction de notation qui convertit chaque sortie en un verdict que vous pouvez compter.
  • Les tableaux de bord, le suivi des traitements et les ensembles de données hébergés sont des fonctionnalités conçues autour de ces éléments. Certaines d’entre elles sont véritablement utiles, mais le cœur du système tient en environ quatre-vingts lignes de Python, et un code aussi compact continuera de fonctionner longtemps après que n’importe quelle plateforme ait changé ou disparu.

    Étape 1 : Rassembler cent entrées réelles

    Prenez les données issues de vos journaux de production. Évitez les exemples synthétiques, les cas pour lesquels vous savez déjà qu’ils fonctionnent, ainsi que les échantillons nettoyés. Une centaine de cas suffit pour révéler des problèmes réels tout en étant suffisamment peu nombreux pour pouvoir être étiquetés manuellement par la suite, ce qui s’avère essentiel. Incluez délibérément des entrées difficiles et complexes : un ensemble de cas simples produit une note stable même lorsque votre système se dégrade.

    Leur chargement est trivial :

    import json
    with open("requests.jsonl") as f:
        cases = [json.loads(line) for line in f][:100]
    

    Remarquez que l’extraction des cent premières lignes prend ce qui se trouve en haut du fichier, ce qui peut provenir d’une seule journée ou d’un seul client. Mélanger les données avant extraction, ou sélectionner des échantillons provenant de différentes périodes, permet d’obtenir un ensemble plus représentatif.

    Si vous n’avez pas encore de journaux, créez vous-même ces cent cas, mais attendez-vous à ce que les résultats semblent meilleurs que la réalité tant que le trafic réel ne viendra pas les remplacer.

    Étape 2 : Séparer les vérifications de code des décisions subjectives

    C’est l’étape que les équipes sautent le plus souvent, et elle influence tout ce qui suit. De nombreuses propriétés peuvent être vérifiées de manière déterministe, et celles-ci ne doivent jamais être envoyées à un modèle :

    • La sortie est un JSON valide.
    • Une catégorie appartient à un ensemble connu.
    • Un nombre se situe dans une plage autorisée.
    • Les champs requis sont présents.
    • La réponse reste en deçà d’une limite de longueur.

    Chacun de ces points constitue une assertion. La fonction suivante (en Python, quel que soit son intitulé) exécute quelques-unes d’entre elles et renvoie un dictionnaire contenant les résultats nommés :

    def code_checks(output):
        checks = {}
        try:
            parsed = json.loads(output)
            checks["valid_json"] = True
            checks["has_fields"] = all(k in parsed for k in ("answer", "confidence"))
        except json.JSONDecodeError:
            checks["valid_json"] = False
            checks["has_fields"] = False
        checks["under_limit"] = len(output) < 2000
        return checks
    

    Retourner des vérifications nommées plutôt qu’un seul booléen vous permet de savoir quelle propriété a échoué en cas d’erreur. Ce qui reste après les vérifications déterministes est la partie nécessitant un jugement : la réponse a-t-elle répondu à la vraie question, a-t-elle introduit des faits absents dans la source, a-t-elle refusé alors qu’elle aurait dû accepter ? Ces propriétés ont besoin d’un juge, et un juge a besoin de validation.

    Étape 3 : Écrire un juge qui évalue une seule chose

    Donnez à chaque appel de juge exactement un critère. Une grille d’évaluation demandant cinq qualités en même temps produit un verdict mixte, et en cas d’échec, on ne peut pas déterminer quelle qualité manquait. Le juge ci-dessous vérifie une seule propriété, les affirmations non étayées, et exige un format de réponse fixe :

    JUDGE = """You are grading one property of a response.
    
    PROPERTY: Does the response contain any claim that is not supported by the
    source text provided?
    
    SOURCE:
    {source}
    
    RESPONSE:
    {response}
    
    Answer with exactly one word, PASS or FAIL, then a new line, then one
    sentence explaining your verdict. Do not explain anything else."""
    
    def judge(source, response, call_model):
        out = call_model(JUDGE.format(source=source, response=response))
        verdict = out.strip().split("\n")[0].strip().upper()
        return verdict == "PASS", out
    

    La contrainte de format est tout aussi importante que le critère lui-même. Exiger qu’une seule parole, PASS ou FAIL, apparaisse sur la première ligne permet de parser facilement le résultat. Les verdicts en format libre varient d’une invocation à l’autre, et si une erreur de parsing est un jour considérée comme un succès, tous les scores rapportés par la suite seront surévalués sans que personne ne s’en aperçoive. Cette implémentation privilégie la prudence : tout ce qui n’est pas exactement PASS, y compris PASS. ou un verdict encadré par du formatage, est considéré comme une échec. Il est utile de consigner la fréquence à laquelle cela se produit, car une augmentation soudaine des réponses non parseables constitue en soi un signal.

    Étape 4 : Exécuter tout le processus et conserver la sortie brute

    Conservez la sortie complète ainsi que l’explication du juge pour chaque cas, et non seulement le verdict. Lorsqu’un score change, vous voudrez savoir ce qui a réellement changé, et un booléen enregistré ne vous permettra pas de le déterminer.

    results = []
    for case in cases:
        output = call_model(case["prompt"])
        passed, reasoning = judge(case["source"], output, call_model)
        results.append({
            "id": case["id"],
            "output": output,
            "code_checks": code_checks(output),
            "judge_pass": passed,
            "judge_reasoning": reasoning,
        })
    rate = sum(r["judge_pass"] for r in results) / len(results)
    print(f"{rate:.1%} pass on {len(results)} cases")
    

    La boucle enregistre les vérifications déterministes ainsi que le résultat du juge, bien que le taux principal ne prenne en compte que celui-ci. En pratique, il conviendrait de rapporter les deux, car une réponse qui échoue à la validation JSON constitue un échec quel que soit l’avis du juge. Notez également que c’est la même fonction call_model qui génère et évalue ici ; utiliser un modèle différent ou plus performant pour le juge est une pratique courante, mais quel que soit celui que vous choisissez, c’est l’étape suivante qui en fait la fiabilité.

    À ce stade, vous disposez d’un pourcentage. Il ne signifie rien pour l’instant.

    Étape 5 : Calibrer le juge à l’aide de vos propres étiquettes

    C’est l’étape que la plupart des guides omettent, et elle prend environ trente minutes. Sélectionnez cinquante des cent cas et étiquetez-les vous-même comme PASS ou FAIL, sans consulter d’abord le verdict du juge. Ensuite, comparez les deux ensembles d’étiquettes :

    agree = sum(1 for r, human in zip(results[:50], human_labels)
                if r["judge_pass"] == human)
    print(f"judge agrees with me on {agree}/50 = {agree/50:.0%}")
    both_fail = sum(1 for r, h in zip(results[:50], human_labels)
                    if not r["judge_pass"] and not h)
    judge_fails = sum(1 for r in results[:50] if not r["judge_pass"])
    human_fails = sum(1 for h in human_labels if not h)
    print(f"judge caught {both_fail}/{human_fails} of the failures I found")
    print(f"judge flagged {judge_fails - both_fail} things I considered fine")
    

    Le script indique trois indicateurs : l’accord global, le nombre d’échecs que le juge a également détectés, et le nombre de cas où le juge a échoué alors que vous les considériez acceptables. Le code suppose que human_labels est une liste de booléens dans le même ordre que les cinquante premiers résultats, et il provoquera une division par zéro si vous n’avez trouvé aucun échec, ce qui est en soi un indice que votre ensemble de données est trop simple.

    Pourquoi l’accord brut induit en erreur

    L’accord global est l’indicateur le moins fiable, même s’il est celui qui est le plus souvent cité. Si 90 % de vos cas sont valides, un juge qui répond « PASS » à tout atteint 90 % d’accord sans avoir détecté quoi que ce soit.

    L’indicateur qui compte : les échecs détectés

    Ce qui mérite votre attention, c’est la proportion de vos propres étiquettes FAIL que le juge a également marquées. En termes de classification, il s’agit du taux de détection par le juge pour la classe des échecs. Un juge qui repère deux des onze échecs que vous avez identifiés ne note rien ; il approuve tout en ajoutant une explication convaincante. Le troisième indicateur, les faux positifs, est également important, car un juge qui marque comme erronées des réponses correctes vous poussera à « corriger » des éléments qui n’étaient jamais défectueux.

    Lorsque vous et le juge n’êtes pas d’accord

    Lisez chaque cas où votre étiquette diffère de celle du juge. Il n’y a que trois explications possibles, chacune ayant sa propre solution.

    La grille d’évaluation est ambiguë

    Le juge a tort parce que la description de la propriété laisse place à l’interprétation. Affinez-la. Un bon test consiste à voir si vous pouvez exprimer cette propriété en une seule phrase que tout nouveau collègue appliquerait exactement comme vous.

    Vos propres étiquettes sont incorrectes

    Cela arrive plus souvent que les gens ne l’imaginent. L’étiquetage humain varie au fil de cinquante cas, en particulier pour les exemples borderline vers la fin d’une session. Relabellez les cas contestés avec un regard neuf avant de blâmer le juge.

    Cette propriété est intrinsèquement subjective

    Certaines qualités ne peuvent être évaluées de manière cohérente par personne. Si c’est le cas, aucun juge ne sera fiable. Soit divisez cette propriété en sous-propriétés plus petites et vérifiables, soit acceptez qu’elle ne puisse pas être notée.

    Itérez : ajustez la grille d’évaluation, redéterminez les étiquettes si nécessaire, et relancez le processus jusqu’à ce que le juge repère suffisamment d’erreurs pour que vous ayez besoin de défendre votre résultat lors d’une réunion. Ensuite, figez la grille d’évaluation et enregistrez sa version avec chaque résultat futur. Les évaluations réalisées avec des grilles différentes ne peuvent pas être comparées. Pour savoir comment maintenir un juge en bon état lorsqu’il fonctionne en continu en production, consultez gérer LLM en tant que juge dans un système de production dynamique.

    Ce que vous obtenez avec l’évaluation calibrée

    Le résultat est un chiffre que vous pouvez afficher à côté d’une modification et en être convaincu. Avant de modifier un prompt, de changer de modèle ou d’adapter l’étape de récupération des données, vous effectuez une mesure ; vous en faites une autre par la suite. La différence entre ces mesures ne contient d’informations que parce que vous avez d’abord confirmé que l’instrument en question est fiable, c’est-à-dire qu’il correspond aux critères d’un être humain.

    Puisque le code ne dépend que de la fonction qui appelle votre modèle, il continue de fonctionner lorsque vous changez de fournisseur ou que l’écosystème des outils évolue, ce qui est bien plus que ce que l’on peut dire de la plupart des solutions recommandées pour cette tâche.

    Limites d’une évaluation sur cent cas

    Trois contraintes méritent d’être comprises avant de s’appuyer sur cette approche.

    Les petites régressions sont invisibles

    Une baisse de 91 à 89 % correspond à deux cas sur cent, ce qui est indiscernable du bruit avec une telle taille d’échantillon. Pour détecter de petits effets, il faut soit beaucoup plus de données, soit une comparaison par paires : exécuter les deux versions sur les mêmes entrées et ne compter que les cas dont le verdict a changé. Les comparaisons par paires éliminent la majeure partie de la variation due à la difficulté des cas.

    L’étiquetage manuel n’est pas scalable

    Les étiquettes manuelles perdent leur praticité après quelques centaines de lignes, et la qualité diminue au fil des sessions. Votre jugement concernant l’affaire quarante-huit n’est pas le même que pour l’affaire deux, ce qui introduit de réels erreurs dans la calibration elle-même. Étiqueter en plusieurs sessions courtes plutôt qu’en une seule longue réduit cette dérive.

    Expiration de la calibration

    Un juge validé sur les résultats d’aujourd’hui peut ne plus être fiable pour ceux de demain. Lorsque vous changez le modèle sous-jacent à vos caractéristiques, ses modes de défaillance changent également, ce qui oblige à le valider à nouveau plutôt qu’à l’utiliser à nouveau. C’est un travail fastidieux, et c’est précisément ce qui distingue une mesure d’un rituel.

    Deux ajouts une fois qu’il fonctionne

    • Versionnez tout. Conservez la version du modèle ainsi que la version de la grille d’évaluation avec chaque résultat. Un score manquant l’une ou l’autre de ces informations ne peut être comparé à aucun autre score, de la même manière qu’une valeur de référence sans son contexte n’apporte que peu d’informations.
    • Gardez l’ensemble des cas de désaccord. Les cas où vous et le juge n’êtes pas d’accord sont les lignes les plus utiles de toute l’exercice. Les relancer après chaque modification de la grille d’évaluation est le moyen le plus rapide de déterminer si cette modification a été bénéfique.

    Ces exemples privilégient la clarté par rapport à une robustesse maximale. Considérez-les comme des modèles à lire et à adapter, en ajoutant des tentatives répétées, un traitement des erreurs et une persistance selon les besoins de votre configuration.

    Points clés

    • Une évaluation consiste en un ensemble de données, un générateur et un évaluateur ; tout le reste relève d’outils optionnels.
  • Utilisez des entrées réelles et difficiles, et vérifiez tout ce qui est déterministe à l’aide de code plutôt que d’un modèle.
  • Donnez à chaque juge un critère ainsi qu’un format de sortie strict et interprétable.
  • Rapportez le nombre d’erreurs identifiées par les humains que le juge détecte, et non seulement le degré global d’accord.
  • Gelez et versionnez la grille d’évaluation, révalidez-la après les modifications du modèle, et utilisez des comparaisons par paires lorsque les effets sont faibles.