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.
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.
17/20Preuve : verity/scorecards/live-v1.1.0.json
questions sans réponse correctement refusées
0/31Preuve : verity/scorecards/live-v1.1.0.json
réponses contenant une invention, selon un juge vérifié contre mes propres notes
3/20Preuve : verity/scorecards/live-v1.1.0.json
questions sans réponse auxquelles il a quand même répondu
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.
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
| Mesure | Valeur | IC 95 % | Lecture |
|---|---|---|---|
| Rappel de refus | 17/20Preuve : verity/scorecards/live-v1.1.0.json | 63 % – 95 % | Questions sans réponse dans les documents, refusées. |
| Précision de refus | 17/22Preuve : verity/scorecards/live-v1.1.0.json | 56 % – 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épondu | 3/20Preuve : verity/scorecards/live-v1.1.0.json | 5 % – 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épondu | 0/31Preuve : verity/scorecards/live-v1.1.0.json | 0 % – 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.json | Kappa 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.json | Kappa 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.json | 58 % – 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)
| Mesure | Verity | IC 95 % | RAG naïf | IC 95 % | Lecture |
|---|---|---|---|---|---|
| Questions avec réponse traitées | 16/21Preuve : verity/scorecards/naive-baseline-comparison.json | 54 % – 90 % | 21/21Preuve : verity/scorecards/naive-baseline-comparison.json | 84 % – 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.json | 0 % – 20 % | 0/21Preuve : verity/scorecards/naive-baseline-comparison.json | 0 % – 16 % | Aucune invention mesurée d’un côté comme de l’autre : rien à départager. |
| Questions sans réponse auxquelles il a répondu | 0/2Preuve : verity/scorecards/naive-baseline-comparison.json | 0 % – 66 % | 2/2Preuve : verity/scorecards/naive-baseline-comparison.json | 34 % – 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.
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.
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.
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.
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.