No. 01 · Étude de cas

Verity

Un agent qui répond aux questions sur des contrats en citant la clause mot pour mot, et qui refuse quand il n’a pas de preuve.

Code : matteogallo-ai/verity

En bref

Ce que ça fait
Il répond aux questions sur un ensemble de contrats en citant la clause exacte, et dit « je ne sais pas » quand les documents ne contiennent pas la réponse.
Ce qui est mesuré
Sur des questions de test jamais vues pendant le réglage : s’il refuse les questions sans réponse, et si ses réponses inventent quelque chose.
Défaut principal
Il a répondu à des questions qui n’avaient pas de réponse dans les documents. Ce que j’en ai fait : je l’ai publié, avec les trois questions en cause nommées dans les résultats.

Le problème

En trois phrases, pourquoi ce projet existe.

Les assistants qui répondent sur des documents d’entreprise le font avec assurance, et se trompent juste assez souvent pour être dangereux. Sur un contrat, une réponse fluide mais fausse coûte plus cher qu’un « je ne sais pas ». Verity ne répond qu’en citant la clause mot pour mot, refuse sinon, et mesure s’il refuse à raison.

Ce que j’ai construit

Le chemin d’une question, de la recherche des passages à la réponse ou au refus : on y voit où se décide un refus.

Le chemin d’une question dans VerityQuestion, sur un corpus de contrats et de politiques. Puis : Recherche des passages, par le sens et par les mots,, puis tri des passages les plus utiles. Puis : Rédaction avec citations, chaque citation doit être un extrait exact, du document ; sans citation, refus. Puis : Contrôles en code, sans modèle, citations : couvrent-elles la réponse,, sont-elles pertinentes ? manipulation ?, un seul échec refuse. Puis : Note de confiance et seuil, un modèle note la réponse et peut refuser ;, il ne peut pas annuler un contrôle. Issues : Réponse citée, clause et position ; Refus motivé, cause enregistrée.Questionsur un corpus de contrats et de politiquesRecherche des passagespar le sens et par les mots,puis tri des passages les plus utilesRédaction avec citationschaque citation doit être un extrait exactdu document ; sans citation, refusContrôles en code, sans modèlecitations : couvrent-elles la réponse,sont-elles pertinentes ? manipulation ?un seul échec refuseNote de confiance et seuilun modèle note la réponse et peut refuser ;il ne peut pas annuler un contrôleRéponse citéeclause et positionRefus motivécause enregistrée
Le refus se décide dans l’ordre : pas de citation, puis les contrôles écrits en code, puis la note de confiance et son seuil. Le juge, un second modèle qui note les réponses, n’est pas sur ce chemin : il évalue les exécutions après coup.

Verity est un agent RAG : il cherche d’abord les passages utiles dans les documents, puis rédige sa réponse à partir d’eux. Il garde la position exacte de chaque passage dans son document et renvoie soit une réponse dont chaque citation se retrouve telle quelle dans la source, soit un refus avec sa cause. La démonstration de l’accueil rejoue des réponses d’une exécution réelle.

Mesures

Ce que valent ses réponses et ses refus, chiffres à l’appui, avec leur incertitude et une comparaison à un système plus simple.

Les chiffres viennent de l’exécution de référence, sur des questions de test jamais vues pendant le réglage. Sur les questions dont les documents ne contiennent pas la réponse, Verity en refuse 17/20Preuve : verity/scorecards/live-v1.1.0.json, mais répond quand même à 3/20Preuve : verity/scorecards/live-v1.1.0.json. Le juge, un second modèle chargé de noter les réponses, trouve une invention dans 0/31Preuve : verity/scorecards/live-v1.1.0.json réponses. Ce juge s’accorde avec mes propres notes, posées sans voir les siennes, à κ 0,71Preuve : verity/datasets/eval/judge_validation/agreement.json : c’est un kappa de Cohen, une mesure d’accord qui retire la part due au hasard, de zéro pour le hasard à un pour un accord parfait. Ce chiffre mêle des affirmations réelles et des affirmations piégées exprès ; sur les seules réelles, il tombe à κ 0,48Preuve : verity/datasets/eval/judge_validation/agreement.json, un chiffre fragile.

Comparé à un RAG naïf, le même principe sans aucun contrôle, sur des questions de réglage et sur des exécutions que le budget a arrêtées avant la fin, Verity ne fait pas mieux de façon mesurable : il traite 16/21Preuve : verity/scorecards/naive-baseline-comparison.json questions qui ont une réponse, contre 21/21Preuve : verity/scorecards/naive-baseline-comparison.json, et sur si peu de questions l’écart ne se distingue pas du hasard. C’est le résultat publié, pas le résultat favorable.

Détail des mesures

Une proportion porte son intervalle de confiance à 95 % (IC) : la plage où se trouve probablement la vraie valeur, large quand l’échantillon est petit.

Seuil de refus de l’exécution : 0,00Preuve : verity/scorecards/live-v1.1.0.json.

Exécution de référence, questions de test

MesureValeurIC 95 %Lecture
Rappel de refus17/20Preuve : verity/scorecards/live-v1.1.0.json63 % – 95 %Questions sans réponse dans les documents, refusées.
Précision de refus17/22Preuve : verity/scorecards/live-v1.1.0.json56 % – 90 %Refus qui portaient sur une question vraiment sans réponse ; les autres viennent des contrôles de citation.
Questions sans réponse auxquelles il a répondu3/20Preuve : verity/scorecards/live-v1.1.0.json5 % – 37 %Le défaut principal : une réponse livrée là où les documents n’en contiennent pas.
Taux d'hallucination (juge), questions avec réponse auxquelles il a répondu0/31Preuve : verity/scorecards/live-v1.1.0.json0 % – 12 %Selon le juge vérifié ci-dessous. Zéro observé n’est pas zéro garanti : d’où l’intervalle.
Accord juge–humain à l'aveugle, kappa de Cohen (60 affirmations)κ 0,71Preuve : verity/datasets/eval/judge_validation/agreement.jsonKappa n’est pas une proportion : le fichier source ne lui donne pas d’intervalle.Accord du juge avec mes notes posées à l’aveugle, affirmations réelles et piégées.
Accord juge–humain à l'aveugle, kappa de Cohen (40 affirmations réelles)κ 0,48Preuve : verity/datasets/eval/judge_validation/agreement.jsonKappa n’est pas une proportion : le fichier source ne lui donne pas d’intervalle.Fragile : presque aucune affirmation réelle n’est jugée non étayée par l’humain, si bien qu’un désaccord le déplace fortement.
Affirmations piégées que le juge trouve non étayées (borne haute)16/20Preuve : verity/datasets/eval/judge_validation/agreement.json58 % – 92 %Borne haute : une affirmation piégée compte comme détectée dès qu’une de ses parties est rejetée.

Contre un RAG naïf (questions de réglage, exécutions incomplètes)

MesureVerityIC 95 %RAG naïfIC 95 %Lecture
Questions avec réponse traitées16/21Preuve : verity/scorecards/naive-baseline-comparison.json54 % – 90 %21/21Preuve : verity/scorecards/naive-baseline-comparison.json84 % – 100 %Non distinguables selon le fichier de comparaison. Verity refuse davantage, sans gain mesuré en échange.
Taux d’invention (juge)0/16Preuve : verity/scorecards/naive-baseline-comparison.json0 % – 20 %0/21Preuve : verity/scorecards/naive-baseline-comparison.json0 % – 16 %Aucune invention mesurée d’un côté comme de l’autre : rien à départager.
Questions sans réponse auxquelles il a répondu0/2Preuve : verity/scorecards/naive-baseline-comparison.json0 % – 66 %2/2Preuve : verity/scorecards/naive-baseline-comparison.json34 % – 100 %L’avantage attendu de Verity, mais sur si peu de questions qu’il ne se distingue pas du hasard.

Le budget a arrêté les deux exécutions avant la fin ; la comparaison porte sur les questions que les deux ont terminées (fichier de comparaison).

Ce qui n’a pas marché, et ce que j’ai fait

Deux défauts que les chiffres seuls ne montrent pas, et ce que j’en ai fait.

Le seuil de confiance ne décide rien

Verity donne une note de confiance à chaque réponse et refuse sous un seuil. Le code refuse par défaut sous 0,55Preuve : verity/src/verity/config.py. L’exécution de référence a tourné à 0,00Preuve : verity/scorecards/live-v1.1.0.json : le seuil a été choisi sur les questions de réglage par une règle fixée à l’avance, et aucun seuil ne faisait mieux qu’un autre.

The threshold decides nothing on this data.

Chaque refus vient d’un contrôle écrit en code ou du modèle de notation lui-même ; aucun n’est dû au seuil. La note de confiance est l’auto-évaluation du modèle, et elle ne correspond pas à sa fréquence réelle d’erreur. Ce que j’ai fait : le publier. Le seuil réel figure à côté de celui du code, et un refus fondé sur une confiance fiable est dans la feuille de route au lieu d’être revendiqué.

Une citation exacte d’un texte mal extrait

Premium Support shortens the Priority 1 response time to thirty (30) minutes, twentyfour [sic, PDF extraction defect] hours a day, seven days a week.

Dans le PDF du bon de commande, « twenty- » finit une ligne et « four » commence la suivante ; l’extraction du texte a recollé les deux sans le trait d’union. Le contrôle « mot pour mot » l’a accepté, puisque la citation recopie fidèlement le passage fautif : il prouve que la citation est dans le texte extrait, pas que le texte extrait est fidèle au document.

Un audit a proposé une règle : refuser toute réponse dont un chiffre n’est pas dans ses citations. Mesurée sur les exécutions publiées avant de l’activer, elle refusait des réponses justes, pour des chiffres calculés, des chiffres repris de la question, des identifiants, et ici une durée écrite en chiffres dans la réponse et en lettres dans la citation fautive. Elle n’est pas activée (mesure).

Trois décisions

Trois choix d’architecture, chacun écrit dans une note de décision (ADR) avec ses raisons et ses conséquences : de quoi juger le raisonnement, pas seulement le résultat.

  1. Le refus est une réponse à part entière, et il est mesuré

    Le jeu d’évaluation contient volontairement des questions sans réponse, et la justesse des refus fait partie des mesures principales. Une contribution qui fait répondre plus souvent en répondant à tort est une régression. La note promettait un refus « calibré », c’est-à-dire une confiance qui correspond à la fréquence réelle d’erreur ; la mesure l’a contredite, et la note le dit en tête.

    ADR 0003

  2. On reproduit le fichier d’exécution, pas la sortie du modèle

    Aucun appel ne règle l’aléa du modèle. Une exécution réelle est enregistrée une fois, avec la version du code, les modèles et le détail de chaque question, et n’est jamais réécrite ; tous les chiffres publiés sont recalculés depuis ce fichier. Deux exécutions de la même version peuvent différer : c’est l’aléa du modèle, rapporté comme tel.

    ADR 0007

  3. Des fichiers de consignes lus localement, sans PromptLang

    Une note antérieure prévoyait de compiler les consignes du modèle avec PromptLang, mon autre projet ; cette intégration n’a jamais été construite. Plutôt que de garder la promesse, j’ai inscrit l’état réel : un petit lecteur de fichiers testé, une erreur si une variable manque, les contenus non fiables balisés comme données. La dépendance aurait servi un récit, pas un besoin.

    ADR 0005

Comment c’est construit

Pour un lecteur technique : la pile, les outils et les modèles de l’exécution de référence.

Python, FastAPI, Postgres avec pgvector (ou un stockage en mémoire), modèles locaux pour comparer le sens des textes et trier les passages, clients Anthropic et OpenAI derrière un routeur de repli, interface Next.js. Outillage : uv, ruff, mypy strict, pytest, GitHub Actions.

Exécution de référence : agent claude-sonnet-4-6, juge claude-opus-5-5, comparaison du sens BAAI/bge-small-en-v1.5. Architecture détaillée : docs/architecture.md.

La suite

Ce qui manque encore, d’après le projet lui-même.

  • Une exécution publiée de la version actuelle : celles de référence précèdent les consignes actuelles du modèle.
  • Un refus fondé sur une confiance fiable : l’erreur de confiance mesurée sur le test ne le permet pas.
  • Un juge d’une autre famille de modèles, et un second annotateur humain.
  • La lecture de pages web : le code existe, aucun test ne l’exerce.

Feuille de route · ce que les chiffres ne disent pas