Demandez à un LLM généraliste quelle est la politique de remboursement de votre entreprise ou ce que disent les tickets de support de la semaine dernière, et il vous dira qu’il ne sait pas ou inventera quelque chose de plausible. Sa connaissance s’arrête à ce qui figurait dans ses données d’entraînement. Il n’a jamais vu vos documents et ne peut pas les consulter en cours de conversation. La retrieval-augmented generation (RAG) règle exactement ce problème : avant que le modèle ne réponde, une étape séparée trouve le texte pertinent dans vos propres données et le transmet au modèle dans le prompt.

Comment fonctionne vraiment la retrieval-augmented generation

Un appel LLM classique tient en une étape : un prompt en entrée, une réponse en sortie, en utilisant uniquement ce que le modèle a appris pendant l’entraînement. Le RAG scinde cela en deux : d’abord récupérer, puis générer.

  1. Vos documents sont découpés en chunks, convertis en vecteurs (embeddings) et stockés à l’avance dans une base de données vectorielle.
  2. Au moment de la requête, la question de l’utilisateur est transformée en embedding de la même façon et comparée aux chunks stockés pour trouver les correspondances les plus proches.
  3. Les meilleures correspondances sont insérées dans le prompt comme contexte, et c’est seulement à ce moment-là que le LLM génère une réponse.

Voyez cela comme un examen à livre ouvert face à un examen à livre fermé. Un LLM à livre fermé répond uniquement de mémoire, ce qui explique pourquoi il invente des choses quand la question sort de ce qu’il a mémorisé. Le RAG lui met la bonne page sous les yeux, si bien qu’il répond à partir de quelque chose qu’il a devant lui plutôt que de quelque chose qu’il reconstruit.

Le modèle lui-même ne change jamais. Ses poids restent exactement ceux d’avant l’ajout du RAG ; le retrieval est la seule pièce nouvelle. C’est aussi ce qui rend le RAG bon marché à maintenir à jour : vous modifiez un document et la requête suivante en tient compte.

Construire une pipeline RAG : chunking et embedding de vos données

L’ingestion est la moitié de la pipeline qui s’exécute avant que quiconque pose une question. Elle transforme vos documents en quelque chose qu’une recherche par similarité peut interroger.

Les documents sont trop longs pour être transformés en embedding comme une seule unité. Un PDF de 20 pages converti en un seul vecteur finit par correspondre un peu à chaque requête et bien à aucune, donc vous le découpez d’abord en chunks. Un découpeur à taille fixe avec chevauchement suffit pour commencer :

1
2
3
4
5
6
7
8
def chunk_text(text, chunk_size=500, overlap=50):
    chunks = []
    start = 0
    while start < len(text):
        end = start + chunk_size
        chunks.append(text[start:end])
        start = end - overlap
    return chunks

500 caractères avec 50 de chevauchement est un point de départ raisonnable. Les pipelines en production découpent en général par nombre de tokens plutôt que par caractères, avec un découpeur qui connaît le tokenizer pour qu’une limite de chunk ne tombe pas au milieu d’un mot. Le chevauchement garantit qu’une phrase coupée entre deux chunks reste entière dans au moins l’un des deux.

Chaque chunk est ensuite transformé en embedding et stocké. Chroma est une bonne première base de données vectorielle car elle tourne dans le même processus, sans serveur à déployer, et sa fonction d’embedding par défaut (all-MiniLM-L6-v2) ne nécessite aucune clé API :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import chromadb

client = chromadb.Client()
collection = client.create_collection(name="support-docs")

collection.add(
    ids=["doc1", "doc2", "doc3"],
    documents=[
        "The refund window is 30 days from the delivery date.",
        "Refunds go back to the original payment method within 5 business days.",
        "Store credit never expires and can be used on any future order.",
    ],
)

C’est toute la phase d’ingestion : découper, transformer en embedding, stocker. Elle s’exécute une fois par document, puis à nouveau chaque fois qu’un document change.

Si vous préférez ne pas installer de base de données vectorielle sur votre machine, la plupart d’entre elles (Chroma, Weaviate, Milvus) fournissent une image officielle et tournent bien dans un container ; consultez qu’est-ce que Docker et à quoi ça sert si vous n’en avez jamais utilisé.

Comment fonctionne le retrieval : embedding de la requête et classement des résultats

Au moment de la requête, le même modèle d’embedding transforme la question de l’utilisateur en vecteur, et la base de données renvoie les chunks stockés les plus proches, selon la métrique de distance configurée — cosine similarity et L2 au carré sont les deux que vous rencontrerez le plus souvent :

1
2
3
4
5
6
7
8
results = collection.query(
    query_texts=["How long do I have to return something?"],
    n_results=2,
)

print(results["documents"])
# [['The refund window is 30 days from the delivery date.',
#   'Refunds go back to the original payment method within 5 business days.']]

La requête n’utilise jamais le mot « remboursement » (refund) et trouve pourtant la bonne correspondance. C’est toute la raison de faire de l’embedding plutôt qu’un grep : deux textes se retrouvent proches dans l’espace vectoriel quand ils veulent dire des choses similaires, pas quand ils partagent des mots.

La dernière étape assemble les chunks récupérés dans le prompt :

1
2
3
4
5
6
7
8
context = "\n".join(results["documents"][0])

prompt = f"""Answer the question using only the context below. If the context doesn't contain the answer, say so.

Context:
{context}

Question: How long do I have to return something?"""

prompt part vers l’API de chat completion que vous utilisez déjà. Le RAG n’impose ni modèle ni fournisseur particulier ; c’est un schéma qui décide ce que vous mettez dans le prompt avant de l’envoyer. Si cet appel API nécessite une clé, gardez-la hors de votre image comme n’importe quel autre identifiant : en variable d’environnement ou en secret monté, jamais en argument de build. Variables d’Environnement et Secrets dans Docker détaille la différence.

RAG contre fine-tuning : lequel résout votre problème

Les deux visent à rendre un LLM meilleur sur votre cas d’usage précis, mais ils changent des choses différentes.

RAGFine-tuning
Ce qui changeRien dans le modèle ; vous ajoutez une étape de retrievalLes poids du modèle lui-même, via un entraînement supplémentaire
Mettre à jour la connaissanceVous modifiez ou ajoutez un document, la requête suivante le voitRéentraîner (ou refaire une passe de fine-tuning)
Adapté àLes faits, les données à jour, tout ce qui change souventLe ton, le format de sortie, les schémas de raisonnement propres à la tâche
Coût de mise à jourFaible — aucun entraînementÉlevé — nécessite un entraînement et un jeu de données
Échoue quandLe retrieval rate ou classe mal le chunkLe comportement recherché ne se ramène pas à des exemples entrée-sortie

Un bot de support dont la politique de remboursement change chaque trimestre est un problème de RAG : faites du fine-tuning sur la politique du trimestre dernier et il sera sûr de lui mais faux sur la nouvelle. Un modèle qui doit produire un schéma JSON précis à chaque fois, ou garder un style de raisonnement particulier, se rapproche davantage d’un problème de fine-tuning : aucune quantité de contexte récupéré ne change la façon dont un modèle formate sa sortie. Beaucoup de systèmes en production combinent les deux : un modèle fine-tuné pour le ton et la structure, alimenté en faits par le RAG.

Où casse la retrieval-augmented generation

Les démos de RAG paraissent faciles parce que la démo a trois documents et une réponse évidente. Les systèmes en production ont des milliers de documents, et chacun des points suivants peut discrètement gâcher une réponse :

  • Un chunking qui ignore la structure. Un découpeur à taille fixe qui coupe un tableau en deux, ou sépare un titre du paragraphe qu’il introduit, produit des chunks qui, hors contexte, ne veulent rien dire — et ce qui ne veut rien dire s’embed et se récupère mal.
  • Un index périmé. La recherche vectorielle n’a aucune notion du temps. Si un document est mis à jour mais jamais réembeddé, c’est toujours l’ancienne version qui est récupérée, et rien dans la pipeline ne signale qu’elle est fausse.
  • Le bon chunk existe mais ne se classe pas assez haut. Le retrieval top-k ne renvoie que les k correspondances les plus proches ; si la vraie réponse est le 15e chunk le plus proche et que vous en récupérez 5, le modèle ne la voit jamais.
  • Trop de contexte, mal placé. Entasser dix chunks récupérés dans le prompt ne veut pas dire que le modèle les pèse tous de la même façon. Les modèles sont mesurablement moins bons pour exploiter une information au milieu d’un contexte long qu’au début ou à la fin.
  • Aucune évaluation au-delà de « la réponse de la démo avait l’air correcte ». Sans un jeu de questions et de réponses attendues, une régression de retrieval causée par un changement de chunking ou un bug de réindexation passe inaperçue.

La plupart de ces problèmes viennent du retrieval, pas du modèle, et chacun est visible si vous inspectez les chunks récupérés plutôt que la seule réponse finale.

Comment construire votre première pipeline RAG

Construisez d’abord la version en trois étapes : découpez en chunks une poignée de documents réels, transformez-les en embedding dans Chroma ou un autre store local, et branchez les chunks récupérés sur un prompt exactement comme montré ci-dessus. Posez-lui ensuite les 10 questions que vous attendez de vrais utilisateurs. Vérifiez si les chunks récupérés contiennent la réponse avant de juger la réponse générée — un mauvais retrieval rend une mauvaise réponse inévitable, quelle que soit la façon dont le prompt est écrit. C’est seulement une fois ce cycle validé sur de vraies questions qu’il vaut la peine d’ajuster la taille des chunks, de changer de modèle d’embedding ou d’ajouter un reranker.

Articles associés