Aller au contenu

La boucle d'amelioration mesuree

Methode utilisee le 2026-09-17 pour faire passer le banc des modeles de 5/21 a 21/21 avec qwen2.5-coder:0.5b (roadmap-github#72). Elle ne depend ni du modele ni du sujet : elle sert des qu'on veut ameliorer un score mesurable sans changer la production a l'aveugle.

Le cycle

analyse de l'existant
   -> bench (graine fixe)
   -> 4-5 idees, chacune avec une HYPOTHESE D'IMPACT ecrite AVANT
   -> bench des idees : seules, par paires, toutes ensemble
   -> analyse : confirmees / infirmees / neutres, et POURQUOI les echecs restent
   -> recommencer tant que le seuil n'est pas atteint (ici 99 %)

Une iteration dure 10 a 40 minutes. Cinq ont suffi. Le socle d'une iteration est l'ensemble des variantes retenues par les precedentes : on mesure ce que chaque nouvelle idee ajoute a ce qui marche deja.

Les regles, dans l'ordre ou elles ont compte

  1. Reparer le bench avant de mesurer. Le premier chiffre etait 0/21 pour tous les modeles : une methode disparue, comptee en echec du modele. Un banc qui plante doit refuser de publier (erreurs_techniques -> pas de fichier), jamais se deguiser en resultat.

  2. Graine fixe, toujours. Sans LYRA_SEED, le meme jeu donnait 4, 3 puis 4 sur 7. A graine fixe, deux executions donnent le meme resultat : une difference de 1 est alors un signal, pas du bruit.

  3. Mesurer le mecanisme avant le modele. Avant toute idee de prompt, verifier que le modele VOIT la bonne reponse : rang du bon outil dans les specs remontees, sans appeler le modele (20 secondes). Au depart, le bon outil etait montre pour 8 cas sur 21 et le premier essai n'en montrait qu'un : le score etait plafonne par le RAG, pas par le modele. Trois sessions avaient accuse le modele. Une idee sans effet mecanique ne merite pas de bench (sans_registre, ecartee en 20 secondes).

  4. Une idee = une variante, inactive par defaut. LYRA_EXP="a,b" active exactement ces variantes ; la production ne change pas tant qu'on mesure. Le code d'une variante vit dans un module a part (ephaistos_exp.py) avec sa fonction pure et son test unitaire ; le branchement dans le pipeline tient en une ligne conditionnelle.

  5. Hypothese ecrite avant, chiffre apres. Chaque variante est notee avec son impact suppose (fort / moyen / faible, et sur quels cas) avant le bench. Une hypothese infirmee vaut autant qu'une confirmee : routage, index, consigne_onoff, deux_exemples, top5_direct ont degrade, et on sait pourquoi.

  6. Seules, par paires, toutes ensemble. lexical seule degrade (8/21) et forme la meilleure paire avec top3_direct (13/21). Sans les paires, elle aurait ete jetee. Inversement, en iteration 1, « tout ensemble » etait pire que la meilleure variante seule parce que la combinaison embarquait deux idees nuisibles.

  7. Lire la reponse brute et le prompt exact sur chaque echec restant. C'est la seule facon de classer un echec : donnees (le bon outil n'est pas remonte), banc (la reponse est juste mais comptee fausse), modele (le bon outil est en tete et il repond autre chose). Chaque classe appelle un remede different, et l'iteration suivante cible la classe majoritaire.

  8. Score strict a cote du score tolerant. Si le banc accepte des equivalences (EQUIVALENCES), il publie aussi le score strict (reussis_strict) : comparable aux iterations precedentes, et il rappelle que la table est une decision de banc, pas une amelioration du produit. Un cas irrealisable (outil inexistant) se corrige dans le banc, pas en « faisant passer » le modele.

  9. Retirer compte autant qu'ajouter. La derniere iteration a gagne le dernier cas en supprimant une consigne jugee « neutre » trois iterations plus tot. Pour un petit modele, chaque phrase d'explication a coute ; chaque exemple tire des donnees a rapporte.

  10. Consigner au fil de l'eau. Un commentaire d'issue par iteration (hypotheses AVANT, resultats APRES), un fichier de resultats par iteration dans benchmarks/results/, BENCHMARKS.md regenere. Le topo final s'ecrit tout seul.

Outillage dans ce depot

Piece Role
lyra/models/ephaistos_exp.py les variantes (VARIANTES), la configuration retenue (DEFAUT), actives()
scripts/bench_boucle.py rejoue le banc pour chaque configuration ; --socle, --variantes, --seulement, --iteration
tests/test_campaign_llm.py le banc des modeles (21 cas), EQUIVALENCES, score strict, refus de publier en panne
scripts/gen_benchmarks_md.py BENCHMARKS.md : reference, variantes, une ligne par iteration
benchmarks/README.md protocole, machine, graine, reserves
# une iteration : socle = acquis, variantes = idees du jour
LYRA_SEED=42 .venv/bin/python scripts/bench_boucle.py --iteration 6 \
    --socle exemples_cibles,lexical --variantes idee_a,idee_b,idee_c

# rejouer des configurations precises
LYRA_SEED=42 .venv/bin/python scripts/bench_boucle.py --iteration 6 \
    --seulement "exemples_cibles;exemples_cibles,idee_a"

# mesurer le mecanisme (rang du bon outil) sans modele : voir la section
# « phase 0 » du topo Notion du 2026-09-17 ; a industrialiser (voir ci-dessous)

Pieges rencontres

  • Mesurer sous charge. Un pytest en parallele d'un bench a produit des timeouts publies comme latences. Une mesure = machine au repos, une seule chose a la fois. Les durees restent indicatives, les verdicts sont surs.
  • L'index derive de ses sources. L'index v3 datait d'avant les declencheurs qu'il etait cense contenir ; et un regex ([^E]+?) privait 38 outils sur 88 de leurs paraphrases sans que rien ne le signale. Verifier le contenu reel de l'index (les documents), pas la presence des collections.
  • Un chemin qui ignore les variantes. Les modeles autres que 0.5b passaient par TOON, ou analyze() n'applique aucune variante : comparer « a configuration egale » exigeait de le savoir.
  • Une dependance installee a la main. rank_bm25 etait dans le venv et dans l'installeur, pas dans uv.lock : CI rouge, et une variante active par defaut aurait leve en production. Le code se degrade sans lever, le test fait importorskip.
  • Deux dictionnaires aux memes cles (triggers_map, examples_map) : un patch par regex globale touche les deux. Patcher par plage.

Ce que la methode ne garantit pas

  • Elle optimise le score sur LE banc : 21 cas, 3 serveurs, formulations proches des regles. La generalisation se mesure sur un jeu tenu a l'ecart (paraphrases inedites, tous les serveurs), qui n'existe pas encore.
  • Elle a ete reglee sur un modele (le 0.5b). Les autres en profitent (1b 8 -> 18, 3b 6 -> 18, 7b 6 -> 19) sans avoir ete cibles.
  • Un seul run par configuration, a graine fixe : reproductible, mais pas une distribution. Changer de graine changerait des cas a la marge.

Le second banc : ce que la boucle a appris le lendemain

Le jeu « hors regles » (tests/cases_hors_regles.py, 51 formulations inedites, 5 serveurs, controle mecanique contre les regles et l'index) a ete rejoue avec la meme boucle le 2026-09-18 :

  • Le 21/21 ne generalisait pas : 13/51 au depart. Quatre iterations : 13 -> 14 -> 21 -> 26/51, puis plateau. Le rendement a ete porte par les donnees (paraphrases pour les 34 outils qui n'en avaient pas, index sans doublon, lexique des mots d'equipement), les reglages de code valant +1 a +3.
  • Le recall ne suffit pas : en iteration 1 le bon outil est passe de 7 a 20 fois en tete et le score n'a pas bouge. Sans exemple attache a la spec, un 0.5b baptise l'outil avec un mot de la requete. Lire la reponse brute reste l'etape qui classe l'echec.
  • Une regle fausse est pire qu'un modele faux : six regles repondaient a cote sur des phrases inedites, sans confirmation vocale en mode performance. Ecrire un jeu hors regles est aussi un test des regles.
  • Optimiser un jeu coute a l'autre : la configuration hors regles fait 19/21 sur le premier banc. Decider sur les deux jeux ensemble, et verifier ce que les regles interceptent en usage reel.
  • Sur du langage libre, la taille du modele compte (0.5b 51 %, 3b 63 %, 7b 71 %) alors qu'elle ne comptait pas sur le premier banc -- jusqu'a ce que le tri des specs soit assez sur pour decider a la place du modele.
  • Quand le tri est net, ne pas demander au modele. Mesure sans modele : le rang 1 du tri, quand son score est net, etait le bon outil 27 fois sur 27. outil_force_si_net impose alors l'outil et laisse au modele les arguments et les cas ambigus. Avec les cartes de tri, c'est ce qui a mene le 0.5b de 39 a 51/51 -- et le 1.5b ne faisait pas mieux que le 0.5b a configuration egale.
  • Le vote n'aide pas un modele systematique. Trois appels sur trois ordres de specs ont degrade le score : le 0.5b n'est pas bruite, il est attire par le meme voisin a chaque fois. Corriger ce qu'on lui montre vaut mieux que le reinterroger.
  • Un jeu qui a servi a onze iterations n'est plus une mesure de generalisation, meme sans copie de phrase : les cartes ont ete completees en regardant ses echecs. Le troisieme jeu, tenu a l'ecart, dira ce que vaut le 72/72.

Le troisieme jeu : ce que valait le 72/72, et comment on repart

Le troisieme jeu (tests/cases_hors_regles_2.py, 100 phrases, ecrit apres la boucle) a donne 56/100 en mesure unique a la configuration qui faisait 72/72 : un jeu itere mesure la boucle, pas le produit. La suite, le 2026-09-19 au soir, a fixe le protocole a trois jeux :

  1. Sceller le jeu suivant avant de toucher a quoi que ce soit. Le quatrieme jeu (tests/cases_hors_regles_3.py, 50 phrases, vrais noms de machines, dix outils jamais mesures) a ete ecrit et controle AVANT les leviers ; le troisieme devient le jeu de developpement. Le controle du jeu a encore trouve trois regles fausses (corrigees, tests de regression).
  2. Des leviers qui ne dependent pas des phrases. L'inventaire reel des machines (vm_status au demarrage du pipeline, ou LYRA_VMS) a la place d'un motif de nom ; un lexique de langue courante ecrit domaine par domaine (allumage, extinction, volume, lumiere, lecture, machines, sauvegardes) ; les memes verbes dans les cartes de tri. Mesure mecanique avant le banc : rang 1 pour 41 -> 60 cas sur 100, absents 13 -> 5.
  3. Le banc confirme le mecanisme : 56 -> 58 / 64 / 64 seules, 67 / 66 / 68 par paires, 70 les trois ensemble (it14) ; regression 21/21 et 50/51.
  4. Lire les echecs restants avec le classement automatique : le 0.5b copie parfois l'entite en nom d'outil (fedora_base pour "reveille fedora-base") -> outil_par_machine (+2, comme prevu) ; une seconde table de mots ordinaires (+9) ; assouplir le critere net (seuil 1) est infirme (-4 : la precision mecanique 36/5 contre 29/2 l'annoncait, trois faux nets imposent trois mauvais outils). It15 : 82/100 sur le jeu 3.
  5. Une seule mesure du jeu scelle, et c'est le seul chiffre qu'on publie comme generalisation : 41/50 sur le quatrieme jeu. Ses neuf echecs (sept confusions dont trois sur un nombre, deux questions d'etat lues comme des ordres) ont guide l'iteration 16 -- il ne sera donc pas remesure, et un cinquieme jeu a ete scelle avant d'ecrire une ligne.
  6. It16, la premiere iteration ou le mecanisme a refuse une idee avant le banc : la premiere version de nombres_tri creait deux faux nets ("tu peux me mettre la tele ?" est un ordre poli, "combien de temps ... avec uptime" une commande) et faisait cibler seek par "trente pour cent" ; scripts/bench_recall.py l'a montre en vingt secondes, la version corrigee a une precision inchangee (38/2) et gagne 4 rangs 1. Le banc dit ensuite 82 -> 83 sur le jeu 3 : les deux idees visaient les echecs du jeu 4, pas ceux du jeu 3, et le jeu 2 perd un cas (50/51 : "verifie l'integrite des sauvegardes ?" -- la question d'etat pousse backup_status devant backup_verify, qui n'est pas dans les cibles d'etat ; prochaine hypothese, pas de retouche apres coup). Cinquieme jeu, mesure unique : 35/50 -- six echecs sur dix cote Hue, deux noms de machines rendus comme des outils TV. Un jeu scelle mesure aussi la variance entre jeux : 41/50 puis 35/50 pour deux configurations a un point d'ecart sur le jeu 3.

Pieges de ces deux iterations : un guetteur until ! pgrep -f "<ligne du banc>" matche sa propre ligne de commande et ne rend jamais la main (guetter un fichier marqueur) ; un mot ordinaire peut etre nuisible sur l'autre jeu ("point" -> status casse "point de restauration" = snapshot) : le recall mecanique sur les deux jeux le voit en vingt secondes, avant le banc.

La boucle "99 % sur chaque jeu" (iterations 17 a 21)

Consigne du 2026-09-20 : iterer tant que le 0.5b est sous 99 % sur chaque jeu. Les jeux 1 a 5 deviennent des jeux de developpement ; un sixieme est scelle avant d'ecrire une ligne (son controle trouve encore une regle fausse).

Iteration Levier Jeu 3
depart (38 variantes) -- 83
17 cartes_17 : les mots qui departagent les egalites a un point ("sans le son", home cinema, "un poil", pc/synchronisee, "le point", "plus froide", l'equipement designe le serveur...), lumiere_sans_verbe, nom_de_vm rejouee 92
18 force_definitif : un outil impose par le tri net n'est plus remis en jeu par la seconde passe 93
19-21 corrections trouvees en deboguant (le critere net ne lisait pas le bonus machine ; l'outil impose etait applique apres le complement d'arguments ; "coupe le son" ne cible "on" que chez Denon ; une commande l'emporte sur la question ; le niveau d'une piece est un groupe) 100

Resultat sur les cinq jeux de developpement : 21/21, 51/51, 100/100, 50/50, 50/50. Sixieme jeu, scelle, mesure une fois : 42/50.

Ce que ces cinq iterations ont appris :

  • Le mecanisme d'abord, encore. scripts/bench_recall.py (avec les equivalences du banc) a refuse cinq versions intermediaires en vingt secondes chacune : un mot ajoute a une carte cree un faux net sur un autre jeu ("mets un effet qui bouge", "la lampe du bureau" poussee vers le groupe, "balance ce lien sur la tele" poussee vers la TV, "un peu moins de lumiere" poussee vers l'extinction). Zero faux net sur les quatre jeux avant chaque banc : le banc n'a jamais contredit la mesure mecanique.
  • Un tri net non impose est un bug, pas une limite du modele. Cinq echecs avaient le bon outil en rang 1 avec un tri net ; le modele repondait autre chose parce que la seconde passe remplacait la decision. Lire le prompt exact ET le chemin du code qui suit la reponse.
  • L'equipement designe le serveur ("tele" -> tv, "lampe" -> hue) : ca ne change pas l'ordre entre outils d'un meme serveur mais eleve le score absolu, et le tri devient net la ou il ne l'etait qu'a un point. Sauf quand l'equipement n'est pas l'appareil vise ("balance ce lien sur la tele", "l'ampli sur l'entree tele") : la carte s'eteint si un autre appareil est nomme.
  • Deux formulations ambigues restent ambigues ("allume l'entree" : une lampe ou un groupe ? ni l'un ni l'autre n'existe sur le pont Hue). L'inventaire reel (comme pour les VM) serait la reponse generique ; ici on laisse le modele trancher au lieu d'inventer une regle.
  • 100 % sur un jeu itere ne dit rien du produit : c'est le sixieme jeu qui compte, et il ne sera pas remesure.

Consolidation (2026-09-20, apres la boucle)

Le module des variantes etait devenu une dette : 1 581 lignes, 59 variantes declarees dont 17 refutees ou neutres encore branchees. Le lot G a retire ces 17 du code (dedup, routage, index, json_format, indice_url, couleurs, top5_direct, consigne_onoff, deux_exemples, exemple_description, exemple_discriminant, question_etat, boost_sur_etendue, top1_si_net, resolution_floue, vote_rotation, net_assoupli) : leurs mesures restent dans benchmarks/results/ et dans ce document, c'est leur seule trace utile. Les 42 retenues forment DEFAUT et restent activables une a une par LYRA_EXP : c'est ce qui permet l'ablation (« que perd-on si on retire celle-ci ? ») et la prochaine boucle. Les tables de mots (cartes de tri, lexiques, exemples Denon) sont dans lyra/models/cartes.py, donnees seules ; la logique fait environ 800 lignes. Preuve d'equivalence : recall mecanique identique sur les quatre jeux, banc 21/21, 51/51, 100/100.

Regle pour la suite : une variante refutee est retiree du code a l'iteration suivante, pas conservee « au cas ou ». Le code porte la configuration ; le document et les resultats portent l'histoire.

Le banc mesurait un chemin que le demon n'executait pas (recette du 2026-09-23)

La recette manuelle (vingt phrases, page Notion « batterie test complet », section 15) a donne des reponses qui n'avaient rien a voir avec le banc : « reveille fedora-base » -> tv.power_on, « archive test-vm » -> tv.mute, « de la lumiere dans le salon » -> bavardage. Cause : le banc appelait EPHAISTOS directement avec les huit specs prefixees tool_name: ; le pipeline enrichi (celui du demon) lui envoyait une spec, sans prefixe (le modele lisait le premier mot de la description comme nom d'outil), puis laissait le contexte de session ([ctx: last_mcp=...], copie tel quel dans les arguments par le 0.5b) et le classificateur d'intention (1b, qui range « silence sur l'ampli » en discussion) ecraser le resultat. Deux defauts d'etat s'ajoutaient : une question laissee en attente par un appel one-shot etait relue comme la reponse de l'appel suivant, et le normaliseur d'argot mettait les URL en minuscules (un identifiant YouTube est sensible a la casse).

Corrections : une fonction partagee construit les specs pour le banc ET le pipeline (specs_pour_ephaistos), toutes sont transmises, pas d'injection de contexte avec les variantes, une regle qui matche ou un tri qui reconnait un mot d'outil est une demande quoi qu'en dise le classificateur, les questions en attente sont oubliees en one-shot, les URL sont protegees.

La regle qui manquait : mesurer le chemin de production, pas un chemin parallele. tests/test_campaign_llm.py --reel passe maintenant par EnhancedPipeline.process, une session par cas, et compte a part les refus corrects (VM absente de l'inventaire reel). Resultat sur le chemin reel, configuration finale : 21/21, 43/43 (+8 refus corrects), 91/92 (+8 refus). Le seul echec restant (« fais danser les lumieres avec la musique » -> hue_beat_set au lieu de hue_beat_start) est une confusion du modele, pas un defaut de chemin.

A industrialiser

  • Le releve de recall (rang du bon outil sans modele, precision du critere net) est un script de session : en faire scripts/bench_recall.py publie dans benchmarks/.
  • Un cinquieme jeu, tenu a l'ecart a son tour, le jour ou le quatrieme aura servi a une iteration.