Étude de cas : fiabiliser le matching par l'éval
Le point de départ : optimiser à l'aveugle
La leçon précédente se terminait sur un manque : des golden sets qui dormaient dans samples/ sans harness pour les rejouer. Améliorer le matcher ressemblait alors à ça : modifier le scoring, tester trois documents à la main, « ça a l'air mieux », merger. Personne ne pouvait dire si un changement améliorait globalement ou déplaçait le problème. Cette étude de cas raconte comment la boucle a été fermée — en une dizaine de jours et une série de MRs — et ce qu'elle a rapporté, chiffres à l'appui.
Étape 1 — L'outil AVANT les fixes : le harness d'éval
Première décision structurante : ne toucher à aucun algorithme avant d'avoir un banc de mesure. Le harness, isolé sous eval/ (« le supprimer ne casse rien ») :
- catalogue client réel chargé par le vrai chemin d'import, dans un tenant jetable dédié à l'éval ;
- runner in-process : chaque cas rejoué dans le vrai graphe d'orchestration (
create_orchestrator_graph().ainvoke()) — on mesure la prod, pas une maquette simplifiée ; - scorecard :
line_f1,precision,recall,match_top1,hallucination_rate; - UI de revue des golds (petit serveur FastAPI + SPA) pour relire et corriger la vérité terrain à la main ;
- push Langfuse : chaque run devient un dataset run comparable aux précédents dans l'UI (module 7 !) ;
- ergonomie 3 commandes :
just eval-setup/just eval-ui/just eval— un outil que personne n'utilise n'améliore rien.
Étape 2 — Le juge : un corpus de cas durs, un piège par cas
Neuf documents réels ajoutés d'un coup (~184 lignes gold), chacun ciblant UN mode de défaillance étiqueté (case_focus) : le formulaire de 200 références dont 6 seulement commandées (anti-hallucination), les quantités en toutes lettres (« 1 palette », « reliquat »), la couche texte fragmentée (« CO M M A N D E »), les lignes cachées dans un commentaire libre, le bon de commande manuscrit scanné, la photo de téléphone…
Et le geste le plus pro de toute l'histoire : 22 golds de la première version ont été corrigés « par preuve catalogue » — l'équipe a évalué son propre juge avant de s'en servir. Un gold faux est pire que pas de gold : il punit les bons fixes.
Étape 3 — La boucle : 1 fix = 1 branche = son delta dans la MR
La règle de l'epic qui pilote le chantier : chaque MR affiche son score avant/après sur le corpus. La revue de code devient une revue de résultats. Florilège des fixes, chacun né d'un cas d'éval :
- Doctrine quantités — le principe le plus généralisable : « l'extraction TRANSCRIT, un filtre déterministe DÉCIDE ». Fini le
qty=1par défaut qui transformait un formulaire de 161 références en devis de 161 lignes : le LLM transcrit tout (quantity=nullsi rien d'écrit), un filtre décide « commandé = quantité écrite » ; - Réparer l'image avant le modèle — un scan à l'envers mettait TOUS les moteurs OCR à 0.00. Verdict : « c'est un bug d'image, pas d'IA » → transpose EXIF + retry à 180° (Pillow pour les images, pypdf pour les PDF), en gardant le meilleur des deux essais ;
- Épingler les modèles — l'alias
ocr-latestfaisait bouger les scores sans aucun déploiement (le fournisseur change le modèle sous l'alias). Pin de version : la baseline redevient stable — on ne mesure rien sur un sol qui bouge ; - Le matcher « réf d'abord » — l'éval a prouvé que des références présentes au catalogue perdaient : le hit exact n'était qu'un candidat parmi d'autres, dilué par le scoring nom-centré. Redesign en étages : forme canonique (majuscules, alphanumérique seul), l'exact sur les colonnes d'identifiants devient décisif, le fuzzy est borné, la corroboration départage — match@1 isolé : 0.86 → 0.89 ;
- Benchmark à contrat égal — une commande dédiée compare 4 modèles de vision avec le prompt et le schéma exacts de la prod : le spécialiste OCR (0.88) y bat les généralistes (0.79-0.83), et le meilleur généraliste est identifié comme fallback. Sans contrat égal, un benchmark ne compare que des prompts.
Les résultats agrégés du chantier
- hallucinations de lignes : 0.16 → 0.10
- justesse des quantités : 0.78 → 0.83
- document scanné à l'envers : 0 → 119 lignes extraites
- match@1 bout-en-bout : 0.57 → 0.68 ; match@1 isolé : 0.86 → 0.89
Et une découverte majeure au passage, promue en ticket : le cache d'apprentissage empoisonné (leçon précédente : il écrit dès l'extraction, pas à la validation) — un premier import raté « apprend » ses erreurs et les re-sert. L'éval n'améliore pas que les scores : elle révèle les défauts d'architecture.
Épilogue (une semaine plus tard) : le premier correctif est mergé — un trust gate sur le cache : seuls les matchs confiants sont mémorisés et re-servis (fini d'apprendre les erreurs d'un import raté). La réforme complète (write-on-validation : n'apprendre que ce qu'un humain a validé, + invalidation) reste spécifiée pour la suite. Deux autres retombées directes de l'éval ont suivi le même chemin : le recall des références exactes (les colonnes ean/mpn/source_external_id ignorées par la récupération de candidats) et le « Relancer l'analyse » scopé à la pièce jointe rejouée. La boucle mesure → découverte → fix → re-mesure tourne en régime de croisière.
Les six principes (à voler pour n'importe quel système IA)
- L'outil de mesure avant les fixes — pas de baseline, pas d'optimisation ;
- Des cas durs réels, un mode de défaillance par cas — le corpus est un cahier des charges exécutable ;
- Le juge s'audite aussi — corriger les golds par preuve avant de s'en servir ;
- 1 fix = 1 branche = son delta chiffré — la revue de code devient revue de résultats ;
- Le déterministe avant le modèle — rotation, quantités, références : répare en code ce qui se répare en code, et épingle les versions de modèles ;
- Benchmark à contrat égal, chiffres publiés — même prompt/schéma pour tous les candidats, et chaque avancée annoncée avec son score.
Pourquoi construire le harness AVANT de toucher aux algorithmes ?