Affiner sans Oublier : Itération Continue et Mémoire du LoRA
Affiner sans Oublier : Itération Continue et Mémoire du LoRA
Dans les articles précédents, nous avons posé les fondations : générer des traces ReAct de qualité, entraîner efficacement avec QLoRA et Unsloth, orchestrer les agents sur du matériel réel. Mais une question reste entière : que se passe-t-il quand le premier agent certifié n’est pas assez bon ?
C’est le lot commun du fine-tuning sur domaine étroit. Un premier run d’entraînement produit un agent fonctionnel, mais la validation fonctionnelle — le vrai test, pas la loss — révèle des angles morts. Certaines fonctions proches sont confondues. D’autres ne sont jamais invoquées correctement. L’agent a appris l’essentiel, mais l’essentiel n’est pas suffisant en production.
Cet article raconte la mécanique concrète de l’amélioration itérative d’un agent cyber-agent-engine face à un périmètre d’outils dense : comment identifier les failles, comment les corriger sans tout casser, et pourquoi la tentation du patch chirurgical se retourne contre vous.
1. Le Piège de la Loss et de la Token Accuracy
Après un premier run d’entraînement, les indicateurs standard semblent encourageants : la loss converge proprement, la token accuracy dépasse les 95%. L’agent mérite-t-il d’être déployé ?
Pas encore.
La loss mesure si le modèle reproduit fidèlement les tokens attendus dans les exemples d’entraînement. Ce n’est pas la même chose que de sélectionner la bonne fonction face à une requête réelle en inférence. Ces deux métriques divergent significativement quand le périmètre d’outils est dense et que plusieurs fonctions ont des sémantiques proches.
L’indicateur utile est le score de validation fonctionnelle : soumettre un jeu de requêtes représentatives à l’agent en inférence (pas en entraînement) et mesurer le taux de sélection correcte — bonne fonction, bons paramètres. Ce score, et non la loss, détermine si l’agent est déployable.
Token accuracy élevée ≠ Agent fonctionnel
Score de validation fonctionnelle = Vrai critère de déploiement
Avec un périmètre de plusieurs dizaines de fonctions, un premier entraînement bien conduit peut atteindre un score de validation fonctionnelle autour de 80-85%. C’est honorable, mais insuffisant quand les fonctions restantes sont justement les cas critiques de production.
2. La Boucle d’Amélioration Itérative
Face à un score insuffisant, l’instinct est de relancer l’entraînement avec les mêmes données. C’est une erreur : si l’agent échoue sur certaines fonctions, c’est que les données d’entraînement pour ces fonctions sont insuffisantes ou mal calibrées, pas que le modèle manque d’epochs.
La bonne réponse est une boucle structurée :
┌──────────────────────────────────────────────────────────┐
│ BOUCLE D'AMÉLIORATION ITÉRATIVE │
│ │
│ 1. VALIDATE → Soumettre jeu de tests à l'agent │
│ Mesurer le score fonctionnel par fonction │
│ │
│ 2. ANALYZE → Identifier les fonctions en échec │
│ Classer par type d'erreur : │
│ - Confusion sémantique (fn voisine) │
│ - Absence totale de match │
│ - Paramètres mal formés │
│ │
│ 3. GENERATE → Créer des exemples ciblés │
│ Contrastifs si confusion, denses si absent│
│ │
│ 4. MERGE → Fusionner avec le dataset complet │
│ Déduplication + shuffle global │
│ │
│ 5. RETRAIN → Réentraîner sur le dataset étendu │
│ → retour en 1. │
└──────────────────────────────────────────────────────────┘
Chaque itération améliore le score global. L’amélioration n’est pas linéaire — les dernières fonctions récalcitrantes sont souvent les plus difficiles à corriger.
3. La Confusion Sémantique : Quand les Fonctions Se Ressemblent
L’analyse des échecs révèle un pattern dominant : la confusion sémantique. L’agent n’invoque pas une fonction au hasard — il invoque une cousine qui partage des mots-clés ou un domaine fonctionnel.
Quelques exemples typiques de confusions dans un périmètre réseau/firewall :
- Une fonction qui vide le contenu d’un objet est confondue avec une fonction qui supprime cet objet
- Une fonction qui liste l’état runtime d’un composant est confondue avec une fonction qui lit la configuration de ce composant
- Une fonction qui crée un point de sauvegarde temporaire est confondue avec un snapshot complet du système
- Une fonction qui lit les logs d’un service est confondue avec une fonction qui liste la configuration de ce service
Ces confusions ne sont pas des bugs du modèle. Elles révèlent que le dataset d’entraînement initial ne distinguait pas suffisamment ces fonctions. L’agent a appris des associations correctes mais insuffisamment précises.
4. La Recette des Exemples Contrastifs
Pour corriger une confusion sémantique, les exemples d’entraînement supplémentaires doivent être contrastifs : ils doivent non seulement illustrer la fonction cible, mais aussi insister explicitement sur la distinction avec sa cousine.
La Structure d’un Exemple Contrastif
Un exemple contrastif efficace se construit en trois temps :
1. La description enrichie — dans le tool schema fourni au modèle, la description de la fonction doit contenir la distinction explicite :
"description": "Vide le CONTENU d'un objet (toutes ses entrées membres).
NE PAS CONFONDRE avec delete_X (qui supprime l'objet lui-même) :
cette fonction préserve l'objet mais en efface les membres."
2. La requête ambiguë — la moitié des requêtes générées doit être formulée de façon à ce qu’un modèle naïf soit tenté d’appeler la mauvaise fonction :
"Nettoie toutes les entrées de l'objet PROD-DMZ, mais on garde l'objet pour le reutiliser"
# → ici "nettoie" pourrait activer "delete", mais "on garde l'objet" ancre vers "flush"
3. La réponse avec pensée explicite — la trace ReAct inclut un raisonnement qui nomme la confusion et la résout :
Pensée: La demande est de vider le contenu, pas de supprimer l'objet.
Je dois utiliser flush_X et non delete_X.
Ce dernier point est crucial : le modèle apprend à raisonner sur la distinction, pas seulement à l’associer mécaniquement.
Quantité et Diversité
Une vingtaine à une trentaine d’exemples contrastifs par fonction en échec suffisent, à condition qu’ils soient réellement variés. Générer cinquante fois la même reformulation n’apporte rien — le modèle sur-apprend l’exact pattern sans généraliser.
5. L’Expérience du Patch Ciblé (et Ses Limites)
Fort de ce diagnostic, la première impulsion est tentante : entraîner uniquement sur les nouveaux exemples ciblés, en utilisant l’adaptateur existant comme point de départ (warm-start via PeftModel.from_pretrained(..., is_trainable=True)).
L’intuition semble solide. L’adaptateur existant a déjà appris les 80-85% de fonctions maîtrisées ; inutile de les réapprendre. On fait un patch chirurgical sur les 15-20% restants.
En pratique, cela produit des régressions.
Le Catastrophic Forgetting Localisé
Même avec LoRA — dont la réputation est précisément d’éviter le catastrophic forgetting par rapport au full fine-tuning — un patch ciblé sur un petit dataset homogène provoque des régressions sur des fonctions adjacentes sémantiquement.
Le mécanisme est le suivant : avec seulement quelques dizaines d’examples couvrant une dizaine de fonctions, le gradient est concentré sur une zone étroite de l’espace sémantique. Les poids LoRA se déplacent suffisamment pour perturber des fonctions voisines — pas assez pour briser complètement les fonctions lointaines, mais assez pour dégrader des fonctions qui partagent un même domaine ou des tokens proches.
Résultat observé : on corrige 5-6 fonctions en échec, mais on en régresse 4-5 qui fonctionnaient. Le score net s’améliore à peine.
Avant patch ciblé : 85% de succès
Après patch ciblé : 87% de succès (+2%)
Mais 6 corrigées, 4 régressées
→ gain réel faible, instabilité forte
Ce résultat illustre une limite fondamentale : LoRA n’est pas immunisé contre le catastrophic forgetting, il le réduit. Sur un dataset trop étroit, le modèle “oublie” partiellement les fonctions non représentées.
6. La Stratégie Gagnante : La Dilution dans le Dataset Complet
La leçon est contre-intuitive mais robuste : la bonne façon d’utiliser les exemples ciblés est de les diluer dans le dataset complet.
On ne fait pas un entraînement séparé sur les 200 exemples ciblés. On les injecte dans le dataset existant de plusieurs milliers d’exemples, on déduplique, on shuffle, et on réentraîne tout depuis le début.
Pourquoi ça fonctionne mieux ?
- Distribution équilibrée : les nouvelles fonctions représentent ~2-3% du dataset final, pas 100%. Les gradients n’écrasent pas le reste.
- Raisonnement contextualisé : l’agent re-voit toutes les fonctions dans le même run, maintenant les associations correctes tout en apprenant les nouvelles distinctions.
- Pas de dépendance à l’init : un warm-start depuis un mauvais adaptateur peut ancrer des erreurs. Repartir de zéro (initialisation aléatoire LoRA) avec un meilleur dataset converge vers un meilleur minimum.
La contrepartie est évidente : un entraînement complet prend bien plus longtemps qu’un patch de 50 steps. Mais c’est le bon compromis. L’Usine à LoRAs est conçue pour que ce cycle soit automatisable — la durée est connue et planifiable.
❌ Patch ciblé seul → +2% net, 4 régressions
✅ Exemples ciblés + full → amélioration nette, 0 régression
7. Vers la Convergence : Les Dernières Fonctions Récalcitrantes
Chaque itération réduit le nombre de fonctions en échec. Les premières iterations éliminent les cas faciles — fonctions peu représentées dans le dataset initial, confusions grossières. Les dernières iterations s’attaquent à des distinctions plus subtiles.
Quelques patterns récurrents dans les “dernières fonctions” :
- Fonctions sans signaux forts dans la requête utilisateur : l’utilisateur peut formuler la même demande de six manières différentes et l’agent doit inférer l’intention sans mot-clé direct.
- Fonctions à faible fréquence d’usage réel : peu d’exemples naturels dans la distribution, donc peu d’exemples d’entraînement même avec data augmentation.
- Fonctions qui partagent un namespace ou un préfixe : le tokenizer peut traiter leurs noms comme similaires, ce qui pollue les embeddings.
Pour ces cas, les exemples contrastifs doivent être encore plus travaillés — requêtes ambiguës, pensées explicites détaillées, diversité maximale dans les formulations.
Bilan et Enseignements
L’amélioration itérative d’un agent LoRA sur domaine dense conduit à plusieurs enseignements durables :
Sur la mesure : la loss et la token accuracy ne sont pas des indicateurs de déploiement. Il faut un score de validation fonctionnelle basé sur l’inférence réelle.
Sur le diagnostic : l’analyse fine des échecs — quelle fonction est appelée à la place de laquelle — est plus utile que le simple comptage des erreurs. Elle guide précisément la génération de nouvelles données.
Sur les données : les exemples contrastifs avec pensée explicite sont plus efficaces que les exemples génériques pour corriger des confusions sémantiques. Vingt exemples contrastifs valent plus que cent exemples génériques supplémentaires sur la même fonction.
Sur l’entraînement : diluer les exemples ciblés dans le dataset complet et réentraîner entièrement est plus stable qu’un patch chirurgical, même avec warm-start. LoRA réduit le catastrophic forgetting — il ne l’élimine pas.
Sur l’usine : l’automatisation du pipeline (génération → fusion → entraînement → validation) est ce qui rend l’itération soutenable. Sans elle, chaque cycle serait un projet en soi.
La prochaine étape naturelle est d’observer si ce cycle peut converger vers un seuil proche du 95-97% — et ce que révèlent les derniers points de résistance. C’est l’objet du prochain article.
Navigation dans la série :
- Article 1 : L’Usine à Cerveaux — Automatiser la Spécialisation des LLM
- Article 2 : Apprendre à l’IA à “Réfléchir” — Le Moteur de Traces ReAct
- Article 3 : La Forge Technique — Optimiser l’Entraînement avec Unsloth & QLoRA
- Article 4 : Du Modèle au Système — Orchestration & Validation Bare-Metal
- Article 5 : L’IA au Service de l’Infrastructure — Extension IaC & Vision Future
