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¶
-
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. -
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. -
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). -
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. -
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_directont degrade, et on sait pourquoi. -
Seules, par paires, toutes ensemble.
lexicalseule degrade (8/21) et forme la meilleure paire avectop3_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. -
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.
-
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. -
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.
-
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.mdregenere. 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
pytesten 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_bm25etait dans le venv et dans l'installeur, pas dansuv.lock: CI rouge, et une variante active par defaut aurait leve en production. Le code se degrade sans lever, le test faitimportorskip. - 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_netimpose 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 :
- 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). - Des leviers qui ne dependent pas des phrases. L'inventaire reel des
machines (
vm_statusau demarrage du pipeline, ouLYRA_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. - 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.
- Lire les echecs restants avec le classement automatique : le 0.5b copie
parfois l'entite en nom d'outil (
fedora_basepour "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. - 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.
- It16, la premiere iteration ou le mecanisme a refuse une idee avant le
banc : la premiere version de
nombres_tricreait deux faux nets ("tu peux me mettre la tele ?" est un ordre poli, "combien de temps ... avec uptime" une commande) et faisait ciblerseekpar "trente pour cent" ;scripts/bench_recall.pyl'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 poussebackup_statusdevantbackup_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.pypublie dansbenchmarks/. - Un cinquieme jeu, tenu a l'ecart a son tour, le jour ou le quatrieme aura servi a une iteration.