La catégorie d'échec que les evals existent pour détecter n'est pas « le modèle retourne une erreur ». C'est « le modèle retourne quelque chose de plausible, subtilement faux, d'une façon qui ne devient visible que trois étapes en aval ». Les fournisseurs mettent à jour les modèles, les comportements de rate limiting changent, la gestion de la fenêtre de contexte évolue entre les versions mineures. Rien de tout ça ne s'annonce.
La réaction instinctive est de construire une suite d'evals complète : jeux de données dorés, évaluateurs humains, pipelines LLM-as-judge automatisés, tableaux de bord de régression. Tout ça a de la valeur. Rien de tout ça n'est ce qu'une équipe de 12 personnes devrait passer son premier sprint à faire. Les 80 % de valeur viennent d'une surface beaucoup plus petite que ce que les gens imaginent.
Ce qu'il vous faut réellement, c'est un harnais qui tourne à chaque PR touchant un modèle, se termine en moins de deux minutes et produit un résultat binaire réussite/échec sur lequel une porte CI peut agir. Les six patterns d'assertion ci-dessous sont ce sur quoi nous avons convergé après avoir exécuté ça sur une douzaine de builds d'agents. Ils ne couvrent pas tout. Ils couvrent ce qui casse le plus souvent.
Les swaps de modèle cassent les choses en silence
Chaque incident de production que nous avons retracé jusqu'à une régression de modèle depuis 2024 avait la même forme : le modèle n'a pas généré d'erreur. Il a retourné une sortie plausible qui violait une propriété structurelle que le système en aval tenait pour acquise. Champ renommé dans le JSON. Citation pointant vers un document de contexte inexistant. Invocation d'outil ignorée. Une réponse deux fois plus longue que ce que le system prompt attend. La surface d'échec est structurelle, pas sémantique, et c'est la seule raison pour laquelle un eval rapide est possible.
La correction sémantique est difficile. La conformité structurelle est bon marché. Un harnais à six patterns vise directement la couche bon marché, accepte qu'il va rater le glissement de sens, et utilise le temps économisé pour réellement tourner à chaque PR. L'échange en vaut la peine, car un déclencheur lent qu'on désactive parce qu'il est agaçant est pire qu'aucun déclencheur du tout.
Les six patterns, annotés
Chaque « ligne » est une fixture pytest qui encapsule un appel au modèle et assertit une propriété structurelle de la sortie. L'appel au modèle lui-même est réel, sans mock. Le corpus de cas de test est minimal : trois à cinq par pattern, choisis pour être maximalement diagnostiques plutôt que maximalement exhaustifs.
import pytest, json, re from agent import run, MODEL # ── 1. FORMAT LOCK ────────────────────── def test_json_schema(case): out = run(case.prompt) assert json.loads(out).keys() == case.schema # ── 2. REFUSAL SURFACE ────────────────── def test_no_refusal(case): out = run(case.prompt) assert not any(t in out for t in REFUSAL_TOKENS) # ── 3. CITATION INTEGRITY ─────────────── def test_citations_grounded(case): out = run(case.prompt, ctx=case.context) refs = re.findall(r'\[(\d+)\]', out) assert all(int(r) <= len(case.context) for r in refs) # ── 4. LENGTH CONTRACT ────────────────── def test_length_bounds(case): out = run(case.prompt) assert case.min_tokens <= len(out.split()) <= case.max_tokens # ── 5. TOOL CALL SEQUENCE ─────────────── def test_tool_sequence(case): trace = run_with_trace(case.prompt) assert [t.name for t in trace.calls] == case.expected_tools # ── 6. REGRESSION DELTA ───────────────── def test_regression_delta(case): score = similarity(run(case.prompt), case.golden) assert score >= 0.82 # cosine vs. pinned output
REFUSAL_TOKENS inclut « I can't », « I'm unable », « I cannot help » et huit variantes. Les faux positifs sont rares sur des prompts spécifiques à une tâche.[N] pointe vers un vrai document de contexte. Détecte les modèles qui hallucinent des numéros de citation quand la fenêtre de contexte est presque pleine ou le retrieval de mauvaise qualité.Les cas de test sont la partie difficile, pas le harnais. Cinq cas par pattern, choisis pour être maximalement stressants pour ce mode d'échec précis, surpassent cinquante cas génériques à chaque fois. Pour le format lock : un prompt qui a historiquement produit un JSON valide juste à la limite de la capacité d'instruction du modèle. Pour la refusal surface : une requête légitime mais à la limite qui a parfois posé problème aux versions précédentes du modèle.
Pourquoi ces six, pas d'autres
Nous sommes arrivés à cette liste par postmortem. Chaque incident de production impliquant une régression de modèle depuis 2024 a été retracé jusqu'à une cause racine. Ces six patterns couvrent 80 % de ces causes racines. Les 20 % restants étaient spécifiques au domaine et nécessitaient des evals sur mesure : intéressant, mais pas généralisable.
| pattern | détecte | coût / exécution |
|---|---|---|
format_lock | Glissement de schéma, injection de délimiteurs JSON, renommage de champ, imbrication inattendue après mise à jour du modèle | |
no_refusal | Taux de refus accru sur les requêtes dans la distribution, nouvelles collisions de filtre de sécurité, régressions liées à des changements de politique | |
citations_grounded | IDs de référence hallucinés, citations hors limites, duplication de citations sur des fenêtres de contexte pleines | |
length_bounds | Régressions de verbosité, troncature sur de longues sorties, patterns de préambule/postambule incontrôlés | |
tool_sequence | Étapes de vérification ignorées, réordonnancement des appels d'outils, appels d'outils manquants sur des requêtes ambiguës | |
regression_delta | Glissement de sens sur les cas dorés, changement de ton, faits hallucinés qui n'étaient pas présents précédemment |
Le coût est relatif : un pip correspond à une assertion peu coûteuse en tokens (vérification structurelle sur la chaîne de sortie), deux pips nécessitent un second appel au modèle ou une recherche de retrieval, trois pips nécessitent une comparaison d'embeddings. La suite complète à cinq cas par pattern tourne en environ 90 secondes sur une connexion chaude et coûte moins de 0,04 $ en dépenses d'API.
regression_delta n'est pas principiel, il est empirique. Nous l'avons calibré sur dix-huit mois de sorties dorées et avons constaté un taux de faux positifs inférieur à 3 %, tout en capturant chaque régression de sens qui nous importait. Le vôtre sera différent. Calibrez-le, puis épinglez-le.
Une vraie exécution : un seul échec
Voici la sortie d'une exécution déclenchée par une mise à jour de dépendance qui a silencieusement fait passer le client modèle de claude-sonnet-4-5 à une version plus récente. La suite l'a détecté en 94 secondes. L'échec était dans tool_sequence : le modèle mis à jour a ignoré l'appel verify_permissions que l'ancienne version faisait systématiquement avant d'écrire dans une ressource.
collected 30 items PASSED test_json_schema[write-task-0] PASSED test_json_schema[write-task-1] PASSED test_json_schema[write-task-2] PASSED test_json_schema[lookup-0] PASSED test_json_schema[lookup-1] PASSED test_no_refusal[edge-query-0] PASSED test_no_refusal[edge-query-1] PASSED test_no_refusal[edge-query-2] PASSED test_no_refusal[edge-query-3] PASSED test_no_refusal[edge-query-4] PASSED test_citations_grounded[rag-fullctx-0] PASSED test_citations_grounded[rag-fullctx-1] PASSED test_citations_grounded[rag-fullctx-2] PASSED test_citations_grounded[rag-sparse-0] PASSED test_citations_grounded[rag-sparse-1] PASSED test_length_bounds[summary-short-0] PASSED test_length_bounds[summary-short-1] PASSED test_length_bounds[summary-long-0] PASSED test_length_bounds[summary-long-1] PASSED test_length_bounds[summary-long-2] FAILED test_tool_sequence[write-with-verify-0] FAILED test_tool_sequence[write-with-verify-1] FAILED test_tool_sequence[write-with-verify-2] PASSED test_tool_sequence[read-only-0] PASSED test_tool_sequence[read-only-1] PASSED test_regression_delta[golden-0] PASSED test_regression_delta[golden-1] PASSED test_regression_delta[golden-2] PASSED test_regression_delta[golden-3] PASSED test_regression_delta[golden-4] ───────────────────────────────────────────────── FAILED test_tool_sequence[write-with-verify-0] AssertionError: expected: ['fetch_resource', 'verify_permissions', 'write_resource'] got: ['fetch_resource', 'write_resource'] ───────────────────────────────────────────────── 27 passed, 3 failed in 94.2s Model under test: claude-sonnet-4-6 (bumped from claude-sonnet-4-5)
La surface d'échec est précise : le modèle a ignoré verify_permissions sur les opérations d'écriture dans les trois cas du chemin d'écriture, et a réussi proprement sur les opérations en lecture seule. C'est un signal clair, pas de l'instabilité, pas un problème de seuil. La PR a été bloquée. Le system prompt a été mis à jour pour rendre l'étape de vérification explicite plutôt qu'implicite, et la suite a réussi à la relance.
C'est à ça que sert le harnais. Pas à prouver que le modèle est bon. À prouver qu'il ne s'est pas dégradé sur ce qui compte le plus, assez vite pour que le signal soit actionnable avant le déploiement.
Sa place dans le CI
Le harnais tourne en deux modes : le niveau rapide à chaque PR touchant un fichier lié au modèle, et le niveau lent sur des exécutions nocturnes planifiées et avant tout déploiement en production. Le niveau rapide exécute les six patterns à trois cas chacun. Le niveau lent passe à dix cas par pattern et ajoute un ensemble d'evals spécifiques au domaine trop coûteux pour tourner à chaque push.
bump modèle
au modèle modifié
3 cas × 6 patterns
tourne en parallèle
de porte
doivent passer
domaine (nuit / déploiement)
10 cas × 6 + custom
déploiement
tout échec
Un point de friction rencontré tôt : le harnais a besoin d'un identifiant de modèle stable pour être significatif. Si MODEL se résout à « latest » au moment du test, le résultat du test est non reproductible et la porte est inutile. Chaque exécution de test enregistre la version du modèle résolue, et la porte bloque si le hash de version diffère de ce qui a été examiné. La version est ce que vous testez : rendez-la explicite.
Un second point de friction : paralléliser les appels au modèle dans le CI heurtera les limites de débit si vous n'êtes pas prudent. Nous exécutons les six patterns en séquence mais parallélisons les cas au sein de chaque pattern. Trois cas en parallèle est sûr aux limites de débit standard ; au-delà, il faut une clé API d'évaluation dédiée avec des quotas élevés.
Ce que les six lignes ne capturent pas
C'est important. Un harnais dont les équipes croient qu'il couvre plus qu'il ne couvre réellement est plus dangereux qu'un harnais dont elles savent qu'il est partiel.
regression_delta avec un seuil fixe de 0,82 peut ne pas le détecter. Chaque delta individuel est sous le seuil ; le glissement cumulatif est significatif. Nous répondons à ça en rétablissant les sorties dorées trimestriellement et en traçant les scores delta dans le temps plutôt que de simplement vérifier le booléen.
regression_delta est un plancher de similarité, pas une vérification de correction. Une réponse peut être très similaire à la sortie dorée et contenir quand même une erreur factuelle. Détecter la correction nécessite soit une révision humaine, soit une couche model-as-judge, aucune des deux n'ayant sa place dans une porte CI de moins de deux minutes.
Le harnais est un plancher, pas un plafond. Construisez à partir de lui. Sa valeur n'est pas d'être exhaustif : c'est d'être assez rapide pour tourner réellement, assez bon marché pour ne pas faire débat, et assez précis pour bloquer les déploiements qui comptent.
Si vous souhaitez que nous vous aidions à brancher un harnais comme celui-ci dans votre CI, le formulaire de contact est le moyen le plus rapide. Nous offrons des révisions de 30 minutes pour les stacks d'agents en production, gratuitement.