blog.vfg.lol

L'agent ne peut pas être son propre juge : audit d'un essai dual-agent local

Un vieil essai d'IA locale retrouvé, 106 fichiers audités par 3 agents, et une leçon : quand le modèle écrit lui-même PASS, ce PASS ne vaut rien. Plus le stockage et le traitement d'un corpus agentique.

Méthode : tout ce qui suit est daté, mesuré et rejouable. Chaque chiffre vient d'un script ou d'une commande, jamais d'une impression. C'est la règle que le système s'est donnée en septembre — et elle s'applique à lui-même, y compris contre un de ses propres constats (voir plus bas).

TL;DR

Un ancien essai d'IA locale refait surface : deux agents sur Telegram, pilotés par un serveur de modèles en local. 106 fichiers de traces dormaient dans un dossier non versionné, hors workspace. Trois agents d'audit les ont lus un par un. Ce qu'ils ont trouvé n'est pas un bug de code, c'est un vice de méthode :

  • le modèle auditait et signait son propre verdict — 6 audits « PASS » fabriqués sans avoir rien exécuté ;
  • le corpus est désormais rapatrié, versionné et converti en store requêtable (72 traces → un JSONL) ;
  • son taux de rappel mémoire réel est de 3/10, pas les 13 % annoncés d'abord — un de mes propres constats était faux, et c'est la mesure qui l'a repris.

En une phrase : un système qui se note lui-même ne mesure rien.

Ce que c'était

L'essai (juin 2026) tenait en trois pièces :

  1. Un routeur qui reçoit les messages Telegram et choisit l'agent : une sonde rapide et un agent profond.
  2. Deux modèles locaux chargés dans un serveur de modèles sur la machine : un petit rapide (~8 000 tokens de contexte, ~5 s de réponse) et un moyen lent (~20 000 tokens, ~48 s de réponse).
  3. Une instrumentation maison : chaque question produisait une trace markdown datée (modèle, contexte consommé, durée, mémoire des deux « lanes »), plus deux suites — un benchmark de performance et une sonde d'infrastructure.

Idée saine : séparer un agent pour les questions courtes d'un agent pour les tâches profondes, et tout tracer. C'est ce qui rend l'audit possible quatre mois plus tard.

Ce que la mesure a révélé

1. Le verdict écrit par le sujet

Six traces montrent l'agent profond produisant un tableau d'audit avec VERDICT: PASS — alors que le prompt injecté ne contenait aucune sortie de commande. Il n'avait pas exécuté npm run agent:probe, il a inventé que tout allait bien. Un autre a fabriqué un livrable daté de 2023 avec un préfixe de projet qui n'existe pas.

C'est la faute cardinale : le LLM audité était aussi le juge. Un PASS produit par le système qu'on mesure est du bruit, pas une preuve.

2. La fuite de raisonnement

Neuf réponses finales ne sont que du chain-of-thought : « Okay, let's see… the user is asking… ». Le raisonnement du modèle s'est retrouvé publié à la place de la réponse. Sur des questions triviales (« Dis PONG »), le contexte injecté pesait plus que la réponse.

3. Les hallucinations, dont une qui valide sa propre invention

  • « La Suisse est UTC+1 toute l'année » — faux (elle observe l'heure d'été), affirmé deux fois.
  • Une ville inexistante, une API inventée, une heure fabriquée, et un tableau à quatre critères PASS auto-validant l'invention. 146 secondes pour produire ça.
  • Des commandes qui n'existent pas (un « lock pc » imaginaire), puis, à la relance, l'aveu de ne pas avoir lancé de script — suivi d'un faux code source pour compenser.

Motif commun : sans accès au réel, l'agent fabrique. Un prompt « exécute ceci » adressé à un agent sans outil ne produit pas un refus, il produit une fiction bien formatée.

4. Le chiffre que personne n'avait

Le corpus contient un test de mémoire intelligent : on donne un mot-clé, on demande plus tard de le rappeler. L'audit initial a conclu « échec ≈ 13 % ». En séparant proprement déclaration (« mon mot-clé est X ») et rappel (« quel mot-clé ai-je donné ? »), le vrai taux tombe : 3 rappels réussis sur 10, soit 70 % d'échec.

Autrement dit : mon premier constat était faux, et c'est la mesure qui l'a corrigé — pas la prose. C'est exactement le bug que la méthode d'audit VFG traque : l'instrument mesure sa propre trace.

5. Les instruments eux-mêmes

  • Une synchronisation de contexte annonçait 8 192 tokens rechargés, alors que le snapshot en montrait 15 421 : le compteur mentait sur son propre état.
  • Le compteur de tokens estimés était plafonné (des tours courts à ~1 token pour 5 caractères, dix tours figés à 572).
  • Les quatre derniers runs de sonde échouent tous en LLM_CONNECTION_REFUSED : le serveur de modèles s'était éteint. Un corpus qui finit sur un refus de connexion n'est pas un corpus de résultats.

Décider où vit la donnée

Après l'audit, une question aussi importante que les conclusions : où stocker tout ça ?

État mesuré de départ : git rev-parse répond « not a git repository ». Le corpus vivait dans un dossier à la racine du disque système, hors du workspace de travail, non versionné. Aucune sauvegarde, aucune requête possible, aucune comparaison dans le temps. Un actif de connaissance qui tient à un dossier local.

Décision retenue : rapatrier sous le hub, dans un dossier dédié, avec trois étages explicites.

agents/
  legacy/      le brut d'origine — lecture seule, jamais modifié
  traces/      le store machine — 1 ligne = 1 question/réponse (JSONL)
  runs/        benchmark + sonde, indexés par identifiant
  scripts/     l'ETL, rejouable et idempotent

Quatre règles ont guidé la conversion :

  1. La question est un champ, pas un nom de fichier. Le legacy l'encodait dans le titre du fichier — tronqué, illisible, non requêtable. La question devient question.
  2. Chaque ligne porte qui a produit le verdict (verdict_source). C'est le garde-fou contre la faute n°1 : on ne peut plus confondre un PASS du modèle et un PASS d'un runner.
  3. Toujours un tableau flags[]. Fuite de raisonnement, rappel raté, hallucination suspectée, troncature : requêtable d'un coup, au lieu d'être relu à la main.
  4. Un fichier par jour, pas un par question. 72 fichiers markdown deviennent 72 lignes.

Traiter : du corpus à l'amélioration

Stocker n'est pas traiter. Le pipeline, une fois le store en place :

flowchart LR
    L["legacy .md<br/>(lecture seule)"] --> E["ETL<br/>idempotent"]
    E --> J["traces.jsonl<br/>1 ligne = 1 Q/R"]
    J --> F["étiquetage<br/>flags + verdict runner"]
    F --> M["métriques<br/>par modèle"]
    F --> EV["jeu étiqueté<br/>non-régression"]
    M --> D["décisions"]
    EV --> G["gate CI"]
    D --> R["règles / skills<br/>distillées"]
  • ETL : un script déterministe. Relancé, il donne le même hash — donc comparable dans le temps.
  • Étiquetage : des heuristiques posent les flags ; elles ont déjà repris un constat humain erroné.
  • Métriques : taux d'hallucination, taux de rappel, latences p50/p95, par modèle — le chiffre qui manquait pour dire quel agent garder.
  • Jeu étiqueté : les 72 traces deviennent une base de non-régression. On change un prompt ou un modèle, on rejoue, on compare. Les erreurs documentées deviennent des tests.
  • Distillation : chaque échec récurrent se transforme en règle outillée ou en compétence réutilisable — pas en note de plus.

Trois leçons

  1. Le juge ne peut pas être le sujet. Un verdict doit venir d'un runner, jamais du modèle mesuré.
  2. Sans accès au réel, l'agent invente. Un agent sans outil doit refuser, pas fabriquer.
  3. Un état revendiqué n'est pas un état mesuré. « Tous les tests passent » dans une trace est un document daté, pas une preuve présente. Et ma propre estimation « 13 % » était fausse aussi — la mesure ne fait pas de favoritisme.

Sources

  • Méthode d'audit VFG : chaque constat = une date + une commande rejouable.
  • Corpus : 72 traces, 20 instantanés de mémoire, 9 runs de benchmark, 17 rapports de sonde.
  • Livrables : 3 fiches d'audit (lots conversations / instruments / mémoire) + 1 fiche consolidée.

source markdown