RAG de zéro : découper, intégrer, stocker, récupérer, répondre avec du contexte
Une explication pas à pas d’un pipeline RAG utilisant LangChain et FAISS : segmentation en chunks, génération d’embeddings, récupération d’informations, ancrage des prompts, erreurs fréquentes et prochaines améliorations.
Sources: RAG, Explained the Way I Wish Someone Had Explained It to Me
Si vous demandez à un modèle de chat quelles sont les conditions de remboursement d’une entreprise donnée, il admettra rarement son ignorance. Le plus souvent, il produit un paragraphe fluide et crédible qui n’a rien à voir avec la politique réelle. La cause n’est pas un défaut : un modèle de langage est entraîné une seule fois, puis figé. Il n’a jamais lu de PDF privé, de wiki interne ou d’espace de travail Notion, et il ne connaît rien de ce qui a été écrit après la date de fin de son entraînement.
La génération augmentée par récupération de données, généralement abrégée en RAG, constitue le remède standard, et l’idée en est presque étonnamment simple. Au lieu d’attendre que le modèle se souvienne d’une réponse, le système trouve le texte pertinent et le place directement dans la demande, puis demande au modèle de le lire et de répondre. Tout le reste relève de l’ingénierie visant à rendre cette recherche fiable. L’exemple ci-dessous montre un bot de support qui répond aux questions issues d’un PDF de politique d’entreprise.
Deux étudiants, un examen
Imaginez deux personnes passant le même test. La première a mémorisé un manuel il y a des mois et écrit en se fiant à sa mémoire. Si le manuel était obsolète ou qu’un chapitre avait été partiellement oublié, ses réponses seraient assurées mais fausses. La seconde personne dispose d’un livre ouvert. Avant d’écrire, elle consulte la page appropriée, la lit, puis combine ces informations avec son raisonnement général.
Le deuxième étudiant gagne généralement, et ce non pas grâce à une intelligence supérieure. Consulter des informations avant de répondre est simplement une meilleure méthode. Dans un système RAG, le modèle de langage incarne le deuxième étudiant, les données de l’entreprise constituent le livre ouvert, et la base de données vectorielle sert d’index indiquant quelle page consulter.
Deux pipelines, pas un
Tout système RAG, aussi sophistiqué soit-il, se compose de deux pipelines distincts reliés entre eux.
- L’indexation s’effectue hors ligne, une fois au départ, puis à nouveau chaque fois que les documents changent. Les fichiers bruts deviennent un index recherchable.
- La récupération et la génération ont lieu pour chaque question : l’index est consulté, les meilleurs éléments sont extraits et transmis au modèle.
Les débutants essaient souvent d’imaginer un seul processus long, ce qui rend les choses confuses. Le diviser en deux aide, car les deux parties s’exécutent à des moments différents et résolvent des problèmes distincts.
Création de l’index
Chargement des documents
Commencez par transformer le contenu brut en texte brut. Les PDF, les fichiers Word, les pages web et les notes exportées nécessitent tous un outil de chargement capable de restituer du texte.
from langchain_community.document_loaders import PyPDFLoader
loader = PyPDFLoader("company_policy.pdf")
documents = loader.load()
print(f"Loaded {len(documents)} pages")
Diviser le texte en blocs
La division du texte en blocs mérite plus d’attention qu’elle n’en reçoit généralement. Une première erreur très courante consiste à envoyer tout un document dans le stockage vectoriel en tant qu’unité énorme. Un modèle d’incorporation transforme un passage en un vecteur de taille fixe. Si l’on lui fournit vingt pages, le résultat est une moyenne floue de tout, ce qui peut indiquer que le fichier traite globalement des politiques de l’entreprise, mais sans pouvoir exprimer un paragraphe précis concernant une période de remboursement de trente jours.
La solution consiste à découper les documents en petits fragments qui se chevauchent. Ce chevauchement est important car une phrase coupée exactement à une frontière perd son contexte des deux côtés, et une marge d’environ dix à vingt pour cent de la taille du bloc agit comme filet de sécurité.
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=500, # characters per chunk
chunk_overlap=80, # overlap between consecutive chunks
separators=["\n\n", "\n", ". ", " "]
)
chunks = splitter.split_documents(documents)
print(f"Split into {len(chunks)} chunks")
À titre indicatif, des blocs trop grands rendent la récupération floue, tandis que des blocs trop petits enlèvent le contexte. Une fourchette de départ d’environ 300 à 800 caractères est raisonnable, à ajuster ensuite après avoir examiné des réponses réelles.
Intégrer chaque bloc
Un modèle d’embedding convertit du texte en une liste de nombres qui reflètent son sens et non seulement sa formulation. Deux phrases qui disent la même chose se retrouvent comme des vecteurs situés l’un près de l’autre, même lorsqu’elles ne partagent aucun mot.
from langchain_openai import OpenAIEmbeddings
embedding_model = OpenAIEmbeddings(model="text-embedding-3-small")
# under the hood, this is what happens per chunk:
vector = embedding_model.embed_query("30-day refund window for annual plans")
print(len(vector)) # e.g. 1536 numbers representing this sentence's meaning
On peut l’imaginer comme une carte plate où les idées apparentées se regroupent en clusters.
"refund policy" •
\
• "money-back guarantee"
"vacation days" •
\
• "paid time off"
Une phrase comme « politique de remboursement » apparaît à côté de « garantie de remboursement », bien que les mots diffèrent, et c’est là tout le principe de la recherche sémantique. Cela explique pourquoi RAG surpasse l’ancienne méthode de recherche par mots-clés basée sur Ctrl+F.
Stocker les vecteurs
Chaque vecteur est stocké dans une base de données vectorielle accompagné du texte original. Cette base constitue l’index que consulte l’utilisateur comme s’il feuilletait un livre ouvert.
from langchain_community.vectorstores import FAISS
vectorstore = FAISS.from_documents(chunks, embedding_model)
vectorstore.save_local("faiss_index")
FAISS est un point de départ idéal car il est gratuit et fonctionne sur une machine locale, et Chroma est tout aussi simple. Des services gérés tels que Pinecone ou Weaviate s’avèrent utiles lorsque l’application atteint une échelle de production importante. Le principe reste le même d’un outil à l’autre, seuls le stockage et l’infrastructure diffèrent.
L’indexation est maintenant terminée. Le résultat est une mémoire indexable constituée des documents, qui n’a besoin d’être reconstruite que lorsque ces documents changent, et non pour chaque question posée par un utilisateur.
Répondre à une question
Cette étape s’exécute chaque fois que quelqu’un tape une requête.
Intégrer la question avec le même modèle
query = "Can I get a refund on my annual subscription?"
La question doit être incorporée avec exactement le même modèle qui a été utilisé lors de l’indexation. Les gens se trompent constamment à ce sujet. Si l’index a été créé avec un modèle d’incorporation et consulté avec un autre, les vecteurs se trouvent dans des espaces sémantiques différents, et la recherche de similarité cesse silencieusement de fonctionner. Aucune erreur n’apparaît, seuls des résultats médiocres se présentent ; donc la règle est d’utiliser un seul modèle du début à la fin.
Récupérer les fragments les plus proches
retriever = vectorstore.as_retriever(search_kwargs={"k": 4})
relevant_chunks = retriever.invoke(query)
for chunk in relevant_chunks:
print(chunk.page_content[:100], "...")
En interne, FAISS effectue une recherche de similarité. Il mesure la distance, généralement la similarité cosinus, entre le vecteur de la question et le vecteur de chaque morceau stocké, puis retourne ceux qui sont les plus proches. C’est à nouveau l’idée des clusters sur une carte, appliquée à l’envers pour trouver le cluster auquel appartient la question.
Coller les morceaux dans une instruction
C’est cette étape qui permet au RAG de fonctionner : le texte récupéré est littéralement inséré dans l’instruction.
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_template("""
Answer the question using ONLY the context below.
If the answer isn't in the context, say "I don't have that information."
Context:
{context}
Question: {question}
""")
L’instruction de répondre en se basant uniquement sur le contexte fourni permet au modèle de rester ancré dans les faits et l’empêche d’inventer des éléments. Elle distingue un examen à livre ouvert d’un examen à livre fermé où l’improvisation est autorisée.
Le connecter en chaîne
Les différents éléments sont alors assemblés en une chaîne RAG, c’est-à-dire ce que les gens entendent lorsqu’ils parlent de créer une telle chaîne. Dans LangChain, cela s’écrit généralement à l’aide de l’opérateur pipe, qui transmet la sortie du côté gauche à l’étape suivante du côté droit.
from langchain_openai import ChatOpenAI
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
llm = ChatOpenAI(model="gpt-4o-mini")
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
answer = rag_chain.invoke("Can I get a refund on my annual subscription?")
print(answer)
En se lisant de gauche à droite, la chaîne se narre elle-même. La question est introduite, des fragments sont récupérés et formatés en contexte tandis que la question reste inchangée, le modèle est appelé, puis le texte brut est extrait de sa réponse.
Lorsque l’on exécute ce processus, au lieu d’une supposition erronée, on obtient quelque chose comme : oui, les abonnements annuels donnent droit à un remboursement intégral dans un délai de trente jours après l’achat, conformément à la politique de remboursement. La réponse est fondée et précise, et elle peut être retrouvée dans un paragraphe réel du PDF.
Pourquoi ne pas coller tout le texte ?
Une objection légitime est que les fenêtres de contexte sont désormais très grandes. Pourquoi ne pas inclure tout le document dans chaque demande et éviter la recherche vectorielle ?
- Coût et latence. Chaque token envoyé coûte de l’argent et prend du temps. Avec une base de connaissances de 500 pages, envoyer tout le contenu pour chaque question est gaspilleur lorsque la réponse se trouve dans un seul paragraphe.
- Récupération dégradée. Même avec de grandes fenêtres, les modèles traitent de manière significative moins bien les informations enfouies au milieu du prompt que celles qui leur sont transmises directement et brièvement.
- Échelle. Coller tout le contenu échoue complètement dès que la base de connaissances dépasse la taille de toute fenêtre, que ce soit avec des milliers de PDF, une wiki complète d’entreprise ou des années de tickets de support.
RAG permet à un système de passer d’un seul document à toute une base de connaissances tout en maintenant la même taille pour le prompt. Seule l’étape de récupération consulte l’ensemble des données, et le prompt ne reçoit que quelques fragments pertinents.
Erreurs qui apparaissent tôt
- Un découpage insuffisant gâche la récupération des informations, quel que soit la qualité du modèle. Si une table est coupée en deux ou qu’une phrase s’interrompt au milieu d’une idée, rien ne peut corriger cela par la suite. Vérifiez la qualité de la récupération avant de blâmer le modèle.
- Des modèles d’embedding incompatibles rendent les scores de similarité sans signification. Un index créé avec text-embedding-3-small ne peut pas être consulté avec un autre modèle.
- Récupérer trop peu ou trop de fragments nuit dans les deux cas. Avec k=1, la réponse réelle peut être manquée, tandis qu’avec k=20, le modèle est submergé par le bruit et les coûts. Commencez par trois à cinq et ajustez en fonction des résultats.
- En l’absence de solution de secours en cas d’incertitude, le modèle continuera à inventer une réponse, même lorsqu’un contexte pertinent est disponible. Précisez clairement ce qu’il convient de faire lorsque le contexte ne contient pas la réponse.
Que faire ensuite
Lorsque le cycle de base fonctionne bien, trois améliorations constituent les prochaines étapes logiques.
- La recherche hybride combine la similarité vectorielle avec la correspondance par mots-clés, car certaines requêtes, comme les codes ou noms de produits exacts, sont mieux traitées grâce aux mots-clés.
- Le réclassement récupère d’abord un ensemble plus large de candidats, puis permet à un deuxième modèle, plus précis, de les réordonner avant que le modèle de langage ne les examine.
- Le filtrage des métadonnées associe des détails tels que la date, la source ou le département à chaque fragment, ce qui permet de restreindre les résultats avant même que la recherche par similarité ne soit lancée.
Aucun de ces éléments n’a d’importance tant que le cycle de base — fragmenter, encoder, stocker, récupérer et générer — fonctionne correctement. Le moyen le plus rapide de vraiment le comprendre est de charger un PDF important, de casser délibérément quelque chose, comme des modèles d’encodage incompatibles ou des fragments sans chevauchement, et d’observer la dégradation des réponses. Avec RAG, les modes de défaillance enseignent plus que le parcours sans problème.
Lectures complémentaires
- Explication de la génération améliorée par la recherche : résolution des écarts de connaissances des LLM — Découvrez pourquoi les LLM génèrent des informations fausses et deviennent obsolètes, puis suivez pas à pas comment la RAG récupère, fragmente, incorpore et améliore les prompts pour y remédier.
- Empoisonnement des données RAG : comment hacker un processus sans toucher au LLM — Comment les attaquants contaminent les bases de connaissances RAG via des documents, des métadonnées et une injection indirecte de prompts, ainsi que les mécanismes de contrôle du cycle de vie des données qui permettent réellement de s’en protéger.