
La plupart des hallucinations RAG sont des erreurs d’extraction : sept modèles pour un contrat de génération typé
la même forme de réponse. L’un veut un numéro unique avec une citation, un autre une liste avec un élément par ligne, un troisième un oui ou un non avec une mise en garde. Forcez-les tous à passer un appel de génération et chaque forme dégrade les autres. Le correctif est un petit ensemble de modèles de génération, un par forme de réponse.
Cet article accompagne Intelligence documentaire d’entreprisela série dont la philosophie est exposée dans Amplify the Expert. Il zoome sur brique 4 (génération) de l’architecture à quatre briques et repousse le vocabulaire utilisé par les équipes lorsqu’une réponse RAG est fausse.
Hallucinationau sens strict, est une fabrication à partir de la mémoire paramétrique du modèle : le LLM invente un fait sans aucun fondement dans l’entrée. Dans RAG, le modèle lit le contexte ; lorsque la réponse est fausse, la cause est en amont, dans la chaîne d’extraction (analyse, question, récupération, ou le contrat de génération lui-même). Appeler chaque échec un hallucination clôt le débat sur la question de savoir où agir. Nommer la vraie cause l’ouvre à nouveau.
L’histoire dominante a une génération comme envoyer les morceaux récupérés + la question au LLM, récupérer une chaîne de réponse. Nous rejetons tout ce cadre. La réponse n’est pas une chaîne. Il s’agit d’un objet Pydantic typé avec des citations, des indicateurs de fidélité et des champs d’auto-évaluation. Le LLM est une fonction, pas un oracle. Le schéma est le contrat. Le validateur s’exécute avant que l’utilisateur ne voie quoi que ce soit.

📓 Reproduire les sept motifs du carnet expédié: remplissez le formulaire tapé AnswerWithEvidence schéma sur une question de corpus de courtier, regardez chaque drapeau d’auto-évaluation se retourner, puis réexécutez avec le schéma décomposé pour voir comment un petit modèle rattrape un modèle frontière. Dépôt → doc-intel/notebooks-vol1.

La base naïve sur laquelle cet article repousse

Le pipeline naïf demande une réponse au LLM et fait confiance à ce qui revient. Il n’y a pas de séparation entre trouvé et inventépas d’endroit pour enregistrer confianceet aucun moyen de signaler pas trouvé comme un résultat de première classe. Nous remplissons le schéma à la place : chaque champ est une question posée par le pipeline, et chaque réponse est vérifiable.

Vous trouverez ci-dessous les sept modèles qui maintiennent la génération du côté des contrats dactylographiés. Les six premiers constituent le contrat lui-même. Le septième cite une contrainte que le contrat doit respecter lorsque le modèle est petit.
Modèle 1 – Le LLM est une fonction, pas un oracle
Le schéma de réponse est un contrat : une récupération structurée à l’entrée, un objet Pydantic typé à la sortie, avec des citations, des indicateurs de fidélité et des champs de retour de pipeline, jamais « la réponse sous forme de chaîne ». Tout ce que fait la brique de génération sert à remplir ce contrat, champ par domaine.
Prendre « quel est le montant de la prime ? ». La ligne de base naïve renvoie une phrase : "The premium is $124 per month, per the contract.". Pour l’utiliser en aval, vous devez analyser le numéro à partir de la prose, et rien ne vous dit d’où il vient ni dans quelle mesure le modèle était sûr. La série renvoie une ligne à la place : Amount(value=124.0, currency="USD", unit="month", evidence=[Span(line_start=42, line_end=42, quote="premium of $124 / month")], answer_found=True, confidence=0.95). La valeur est déjà saisie, les preuves pointent vers la ligne exacte et les champs d’auto-évaluation l’accompagnent. L’appelant lit des valeurs structurées, pas du texte qu’il doit relire.
→ L’article 8A (le contrat de réponse) développe le contrat dactylographié dans son intégralité.
Modèle 2 – Extraire les valeurs saisies, ne jamais calculer
Demander Amount(value, currency, unit) et laissez Python comparer avec un taux de change visible ; le calcul à l’intérieur du modèle efface la piste d’audit et effectue silencieusement des conversions qu’aucun humain ne peut rejouer. Le LLM extrait, Python calcule.
Prendre « la prime est-elle supérieure à 100 euros par mois ?. Demandez directement au LLM et il répond "yes, the premium is around 130 EUR / month"après avoir converti 124 $ en euros avec un taux qu’il ne vous montre jamais. Vous ne pouvez pas vérifier le calcul et le mois prochain, la même question pourrait être convertie à un taux invisible différent. La série extrait à la place la valeur brute, Amount(value=124, currency="USD", unit="month")et laisse Python faire la comparaison avec un taux qui figure dans le journal d’audit, 1 USD = 0.92 EUR. Cela donne 114 euros, donc la réponse est Trueet chaque étape, l’extraction, le débit et la comparaison, peut être rejouée et vérifiée à la main.
→ L’article 8A (le contrat de réponse) couvre les valeurs saisies et la discipline sans calcul.
Modèle 3 – Exhaustivité à partir de la structure, pas d’auto-évaluation
Le LLM à l’intérieur de la portée de récupération ne peut pas voir si la page suivante continue une liste ; la récupération extrait une page supplémentaire et le pipeline vérifie une limite de section, donc l’exhaustivité est déterministe et ancrée dans la structure. Un modèle qui ne peut voir que ce qui lui a été envoyé ne peut pas juger de ce qui manque.
Prendre « lister toutes les exclusions ». La section sur les exclusions commence à la page 7 et la section suivante, « Limites »commence à la page 10, donc la réponse se trouve aux pages 7 à 9. Naive RAG remet ces pages au LLM et demande « la liste est-elle complète ? ». Le modèle ne peut voir que ce qui lui a été donné, il lit donc les pages 7 à 9 et dit oui, sans aucun moyen de savoir si une dixième exclusion a déjà été récupérée. La série vérifie plutôt la structure au moment de la récupération : la page suivante appartient-elle toujours à la section des exclusions ? Si c’est le cas, récupérez-le ; une fois qu’il traverse « Limites »arrêt. L’exhaustivité devient un fait indiquant où se termine la section, et non une supposition que le modèle fait sur le texte qu’il ne peut pas voir.
Modèle 4 – Deux booléens, pas un flotteur de confiance
Hors corpus, partiel et complet, chaque itinéraire mène à une action suivante différente, et deux drapeaux forcent des décisions plus claires qu’une seule échelle. Le cadre de confiance flottant regroupe trois résultats différents en un seul chiffre.
Prendre « lister toutes les exclusions » encore une fois, mais cette fois, la récupération a trouvé la page 7 et a manqué la page 8. Un seul confidence = 0.6 ne dit presque rien à l’orchestrateur : il ne peut pas dire si deux des trois exclusions sont revenues ou si la réponse entière a été inventée. La série divise cela en deux booléens. answer_found = True dit que la question peut être répondue à partir de ce document, et complete_answer_found = False dit que la liste qui est revenue est partielle. Ces deux drapeaux correspondent à trois prochains mouvements clairs : continuer à récupérer lorsque la réponse est trouvée mais incomplète, expédier lorsqu’elle est trouvée et terminée, et refuser lorsqu’elle n’est pas trouvée du tout. Un seul flotteur regroupe ces trois résultats en un seul nombre et fait perdre la décision.
→ L’article 8A (le contrat de réponse) explique la division en deux booléens.
Modèle 5 – Une invite par forme, envoyée au moment de l’exécution
Un répartiteur compose BASE + un fragment de forme + des contraintes facultatives et enregistre les fragments appliqués, de sorte qu’un mauvais format six mois plus tard soit traçable, contrairement à une méga-invite qui génère des clauses non commentées. Une seule invite à laquelle tout le monde continue d’ajouter est une odeur courante de base de code RAG ; le composer à partir de fragments nommés est ce qui permet de reconstituer l’invite de chaque réponse.
Prendre « date d’entrée en vigueur ? »dont la forme de réponse est Date. Une ligne de base naïve exécute une invite générique qui demande toujours « une réponse »quelle que soit la question. La série compose l’invite à partir de pièces : l’invite BASE, plus un Date fragment de forme qui dit "return YYYY-MM-DD"plus le DateAnswer schéma. Le journal d’audit enregistre exactement quelles pièces ont été utilisées, prompt_id=BASE@v3 + shape=date_v2. Alors quand le modèle revient « 1er juillet 2026 » six mois plus tard et que quelqu’un doit expliquer le mauvais format, l’équipe peut reconstruire cette invite exacte à partir du journal et reproduire le bug, au lieu de deviner quelle clause d’une méga-invite de mille lignes a été déclenchée.
→ L’article 8B (assemblage rapide) conçoit le répartiteur rapide.
Modèle 6 – Aucun modèle de raisonnement sur l’extraction JSON
Le schéma contraint déjà le travail, donc la « réflexion » supplémentaire ajoute de la latence et s’éloigne du schéma sans ajouter de précision. Les modèles de raisonnement gagnent leur place sur des tâches ouvertes ; lors d’une extraction contrainte par un schéma, ils sont sur-conçus.
Prendre « quel est le montant déductible ? ». Une équipe espérant une extraction plus propre passe à un modèle de raisonnement. Il réfléchit pendant huit secondes et renvoie le même nombre qu’un modèle simple renvoyé en une seule, car le schéma avait déjà épinglé la sortie dans un seul champ saisi. Les jetons de raisonnement coûtent de la latence et de l’argent et n’achètent rien, car il n’y a pas de jugement illimité à porter. La position qui suit est simple : utilisez le plus petit modèle qui remplit le schéma de manière fiable et conservez le raisonnement pour les étapes qui en ont réellement besoin, comme l’arbitre LLM à la fin de la récupération.
Modèle 7 – Décomposer pour les petits modèles, un appel pour les grands
Un modèle frontière peut remplir un schéma composé (extrait + conversion + format) en un seul appel ; un petit modèle ne le peut pas, et le demander invente silencieusement les champs dérivés. La taille du modèle définit la granularité du contrat, pas sa forme.
Prendre « la prime est-elle supérieure à 100 euros par mois ? avec un schéma composé, PremiumComparison(raw_amount: Amount, converted_eur: float, exchange_rate: float, over_100_eur: bool). GPT-4.1 remplit correctement chaque champ en un seul appel : il extrait $124/month dans raw_amountse convertit en 114.08 EUR avec un tarif visible, et des retours over_100_eur=True. Remettez le même schéma à llama-3.2-3B et le modèle revient raw_amount peuplé, converted_eur inventé à 117.6 (d’un imaginaire 0.95 évaluer le journal ne porte jamais), et over_100_eur=True dérivé de ce taux inventé. Le petit modèle n’échoue pas à la validation JSON. Il remplit tous les domaines. Trois des quatre sont fabriqués.
La solution consiste à décomposer le contrat en étapes que le petit modèle peut gérer de manière isolée. Premier appel : extrait Amount(value=124, currency="USD", unit="month"). Python prend le relais : recherche de taux (enregistrée, 1 USD = 0.92 EUR), conversion (124 * 0.92 = 114.08 EUR), seuil (114.08 > 100 = True). Deuxième appel facultatif si un résumé en langage naturel est nécessaire : restituez la ligne composée complétée avec une invite au format simple. Même rangée finale, même précision, pas d’intermédiaire fabriqué.
La règle : la taille du modèle détermine le nombre d’appels d’extraction, et non la forme du schéma. Gros modèle = un appel dense. Petit modèle = une chaîne d’extraits typés avec le calcul Python au milieu et un appel de format facultatif à la fin. Le contrat dactylographié survit d’une manière ou d’une autre ; seule la granularité change.
L’article B05 (choix d’un modèle) évalue le compromis de granularité sur une flotte de treize modèles (lien à venir).
Les sept modèles partagent un même objectif : refuser le cadre LLM-as-oracle et traiter la génération comme le remplissage d’un contrat dactylographié. Le schéma est le contrat ; les citations et les champs d’auto-évaluation sont des résultats de premier ordre ; le validateur s’exécute avant que l’utilisateur ne voie quoi que ce soit. Le modèle 7 ajoute la nuance opérationnelle : le même contrat s’applique à toutes les tailles de modèle, mais un petit modèle doit être divisé en étapes que le modèle peut remplir sans inventer de valeurs dérivées. Les plongées profondes (8A, 8B, 8C) fournissent du code exécutable sur des documents réels ; cette pièce est le catalogue qui nomme chaque modèle et pointe vers le code qui l’implémente.
Dans tous les secteurs et métiers
La discipline du contrat typé (schéma Pydantic + citations + champs d’auto-évaluation) est valable dans tous les domaines. Ce qui change par question, c’est la forme du schéma. Le modèle de contrat reste le même. Cinq secteurs ci-dessous, cinq formes de réponse, un validateur effectuant les mêmes contrôles sur chacun d’eux.

Le validateur effectue les mêmes vérifications sur les cinq lignes : les lignes s’étendent dans les limites du document, les citations textuelles correspondent aux lignes citées, les contraintes de format sont respectées (un Duration analyse en fait comme une durée, un Amount porte la devise et l’unité). La ligne légale illustre la division en deux booléens (modèle 4) : la réponse a été trouvée dans ce document mais n’est pas complète sans une référence croisée, donc l’orchestrateur continue la récupération au lieu d’envoyer une réponse partielle. La ligne médicale montre la piste d’audit (modèle 1) : la mise en garde capture une inconnue connue que le LLM n’a pas pu résoudre.
Où ces modèles atterrissent dans la série
Les articles numérotés développent chaque modèle en code, avec des notebooks exécutables :
Sources et lectures complémentaires
La plupart des écrits sur la génération LLM proviennent de chatbots et de textes ouverts. La série suppose quelque chose de différent : la réponse doit être vérifiable et le LLM doit refuser d’inventer.



