Accueil / Articles / Lorsqu’un bot RAG cite le montant de la franchise d’assurance incorrect

Lorsqu’un bot RAG cite le montant de la franchise d’assurance incorrect

Un outil de segmentation à taille fixe répartit un montant en dollars sur plusieurs lignes ; il faut reconstruire l’analyse et passer par quatre niveaux de segmentation — des références récursives aux récupérations dans le document parent — avant de pouvoir faire confiance aux résultats.

2560 mots

Tout ce qui suit constitue un récit composite à des fins pédagogiques. Les noms d’entreprises, les personnes et les montants en dollars sont fictifs ; les modes de défaillance décrits correspondent à ceux que rencontrent réellement les systèmes RAG utilisés dans l’assurance régulée et d’autres domaines à haut risque.

Dans la soirée d’un mardi, un responsable des sinistres a contacté le service ingénierie : l’assistante avait indiqué à un client que son franchise en cas d’inondation était de 500 dollars, alors que le contrat prévoyait 5 000 dollars. Le bot d’augmentation de la récupération d’informations — « Atlas » — était en service depuis trois semaines, équipé d’un stockage vectoriel, d’un modèle d’incorporation solide, d’un grand modèle de langage performant et d’une interface utilisateur soignée. Ce qu’il manquait, c’était une stratégie de segmentation fiable.

Le fragment récupéré ressemblait à ceci :

...applies to all covered perils. Deductible: $5
00 for flood events in Zone A, $500 for wind events, and $1,0

L’extraction du PDF avait séparé un nombre sur plusieurs lignes. Un outil de segmentation à taille fixe a ensuite divisé chaque segment en parties de 500 caractères, en partant de cette séparation. L’outil d’incorporation a indexé des fragments contenant les valeurs « 5 $ » et « 500 $ ». Le modèle a donné une réponse affirmative — mais fausse.

Ce qui suit décrit comment le pipeline d’ingestion a été reconstruit : une étape de parsing qui fixe la limite maximale, suivie de quatre niveaux de segmentation qui s’approchent progressivement de cette limite à un coût croissant.

Une approche structurée permet de rester honnête sur les coûts : presque tous les coûts de segmentation correspondent à des calculs d’indexation effectués une seule fois. Ces calculs se déroulent hors ligne, avant toute demande de l’utilisateur. Le seul coût lié à la segmentation qui apparaît dans le p95 des requêtes est celui du modèle d’embedding lui-même. Le fait que les niveaux supérieurs soient « chers » signifie qu’ils sont coûteux une fois par document, et non par requête.

Étape 0 : On ne peut pas segmenter une structure que l’on a détruite lors du parsing

Le premier réflexe consiste à utiliser un outil de segmentation plus intelligent. La meilleure première question est : afficher le texte à segmenter, et non le PDF. Le texte initial d’Atlas ressemblait à un mur de caractères ; les titres se mélangeaient au corps du texte, et un tableau de déductions s’étalait en une seule phrase où les valeurs des différentes lignes se trouvaient côte à côte. Le texte de présentation de page (“Formulaire d’assurance HO-3 — Page 14 sur 62”) se répétait tous les quelques milliers de caractères.

Aucun outil de segmentation ne peut restaurer des limites déjà détruites par le analyseur. Si les titres ont disparu, un outil de séparation des en-têtes Markdown n’a plus rien sur quoi opérer. Si les tableaux se sont dissous, rien ne permet de conserver l’intégrité des lignes.

Associez chaque type de contenu d’entrée à un analyseur qui génère une structure que l’outil de segmentation pourra utiliser :

PDF et DOCX natifs (formulaires de politique, endossements, directives). Préférer des analyseurs conscients du layout — LlamaParse, Unstructured.io, Docling, Azure Document Intelligence — plutôt que des extractions de texte naïves. Exiger du markdown avec des types d’éléments (titre, tableau, liste), et non simplement une chaîne de caractères.

PDF scannés, images et fax. Utiliser du OCR (Tesseract est le moins fiable ; PaddleOCR, Textract, Document AI, Azure sont plus performants). Demander le texte ainsi que les blocs de layout, accompagnés d’une confiance par bloc. Cette confiance permettra plus tard de signaler que « ce chiffre est incertain — ne pas en déduire les franchises. »

HTML et wikis internes. Supprimer les éléments superflus pour générer du markdown propre avec les titres conservés.

Diapositives et feuilles de calcul. Préserver les limites entre diapositives ou feuilles afin qu’aucun groupe ne mélange des diapositives non liées.

Audio et vidéo (appels liés aux sinistres, webinaires). Whisper, Deepgram ou AssemblyAI doivent générer une transcription indiquant qui a parlé et quand. Si les interlocuteurs ne sont pas séparés, des déclarations contradictoires d’un ajusteur et d’un client peuvent se mélanger en un seul paragraphe trompeur.

Après une re-analyse tenant compte de la mise en page, la section relative aux déductions ressemblait à ceci :

## Section 4 — Deductibles

### 4.2 Peril-specific deductibles

| Peril          | Zone A  | Zone B  |
|----------------|---------|---------|
| Flood          | $5,000  | $2,500  |
| Wind / hail    | $500    | $500    |
| Named storm    | $1,000  | $1,000  |

Le tableau a été restitué, tout comme l’arborescence des en-têtes. « 5 000 $ » était à nouveau considéré comme un seul token. Le code de segmentation n’avait pas encore été modifié, ce qui rendait la récupération des données déjà plus fiable.

Niveau 1 : Segmentation syntaxique — peu coûteuse, rapide, et point de départ idéal

Taille fixe : le prototype qui a été livré par hasard

Diviser tous les N caractères ou tokens (CharacterTextSplitter, tiktoken). Le coût d’indexation est presque nul. Cela convient pour des pics temporaires, mais c’est fatal en environnement de production. Cela coupe au milieu d’une phrase, d’un tableau ou d’un numéro — exactement comme dans les incidents entraînant des déductions. Règle établie après cette nuit-là : les fenêtres de taille fixe ne doivent jamais être utilisées dans un carnet de notes.

Les équipes découvrent sans cesse le même schéma : un corpus de démonstration composé de courts articles de blog survit à la segmentation par caractères, puis l’arrivée du premier PDF multi-colonnes ou d’une attestation numérisée par OCR entraîne une chute brutale de la qualité des réponses. Considérez les fenêtres de taille fixe comme un outil d’aide pour des expériences locales, en veillant à les remplacer avant toute utilisation par des utilisateurs externes.

Superposition : un réglage, pas une stratégie

chunk_overlap fait répéter la fin du chunk N au début de N+1. Une proportion de dix à quinze pour cent est courante (par exemple 50 sur un chunk de 500 tokens). L’overlap constitue une assurance peu coûteuse pour les niveaux 1–2 ; il ne corrige pas une mauvaise frontière, il la duplique simplement. On peut s’attendre à environ 10 % de vecteurs supplémentaires et à davantage de résultats dupliqués dans le top-k lorsque la correspondance se situe dans l’overlap.

Lorsque les doublons dominent la liste des voisins, les outils de réclassification et le LLM gaspillent deux fois la fenêtre de contexte sur la même phrase. La suppression des doublons au moyen d’un identifiant parent ou de hachages de texte presque identiques après la récupération constitue une mesure d’atténuation peu coûteuse si l’overlap doit rester élevé pour d’autres raisons.

Regroupez des phrases entières dans un budget (NLTK, spaCy, LlamaIndex SentenceSplitter). Il ne coupe jamais au milieu d’une phrase — ce qui empêcherait la séparation des numéros par saut de ligne — mais il ne reconnaît pas les titres, les tableaux ou les listes d’exclusion. Très efficace pour les transcriptions de gardes ; médiocre pour les formulaires de politiques structurées.

Caractère récursif : la valeur par défaut à utiliser en premier

RecursiveCharacterTextSplitter teste les séparateurs dans un ordre de priorité — ligne vide, saut de ligne, phrase, mot — et n’utilise d’autres méthodes que si un élément reste trop grand, puis il fusionne les petits éléments jusqu’à atteindre la taille chunk_size. Une valeur de référence courante est de 500 tokens avec 50 % d’overlap :

from langchain_text_splitters import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=50,
    separators=["\n\n", "\n", ". ", " ", ""],
)
chunks = splitter.split_text(policy_markdown)

Dans la section des déductions réanalysée, tous les éléments de 4.2 ont été conservés, y compris le tableau, car des lignes vides formaient naturellement une unité inférieure à 500 tokens. Le bug a disparu. Envoyez recursive-500/50 en tant que base de référence pour évaluation, et non comme le design final.

Écrivez ce score de base sur un tableau blanc et refusez les propositions de outils de segmentation « plus intelligents » qui ne parviennent pas à le surpasser sur les mêmes questions de référence. De nombreuses idées coûteuses semblent brillantes jusqu’à ce qu’on les compare à un outil de segmentation récursif simple utilisant du Markdown propre.

Niveau 2 : Segmentation tenant compte de la structure — couper là où le document trace déjà des séparations

Avec un ensemble de référence composé d’environ 80 questions réelles, recursive-500/50 a atteint environ 71 %. Les échecs se sont concentrés sur de longues sections couvrant plusieurs segments, ainsi que sur des réponses où le modèle ne pouvait pas déterminer quelle politique ou section était à l’origine de la réponse.

Séparation des en-têtes Markdown / HTML

MarkdownHeaderTextSplitter / HTMLHeaderTextSplitter séparent le texte en fonction de la hiérarchie des titres et intègrent le chemin du titre dans les métadonnées :

from langchain_text_splitters import MarkdownHeaderTextSplitter

header_splitter = MarkdownHeaderTextSplitter(
    headers_to_split_on=[("#", "h1"), ("##", "h2"), ("###", "h3")]
)
sections = header_splitter.split_text(policy_markdown)
# then apply the recursive splitter inside each section

Chaque fragment issu de la sous-section 4.2 doit comporter un fil d’Ariane tel que « formulaire HO-3 → franchises → tableau spécifique au risque ». Ce fil d’Ariane doit être concaténéné au texte avant son intégration afin que le vecteur encode à la fois l’emplacement et les chiffres. Les erreurs de type « Quelle section ? » disparaissent ; les citations peuvent indiquer « conformément à la Section 4.2 ». Le coût reste presque nul — mais uniquement si l’Étape 0 a généré un vrai format Markdown. Le texte aplati devient alors silencieusement un seul gros bloc de texte. Cela fonctionne le mieux sur les wikis, les documents techniques et les politiques structurées en titres.

Les champs de métadonnées doivent être transmis avec le vecteur : identifiant du document source, chemin de la section, date d’entrée en vigueur, ainsi que la juridiction si les politiques diffèrent selon les États. Ces champs permettent d’utiliser des filtres (« uniquement HO-3 en vigueur après 2024-01-01 ») que la recherche par similarité pure ne peut pas prendre en compte.

Conscient du layout / par titre

Les outils non structurés chunk_by_title (ainsi que leurs équivalents dans LlamaParse/Docling) respectent les limites des éléments du analyseur : les tableaux restent intacts, les titres démarrent les blocs de texte, et les listes conservent leur phrase d’introduction. Outil idéal pour les contrats et les PDF complexes. Les API d’analyse coûtent de l’argent une seule fois lors de la création de l’index. Un analyseur peu coûteux annule cet avantage — ne achetez pas un volant luxueux pour une voiture sans moteur.

Conscient du code (AST)

Inutile pour les corpus de politiques, essentiel pour Terraform/Python RAG. La séparation par ligne permet de distinguer les signatures des corps de texte. tree-sitter ou d’autres outils de séparation récursive conscients du langage effectuent la coupure aux nœuds de fonction ou de classe. C’est le choix par défaut lorsque le corpus comprend des dépôts GitHub ou du code IaC.

Mélanger des politiques de prose et du code source dans un même index sans utiliser de séparateurs entraîne généralement le pire des deux mondes : des fonctions tronquées au milieu de leur contenu et des tables de politiques déformées par des heuristiques axées sur les accolades.

Niveau 3 : Fragmentation basée sur un modèle — ne payer l’intelligence artificielle que lorsque la structure est absente

Au niveau d’environ 84 % sur l’ensemble de test, une nouvelle catégorie d’échec est apparue : des transcriptions d’appels non structurées sans en-têtes. Des blocs récursifs de 500 tokens couvraient des changements de sujet — demande d’indemnisation puis adresse postale — si bien que les embeddings représentaient en moyenne deux sujets différents et ne correspondaient à aucun d’eux correctement.

Fragmentation sémantique

Intégrez les phrases une par une ; coupez là où la similarité cosinus consécutive descend en deçà d’un seuil (SemanticChunker, n’importe quel outil d’incorporation fiable). Une seule passe d’incorporation au moment de l’indexation. Cette méthode est transformative pour le langage parlé ; les seuils dépendent du corpus. Le seuil utilisé pour les appels téléphoniques a fragmenté le texte dense des politiques en petits morceaux — utilisez donc le chunking sémantique uniquement pour le langage parlé non structuré et conservez les politiques au niveau 2.

Le routage selon la classe du document — langage parlé, formulaire ou wiki — est préférable à la recherche d’un chunker universel. Le coût lié à l’orchestration consiste en un classificateur ou en un simple changement de type de fichier lors de l’ingestion ; l’avantage est moins d’incorporations floues.

Chunking par proposition / fait atomique

Chunking agent

Remettez un document complet à un modèle phare pour qu’il détermine les limites. Cette méthode fonctionne mais présente de graves limites en termes d’échelle : le coût augmente linéairement avec la taille du corpus, tandis que la valeur ajoutée ne suit pas cette tendance. Elle convient pour quelques centaines de documents importants, mais pas pour des millions.

Niveau 4 : Fragmentation en temps de recherche — rechercher de petites unités, restituer de plus grandes

Les niveaux 1 à 3 partent du principe que l’unité indexée est également l’unité restituée. Cela impose un compromis erroné : les petits fragments permettent des embeddings propres, mais privent le LLM de contexte ; les grands fragments fournissent du contexte, mais altèrent la qualité des embeddings. Ajuster constamment chunk_size ne permet jamais de trouver un équilibre universel.

Le niveau 4 sépare ces rôles : une unité de recherche adaptée à l’outil d’embedding et une unité de restitution adaptée au LLM. Test pratique : si vous avez besoin d’un second stockage (docstore, carte parente, arbre), vous êtes au niveau 4.

Document parent / du petit au grand

Index des enfants de petite taille (150–200 tokens). En cas de correspondance, retournez le parent plus grand (toute la section 4.2 ou une sous-section d’environ 1 500 tokens) :

from langchain.retrievers import ParentDocumentRetriever
from langchain.storage import InMemoryStore

parent_splitter = RecursiveCharacterTextSplitter(chunk_size=1500)
child_splitter = RecursiveCharacterTextSplitter(chunk_size=200)

retriever = ParentDocumentRetriever(
    vectorstore=vectorstore,
    docstore=InMemoryStore(),     # the "second store"
    child_splitter=child_splitter,
    parent_splitter=parent_splitter,
)
retriever.add_documents(policy_docs)

LlamaIndex AutoMergingRetriever propose une hiérarchie similaire. Le coût supplémentaire d’indexation est presque nul — vous insérez le même texte en parties plus petites. Sur l’ensemble de test, ce seul changement a permis à Atlas d’atteindre ~91 % contre ~84 % auparavant : la requête « what’s my flood deductible » a trouvé la ligne de tableau exacte, tandis que le LLM a continué à prendre en compte les notes environnantes (y compris la définition de la zone A). Coût opérationnel : un docstore indexé par l’ID du parent qui reste synchronisé lors des mises à jour.

La suppression et la gestion des versions sont importantes ici : lorsque la section 4.2 est révisée, les enfants et le parent doivent être mis à jour ensemble, sinon le système récupérera un enfant précis associé à un parent obsolète. Considérez l’insertion comme une publication transactionnelle du parent, des enfants et des embeddings — et non comme trois tâches indépendantes.

Récupération contextuelle

Au préalable de l’incorporation, demandez à un mini LLM une ou deux phrases contextuelles à insérer en tête du texte ; associez-les au moteur BM25. Des fragments tels que « Zone A : 5 000 $ / Zone B : 2 500 $ » deviennent alors récupérables. Le coût correspond à une requête vers l’LLM par morceau — ce qui est très cher sans mise en cache des prompts ; avec la mise en cache, le préfixe du document reste accessible et les requêtes par morceau restent peu coûteuses.

Morcellement tardif

Incorporez l’ensemble du document à l’aide d’un outil d’incorporation pour contexte long, puis combinez les vecteurs de tokens au sein de chaque morceau. Les vecteurs des morceaux conservent le contexte du document sans nécessiter de requêtes vers l’LLM pour chaque morceau. Cela rivalise avec la récupération contextuelle, mais vous oblige à utiliser des modèles d’incorporation pour contexte long.

Hierarchique / RAPTOR

Regrouper les blocs, faire des synthèses, puis regrouper ces synthèses en un arbre ; indexer chaque niveau. Les questions générales ciblent les synthèses ; les détails ciblent les feuilles. Le coût au niveau 4 est élevé et problématique lorsque les documents changent — les reconstructions sont coûteuses. Les mises à jour trimestrielles des politiques ont fait de RAPTOR le choix idéal pour Atlas.

Les bases de connaissances statiques — des encyclopédies internes qui changent chaque année — peuvent supporter les reconstructions de l’arbre. Les formulaires d’assurance dynamiques ne le peuvent pas. Ne choisir des méthodes hiérarchiques que lorsque le taux de changement et le budget alloué aux reconstructions sont clairement définis.

Ce que dit le document reconstruit

Six semaines après l’exercice de simulation sur Slack, Atlas atteignait près de 93 % d’efficacité sur un ensemble d’environ 300 questions. La version abrégée pour l’examen du design :

Commencez par une segmentation récursive des caractères associée à des coupes tenant compte de la structure, aux alentours de 500 tokens et d’un chevauchement de 50 % — cela coûte presque rien et fournit une base de référence mesurable. Mettez d’abord à jour le système pour permettre la récupération du document parent, afin que les tailles des résultats correspondants et ceux fournis puissent différer. Ce n’est qu’ensuite que vous devriez investir dans une récupération contextuelle si l’ensemble d’évaluation continue de pointer des problèmes de rappel.

Rien de ce qui a été mentionné ci-dessus ne peut corriger une analyse incorrecte. Corrigez d’abord l’extraction avant d’ajuster les outils de segmentation.

La version une page

Étape 0 — Analyse. Associez des analyseurs aux entrées : markdown structuré pour les documents natifs ; confiance textuelle + mise en page + OCR pour les scans ; markdown à en-têtes propre pour le HTML ; limites des diapositives pour les présentations ; transcriptions chronologisées pour l’audio. Cela définit la limite maximale possible.

Niveau 1 — Syntaxique. Taille fixe uniquement pour les prototypes. Le chevauchement est réglable (10–15 %) et entraîne des doublons parmi les résultats top-k. La segmentation des phrases respecte la grammaire mais ignore la logique du document. Une récursivité de 500/50 constitue la référence par défaut, mais ce n’est pas une fin en soi.

Niveau 2 — Conscient de la structure. La segmentation des en-têtes inclut les chemins des sections ; sa qualité dépend du parseur utilisé. Le mode de mise en page by_title conserve l’intégrité des tableaux ; les parseurs peu performants annulent cet effet. La segmentation AST est utilisée pour les dépôts de code.

Niveau 3 — Basé sur un modèle. Les coupes sémantiques se font en fonction de la similarité ; leur ajustement dépend du corpus. Les propositions améliorent la précision sur de petits corpus, mais s’éloignent du texte original. Les coupes agnitives sont rarement justifiées à grande échelle.

Niveau 4 — Temps de récupération. Correspondance sur de petites unités ; livraison des grandes unités. Le document parent représente la mise à niveau offrant le meilleur ROI. La récupération contextuelle nécessite du cache. Le découpage tardif est moins coûteux mais limité par l’outil d’incorporation. RAPTOR devient obsolète avec les mises à jour. S’il nécessite un deuxième stockage, c’est le niveau 4.

L’incident en question ne portait jamais vraiment sur le choix entre LangChain et LlamaIndex. Il s’agissait plutôt de respecter la structure du document avant d’appliquer des calculs, d’évaluer les outils de découpage à l’aide de vraies questions des clients, et de refuser de fournir au modèle des fragments ne pouvant absolument pas contenir une information numérique complète.

Construisez l’ensemble optimal à partir des données qui ont déjà causé des problèmes au produit — franchises incorrectes, exclusions manquantes, garanties confuses — et non à partir d’informations synthétiques aléatoires. Évaluez séparément la justesse des réponses et le degré de fidélité des citations afin qu’un chiffre erroné ne soit jamais présenté comme une victoire. Lorsqu’un nouvel algorithme est introduit, exigez qu’il dépasse la référence récursive pour ces deux indicateurs avant d’être mis en production.

Finalement, prévoyez un mécanisme d’arrêt d’urgence : si le niveau de confiance du OCR pour une valeur numérique est faible, ou si les versions parentale et enfant ne coïncident pas, l’assistant doit refuser de répondre à la question relative à la franchise et signaler le problème plutôt que d’inventer un niveau de confiance. Les utilisateurs pardonnent bien plus facilement une réponse du type « impossible de vérifier cela à partir du formulaire soumis » qu’une réponse erronée concernant une somme de cinq cents dollars. Ce scénario de refus doit figurer dans les exigences du produit, et non seulement dans un diaporama d’analyse a posteriori présenté une fois les dommages causés.