Programmer avec des agents IA : consigne, tests et vérification
Comment écrire pour un agent une consigne avec des critères d’acceptation, pourquoi travailler par petits pas, une expérience de test de mutation (combien d’erreurs détectent les tests du cas nominal, les cas limites et les tests de propriétés), qui doit écrire les tests et à quoi faire attention.
Série Bases de l’IA Partie 51 sur 52
- Carte de l’intelligence artificielle : qu’est-ce qui est quoi et comment tout s’emboîte
- Brève histoire de l’intelligence artificielle : de Turing aux modèles de langage
- Données, modèle, entraînement, inférence : les notions de base sur un seul exemple
- Lire la notation mathématique : symboles, indices, sommes et quantificateurs
- Ensembles et relations : les briques dont sont faites les définitions en IA
- Fonctions et applications : pourquoi tout modèle d’IA est une fonction
- Logique et démonstrations : les règles selon lesquelles on raisonne en IA
- Graphes et arbres : la structure sur laquelle reposent la recherche et les réseaux de neurones
- Algèbre linéaire pour l’apprentissage automatique : vecteurs, matrices et pourquoi l’IA multiplie surtout
- Dérivées, gradient et règle de dérivation en chaîne : comment un modèle sait dans quelle direction apprendre
- Descente de gradient : comment un modèle apprend
- Qu’est-ce que la probabilité : du lancer de dé aux axiomes de Kolmogorov
- Probabilité conditionnelle et théorème de Bayes : comment réviser une estimation quand on apprend du nouveau
- Variable aléatoire et fonction de répartition : une seule fonction qui dit tout de la loi
- Fonction de densité et fonction de masse : pourquoi une variable continue a une probabilité nulle en chaque point
- Espérance, variance et covariance : où se situe une loi, quelle est sa largeur et ce qui varie avec quoi
- Panorama des lois de probabilité : Bernoulli, binomiale, Poisson, uniforme, normale et exponentielle
- Loi des grands nombres et théorème central limite : pourquoi les moyennes sont si prévisibles
- Statistique et estimation de paramètres : maximum de vraisemblance, MAP et intervalles de confiance
- Théorie de l’information : entropie, entropie croisée et divergence KL — d’où viennent les fonctions de perte
- Recherche dans l’espace d’états : BFS, DFS, UCS et A* en animations
- Jeux et adversaires : minimax, élagage alpha-bêta et Monte Carlo tree search
- Satisfaction de contraintes : backtracking, propagation de contraintes et AC-3 sur un sudoku et une carte des régions tchèques
- Logique et systèmes à base de connaissances : chaînage avant et arrière, résolution et systèmes experts
- Planification : STRIPS, monde des blocs, heuristiques par relaxation et planification dans les agents IA
- Recherche locale et recuit simulé : un voyageur de commerce à travers les chefs-lieux de région tchèques
- Algorithmes génétiques : population, croisement, mutation et sélection sur un sac à dos de randonnée
- Stratégies d’évolution et programmation génétique : de la règle du cinquième à la loi de Kepler
- Optimisation par colonies de fourmis : comment une piste de phéromone trouve un chemin court
- Optimisation par essaim de particules : inertie, expérience propre et expérience de l’essaim
- Neuroévolution : faire évoluer les poids et la forme des réseaux de neurones, et l’algorithme NEAT
- Ensembles flous et fonctions d’appartenance : comment écrire « un peu chaud »
- Logique floue et probabilité : pourquoi « un peu chaud » ne veut pas dire « probablement chaud »
- Logique floue : t-normes, implications et règles SI–ALORS
- Commande floue : Mamdani et Sugeno, du ventilateur au pendule inversé sur un chariot
- Clustering flou : l’algorithme fuzzy c-means et les points entre les clusters
- Systèmes neuro-flous (ANFIS) : des règles floues qui apprennent à partir des données
- Chaînes de Markov : matrice de transition, distribution stationnaire et PageRank
- Modèles de Markov cachés : l’algorithme de Viterbi et le casino malhonnête
- Réseaux bayésiens : pourquoi l’herbe est mouillée et ce que cela implique pour l’arroseur
- Le filtre de Kalman : comment estimer, à partir de mesures imprécises, où se trouve une voiture et à quelle vitesse elle roule
- Types d’apprentissage automatique : supervisé, non supervisé, semi-supervisé et plus
- Prompts, contexte et RAG : comment donner au modèle les bonnes informations
- Autoencodeurs variationnels (VAE) : de nouvelles images à partir d’un espace latent continu
- Réseaux antagonistes génératifs (GAN) : le faussaire contre le détective
- Modèles de diffusion : comment une image naît du bruit pur
- Processus de décision markoviens : états, actions, récompenses et équations de Bellman
- Q-learning et DQN : apprendre la valeur des actions, du tableau au réseau de neurones
- Policy gradient et PPO : apprendre directement la politique
- Comment fonctionne un agent IA : un modèle de langage en boucle avec des outils
- Programmer avec des agents IA : consigne, tests et vérification
- Évaluation et sécurité des systèmes d’IA : comment mesurer et comment surveiller
Programme complet de la série 52 sur 83 sujets publiés
- Ce qui doit figurer dans une consigne pour qu’un agent sache quand c’est terminé.
- Combien d’erreurs détectent des tests plus ou moins approfondis et pourquoi les cas limites comptent.
- Quelles erreurs font les agents et comment les trouver lors de la vérification.
Utile à connaître avant : Comment fonctionne un agent IA : un modèle de langage en boucle avec des outils, Comment aborder le développement avec des agents IA
Dans la partie précédente, nous avons vu comment un agent fonctionne de l’intérieur. Cette partie est pratique : comment lui confier du travail, comment le vérifier et comment ne pas perdre de vue ce qui se passe dans le projet. La plupart des conseils se résument en une phrase — vous devez donner à l’agent un moyen de savoir lui-même que c’est terminé, et vous devez avoir un moyen de le vérifier. Une expérience va montrer à quel point cela compte.
Cette partie fait suite à Comment fonctionne un agent IA et à Comment aborder le développement avec des agents IA, qui en disent plus sur le rôle de l’humain et sur les raisons de laisser l’agent proposer d’abord une solution.
1. La consigne : quoi, où et comment savoir que c’est terminé
Définition — critères d’acceptation
Les critères d’acceptation sont des conditions vérifiables que le résultat doit remplir pour que la tâche soit terminée : un comportement concret (« pour un panier vide, renvoie 0 CZK »), des tests qui doivent passer, une commande qui doit s’exécuter sans erreur. Contrairement à la description d’un objectif, on peut trancher sans ambiguïté s’ils sont remplis.
« Range ta chambre » est un objectif. « Rien ne traîne par terre, le lit est fait et la poubelle est vidée » sont des critères d’acceptation. Ce n’est qu’avec eux qu’on peut dire si c’est rangé — et l’enfant (ou l’agent) sait quand il peut s’arrêter.
Définition — la spécification comme précondition et postcondition
On peut écrire les critères d’acceptation de façon précise avec un triplet de Hoare (C. A. R. Hoare, 1969 ; la notation actuelle avec accolades est plus récente) : {P} C {Q} signifie que si la condition P est vraie avant l’exécution du programme C et que C se termine, alors la condition Q est vraie ensuite. P est la précondition (ce que le programme peut supposer de l’entrée), Q la postcondition (ce qui doit être vrai du résultat). Un programme est correct vis-à-vis de la spécification si le triplet est vrai pour tout état qui satisfait P.
{ castka ≥ 0 } s := sazba(castka) { s = 0 pour castka < 1 000 ; s = 0,05 pour 1 000 ≤ castka < 5 000 ; s = 0,1 sinon }
- castka ≥ 0
- précondition : la fonction ne garantit rien pour un montant négatif — elle est là pour qu’il soit clair ce que nous ne devons pas tester et ce que l’appelant doit surveiller
- s := sazba(castka)
- le programme dont on parle ; la spécification ne dit rien de la manière dont il est écrit, l’agent peut donc la remplir comme il veut
- s = 0 / 0,05 / 0,1
- postcondition : exactement trois tranches et leurs bornes (≤ ou <) — c’est justement là qu’on se trompe le plus souvent, c’est pourquoi les valeurs 999, 1 000, 4 999 et 5 000 font partie des critères d’acceptation
Une bonne consigne pour un agent a quatre parties. Non pas parce que ce serait un rituel, mais parce que chacune correspond à une chose que l’agent devrait sinon deviner :
Objectif : Sur la page du panier, affiche une remise selon le montant (dès 1 000 CZK 5 %, dès 5 000 CZK 10 %).
Contexte : Le calcul du prix est dans src/kosik.js, le gabarit dans pohledy/kosik.ejs.
Les frais de port se calculent de façon similaire dans la fonction doprava() — garde le même style.
Contraintes : Ne change pas le format des données dans la base. Aucune nouvelle bibliothèque.
Terminé quand :
- la fonction sazba(castka) renvoie 0 / 0,05 / 0,1 et les tests de test/kosik.test.js passent,
- tu ajoutes des tests pour les bornes 999, 1 000, 4 999 et 5 000 CZK,
- npm test s’exécute sans erreur et tu me montres la différence (git diff).
Le contexte épargne des recherches à l’agent (et à vous des tokens — voir le coût du contexte). Les contraintes l’empêchent de « résoudre » le problème autrement que vous le souhaitez. Et les critères d’acceptation lui donnent le retour sans lequel les longues tâches s’effondrent.
2. Petits pas, vérifications fréquentes
Dans la partie sur les agents, nous avons calculé qu’une tâche de vingt étapes, dont chacune réussit dans 90 % des cas, n’aboutit sans vérification que dans 12 % des cas. La même règle vaut pour vous : plus le morceau de travail que l’agent rend d’un coup est gros, plus il est difficile à vérifier et plus la correction coûte cher. Conséquences pratiques :
- Un changement = une tâche. Découpez un gros chantier en étapes qu’on peut vérifier séparément (d’abord le calcul, puis l’affichage, puis la modification des données).
- Le système de gestion de versions comme filet de sécurité. Avant chaque tâche, un état propre dans git ; après, un commit. Si quelque chose tourne mal, vous revenez en arrière en une seule commande.
- Lisez la différence, pas le résumé. Le « terminé, tout fonctionne » de l’agent est une affirmation. La différence dans le code (git diff) et la sortie des tests sont des preuves.
Psuccès = pn
- p
- probabilité qu’une étape réussisse ; elle est là parce que chaque étape est une nouvelle occasion de se tromper
- n
- nombre d’étapes entre deux vérifications ; il est en exposant parce que les probabilités d’étapes indépendantes se multiplient — et avec chaque étape supplémentaire sans vérification, la chance baisse géométriquement, pas linéairement
- Psuccès
- chance que toute la portion entre deux vérifications se passe bien et que rien ne soit à refaire
Exemple — combien de travail épargne une vérification toutes les cinq étapes
Sans vérification : 0,920 = 0,1216, soit 12 %. Avec une vérification toutes les cinq étapes : 0,95 = 0,5905 pour chacune des quatre portions.
Supposons que la vérification détecte toujours l’erreur et que la portion ratée soit refaite. Le nombre d’essais jusqu’au premier succès suit une loi géométrique d’espérance 1 / Psuccès. Toute la tâche sans vérification coûte donc en moyenne 20 / 0,1216 ≈ 165 étapes, avec une vérification toutes les cinq étapes 4 · 5 / 0,5905 ≈ 34 étapes et avec une vérification après chaque étape 20 / 0,9 ≈ 22 étapes. Les petits pas n’augmentent pas la chance qu’une étape réussisse — ils réduisent la quantité de travail jetée en cas d’erreur.
3. Les tests comme garde-fou : une expérience
À quel point les tests aident-ils ? Prenons cinq petites fonctions comme un agent en écrit couramment — moyenne, année bissextile, troncature d’un texte, recherche dichotomique et remise selon le montant — et fabriquons-en des versions volontairement cassées.
Définition — test de mutation
Un mutant est une copie du programme avec une seule petite modification automatique : < au lieu de <=, - 1 au lieu de + 1, || au lieu de &&, une autre constante. Une suite de tests détecte le mutant quand au moins un test échoue sur lui. La proportion de mutants détectés mesure à quel point les tests surveillent bien les erreurs. Les mutants qui se comportent exactement comme l’original (équivalents) ne sont pas comptés.
C’est l’essai d’un détecteur de fumée : vous allumez exprès une petite bougie et vous regardez s’il bipe. Ici, vous glissez exprès une petite erreur dans le code — exactement le genre d’erreur que font les humains comme les agents — et vous regardez si les tests la remarquent.
À partir du code source des cinq fonctions, 32 mutants ont été créés automatiquement. 3 d’entre eux ne se
sont jamais distingués de l’original sur 2 000 entrées aléatoires (par exemple a[s] <= x au lieu de a[s] < x
dans la recherche dichotomique, où la ligne précédente a déjà traité l’égalité) ; il reste donc 29 vraies erreurs. Face à elles,
quatre niveaux de tests, chacun incluant les précédents :
- Cas nominal — un ou deux exemples typiques par fonction. Ce sont les tests qu’on obtient quand on dit seulement « ajoute des tests ».
- Cas limites — tableau vide, bornes des tranches (999, 1 000, 4 999, 5 000 CZK), années 1900, 2000 et 2100, texte exactement à la longueur limite.
- Propriétés — 200 entrées aléatoires et des règles qui doivent toujours être vraies : le résultat de la troncature a au plus max caractères et est un début du texte, l’indice trouvé pointe vraiment sur le nombre cherché, la moyenne concorde avec un calcul écrit autrement.
Les tests du cas nominal n’attrapent que 15 erreurs sur 29 (52 %). Ils passent même avec une fonction qui renvoie 1 pour un tableau vide, avec une remise qui ne commence qu’à 1 001 CZK, ou avec une recherche dichotomique qui ne trouve rien dans un tableau d’un seul élément. L’ajout des cas limites fait monter la proportion à 90 %, les tests de propriétés à 97 %.
| fonction | mutants | cas nominal | + cas limites | + propriétés |
|---|---|---|---|---|
prumer | 5 | 4 | 5 | 5 |
jePrestupny | 8 | 5 | 5 | 7 |
zkrat | 3 | 2 | 3 | 3 |
najdi | 6 | 3 | 6 | 6 |
sazba | 7 | 1 | 7 | 7 |
MS = |O| / ( |M| − |E| )
- MS
- score de mutation — proportion des vraies erreurs que la suite de tests attrape — la hauteur de la barre dans le graphique
- |O|
- nombre de mutants détectés (au moins un test a échoué sur eux, ou ils ont planté ou bouclé indéfiniment) ; ce que les tests savent faire
- |M|
- tous les mutants fabriqués — la réserve d’erreurs à laquelle nous mesurons les tests
- |E|
- mutants équivalents ; on les soustrait parce qu’ils ne diffèrent pas de l’original, et qu’aucun test ne peut donc les détecter — sinon même des tests parfaits n’atteindraient pas 100 %
Chez nous |M| = 32 et |E| = 3, le dénominateur vaut donc 29. Cas nominal : 15 / 29 = 0,517 ; avec les cas limites 26 / 29 = 0,897 ; avec les propriétés 28 / 29 = 0,966. La procédure qui a produit ces nombres :
pour chaque fonction f :
original = compile(code de f)
pour chaque opérateur (de → vers) et chaque occurrence de « de » dans le code :
mutant = compile(code où cette occurrence de « de » est remplacée par « vers »)
si le mutant ne diffère jamais de l’original sur 2 000 entrées aléatoires :
équivalent — non compté, suivant
pour chaque niveau de tests u :
détecté[u] = un test du niveau u échoue sur le mutant,
plante, ou boucle indéfiniment (garde-fou après 10 000 passages)
MS(u) = nombre de détectés au niveau u / nombre de non équivalents
Le plus intéressant est l’année bissextile. Les cas limites laissent passer les mutants qui considèrent comme bissextile toute année paire,
ou qui écartent les années divisibles par dix — donc aussi l’année 2020. Les tests avec les années 1900, 2000 et 2100 ne s’en aperçoivent pas, car
pour celles-ci ils répondent correctement par hasard. Le test de propriété avec des années aléatoires trouve les deux erreurs. Le seul mutant qui a survécu
à tout est (rok % 4 === 0 && rok % 100 !== 0) || rok % 200 === 0 :
il ne diffère que pour les années divisibles par 200 mais pas par 400 (200, 600, 1000…), et parmi les 200 années aléatoires il n’y en avait aucune.
Même les tests aléatoires ne sont pas tout-puissants.
Psurvit = ( 1 − q )n
- q
- proportion des entrées sur lesquelles le mutant diffère de l’original ; 1 − q est la chance qu’une entrée aléatoire ne le détecte pas
- n
- nombre d’entrées aléatoires ; les entrées sont tirées indépendamment, donc les chances de « pas détecté » se multiplient
- Psurvit
- probabilité que les n entrées ratent toutes le mutant — l’erreur passe, même si la propriété est écrite correctement
Exemple — pourquoi le mutant avec l’année divisible par 200 a survécu
Le test de propriété tire des années de 0 à 2 399. Le mutant rok % 200 ne diffère de l’original que pour 6 d’entre elles (200, 600, 1000, 1400, 1800, 2200), donc q = 6 / 2400 = 0,0025 et avec n = 200 il survit avec une probabilité (1 − 0,0025)200 = 0,606 — à peu près six fois sur dix. Sa survie n’était donc pas de la malchance, mais le résultat attendu.
Le mutant rok % 2 diffère pour 600 années (q = 0,25) : il survit avec une probabilité 0,75200 ≈ 10-25, autrement dit pratiquement jamais. Le mutant rok % 10 diffère pour 96 années (q = 0,04) et survit avec une probabilité 0,96200 = 0,00028.
Combien d’entrées faudrait-il pour attraper le mutant de l’année 200 avec une probabilité de 95 % ? De (1 − q)n ≤ 0,05 on tire n ≥ ln 0,05 / ln(1 − q) = −2,996 / −0,00250 ≈ 1 196,8, soit 1 197 entrées. Il est moins cher d’ajouter le cas limite de l’année 200 ou 600 — qui connaît la règle du calendrier sait où chercher.
4. Qui doit écrire les tests
Dans l’expérience, nous avons écrit les tests d’après la consigne (ce que la fonction doit faire), et non d’après le code. C’est essentiel. Quand l’agent écrit le code puis aussitôt les tests qui vont avec, les tests risquent de ne faire que répéter ce que fait le code — y compris ses erreurs. Ils passent, et ne prouvent rien. Ce qui aide :
- Les tests (ou au moins leur liste) d’abord. Laissez l’agent écrire d’abord les tests d’après la consigne, lisez-les, et seulement ensuite le code. Les critères d’acceptation de la consigne sont exactement cette liste.
- Énumérer les cas limites. Entrée vide, un seul élément, bornes des tranches, nombres négatifs, caractères accentués. L’agent les ajoute souvent lui-même, mais seulement si vous les demandez.
- Des propriétés là où c’est possible. « Un tableau trié reste trié », « encoder puis décoder = le texte d’origine », « le nouveau calcul donne la même chose que l’ancien ». Des bibliothèques comme fast-check (JavaScript) ou Hypothesis (Python) génèrent elles-mêmes les entrées aléatoires.
Définition — test basé sur les propriétés (property-based testing)
Au lieu de paires concrètes entrée–sortie, on écrit une propriété qui doit être vraie pour toutes les entrées, et l’outil la vérifie sur de nombreuses entrées générées aléatoirement. Quand il trouve un contre-exemple, il le réduit en général au cas d’échec le plus simple. L’idée a été popularisée par la bibliothèque QuickCheck (Koen Claessen et John Hughes, 2000).
Vous ne demandez pas « combien font 7 + 5 », mais « est-il vrai que a + b est toujours égal à b + a ? » — et vous laissez l’ordinateur essayer mille paires aléatoires. Il trouve des cas auxquels vous n’auriez pas pensé.
Une propriété est un énoncé avec le quantificateur « pour tout ». Pour la fonction zkrat de l’expérience, le test l’écrit ainsi :
∀ t, m ≥ 1: |v| ≤ m ∧ ( |t| ≤ m ⇒ v = t ) ∧ ( |t| > m ⇒ v = t[0, |v| − 1) + « … » ), où v = zkrat(t, m)
- ∀ t, m ≥ 1
- pour tout texte et toute longueur — c’est pourquoi on peut la vérifier sur des entrées aléatoires sans avoir besoin de connaître le bon résultat à l’avance
- |v| ≤ m
- la promesse principale de la fonction : le résultat tient dans la longueur ; elle attrape le mutant qui oublie de compter les « … » (
slice(0, max)) - |t| ≤ m ⇒ v = t
- un texte court ne change pas ; sans cela, une fonction qui tronque toujours passerait
- v = t[0, |v| − 1) + « … »
- le résultat tronqué est le début du texte d’origine et se termine par des points de suspension ; sans cela, une fonction qui renvoie par exemple un texte vide passerait
Un outil de test basé sur les propriétés fait alors à peu près ceci :
pour i = 1 … N : # N vaut souvent de 100 à 1 000
x = génère une entrée aléatoire
si propriété(x, f(x)) est fausse :
# réduction : cherche une entrée plus simple sur laquelle cela échoue encore
tant qu’il existe y = simplifie(x) sur laquelle la propriété est fausse :
x = y # texte plus court, nombre plus petit, tableau plus court
signale le contre-exemple x
signale « N entrées ont passé » # ce n’est pas une preuve, juste une bonne nouvelle
La dernière ligne est essentielle. Les tests, même aléatoires, ne prouvent pas la correction — ils ne font que chercher des contre-exemples. Edsger Dijkstra l’a formulé en 1969 ainsi : le test peut montrer la présence d’erreurs, jamais leur absence. La probabilité qu’une erreur rare passe entre les mailles, nous l’avons calculée plus haut.
5. À quoi faire attention
Les agents font d’autres erreurs que les humains. Les plus fréquentes, qui valent la peine d’être cherchées lors de la vérification :
- « Terminé » sans vérification. L’agent annonce un succès sans avoir lancé les tests, ou en ne les lançant qu’en partie. Exigez la sortie des tests.
- Modifier les tests au lieu du code. Quand un test échoue, la « correction » la plus facile est de changer la valeur attendue ou de supprimer le test. Dans la différence, cherchez les changements dans les tests.
- Erreurs étouffées. Un
catchvide, une valeur par défaut au lieu d’une erreur, un résultat écrit en dur pour l’entrée de test. - Du travail en trop. Un fichier réécrit sans rapport avec la tâche, une nouvelle bibliothèque, un format de données « amélioré ». Les contraintes de la consigne et les petits pas gardent cela dans des limites.
- Secrets et actions irréversibles. Mots de passe dans le code, suppression de fichiers, écriture dans la base de production, envoi vers un dépôt distant. Soumettez ces actions à votre approbation.
6. Ne pas perdre la vue d’ensemble
L’agent ne se souvient de rien d’une session à l’autre. Ce qu’il doit savoir à chaque fois — comment le projet se lance et se teste, quelles sont les conventions, ce qu’il faut éviter — va dans un fichier d’instructions à la racine du projet, que l’agent lit au début. Écrivez-y ce que vous avez dû signaler à l’agent plus d’une fois. Et ce que vous seul savez (pourquoi quelque chose est fait ainsi, ce qui a été essayé et n’a pas marché) va dans les commits et la documentation — pour l’agent comme pour vous-même dans six mois.
Le dernier conseil est le plus simple : restez présent. Pas forcément devant l’ordinateur — l’agent peut travailler seul et vous regardez quand il a besoin d’une décision. C’est pour cela qu’existe coden : la consigne, l’approbation des étapes et la vérification du résultat sont sur votre téléphone, pendant que l’agent travaille sur votre ordinateur.
7. Résumé
- Une bonne consigne : objectif, contexte (fichiers, modèles à suivre), contraintes et critères d’acceptation vérifiables.
- Petits pas, un commit après chacun, et vérification de la différence dans le code et de la sortie des tests, pas du résumé.
- Dans l’expérience, les tests du cas nominal ont détecté 52 % des erreurs, avec les cas limites 90 %, avec les propriétés 97 %.
- Écrivez (ou approuvez) les tests d’après la consigne et idéalement avant le code, sinon ils ne font que répéter les erreurs de l’implémentation.
- Surveillez les modifications de tests, les erreurs étouffées, le travail en trop et les actions irréversibles.
Envie d’essayer ? Demandez à un agent IA de lancer sur votre projet un outil de test de mutation (Stryker pour JavaScript, mutmut pour Python) et de vous montrer les mutants qui ont survécu. Chacun d’eux est un test qui manque.
Vous voyez les choses autrement ? Écrivez à support@coden-app.com.