4 citations sur 4 vérifiées. 6 valeurs justes sur 24. Les valeurs existent bien, mais pas à la bonne place !
Contexte
Je suis en train de construire un petit package PHP pour analyser mes factures de climatisation. Les prix augmentent, mais difficile de savoir de « combien » précisément. Exemple, avec les chiffres d'une de mes vraies factures :
// (prix actuel - prix contrat) / prix contrat
((0,07405888 - 0,06178300) / 0,06178300) × 100 = 19,87 % -> +20 % sur le kWh
Voici le flux du script de démo :
L'idée : fournir au LLM, ici un modèle local (gemma4:e2b) qui tourne avec Ollama sur mon Mac, trois ingrédients :
- un prompt ;
- l'extraction linéarisée d'une facture PDF ;
- le schéma JSON, passé via le paramètre
format, pour obtenir une réponse toujours structurée de la même façon.
Je demande aussi au modèle, pour chaque ligne, une citation : la ligne de la facture d'où il a tiré ses valeurs, copiée mot pour mot.
{ "label": "ACS", "base": "18,34", "citation": "ACS 2.475,00 0,00740931 €/L 21,00 18,34 € 3,85 € 22,19 €" }
Le code vérifie ensuite que la citation existe dans le texte d'origine de la facture, puis que chaque valeur figure dans la citation.
Tout est vert… puis plus rien
Avec une première version du prompt, les valeurs sont presque toutes justes, mais les citations ne servent à rien : le modèle n'y met que le libellé (« ACS ») au lieu de la ligne complète.
Je modifie donc mon prompt pour préciser ce qu'est une citation :
La citation est la ligne complète du tableau, telle qu'imprimée, et l'unité est celle du prix (€/L), pas celle de la quantité.
Et hourra : 24/24 pour les valeurs (6 champs × 4 lignes) et 4/4 pour les citations. C'est plié, terminé, bonsoir !
Mais ensuite, je me plante dans la normalisation des espaces : j'envoie au LLM le texte extrait normalisé. Et là, c'est la catastrophe : mon score de valeurs tombe à 6/24, alors que les citations sont toujours correctes, 4/4.
Toutes les valeurs sont présentes, mais pas dans les bons champs. Exemple sur la ligne ACS :
| Champ | Valeur attendue | Valeur extraite |
|---|---|---|
quantity |
2.475,00 | 2.475,00 ✓ |
price |
0,00740931 | 0,00740931 ✓ |
vat_percent |
21,00 | 0.00000000 ✗ |
base |
18,34 | 21,00 ✗ |
taxes |
3,85 | 18,34 ✗ |
subtotal |
22,19 | 3,85 ✗ |
Chaque montant a glissé d'une colonne.
Ce que la citation ne voit pas
Le schéma garantit une réponse structurée. La citation garantit que les valeurs extraites existent bien dans la source. Ni le schéma, ni la citation ne garantissent que le LLM range chaque valeur dans la bonne case.
Pourquoi les colonnes ont glissé
La normalisation a supprimé les tabulations qui permettaient au LLM d'identifier les colonnes de la facture.
Ce que pdfparser produisait (texte brut) :
Concepto → Cant. → Tarifa → %IVA → Base → Impuestos → Subtotal
ACS → 2.475,00 → 0,00740931 €/L → 21,00 → 18,34 € → 3,85 € → 22,19 €
Les flèches représentent une tabulation. 7 colonnes d'en-tête, 7 cellules de données. Une tabulation marque toujours le passage à la cellule suivante. Les espaces, eux, n'apparaissent qu'à l'intérieur d'une cellule (0,00740931 €/L, 18,34 €). Le modèle n'a qu'à compter les tabulations.
Après ma normalisation (tout devient espace) :
Concepto Cant. Tarifa %IVA Base Impuestos Subtotal
ACS 2.475,00 0,00740931 €/L 21,00 18,34 € 3,85 € 22,19 €
La ligne de données passe de 7 à 11 éléments : le modèle n'a plus de moyen fiable de deviner où finit une cellule, et il a probablement mal compté.
4 citations sur 4 vérifiées. 6 valeurs justes sur 24. Les valeurs existent bien, mais pas à la bonne place !
Et alors ?
Je l'ai détecté en local parce que je confronte les valeurs extraites aux valeurs attendues, avec un fichier que je connais à l'avance. En production, ce fichier n'existe pas.
Et en production, la même chose arrive sans erreur de notre part : quand un fournisseur change la mise en page de ses factures, quand on passe par un OCR, ou quand on change d'extracteur PDF. L'entrée change, et la structure disparaît silencieusement. Si je m'appuyais uniquement sur les citations, la facture serait passée.
Un contrôle qui, lui, l'aurait vu
Une solution possible, dans mon cas, est d'ajouter un point de contrôle déterministe : un calcul croisé (cross-checking) qui vérifie que les calculs sont corrects.
En reprenant la ligne ACS :
quantité × prix = 2 475 × 0,00740931 = 18,34, alors que le modèle a mis 21,00 dans base. Le calcul échoue, et l'extraction est rejetée.
Chaque contrôle voit une chose, et en laisse passer une autre :
| Contrôle | Ce qu'il prouve | Ce qu'il laisse passer |
|---|---|---|
| Schéma JSON | Les champs existent, au bon type | Une valeur dans le mauvais champ |
| Citation | La valeur existe dans le document, sur la bonne ligne | Une valeur dans la mauvaise colonne |
| Calcul croisé | Les valeurs sont cohérentes entre elles | Une erreur qui garde les calculs justes (rare, mais possible) |
Ce que je change dans mon pipeline
- Je ne touche plus à l'entrée du modèle. Le texte envoyé au LLM reste brut, tabulations comprises. La normalisation sert uniquement à comparer, et elle s'applique à des copies.
- Je ne considère plus une citation comme une preuve suffisante. Elle prouve qu'une valeur existe dans le document. Pour savoir si elle est dans la bonne case, j'ajoute un contrôle qui relie les valeurs entre elles : quantité × prix = base, et somme des lignes = total.
- Je mesure avant de croire. Chaque changement (prompt, modèle, extracteur PDF) repasse sur des factures dont je connais les vraies valeurs. Sans ce score, j'aurais vu 4/4 citations vérifiées, et je serais passé à la suite.
Et vous, comment vérifiez-vous que vos extractions LLM rangent les bonnes valeurs dans les bonnes cases ?

Top comments (0)