Évaluation, coût et exploitation
Le 15 octobre, un premier passage sur Vertex AI a rendu une analyse irréprochable en apparence : 204 champs exacts, contrôles verts, mémo vérifié. Il lui manquait quatre signaux d'alerte sur treize (rappel de 69 %), écartés parce que le modèle avait résumé au lieu de citer. Le 16 octobre, même modèle, même corpus, après correction : trois citations sur six rejetées à la première tentative pour Maison Ferlac, toutes rattrapées par la relecture. Sans évaluation, personne n'aurait vu la première panne, ni su que la parade sert réellement. Un système qui sert une décision d'investissement se mesure à chaque changement de modèle, de consigne ou de format de document, et la mesure doit dire ce qu'elle ne couvre pas.
Le mécanisme : quatre mesures, quatre questions
flowchart LR
A["analyse.json"] --> F["champs : lu = annoté à 0,5 k€ près ?"]
N["annotations/*.json"] --> F
A --> R["ratios : calcul sur l'extraction contre calcul sur la vérité"]
N --> R
A --> S["signaux : chaque signal annoté retrouvé ?"]
N --> S
A --> M["mémo : sources et nombres vérifiés ?"]
F & R & S & M --> E{"seuils e2e"}
E -- "tous atteints" --> OK["E2E OK, code 0"]
E -- "un seul manqué" --> KO["E2E ÉCHEC, code 1"]
| Mesure | Question posée | Ce qu'elle ne voit pas |
|---|---|---|
| Exactitude des champs (204 par passage) | le modèle a-t-il bien recopié chaque montant ? | un champ absent du schéma (une provision citée seulement en note) |
| Écart des ratios | une erreur de lecture se propage-t-elle aux ratios ? | une formule de ratio fausse : elle est la même des deux côtés (d'où les tests unitaires) |
| Rappel des signaux | les risques annotés sont-ils retrouvés ? | un signal juste mais non annoté (la précision n'est pas mesurée) |
| Mémo vérifié | chaque nombre et chaque source existent-ils ? | un lien de cause inventé (leçon 5) |
L'écart des ratios mérite une explication. evaluation.py recalcule les 16 ratios à partir des annotations, avec la même fonction calculer_ratios que le pipeline, et compare. L'écart ne teste donc pas la formule du ROE : il teste qu'aucune erreur d'extraction n'atteint un ratio. Les formules, elles, sont vérifiées par les tests de test_calculs.py contre des valeurs calculées à la main.
r_ref = calculer_ratios({int(a): e for a, e in ref.items()})
ecart_max = 0.0
for annee, valeurs in analyse["ratios"].items():
for cle, v in valeurs.items():
vr = r_ref.get(int(annee), {}).get(cle)
if v is not None and vr is not None:
ecart_max = max(ecart_max, abs(v - vr) / max(abs(vr), 1e-9))Le seuil d'acceptation de make e2e, dans run.py : exactitude ≥ 98 %, écart des ratios < 1 %, mémo vérifié, aucun état non lu. Le rappel des signaux est affiché mais ne bloque pas. C'est un choix : il dépend de la variance du modèle (la moitié des citations de Ferlac rejetées à la première tentative le 16 octobre, aucune lors d'un passage la veille), et un test qui échoue une fois sur trois sans changement de code apprend à l'équipe à ignorer les échecs.
Vous passez de gemini-2.5-flash à un modèle plus récent. make e2e passe : 204/204 champs, mémo vérifié, rappel 100 %. Pouvez-vous basculer la production ?
Un passage unique ne dit rien de la variance : sur le même corpus, le même modèle a rejeté aucune citation un jour et la moitié de celles de Ferlac le lendemain. Le pire cas sur quelques passages est l'information utile. Annoter cent rapports serait souhaitable à terme, mais n'est pas un prérequis pour comparer deux modèles sur un corpus fixe.
Étendre l'évaluation aux vrais rapports
Le corpus fourni est généré, avec ses annotations, par generation.py. Il couvre des pièges connus (unités en M€, colonnes inversées, page scannée, EBITDA ajusté), pas la diversité des rapports réels. Pour passer à vos documents :
- Annoter peu, mais complètement. Dix rapports représentatifs (secteurs, auditeurs, mises en page), au format de
rapports/annotations/ferlac.json: les 34 champs par exercice, les pages, les signaux avec leur extrait exact. Comptez de l'ordre d'une heure par rapport pour un analyste. - Mesurer l'accord entre annotateurs sur deux ou trois rapports : si deux analystes ne s'accordent pas sur le « résultat d'exploitation » d'un groupe à activités abandonnées, le modèle ne fera pas mieux et l'écart n'est pas une erreur du modèle.
- Garder les échecs. Chaque rapport qui a cassé le pipeline en production rejoint le corpus d'évaluation, avec son annotation. C'est le seul corpus qui grandit dans la bonne direction.
Le coût : mesurer, puis estimer
run.py additionne usage_metadata de chaque événement final. Les jetons de réflexion (thoughts_token_count) sont facturés comme des jetons de sortie et comptés avec eux :
u = ev.usage_metadata
if u and not ev.partial:
jetons["appels"] += 1
jetons["entree"] += u.prompt_token_count or 0
jetons["sortie"] += (u.candidates_token_count or 0) + (u.thoughts_token_count or 0)| Passage (mesuré) | Appels | Entrée | Sortie, réflexion incluse | Coût au tarif public de gemini-2.5-flash |
|---|---|---|---|---|
| Vertex AI, 2026-10-15 | 7 | 22 556 | 25 164 | 0,0068 = 0,07 $ |
| Clé d'API, gemini-3.5-flash, 2026-10-15 | 7 | 19 401 | 30 081 | ordre de grandeur identique |
| Vertex AI, 2026-10-16 (une relecture de citations) | 8 | 24 619 | 24 558 | 0,0074 = 0,07 $ |
La sortie représente plus de 90 % du coût, et la réflexion en constitue la plus grande part. C'est le premier levier : pour l'extracteur, qui recopie des tableaux, un budget de réflexion réduit est à tester (thinking_config), en vérifiant l'exactitude sur cinq passages avant de l'adopter. Le second levier est la page scannée : joindre le PDF entier pour une page multiplie les jetons d'entrée de l'extracteur (leçon 2).
Exploitation
Intégration continue. ci/github-actions.yml lance make test à chaque push (sans clé, 15 tests, environ 2 secondes) et make e2e sur la branche principale seulement, avec la clé en secret et analyse.json conservé comme artefact, y compris en cas d'échec. Le fichier est à copier dans .github/workflows/ de votre dépôt.
Journaux. Chaque étape écrit une ligne par société : extraction et contrôles en échec, signaux cités et rejetés par tentative, mémo vérifié ou réécrit. Deux compteurs suffisent à piloter : citations rejetées à la première tentative, et relectures d'extraction. Une hausse soudaine signale un changement de modèle ou de format de document avant que les utilisateurs ne s'en plaignent.
Clé d'API ou Vertex AI. La clé d'API convient pour un prototype et pour ce corpus fictif. Des rapports de sociétés non cotées sont des données confidentielles : en production, passez par Vertex AI (GOOGLE_GENAI_USE_VERTEXAI=TRUE), dans une région européenne, avec un compte de service dédié. Ce projet ne crée aucune ressource Google Cloud : make cloud-down le confirme au lieu d'échouer.
Résultat obtenu
Évaluation de l'analyse du 2026-10-15 (gemini-3.5-flash, clé d'API, 76,4 s), celle fournie dans exemple/analyse.json :
$ make e2e
Société champs pages contrôles relect. écart ratios signaux citations
Maison Ferlac SAS 68/68 3/3 OK 0 0.00% 6/6 6/6
Helvane Robotique SA 68/68 3/3 OK 0 0.00% 2/2 2/2
Solvéa Énergies SA 68/68 3/3 OK 0 0.00% 5/5 5/5
Exactitude des champs : 204/204 (100.0%) | écart max des ratios : 0.00% | rappel des signaux : 100% | mémo vérifié : oui
Durée 76.4 s, modèle gemini-3.5-flash, 7 appels, 19401 jetons en entrée, 30081 en sortie (réflexion incluse)
E2E OKPassage de contrôle le 2026-10-16 sur Vertex AI (gemini-2.5-flash, europe-west1, PARALLELISME=1) :
$ env -u GOOGLE_API_KEY GOOGLE_GENAI_USE_VERTEXAI=TRUE PARALLELISME=1 make e2e
INFO analyste modèle gemini-2.5-flash, 3 rapports
INFO analyste.agents ferlac : page(s) scannée(s) [6], PDF joint pour lecture visuelle
INFO analyste.agents ferlac : extraction 1, 0 contrôle(s) en échec
INFO analyste.agents ferlac : signaux, tentative 1 : 3 cités, 3 rejetés (citation introuvable)
INFO analyste.agents ferlac : signaux, tentative 2 : 6 cités, 0 rejetés (citation introuvable)
INFO analyste.agents mémo, tentative 1 : 0 problème(s)
Société champs pages contrôles relect. écart ratios signaux citations
Maison Ferlac SAS 68/68 3/3 OK 0 0.00% 6/6 6/6
Helvane Robotique SA 68/68 3/3 OK 0 0.00% 2/2 3/3
Solvéa Énergies SA 68/68 3/3 OK 0 0.00% 5/5 6/6
Exactitude des champs : 204/204 (100.0%) | écart max des ratios : 0.00% | rappel des signaux : 100% | mémo vérifié : oui
Durée 152.0 s, modèle gemini-2.5-flash, 8 appels, 24619 jetons en entrée, 24558 en sortie (réflexion incluse)
E2E OKLa relecture des citations, qui n'avait servi à rien lors du passage précédent (leçon 5), a cette fois rattrapé trois signaux sur six. C'est l'argument pour mesurer sur plusieurs passages : un seul aurait conclu qu'elle était inutile.
Nettoyage, exécuté puis vérifié :
$ make clean
Artefacts supprimés.
$ make reset
Artefacts supprimés.
Environnement supprimé (.env conservé).
$ make cloud-down
Rien à supprimer : ce projet ne crée aucune ressource Google Cloud (ni service, ni bucket, ni index).Le kit de présentation
linkedin/capturer.mjs produit, à partir du tableau de bord lancé par make run, un carrousel de cinq images 1080 x 1350 (capturées depuis carrousel.html) et une vidéo de démonstration de 22 secondes enregistrée par Playwright, convertie en H.264 par ffmpeg (500 ko). Aucune image n'est générée par un modèle : tout ce qui est montré est le logiciel réel, sur l'analyse réelle. Le post ne cite que des chiffres mesurés et rappelle la taille du corpus. L'avertissement « outil pédagogique, pas un conseil en investissement » figure sur la première image et, en entier, sur la dernière : une capture circule sans son contexte.
Ce qui casse en production
- L'évaluation qui ne bouge plus. Symptôme : 100 % à chaque passage pendant six mois, puis des plaintes du comité. Cause : le corpus d'évaluation n'a pas changé, les documents réels si. Parade : chaque incident ajoute un rapport annoté au corpus, et l'exactitude se suit par source de document.
- Le test réel instable en CI. Symptôme :
make e2eéchoue une nuit sur cinq, sur un 429 ou un rappel de signaux à 85 %. L'équipe finit par désactiver le job. Parade : bloquer uniquement sur les mesures stables (champs, ratios, mémo vérifié), suivre les autres en tendance, et réessayer les erreurs de quota au niveau HTTP (leçon 6). - Le coût qui double sans changement de code. Symptôme : facture en hausse, même nombre d'appels. Cause : un nouveau modèle réfléchit davantage par défaut. Parade :
run.pyjournalise les jetons de sortie par passage ; une alerte sur leur médiane le détecte au premier jour. - La capture qui circule seule. Une image du tableau de bord partagée sans contexte devient « l'avis de l'outil sur Solvéa ». Parade : l'avertissement est dans l'en-tête de la page et dans chaque image du kit, pas seulement dans le texte qui l'accompagne.
Règle de décision
Bloquez la livraison sur ce qui est stable et vérifiable (exactitude des champs, écart des ratios, mémo vérifié) ; suivez en tendance ce qui varie (rappel des signaux, durée, jetons). Comparez deux modèles sur au moins trois passages chacun, en regardant le pire cas. N'étendez pas l'usage à des rapports réels avant d'en avoir annoté une dizaine : sans annotation, une exactitude de 100 % ne mesure que le corpus fictif.
Pratique
Le comité veut un indicateur unique « fiabilité de l'analyse » affiché sur le tableau de bord. Que proposez-vous ?
Les mesures d'évaluation portent sur le corpus annoté, pas sur le rapport que le comité regarde : il n'y a pas d'annotation pour un nouveau document. Ce qui est connu pour cette analyse, ce sont ses propres contrôles. Une moyenne mélangerait des grandeurs sans rapport et donnerait une fausse précision.
Après une mise à jour de la consigne de l'extracteur, l'exactitude passe de 204/204 à 202/204 sur un passage. Les deux erreurs portent sur Solvéa, colonnes 2024 et 2025 inversées. Que faites-vous ?
202/204 passe le seuil, mais le seuil n'est pas l'objectif : une inversion de colonnes est exactement le piège du rapport Solvéa. Relancer une fois ne distingue pas la variance d'une régression. Comparer les deux consignes sur plusieurs passages le fait, et un contrôle déterministe ferme le sujet quelle que soit la cause.
Exercice. Ajoutez à run.py une commande repeter N qui lance N analyses (dans sorties/passage-<i>.json), évalue chacune et affiche, pour l'exactitude des champs et le rappel des signaux, le minimum, la moyenne et le maximum.
Voir la solution
def repeter(n: int) -> None:
from statistics import mean
mesures = []
for i in range(1, n + 1):
f = ICI / "sorties" / f"passage-{i}.json"
asyncio.run(analyser(f))
mesures.append(evaluer(f))
for cle in ("exactitude_globale", "rappel_signaux"):
v = [m[cle] for m in mesures]
print(f"{cle} : min {min(v):.1%}, moyenne {mean(v):.1%}, max {max(v):.1%} sur {n} passages")Ajoutez "repeter" aux choices de l'argument commande, un argument -n (type=int, default=3), et la branche elif a.commande == "repeter": repeter(a.n). Avec trois passages, comptez environ trois fois le coût d'un make e2e, soit de l'ordre de 0,20 $. Lancez les passages l'un après l'autre : en parallèle, ils partagent le même quota et les 429 fausseraient la durée mesurée.
- Je sais ce que mesure chacune des quatre mesures, et ce qu'elle ne voit pas
- Je sais choisir ce qui bloque
make e2eet ce qui se suit en tendance - Je sais mesurer le coût réel d'une analyse à partir de
usage_metadata - Je sais étendre le corpus d'évaluation à des rapports réels
-
make e2eaffiche « E2E OK », puismake cleanetmake cloud-downne laissent rien