Tester l'intestable : des evals pour systèmes non-déterministes
Un test unitaire classique repose sur une promesse simple : pour une entrée donnée, j’attends une sortie exacte, et tout écart est un échec. Cette promesse tient parce que le code est déterministe. La fonction Additionner(2, 3) renvoie 5, aujourd’hui, demain, et dans dix ans. C’est cette stabilité qui rend l’assertion possible, et c’est elle qui a façonné toute notre culture du test.
Posez la même exigence à un système qui appelle un LLM, et tout s’effondre. La même question peut produire deux réponses formulées différemment, toutes deux justes. Une mise à jour du modèle peut changer le ton sans changer le fond, ou l’inverse. Votre assertion Assert.Equal(reponseAttendue, reponseObtenue) échouera en permanence, non pas parce que le système est cassé, mais parce que vous lui demandez une chose qu’il ne peut structurellement pas tenir : reproduire une chaîne de caractères au mot près.
Beaucoup d’équipes en tirent une conclusion résignée : on ne peut pas tester ces systèmes, on verra bien en production. C’est faux, et c’est dangereux. On ne peut pas les tester par égalité, c’est entendu. On peut, et on doit, les évaluer. La nuance entre tester et évaluer est tout l’objet de cet article.
Pourquoi l’assertion exacte est le mauvais outil
Le problème de fond, c’est qu’on confond deux questions que le code déterministe nous a habitués à traiter comme une seule. « La sortie est-elle identique à celle prévue » n’est pas « la sortie est-elle bonne ». Pour une fonction pure, les deux se confondent parfaitement, et toute notre outillage de test s’appuie sur cette confusion commode. Pour un système probabiliste, elles divergent franchement.
Une réponse peut être différente de la référence et meilleure. Une autre peut coller au mot près à la référence et rester inutilisable parce que le contexte a changé entre-temps. Tester un système IA, c’est donc abandonner l’idée d’une vérité unique au mot près, et la remplacer par une question de degré. À quel point cette sortie est-elle proche de ce qu’on attend. Respecte-t-elle le format imposé. Contient-elle l’information requise. Évite-t-elle ce qu’elle doit éviter. On passe du booléen au score, et du score à un seuil qu’on décide.
C’est exactement l’esprit que je défendais à propos des skills et de la qualité du output rapportée au coût en tokens : on ne juge pas un livrable généré à sa conformité littérale, mais à sa fidélité fonctionnelle. La question n’est jamais « est-ce le texte que j’avais écrit », c’est « est-ce que ce texte fait le travail ».
Le jeu d’évaluation, votre vraie base de tests
Tout commence par un jeu d’évaluation, souvent appelé golden set. C’est une collection de cas représentatifs : une entrée, et une description de ce qui fait une bonne sortie pour cette entrée. Pas forcément la sortie exacte, mais les critères qu’elle doit remplir, et ceux qu’elle doit éviter.
Ce jeu est l’actif le plus précieux de toute votre démarche qualité, bien plus que le code de test lui-même, qui n’est qu’un harnais. Il encode ce que « bien répondre » veut dire pour votre métier, dans votre contexte, avec vos contraintes. Deux entreprises qui utilisent le même modèle pour des usages différents auront des golden sets totalement différents, et c’est cette différence qui contient toute la valeur.
Le golden set doit grandir avec le temps, et selon une règle stricte : chaque incident en production s’y traduit par un nouveau cas. Un système qui a mal répondu une fois doit avoir, le lendemain, une entrée dans le golden set qui vérifie qu’il ne recommencera pas. C’est la transposition directe des tests de caractérisation que je défendais pour réécrire un legacy : on fige le comportement attendu pour pouvoir évoluer sans régresser. Un golden set qui ne grossit jamais est un golden set qu’on a cessé de prendre au sérieux.
Soigner les cas difficiles, pas seulement les cas moyens
Une erreur fréquente consiste à peupler le golden set de cas faciles et représentatifs « en moyenne ». C’est insuffisant. Ce sont les cas durs qui révèlent les régressions : l’entrée ambiguë, la requête piégeuse, le contexte incomplet, la demande à la limite du périmètre. Je m’efforce de surreprésenter ces cas, parce que c’est là que les modèles diffèrent et que les changements se voient. Un système qui gère bien les cas faciles ne dit rien ; tous les modèles y arrivent.
Trois familles d’évaluateurs
Pour scorer une sortie, on dispose de trois outils, qu’on combine, du moins coûteux au plus puissant.
Les évaluateurs déterministes
Quand un critère est vérifiable mécaniquement, on l’attrape sans LLM. La réponse est-elle un JSON valide. Contient-elle l’identifiant attendu. Reste-t-elle sous la limite de longueur. Cite-t-elle uniquement des sources présentes dans le contexte fourni. Ces vérifications sont rapides, gratuites et parfaitement fiables. On les place toujours en premier, parce qu’elles éliminent les échecs grossiers sans dépenser un seul token, et qu’un système qui échoue sur elles n’a même pas besoin d’être évalué plus finement.
Les évaluateurs sémantiques par similarité
Quand le fond compte mais pas la forme, on compare la sortie à une référence par proximité de sens, via des embeddings. Deux phrases qui disent la même chose autrement obtiennent un score élevé, là où une comparaison textuelle les déclarerait différentes. C’est utile, mais imparfait, et il faut connaître la limite : la similarité sémantique ne sait pas distinguer une nuance qui change tout d’une reformulation anodine. « Le virement est autorisé » et « le virement n’est pas autorisé » sont sémantiquement très proches pour une mesure de similarité, et radicalement opposés pour votre métier.
Le modèle juge
Pour les critères subtils, le ton est-il adapté, la réponse est-elle complète, reste-t-elle dans son périmètre, on demande à un LLM d’évaluer la sortie d’un autre LLM, selon une grille précise. C’est puissant et c’est piégeux. Un juge mal cadré est complaisant, ou biaisé vers les réponses longues qu’il prend pour des réponses complètes, ou sensible à la formulation de sa propre consigne. Le juge doit donc lui-même être évalué, sur des cas dont vous connaissez d’avance le verdict, avant qu’on lui fasse confiance. Un juge qu’on n’a pas testé est juste un second système non-déterministe empilé sur le premier, ce qui n’arrange rien.
Une évaluation, en .NET
Voici la forme que prend, en pratique, un harnais d’évaluation. Il ne ressemble pas à un test unitaire qui passe ou échoue d’un coup. Il agrège des scores et les compare à un seuil décidé par le métier.
public class EvalRunner
{
private readonly IChatClient _chatClient;
private readonly IReadOnlyList<IEvaluator> _evaluators;
public async Task<EvalReport> RunAsync(IEnumerable<EvalCase> cases, int repetitions = 3)
{
var resultats = new List<EvalResult>();
foreach (var cas in cases)
{
// On rejoue chaque cas plusieurs fois : la stabilité fait partie du verdict.
var scoresParRun = new List<double>();
for (var i = 0; i < repetitions; i++)
{
var sortie = await _chatClient.CompleteAsync(cas.Input);
var scores = new List<double>();
foreach (var evaluateur in _evaluators)
scores.Add(await evaluateur.ScoreAsync(cas, sortie.Message.Text));
scoresParRun.Add(scores.Average());
}
resultats.Add(new EvalResult(
cas.Id,
score: scoresParRun.Average(),
ecartType: EcartType(scoresParRun)));
}
var moyenne = resultats.Average(r => r.Score);
return new EvalReport(moyenne, resultats, seuilRequis: 0.85);
}
}
Trois détails comptent plus que le reste de ce code. D’abord, on rejoue chaque cas plusieurs fois et on regarde l’écart type, parce qu’un système qui oscille fortement d’un appel à l’autre est aussi inquiétant qu’un système qui score bas en moyenne : on ne peut pas se fier à un comportement instable, même s’il est bon en moyenne. Ensuite, la sortie échouée n’est pas binaire, on garde le score : savoir qu’on est passé de 0.92 à 0.71 est bien plus actionnable que de savoir qu’un test « est rouge ». Enfin, le seuil de 0.85 est une décision d’architecture, pas une constante technique. Un assistant juridique et un générateur de slogans publicitaires n’ont pas le même droit à l’erreur, et le seuil doit refléter cet enjeu.
Intégrer les evals dans le cycle de vie
Une suite d’evals n’a de valeur que si elle tourne au bon moment, automatiquement, sans dépendre de la bonne volonté de quelqu’un. Je la déclenche à chaque changement de prompt, à chaque mise à jour de modèle, à chaque modification de la chaîne de récupération qui alimente le contexte. C’est le filet qui rend possible la discipline du prompt traité comme du code : sans evals, versionner un prompt ne sert à rien, puisqu’on ne sait pas mesurer l’effet d’un changement.
Le moment le plus critique, c’est le changement de modèle. Migrer vers un modèle plus récent sans rejouer son golden set, c’est accepter que la qualité change dans une direction inconnue. La promesse d’un modèle « meilleur » est globale, établie sur des bancs d’essai génériques. Sur vos cas à vous, avec vos contraintes, il peut très bien régresser sur la dimension précise qui compte pour votre métier. Vous ne le saurez qu’en production, par des incidents, si vous n’avez pas mesuré avant. Les evals transforment une migration de modèle d’un pari en une décision documentée.
Mesurer, c’est garder la main
Le réflexe, devant un système non-déterministe, est de baisser les bras et de juger « à l’œil », au fil des retours et des incidents. C’est exactement renoncer au contrôle, et habiller ce renoncement en pragmatisme. Les evals ne rendent pas le système déterministe, elles ne le peuvent pas. Elles rendent sa qualité observable, donc pilotable, ce qui est tout ce dont un architecte a besoin.
On ne teste pas un LLM comme on teste une fonction pure, c’est acquis. Mais l’alternative à l’assertion exacte n’est pas l’absence de test, c’est un autre outil, conçu pour mesurer des degrés plutôt que constater des égalités. L’architecte qui adopte cet outil retrouve sur l’IA ce qu’il avait sur le reste du système : la capacité de dire, chiffres à l’appui, si un changement améliore les choses ou les dégrade, et de bloquer ce qui les dégrade avant que ça n’atteigne les utilisateurs. Sans cela, on ne fait pas de l’ingénierie sur ses systèmes IA. On fait des vœux, et on appelle les incidents des surprises.
// À lire ensuite