Aller au contenu
English

Expériences contrôlées

  • Leçon 14
  • 45 min
  • Module 5 : Comparer les systèmes de façon responsable

Ce que vous saurez faire

Rendez la comparaison équitable. Changez une seule chose, gardez tout le reste identique, notez chaque réglage et exécutez le modèle plusieurs fois. Une différence dans les résultats aura alors un sens.

Avant de commencerCe qu’il faut savoir et avoir sous la mainEnviron 45 minutes

Ce que vous devez savoir

Ce qu’il vous faut sur votre ordinateur

  • ~/projects/eval-harness à la fin du module 4, avec .venv actif : python -m pytest affiche 34 passed (34 tests réussis). Sinon, copiez l’instantané end-of-m04 comme l’explique Utiliser un instantané.
  • Le dépôt des laboratoires dans ~/projects/nextia-learning-labs, avec les fichiers les plus récents.

Aucun compte ni aucune clé. Cette leçon ajoute un paquet, numpy, pour les calculs du module. Testé avec Python 3.12.13 sur macOS (Apple silicon). Les commandes Windows ne sont pas testées.

Vous voulez savoir si une nouvelle farine donne un meilleur gâteau. Vous préparez donc deux gâteaux et les goûtez. Le gâteau à la nouvelle farine est meilleur. Mais vous avez aussi utilisé un autre four, plus de sucre et un temps de cuisson plus court. Quel changement a fait la différence? Vous ne pouvez pas le dire.

Vous recommencez donc. Même recette, même four, même durée; seule la farine change. Vous notez la recette pour pouvoir la refaire. Et vous préparez chaque gâteau deux fois, car votre four ne donne pas toujours le même résultat. Si les deux gâteaux à l’ancienne farine diffèrent déjà beaucoup, une petite différence entre les farines signifie peu.

Tomás a les chiffres du candidat et veut les comparer. « Le mois dernier, l’ancien assistant a choisi la bonne équipe pour 69 billets de soutien sur 70. Le candidat obtient 88 sur 100 ici. Il fait donc moins bien de 10 points. » Mais les deux chiffres proviennent de billets différents, traités un autre jour. Établir une référence de base a montré que le même assistant obtient 69 sur 70 sur un ensemble, et 97 sur 100 sur un autre. Les billets à eux seuls font varier le chiffre.

Un deuxième problème est moins visible. Le modèle ne donne pas deux fois la même réponse. Priya a envoyé les mêmes 102 billets de développement au candidat cinq fois. À l’exécution 1, le candidat n’a pas répondu au billet de sécurité T-64740 : un parasol est tombé vers un enfant. Personne n’en serait informé. À l’exécution 2, le même billet a été transmis à une personne, comme il le fallait. Si Priya avait fait seulement l’exécution 2, elle aurait indiqué : « aucun problème bloquant sur les données de développement ».

Une expérience contrôlée change une seule chose et garde tout le reste fixe. Ce que vous changez est la variable testée : ici, la requête (prompt). Tout ce que vous gardez fixe est un élément de contrôle : les cas, le modèle et sa version, le schéma de sortie, la limite de sortie. Une différence dans les résultats vient alors du changement ou du hasard. Il ne reste aucune autre cause.

Il reste donc le hasard. Un modèle de langage est stochastique : il choisit chaque mot avec une part de hasard. Deux exécutions sur la même entrée peuvent donc différer. Une exécution d’un système stochastique est un échantillon de son comportement, comme un gâteau de votre four. Répétez donc l’exécution et examinez la dispersion entre les répétitions avant de vous fier à une différence.

Une expérience contrôlée suit trois règles :

  1. Gardez les entrées fixes. Les deux systèmes voient les mêmes cas, ce que prouve la même version des entrées.
  2. Versionnez chaque réglage. L’exécution consigne la requête et son empreinte, le modèle et sa version, ainsi que la version du jeu de données. Changez-en un, et l’identifiant d’exécution change.
  3. Répétez les exécutions stochastiques. Exécutez chaque système plusieurs fois sur les mêmes cas et rapportez la dispersion, pas la meilleure exécution.

Voici, côte à côte, les réglages de l’exécution 1 de la référence de base et du candidat. Chaque réglage provient de la fiche de l’exécution dans outputs/runs.json. Le schéma et la limite de sortie viennent plutôt du journal d’enregistrement.

Deux colonnes de réglages, exécution 1 de la référence de base et exécution 1 du candidat. Identifiants d'exécution baseline-v2-chat-small-r1-b6a2b9 et candidate-v3-chat-small-r1-31b45f. Cas vus : 271 cas, version des entrées 3f90ab808275, identique pour les deux. Modèle : chat-small, gpt-6-luna-2026-09-22, identique. Sortie : schéma strict, limite de 1 000 jetons, identique. Requête : v2 avec l'empreinte 728add8f0b0d, et v3 avec l'empreinte 1f6de460aaf2, indiquée comme seul changement.
L'exécution 1 de la référence de base et du candidat. Seule la requête diffère; une différence dans les résultats vient donc de la requête ou du hasard.
Description textuelle du diagramme

Le diagramme présente une ligne par réglage, avec la référence de base à gauche et le candidat à droite. Les identifiants d’exécution diffèrent, car ils désignent le système et la requête. Les cas vus sont identiques : 271 cas avec la version des entrées 3f90ab808275. Le modèle est identique : chat-small, version gpt-6-luna-2026-09-22. La sortie est identique : un schéma strict et une limite de 1 000 jetons. Seule la requête diffère : v2, avec l’empreinte 728add8f0b0d, contre v3, avec l’empreinte 1f6de460aaf2.

Notez la version des entrées. Les étiquettes ont changé après les premières exécutions (32 prochaines étapes, lors de la révision). La version du jeu de données a donc changé. La version des entrées couvre seulement ce que le système a vu : les identifiants des cas, les textes et les pièces jointes. Elle n’a pas changé; les sorties enregistrées restent donc valides. Étiqueter et versionner explique les deux versions.

Vous pouvez vérifier tout cela avec du code, plutôt que de vous fier au diagramme. C’est la prochaine étape.

Le projet reçoit les huit exécutions répétées, le code de comparaison et une nouvelle commande. Le code de comparaison utilise numpy, une bibliothèque Python pour calculer rapidement sur des listes de nombres. Vous l’installez avec le nouveau requirements.txt.

  1. Récupérez les fichiers les plus récents des laboratoires et allez dans le projet :

    Fenêtre de terminal
    cd ~/projects
    git -C nextia-learning-labs pull
    cd eval-harness
  2. Copiez les nouvelles dépendances, les exécutions enregistrées, le code de comparaison et la ligne de commande depuis l’instantané end-of-m05.

    Fenêtre de terminal
    cp ../nextia-learning-labs/C11/snapshots/end-of-m05/requirements.txt .
    cp ../nextia-learning-labs/C11/snapshots/end-of-m05/outputs/* outputs/
    cp ../nextia-learning-labs/C11/snapshots/end-of-m05/harness/compare.py harness/
    cp ../nextia-learning-labs/C11/snapshots/end-of-m05/harness/__main__.py harness/
  3. Installez numpy. Le .venv doit être actif. Une des dernières lignes commence par Successfully installed numpy (numpy installé avec succès) :

    Fenêtre de terminal
    python -m pip install -r requirements.txt
  4. Exécutez les tests. La dernière ligne est 34 passed. Le nouveau code recevra ses tests dans la prochaine leçon.

    Fenêtre de terminal
    python -m pytest
  5. Listez les exécutions. Chaque système a maintenant cinq exécutions : l’exécution 1 sur les 271 cas, les exécutions 2 à 5 sur les 102 cas de développement. L’exécution practice sert à Compromis et régressions : elle contient une réponse construite, écrite par l’auteur du cours.

    Fenêtre de terminal
    python -m harness runs
    baseline-v2-chat-small-r1-b6a2b9 271 cases contaminated, dev, holdout gpt-6-luna-2026-09-22 started 2026-10-09T14:56:12-04:00
    baseline-v2-chat-small-r2-ef711b 102 cases dev gpt-6-luna-2026-09-22 started 2026-10-09T15:10:14-04:00
    baseline-v2-chat-small-r3-feb481 102 cases dev gpt-6-luna-2026-09-22 started 2026-10-09T15:17:06-04:00
    baseline-v2-chat-small-r4-e2030c 102 cases dev gpt-6-luna-2026-09-22 started 2026-10-09T15:23:21-04:00
    baseline-v2-chat-small-r5-3a8295 102 cases dev gpt-6-luna-2026-09-22 started 2026-10-09T15:28:29-04:00
    candidate-v3-chat-small-r1-31b45f 271 cases contaminated, dev, holdout gpt-6-luna-2026-09-22 started 2026-10-09T15:01:11-04:00
    candidate-v3-chat-small-r2-64de63 102 cases dev gpt-6-luna-2026-09-22 started 2026-10-09T15:12:11-04:00
    candidate-v3-chat-small-r3-548f7f 102 cases dev gpt-6-luna-2026-09-22 started 2026-10-09T15:20:06-04:00
    candidate-v3-chat-small-r4-92e51b 102 cases dev gpt-6-luna-2026-09-22 started 2026-10-09T15:25:18-04:00
    candidate-v3-chat-small-r5-504de7 102 cases dev gpt-6-luna-2026-09-22 started 2026-10-09T15:31:06-04:00
    strong-v2-chat-strong-r1-160302 102 cases dev gpt-6.1-sol-2026-09-29 started 2026-10-09T15:34:30-04:00
    practice-v3-chat-small-r1-31b45f 271 cases contaminated, dev, holdout gpt-6-luna-2026-09-22 started 2026-10-09T15:01:11-04:00

Regardez les heures de début. Priya a exécuté la référence de base et le candidat en alternance, à quelques minutes d’intervalle, le même après-midi. Si le fournisseur a changé quelque chose cet après-midi-là, les deux systèmes y ont été exposés. C’est aussi un élément de contrôle.

Démarrez Python dans le dossier du projet (python). Comparez les fiches des deux exécutions 1, champ par champ :

>>> from harness.run import Run, load_runs, load_outputs
>>> runs = load_runs()
>>> b = runs["baseline-v2-chat-small-r1-b6a2b9"]
>>> c = runs["candidate-v3-chat-small-r1-31b45f"]
>>> [k for k in Run.model_fields if getattr(b, k) != getattr(c, k)]
['run_id', 'system', 'prompt', 'prompt_sha256', 'started', 'finished', 'note']
>>> b.model_version == c.model_version, b.inputs_version == c.inputs_version
(True, True)

Les champs qui diffèrent sont les noms, les heures, la note et la requête. La version du modèle et les entrées sont identiques. C’est une comparaison avec un seul changement, vérifiée par du code.

Les répétitions n’ont vu que les cas de développement. Leur version des entrées diffère donc : elle couvre 102 cas, pas 271. Vérifiez qu’il s’agit de la version des entrées des mêmes 102 cas de développement :

>>> from harness.dataset import load_cases, inputs_version
>>> dev = load_cases(split="dev")
>>> inputs_version(dev)
'9b24fa392841'
>>> runs["candidate-v3-chat-small-r3-548f7f"].inputs_version
'9b24fa392841'

Examinez maintenant la dispersion. La commande repeats évalue chaque exécution d’un système sur les 102 cas de développement :

Fenêtre de terminal
python -m harness repeats baseline
python -m harness repeats candidate
baseline x 5 runs, decisions: totals [90.0, 96.0, 94.0, 92.0, 95.0] out of 102 | min 90, max 96 | 16 cases changed between runs
baseline x 5 runs, criteria: totals [100.0, 97.0, 98.0, 98.0, 97.0] out of 102 | min 97, max 100 | 7 cases changed between runs
candidate x 5 runs, decisions: totals [84.0, 81.0, 76.0, 80.0, 81.0] out of 102 | min 76, max 84 | 24 cases changed between runs
candidate x 5 runs, criteria: totals [85.0, 80.0, 79.0, 82.0, 84.0] out of 102 | min 79, max 85 | 40 cases changed between runs

« Decisions » (décisions) compte les cas où les deux décisions sont correctes : l’équipe, si le cas en a une, et needs_human. « Criteria » (critères) compte les cas où toutes les règles de réponse sont respectées. Le graphique représente les décisions de chaque exécution par un point.

Cinq exécutions enregistrées de chaque système sur les mêmes 102 cas de développement. Chaque système varie de plusieurs cas entre les exécutions; les deux étendues ne se chevauchent pas.
Description textuelle du graphique

Deux rangées de cinq points. La référence de base : 90, 96, 94, 92, 95 sur 102, de 90 à 96. Le candidat : 84, 81, 76, 80, 81 sur 102, de 76 à 84. La pire exécution de la référence de base reste meilleure que la meilleure exécution du candidat.

Lisez le graphique en trois étapes.

  1. Un système varie de lui-même. La référence de base a pris les deux bonnes décisions sur 90 cas à l’exécution 1, et 96 à l’exécution 2. Rien n’a changé entre ces exécutions. Une différence de 6 cas est normale pour ce système.
  2. Les étendues ne se chevauchent pas. La pire exécution de la référence de base (90) dépasse la meilleure du candidat (84). Donc, « le candidat prend de moins bonnes décisions » n’est pas un hasard lié à une seule exécution.
  3. Différents cas varient. Le résultat a changé pour 16 cas entre les exécutions de la référence de base, et pour 24 cas chez le candidat. Un cas qui change est un cas à lire.

Voici un cas qui change. T-81058 demande : « Quelle est l’adresse de votre entrepôt? Je voudrais y déposer mon retour en personne. » Aucune règle n’exige qu’une personne y réponde. Vérifiez ce que la référence de base a répondu à chaque exécution :

>>> base = sorted((r for r in runs.values() if r.system == "baseline"), key=lambda r: r.repeat)
>>> [load_outputs(r)["T-81058"].answer["needs_human"] for r in base]
[True, False, False, False, False]

La même requête, le même modèle, le même billet : une fausse alerte à l’exécution 1, et la bonne réponse aux quatre autres.

Le plus gros problème du candidat n’est pas une mauvaise décision. C’est l’absence de réponse : le modèle raisonne jusqu’à la limite de 1 000 jetons et ne renvoie rien. Comptez ces échecs dans chaque exécution, puis listez les cas critiques parmi ceux interrompus :

>>> critical = {x.case_id for x in dev if x.critical}
>>> cand = sorted((r for r in runs.values() if r.system == "candidate"), key=lambda r: r.repeat)
>>> for r in cand:
... outs = load_outputs(r)
... cut = [x.case_id for x in dev if outs[x.case_id].finish_reason == "length"]
... print(r.repeat, len(cut), [i for i in cut if i in critical])
...
1 11 ['T-64740']
2 16 []
3 19 ['T-81036']
4 14 []
5 14 []
Une grille à cinq colonnes, exécutions 1 à 5. Réponses interrompues à la limite, référence de base : 1, 0, 0, 0, 0. Candidat : 11, 16, 19, 14, 14. Deux billets critiques pour le candidat : T-64740, un billet de sécurité concernant un parasol tombé vers un enfant : aucune réponse à l'exécution 1, transmis à une personne aux exécutions 2 à 5. T-81036, un billet d'accès concernant un colis pour le numéro 16 : transmis à une personne aux exécutions 1, 2, 4 et 5, aucune réponse à l'exécution 3.
Cinq exécutions enregistrées sur les 102 cas de développement. Le candidat est interrompu pour 11 à 19 cas par exécution. Selon l'exécution, un billet critique différent reste sans réponse.
Description textuelle du diagramme

La partie du haut compte les réponses interrompues à la limite de 1 000 jetons dans chacune des cinq exécutions. La référence de base : 1, 0, 0, 0, 0. Le candidat : 11, 16, 19, 14, 14.

La partie du bas suit deux billets critiques dans les exécutions du candidat. La référence de base a transmis les deux à une personne dans les cinq exécutions. T-64740 est un billet de sécurité : un parasol est tombé vers un enfant. Le candidat n’a pas répondu à l’exécution 1 et l’a transmis à une personne aux exécutions 2 à 5. T-81036 est un billet d’accès : un colis pour le numéro 16 a été laissé au client, qui demande à qui il est destiné. Le candidat l’a transmis à une personne aux exécutions 1, 2, 4 et 5, sans répondre à l’exécution 3.

Sur les cinq exécutions, 34 cas de développement différents ont été interrompus au moins une fois, et seulement 3 à chaque exécution. L’échec passe d’un cas à l’autre. Une seule exécution peut donc le manquer : les exécutions 2, 4 et 5 ne montrent aucun cas critique sans réponse. Deux exécutions sur cinq en montrent un. Une décision de mise en production fondée uniquement sur l’exécution 4 manquerait complètement le problème.

C’est pourquoi les règles de mise en production examinent chaque cas de chaque exécution, et pas seulement une moyenne. Compromis et régressions transforme ces cas sans réponse en problèmes bloquants.

Répétez une exécution quand le système est stochastique et que la décision compte. Pour un modèle de langage, c’est presque toujours le cas. Même si un modèle permet de régler le hasard au minimum, ses réponses ne sont pas entièrement reproductibles. La règle reste donc valable.

Combien de répétitions? Examinez d’abord la dispersion d’un système. Si la différence entre les systèmes dépasse largement la dispersion, comme ici, trois à cinq exécutions suffisent. Si la différence est comparable à la dispersion, davantage d’exécutions ne rendront pas un résultat faible plus solide. La prochaine leçon montre comment chiffrer cela.

Les répétitions coûtent de l’argent et du temps. Une exécution du candidat sur les données de développement a coûté environ 0,05 $ US et quelques minutes. C’est peu ici, car l’ensemble et le modèle sont petits. Avec un grand modèle, 10 000 cas et cinq répétitions, c’est une décision budgétaire. Si vous devez économiser, répétez les cas critiques plus souvent que les autres : ils déterminent la mise en production.

Le code déterministe n’a pas besoin de répétitions. Les fonctions d’évaluation dans harness/scorers/ donnent toujours le même résultat sur la même sortie enregistrée. Un seul score suffit donc. C’est une raison d’enregistrer chaque sortie : vous pouvez l’évaluer de nouveau sans frais.

Erreur courante

Rapporter la meilleure exécution

Symptôme : Tomás exécute le candidat cinq fois et rapporte l’exécution 1 : « 84 sur 102, seulement 6 derrière les 90 de la référence de base. » La référence de base vient aussi de l’exécution 1. À l’exécution 3, le candidat obtient 76, soit 14 derrière la même exécution de référence.

Cause : Tomás compare la meilleure de cinq exécutions d’un système à une seule exécution de l’autre. La meilleure exécution est élevée en partie par hasard. La comparaison favorise donc le système qui a eu plus d’essais. La même chose se produit quand vous relancez un test « jusqu’à ce qu’il réussisse ».

Correction : décidez du nombre d’exécutions avant de regarder les résultats. Exécutez les deux systèmes autant de fois sur les mêmes cas. Rapportez chaque exécution : le minimum, le maximum et les problèmes bloquants de chacune. Dans le banc d’évaluation : repeats baseline et repeats candidate, côte à côte.

Exercice guidé : la même vérification pour la référence de base

Section intitulée « Exercice guidé : la même vérification pour la référence de base »
  1. Dans la même session Python, exécutez la boucle de L’échec silencieux, exécution par exécution pour la référence de base : remplacez cand par base.
  2. Avant de l’exécuter, prédisez : combien de réponses la référence de base perd-elle à cause de la limite à chaque exécution?
  3. Exécutez-la. Comparez avec le diagramme des cinq exécutions.
Afficher la sortie attendue
1 1 []
2 0 []
3 0 []
4 0 []
5 0 []

La référence de base a perdu une réponse à l’exécution 1 (T-65450, une question de ramassage, non critique). Elle n’en a perdu aucune aux autres exécutions. L’échec silencieux vient de la nouvelle requête, pas du modèle seul. La requête v3 demande des réponses complètes et précises, et le modèle raisonne plus longtemps pour les écrire.

À vous de jouer : quels changements restent silencieux?

Section intitulée « À vous de jouer : quels changements restent silencieux? »

Les décisions du candidat ont changé entre les exécutions pour 24 cas de développement. Combien ont été interrompus dans au moins une exécution?

  1. Importez repeat_spread depuis harness.compare.
  2. Chargez les sorties des cinq exécutions du candidat dans une liste : outs = [load_outputs(r) for r in cand].
  3. Appelez repeat_spread(dev, outs, "decisions") et récupérez "unstable_cases" (cas instables).
  4. Gardez les cas où au moins une exécution a finish_reason == "length".
Afficher un exemple de réponse
>>> from harness.compare import repeat_spread
>>> outs = [load_outputs(r) for r in cand]
>>> unstable = repeat_spread(dev, outs, "decisions")["unstable_cases"]
>>> silent = [i for i in unstable if any(o[i].finish_reason == "length" for o in outs)]
>>> len(unstable), len(silent)
(24, 22)

Parmi les 24 cas qui changent, 22 sont interrompus dans au moins une exécution. L’instabilité du candidat vient donc surtout d’une seule défaillance : l’absence de réponse, plutôt que d’un mélange d’erreurs différentes. Cela indique une correction à tester : augmenter la limite de sortie, ou demander moins dans la requête. Chaque correction constitue un nouveau candidat, avec ses propres exécutions contrôlées.

Les chiffres de ce module proviennent des enregistrements de chaque système. Les modèles changent vite; la méthode de comparaison reste valable.

Vérifiez votre compréhension

Omar veut comparer la requête v3 sur le modèle chat-strong à la requête v2 sur chat-small, avec une exécution chacun. Quel est le problème de cette expérience, et comment la modifieriez-vous?

Afficher la réponse

Deux éléments changent à la fois : la requête et le modèle. On ne peut donc attribuer une différence à l’un ou à l’autre. Changez un seul élément par comparaison. Par exemple, comparez la requête v3 sur chat-small à la requête v2 sur chat-small, puis changez le modèle séparément. De plus, une seule exécution par version ne suffit pas pour un modèle stochastique. Exécutez chaque version plusieurs fois sur les mêmes cas, avec la même version des entrées, et indiquez la dispersion.

Vérifiez votre compréhension

L'exécution 4 du candidat ne montre aucun cas critique sans réponse dans les données de développement. Cela prouve-t-il que la défaillance silencieuse est corrigée? Expliquez à l'aide des exécutions répétées.

Afficher la réponse

Non. Avec la même requête et le même modèle, T-64740 est resté sans réponse à l’exécution 1, et T-81036 à l’exécution 3. Les exécutions 2, 4 et 5 n’en montrent aucun. La défaillance passe d’un cas à l’autre, et 34 cas de développement différents ont été interrompus au moins une fois. Une exécution sans problème n’est qu’un échantillon. Pour faire confiance à une correction, répétez les exécutions et vérifiez qu’aucun cas critique ne reste silencieux dans chacune. Vérifiez ensuite les données réservées.

Récapitulatif

  • Une expérience contrôlée change un seul élément et garde le reste fixe. Ici, seule la requête a changé : les 271 entrées, la version du modèle et la limite sont restées identiques. Le code l’a vérifié à partir des enregistrements d’exécution.
  • Un modèle de langage est stochastique : cinq exécutions de la même référence de base ont donné de 90 à 96 sur 102. Indiquez la dispersion, pas la meilleure exécution.
  • L’étendue du candidat (76 à 84) ne chevauche pas celle de la référence de base. Sa baisse sur les décisions ne vient donc pas d’une seule mauvaise exécution.
  • Le candidat est interrompu sur 11, 16, 19, 14 et 14 cas de développement. Un billet critique différent reste sans réponse selon l’exécution, et trois exécutions sur cinq n’en montrent aucun.
  • Les répétitions coûtent du temps et de l’argent. Répétez l’exécution du système stochastique, pas celle des outils de notation déterministes, et commencez par les cas critiques.

À suivre : quel degré de certitude peut-on avoir sur une différence observée sur 98 ou 102 cas? Intervalles de confiance, comparaisons appariées et sous-groupes.

Leçon suivante : Incertitude et sous-groupes