Construire sa propre plateforme RAG

Bonjour à toutes et à tous, et bienvenue sur ce nouvel article !

Avant d’entrer dans le vif du sujet, un petit mot : ça fait trois mois que je n’ai rien publié ici, et je m’en excuse. J’étais très pris par une mission chez un client qui refond entièrement sa plateforme data pour l’adapter à l’IA. Un chantier passionnant, mais qui laisse peu de place au reste. Du coup, j’en profite pour vous parler de l’un des sujets sur lesquels j’ai travaillé pour lui : justement, une plateforme RAG.

Depuis l’arrivée des LLM grand public, la même question revient dans toutes les organisations : comment faire répondre un modèle à partir de nos propres documents (procédures, contrats, documentation technique) sans les envoyer chez un prestataire, et sans réentraîner quoi que ce soit ? La réponse porte un nom : le RAG, pour Retrieval-Augmented Generation.

Le principe tient en une phrase : avant de répondre, on va chercher les bons extraits dans une base de documents, et on les donne au modèle. Simple sur le papier. Beaucoup moins dès qu’on ouvre le capot, parce qu’il faut lire des PDF mal fichus, les découper sans casser le sens, les transformer en vecteurs, les ranger quelque part de rapide et de bien cloisonné, et surtout vérifier que tout ça retrouve vraiment la bonne information.

Je vous propose donc de dérouler ensemble la construction d’une plateforme RAG interne, brique par brique : les outils comparés, les arbitrages, et les raisons derrière chaque choix. Et comme d’habitude, pas de jargon gratuit. L’idée, c’est que ça reste lisible même si vous découvrez le sujet.

Au programme

  1. Le RAG en deux temps : la mécanique générale, en amont et en direct.
  2. La philosophie : pourquoi on investit tout sur la préparation des documents.
  3. Pourquoi internaliser : les cinq raisons de ne pas prendre une solution clé en main.
  4. Lire les PDF : le duel Docling contre Mistral OCR 4.
  5. Découper intelligemment : le découpage parent-child, en trois passes.
  6. Le modèle d’embedding : trois candidats multilingues au banc d’essai.
  7. La base vectorielle : Qdrant, Pinecone ou PgVector ?
  8. Vérifier que le découpage est bon : la méthode et les trois métriques.
  9. L’orchestration : comment tout ça tourne sur GCP.
  10. Un simple agent ReAct : le code qui interroge tout ça, en quelques snippets.
  11. Ce qu’il faut retenir : la synthèse en six points.

Bonne lecture !

1. Le RAG en deux temps

Déjà, avant de commencer, je me permets de revenir aux bases: C’est quoi une pipeline RAG ? Une pipeline RAG se découpe en deux moments bien distincts, et c’est vraiment utile de les garder séparés dans sa tête.

Temps 1, en amont et hors ligne : on range les documents

Chaque document passe par une chaîne de préparation : parsing (lire le PDF), transformation en texte structuré, chunking (découpage en morceaux), embedding (transformation de chaque morceau en vecteur), puis stockage en base vectorielle. Bonne nouvelle : ce travail n’est fait qu’une fois par document.

Temps 2, en direct : on répond à l’utilisateur

L’utilisateur pose sa question à l’agent (le LLM). L’agent demande du contexte au retriever, qui va chercher dans la base vectorielle les n documents les plus proches de la question. L’agent récupère ces extraits et rédige sa réponse à partir d’eux.

2. La philosophie : tout miser sur la pipeline

Mettre le maximum d’intelligence sur la pipeline pour s’en sortir avec le minimum d’intelligence sur le requêtage.

Autrement dit : mieux les documents sont préparés, plus le modèle qui répond peut être petit.

Beaucoup d’entreprises ou ingénieurs IA se pressent à développer rapidement leur agent RAG avant de se rendre qu’il ne marchera jamais bien. La cause est souvent la même: La Data (ici les chunks) sont de mauvaises qualité. Les LLM sont puissants, mais ils ont (comme nous) leur limites, ils ont besoin des données fiables et bien structurées. Le but de cette plateforme est, par conséquence, de construire une base vectorielle avec des chunks clairs et bien structurés.

D’ailleurs, un petit modèle, c’est un modèle rapide et peu coûteux, donc une bien meilleure expérience pour l’utilisateur. Tout l’effort est déplacé en amont, là où on ne le paie qu’une fois par document, au lieu de le payer à chaque question.

3. Pourquoi internaliser plutôt qu’acheter une solution clé en main

Quand j’ai proposé à la direction de lancer leur plateforme RAG, j’ai reçu toujours la même question: « Pourquoi internaliser ça chez nous ? Ce n’est pas notre métier. » En vrai, il y a plusieurs arguments:

  • Sécurité : on ne partage qu’un minimum d’informations confidentielles avec l’extérieur.
  • Souveraineté : pas de vendor lock-in, et on maîtrise la technologie de bout en bout.
  • Personnalisation : on choisit le LLM, on maîtrise toute la chaîne RAG, et on fait évoluer chaque brique sur mesure (gestion des images, des tableaux, etc.).
  • Intégration au système d’information : les agents RAG deviennent réutilisables par les autres agents IA, se connectent aux sources internes et respectent les habilitations (qui a accès à quoi).
  • Maîtrise des coûts : entre un petit modèle et un grand modèle, la facture varie d’un facteur dix à volume égal. Pouvoir choisir, c’est pouvoir optimiser. Surtout que ce client paie déjà un grand montant pour un simple chatbot fait par Dydu.

4. Lire les PDF : le parsing

Deux candidats sérieux se présentent : Docling, le champion de l’open source, et Mistral OCR 4, le champion français.

Petite précision au passage : Docling n’est pas un outil unique, c’est un assemblage. RapidOCR pour la reconnaissance de texte, Docling Layout Heron pour la mise en page, TableFormer (IBM / DS4SD) pour les tableaux, et une couche VLM Granite Vision (2 milliards de paramètres) pour interpréter les visuels.

On a donc commencé à comparer ces deux outils sur trois axes. Pour chaque axe, on a généré avec claude des documents avec plusieurs niveaux de difficultés et on a comparé les résultats.

  • Mise en page: La faculté à comprendre la structure des documents (niveaux des titres, paragraphes, images, …). Docliung gagne légèrement sur le layout car, à la différence de Mistral, ils assemblent les pages ensembles à la dernière étape, donc ils arrivent à avoir une image plus complète du document.
  • Tableaux: La faculté à comprendre les tableaux. Les deux outils excellent et arrivent à comprendre des tableaux complexes.
  • Images: la faculté à comprendre les images et les bien décrire. Mistral est clairement le gagnant sur cet axe car c’est un très grand modèle. Avec docling, on a utilisé un modèle opensource à 2B paramètres, donc on s’attendait déjà qu’il soit moins performant.
Critère Docling Mistral OCR 4
Mise en page ★★★★★ ★★★★½
Tableaux ★★★★★ ★★★★★
Images ★★★☆☆ ★★★★★
Coût Direct 0 $ 5 $ / 1 000 pages

Les deux sont au coude à coude sur le texte et les tableaux. L’écart se fait sur les images, où Mistral OCR prend nettement l’avantage, au prix d’un coût (modeste) à la page. On a donc choisit Mistral OCR 4: c’est un outil français, simple, efficace et nous permets de gérer cette phase d’une manière simple.

5. Découper intelligemment : le parent-child chunking

L’idée est plutôt élégante : on cherche sur de petits morceaux très précis, mais on donne au LLM le morceau large qui les contient quand il n’arrive pas à trouver le contexte. Ça se joue en trois découpages successifs.

  1. Découpe par titres. Le markdown est coupé aux titres (de h1 à h6) avec MarkdownHeaderTextSplitter. On respecte la structure logique du document et on garde le fil d’Ariane (h1 > h2 > h3) dans les métadonnées.
  2. Découpe en « parents ». Chaque section est re-découpée pour tenir dans 1 024 tokens, en coupant aux frontières naturelles. Le découpeur essaie les séparateurs du plus fort au plus faible : d’abord entre paragraphes, puis entre lignes, puis entre phrases, et en dernier recours au milieu d’un mot.
  3. Découpe en « children ». Chaque parent est redivisé en fragments de 200 tokens avec un chevauchement de 40. Ce chevauchement évite qu’une idée à cheval sur deux fragments soit perdue.

Au final, les children sont stockés en vecteurs, ce sont eux qu’on cherche, et les parents sont stockés en texte, ce sont eux qu’on renvoie au modèle quand on a besoin de plus de contexte. On gagne la précision de la recherche sans perdre le contexte. Plutôt satisfaisant, non ?

Schéma du découpage parent-child : une section de document encadrée comme parent, et un extrait plus court encadré à l’intérieur comme child.
Le découpage parent-child : le cadre extérieur est le parent, stocké en texte, et le cadre intérieur le child, stocké en vecteur.

6. Le modèle d’embedding

  BGE-M3 multilingual-e5-large gte-multilingual-base
Éditeur BAAI (Beijing Academy of AI) Microsoft Alibaba
Paramètres ~ 568 M ~ 560 M ~ 305 M
Dimension 1 024 1 024 768
Modes de recherche Dense + Sparse + MultiVector Dense Dense
Téléchargements HF / mois 31,9 M 12 M 1,2 M
BGE-M3 se détache nettement : c’est le seul à combiner recherche dense, lexicale et multi-vecteurs, et de loin le plus adopté.

7. La base vectorielle

  Qdrant Pinecone PgVector
Recherche hybride Native Native Non native
Souveraineté On-premise SaaS On-premise
Filtrage métadonnées Ultra performant Très bon Lent
Multi-scope Natif Non Risque de lenteur
Popularité GitHub 32,9 k sans objet 22 k
Qdrant coche les quatre critères : hybride natif, hébergeable chez soi, filtrage rapide et cloisonnement multi-scope.

Le multi-scope mérite un mot, parce qu’on le sous-estime souvent : c’est lui qui permet de servir plusieurs domaines métier depuis une seule base, sans qu’une recherche déborde sur le périmètre d’un autre.

8. Vérifier que le découpage est bon

Le piège, avec un chunking mal réglé, c’est qu’il ne se voit pas : la chaîne répond quand même, juste moins bien. Il faut donc le mesurer.

La méthode, en trois temps

  • Générer un « golden dataset » : on demande à un LLM de rédiger une question factuelle à partir de chaque chunk.
  • Revue humaine : on nettoie les questions ambiguës ou triviales, qui fausseraient la mesure.
  • Test de recherche : on demande à la base vectorielle de retrouver les chunks associés à chaque question.

Les trois métriques suivies

  • Hit rate (rappel) : le bon chunk est-il dans le top-k des résultats ?
  • MRR (Mean Reciprocal Rank) : moyenne de 1/rang. Rang 1 donne 1,0 ; rang 2 donne 0,5 ; rang 3 donne 0,33 ; absent donne 0. Elle récompense les bonnes réponses bien classées.
  • Doc_hit : proportion des cas où au moins un extrait du top-k provient du bon document source.

9. L’orchestration, sur GCP

Chaque domaine métier dispose de son bucket Google Cloud Storage. Quand un document y est déposé, une notification GCS publie un message sur un topic Pub/Sub dédié. Ce message déclenche un service Cloud Run qui exécute toute la chaîne de préparation et écrit dans la base Qdrant, hébergée sur une VM.

Côté usage, chaque agent IA métier interroge la base sur son propre périmètre, d’où l’importance du multi-scope et du filtrage par métadonnées vus plus haut. Ajouter un domaine revient à ajouter un bucket, un topic et un scope : l’architecture est faite pour se répliquer.

Diagramme d’architecture : trois buckets Google Cloud Storage envoient des notifications Pub/Sub vers des topics, qui déclenchent un service Cloud Run.
L’orchestration sur GCP : un bucket et un topic Pub/Sub par domaine métier, un seul service Cloud Run en sortie.

10. Et côté requêtage ? Un simple agent ReAct

Toute cette pipeline n’a d’intérêt que si quelque chose sait s’en servir. Fidèles à la philosophie de la partie 2, on a fait au plus simple : pas d’orchestration alambiquée, pas de routeur maison, juste un agent ReAct classique, monté avec LangGraph et un Mistral Small.

ReAct, c’est Reasoning + Acting : le modèle alterne raisonnement et appels d’outils, en boucle, jusqu’à pouvoir répondre. On ne lui dicte pas quand chercher, on lui donne des outils et il décide. Le cœur de l’agent tient littéralement en deux appels :

Python
llm = ChatMistralAI(
    model="mistral-small-latest",
    temperature=0,
    max_tokens=1024,
).bind_tools(tools)

agent = create_react_agent(
    llm, tools,
    prompt=SYSTEM_PROMPT,
    checkpointer=checkpointer,
)

C’est tout ! Le checkpointer garde l’historique de chaque conversation (une clé thread_id), ce qui permet les questions de suivi du type « et pour l’autre cas ? ». Toute l’intelligence est ailleurs : dans les outils et dans le prompt système.

Un outil, c’est une fonction… et sa docstring

Voici le détail qui surprend souvent : la docstring est la documentation lue par le modèle. C’est elle qui lui dit quand utiliser l’outil. La soigner, c’est piloter le comportement de l’agent sans écrire une ligne de logique.

Python
@tool
def semantic_search(query: str) -> str:
    """Recherche SÉMANTIQUE (par le sens).
    À utiliser en premier. Renvoie des EXTRAITS courts.
    Si un extrait est tronqué ou insuffisant, ouvre sa
    section complète avec expand_context(ref).

    Args:
        query: la question, en langage naturel.
    """
    return _format_children(
        _semantic_child_points(query, poles, N_CHILDREN))

L’agent dispose de quatre outils : semantic_search (par le sens), keyword_search (mots exacts, pour les acronymes, codes et numéros de procédure), hybrid_search (les deux fusionnés) et expand_context. Chaque jeu d’outils est construit scopé sur le périmètre du profil métier concerné : c’est le cloisonnement de la partie 7, appliqué à l’exécution.

La recherche hybride, fusionnée côté base

C’est là que le choix de Qdrant paie : la fusion des deux recherches se fait dans la base, pas dans notre code.

Python
qdrant().query_points(
    collection_name=CHILDREN_COLL,
    prefetch=[
        Prefetch(query=vec_dense, using="dense",
                 limit=k * 2, filter=flt),
        Prefetch(query=vec_sparse, using="sparse",
                 limit=k * 2, filter=flt),
    ],
    query=FusionQuery(fusion=Fusion.RRF),   # fusion RRF
    limit=k, with_payload=True,
)

Deux recherches partent en parallèle, une dense (le sens) et une sparse (les mots), et Qdrant les fusionne en RRF (Reciprocal Rank Fusion). Chaque résultat est noté selon son rang dans chaque liste, pas selon son score brut. C’est ce qui permet de comparer deux classements qui n’ont pas du tout la même échelle.

Le parent-child, vu depuis l’agent

Le découpage de la partie 5 prend ici tout son sens. Une recherche ne renvoie pas des sections entières, elle renvoie quatre extraits courts, chacun accompagné d’un ref vers sa section parente. L’agent ne déplie la section complète que s’il en a vraiment besoin.

Python
N_CHILDREN = 4   # 4 extraits (~200 tokens) par recherche

@tool
def expand_context(refs: list[str]) -> str:
    """Récupère le TEXTE COMPLET des sections (parents)
    correspondant aux ref affichés dans les extraits.
    À utiliser UNIQUEMENT quand un extrait est tronqué
    ou ne suffit pas. Sinon, réponds depuis les extraits.
    """
    records = qdrant().retrieve(
        collection_name=PARENTS_COLL,
        ids=refs, with_payload=True)

L’économie est considérable : la plupart des questions se règlent sur 4 × 200 tokens d’extraits au lieu de plusieurs sections de 1 024. Et quand un extrait est coupé au mauvais endroit, l’agent a toujours le moyen d’aller chercher le contexte complet plutôt que d’inventer la suite.

Ce qui l’empêche d’halluciner

Un agent ReAct est bavard par nature : si on ne l’encadre pas, il comble les trous avec ses connaissances générales. Le garde-fou est entièrement dans le prompt système, avec quatre règles qui font le gros du travail :

  • chercher avant de répondre à toute question factuelle, sans jamais s’appuyer sur des connaissances externes ;
  • n’affirmer que ce qu’un extrait énonce explicitement, car la présence d’un chiffre à côté d’un sujet n’en fait pas la réponse ;
  • en cas d’ambiguïté, ouvrir la section complète, puis qualifier la réponse plutôt que de trancher ;
  • citer ses sources en fin de réponse, au format [source · section · p.X].

Dernier point, invisible mais très rentable : à chaque tour de boucle, l’agent renvoie au modèle le même préfixe (prompt système, définition des outils, historique). En attachant une clé de cache par conversation, avec llm.bind(prompt_cache_key=f"rag-{thread_id}"), ce préfixe commun est facturé à 10 % du tarif d’entrée au lieu de 100 %. Sur un agent ReAct, qui enchaîne facilement trois ou quatre allers-retours par question, la différence se voit vite sur la facture.

11. Ce qu’il faut retenir

  • On investit dans la préparation des documents pour pouvoir répondre avec un LLM léger : meilleure réactivité, coûts maîtrisés.
  • Le parsing se joue entre Docling (gratuit, excellent sur la mise en page) et Mistral OCR 4 (5 $ / 1 000 pages, nettement meilleur sur les images).
  • Le découpage parent-child concilie précision de la recherche et richesse du contexte transmis au modèle.
  • BGE-M3 et Qdrant ressortent des comparatifs, notamment pour la recherche hybride et le cloisonnement par domaine.
  • La qualité ne se suppose pas, elle se mesure : hit rate, MRR et doc_hit sur un jeu de questions revu à la main.
  • Côté requêtage, un agent ReAct de quelques lignes suffit : quatre outils, un prompt strict, et le modèle décide lui-même quand chercher.

Merci de m’avoir lu jusqu’ici ! Si le sujet vous intéresse, ou si vous avez fait des choix différents sur vos propres chaînes RAG, n’hésitez pas à m’écrire via la page Contact. Les retours d’expérience sur ces sujets sont toujours les bienvenus, et à très vite pour un prochain article !