Un système RAG combine la recherche de documents pertinents avec un modèle de langage pour produire des réponses précises et actualisées. Ce guide vous explique clairement, en 7 étapes pratiques, comment créer un RAG simple et efficace, sans théorie inutile ni complexité superflue.
3 principaux points à retenir.
- RAG améliore la précision des LLM en injectant des données à jour via la recherche documentaire.
- Fragmenter et vectoriser les documents est essentiel pour une recherche rapide et cohérente.
- L’intégration fluide avec un modèle de langage forme la clé d’un système RAG performant.
Qu’est-ce qu’un système RAG et comment fonctionne-t-il ?
Un système RAG, ou Retrieval-Augmented Generation, est tout simplement une fusion ingénieuse entre un module de recherche documentaire et un modèle de langage. Cela peut paraître un peu technique, mais l’idée est assez simple : il s’agit de combiner le meilleur des deux mondes pour générer des réponses plus précises et contextualisées. Comment ça marche, vraiment ? En gros, voici le workflow en quelques étapes : lorsqu’un utilisateur pose une question, le système se lance dans une recherche au sein d’une base de données indexée. Il récupère alors les passages les plus pertinents qui entrent en résonance avec la question posée. Ces passages sont ensuite transmis au modèle de langage, qui génère enfin une réponse enrichie et, espérons-le, plus fiable.
L’un des avantages majeurs d’un système RAG est sa capacité à limiter ce qu’on appelle les « hallucinations » des LLM (Large Language Models). Prenez l’exemple d’un professeur qui se trompe dans ses réponses de temps en temps. Si ses données sont obsolètes ou inexactes, ça devient problématique. Le système RAG, en revanche, permet de mettre à jour les réponses à l’aide de données externes, assurant ainsi une actualité et une précision bienvenues.
Un petit schéma serait sans doute très utile ici : visualisez une question utilisateur en haut ; en dessous, votre base de données qui inspire une recherche ; et enfin, un modèle de langage qui produit une réponse. Ce schéma offre une clarté précieuse sur le fonctionnement de ces systèmes. C’est donc une solution incontournable dans notre époque où l’accès à l’information est si rapide et où la précision est cruciale. À l’ère des flux d’informations instantanées, une réponse fiable et à jour n’est pas seulement souhaitable, elle est indispensable.
Si vous souhaitez creuser un peu plus ce sujet fascinant, je vous recommande de consulter cet article sur la génération augmentée par la récupération. Vous y trouverez des informations précieuses qui méritent d’être explorées.
Comment préparer et fragmenter les données textuelles ?
Pour nourrir un système RAG (Retrieval-Augmented Generation), le chargement et le nettoyage des données est une étape cruciale. En effet, les modèles de langage comme ChatGPT ou Gemini ne disposent pas de vos informations récentes ou privées, ce qui représente un obstacle lorsque ces données sont essentielles pour générer des réponses précises. Pensez simplement à vos notes de recherche, aux documents d’entreprise, ou aux fichiers de projet ; autant d’éléments que les LLM ne prennent pas en compte. C’est ici que le travail de préparation des données entre en jeu.
Le processus commence par le chargement de vos documents. Le simple fait d’inclure un fichier texte n’est pas suffisant ; il faut également s’assurer qu’il est propre et exempt de données inutiles qui pourraient entraîner des erreurs lors de la recherche. Pour éviter toute hallucination de la part du modèle, nous devons le nettoyer. Utiliser des outils adaptés pour cela est donc essentiel. Ces outils vont aider à formater le texte de manière à améliorer sa pertinence et sa lisibilité pour le modèle.
Une fois le nettoyage effectué, la prochaine étape consiste à segmenter vos documents en morceaux, ou « chunks », afin de les rendre compatibles avec la taille limitée de contexte des LLM. Utiliser un modèle comme le RecursiveCharacterTextSplitter de LangChain est une très bonne approche pour cela, car il permet de diviser le texte à des points naturels comme des phrases ou des paragraphes. Cela garantit que chaque segment reste cohérent tout en maximisant la quantité d’information transmise au modèle.
import os
import re
def load_documents(folder_path):
docs = []
for file in os.listdir(folder_path):
if file.endswith(".txt"):
with open(os.path.join(folder_path, file), 'r', encoding='utf-8') as f:
docs.append(f.read())
return docs
def clean_text(text: str) -> str:
text = re.sub(r'\s+', ' ', text)
text = re.sub(r'[^\x00-\x7F]+', ' ', text)
return text.strip()
def split_docs(documents, chunk_size=500, chunk_overlap=100):
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(chunk_size=chunk_size, chunk_overlap=chunk_overlap)
chunks = splitter.create_documents(documents)
return chunks
folder_path = "data/"
raw_docs = load_documents(folder_path)
cleaned_docs = [clean_text(doc) for doc in raw_docs]
chunks = split_docs(cleaned_docs)
En suivant ces étapes, vous préparez efficacement vos données pour qu’elles soient prêtes à intégrer un système RAG à la fois précis et performant. Une approche bien orchestrée permet au système d’extraire les bons morceaux d’information et de les utiliser pour générer des réponses pertinentes.
Pourquoi et comment convertir les textes en vecteurs d’embedding ?
Les ordinateurs et le texte brut, c’est un peu comme un poisson et un vélo : ça ne marche pas. Pour que les machines comprennent vraiment le contenu d’un document, elles ont besoin de le transformer en vecteurs numériques, appelés embeddings. Ces vecteurs capturent le sens sémantique des morceaux de texte, permettant ainsi de mesurer la similarité entre une requête utilisateur et des documents existants. Imaginez que vous posiez une question à un LLM (Large Language Model) : sans ces embeddings, le modèle se retrouverait dans un flou total, incapable de différencier des textes pourtant très proches les uns des autres.
D’un point de vue pratique, un outil comme SentenceTransformers avec le modèle all-MiniLM-L6-v2 est extrêmement utile. Ce modèle va générer des embeddings en un clin d’œil, transformant vos textes en vecteurs que l’on peut ensuite comparer. Voici comment cela fonctionne en Python :
from sentence_transformers import SentenceTransformer
import numpy as np
def get_embeddings(text_chunks):
model = SentenceTransformer('all-MiniLM-L6-v2')
embeddings = model.encode(text_chunks, show_progress_bar=True)
return np.array(embeddings)
Chaque embedding ainsi créé contient une représentation numérique des mots, capturant ainsi leurs nuances. Mais le travail ne s’arrête pas là. Pour pouvoir effectuer des recherches efficaces parmi ces vecteurs, un moteur de recherche vectoriel comme FAISS (Facebook AI Similarity Search) s’impose. FAISS est conçu pour effectuer des recherches rapides dans des ensembles de vecteurs, en utilisant la métrique L2 pour trouver ceux qui sont les plus proches.
La construction d’un index FAISS est assez simple. Tout d’abord, vous devez définir une dimension qui correspond à la taille de vos embeddings. Ensuite, vous pouvez ajouter vos vecteurs à l’index et le sauvegarder. Cela vous permettra d’exécuter des recherches beaucoup plus rapidement et efficacement. En résumé, vous pourrez tirer parti de la puissance des embeddings et de la rapidité de FAISS pour améliorer la pertinence de vos résultats.
Voici un tableau récapitulatif des étapes et outils impliqués dans la conversion de texte en vecteurs d’embeddings :
| Étape | Outil |
|---|---|
| Générer des embeddings | SentenceTransformers |
| Rechercher des vecteurs proches | FAISS |
Pour aller plus loin sur le choix du modèle d’embedding approprié pour un système RAG, jetez un œil à cet article éducatif (choisir le bon modèle d’embedding).
Comment récupérer et combiner le contexte pertinent pour la génération ?
Pour récupérer et combiner le contexte pertinent, il est essentiel de transformer la requête utilisateur en un vecteur d’embedding, tout comme nous l’avons fait pour nos chunks de texte. En d’autres termes, chaque question posée par l’utilisateur doit être convertie en une forme que la machine peut comprendre. Cette conversion se fait grâce à des modèles d’embeddings, comme ceux proposés par SentenceTransformers.
Pour ce faire, commençons par charger l’index FAISS dans lequel nous avons préalablement stocké nos embeddings. Cet index nous permet de retrouver rapidement les chunks de texte les plus similaires à notre requête. Voici comment vous pouvez charger cet index :
import faiss
def load_faiss_index(index_path="faiss_index.index"):
"""
Charge l'index FAISS enregistré sur le disque.
"""
print("Chargement de l'index FAISS.")
return faiss.read_index(index_path)
Une fois l’index chargé, il faut également récupérer les métadonnées associées, c’est-à-dire les chunks de texte eux-mêmes. Cette étape est cruciale car, lorsque FAISS retourne les résultats de recherche, nous avons besoin de pouvoir les mapper à leurs textes originaux. Voici comment procéder :
import pickle
def load_metadata(metadata_path="faiss_metadata.pkl"):
"""
Charge les métadonnées contenant les chunks de texte.
"""
print("Chargement des métadonnées textuelles.")
with open(metadata_path, "rb") as f:
return pickle.load(f)
Avec notre index et nos métadonnées en main, nous pouvons maintenant réaliser une recherche vectorielle. Nous allons prendre la requête utilisateur, créer son embedding, et effectuer une recherche à l’aide de FAISS pour obtenir les chunks les plus pertinents :
def retrieve_similar_chunks(query, index, text_chunks, top_k=3):
"""
Récupère les top_k chunks les plus pertinents pour une requête donnée.
"""
# Embedding de la requête
model = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')
query_vector = model.encode([query]).astype('float32')
# Recherche FAISS pour les vecteurs les plus proches
distances, indices = index.search(query_vector, top_k)
print(f"Récupération des top {top_k} chunks similaires.")
return [text_chunks[i] for i in indices[0]]
Les chunks retournés sont ensuite combinés en un seul bloc contextuel, riche et cohérent, qui sera utilisé pour alimenter le modèle de langage. Cette étape d’assemblage a un impact direct sur la qualité et la fiabilité des réponses finales fournies par le système. En fournissant un contexte solide basé sur des informations pertinentes, nous augmentons les chances que le modèle génère une réponse précise et utile.
Retrouver un contexte approprié et bien assemblé est donc tout aussi crucial que d’utiliser un bon modèle de langage. Pour en savoir plus sur les systèmes RAG et leur potentiel, consultez cet article.
Comment générer une réponse précise avec un modèle de langage à partir du contexte ?
Le contexte que nous avons extrait est maintenant prêt à être joint à la question de l’utilisateur, formant ainsi un prompt complet. Cela dit, une bonne approche pour la génération de réponses consiste à utiliser un modèle de langage open-source tel que TinyLlama, offrant une flexibilité sans les coûts exorbitants que l’on pourrait rencontrer avec des solutions commerciales. Pour ce faire, nous allons utiliser la librairie Transformers de Hugging Face, qui rend l’intégration de ce genre de modèles à la fois simple et efficace.
Commençons par charger le tokenizer et le modèle. Cela nous permettra de préparer notre prompt correctement. Voici un extrait de code illustratif :
from transformers import AutoTokenizer, AutoModelForCausalLM
import torch
# Charger le modèle et le tokenizer
model_name = "TinyLlama/TinyLlama-1.1B-Chat-v1.0"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(model_name, torch_dtype=torch.float16, device_map="auto")
Une fois le tokenizer et le modèle chargés, passons à la génération de réponses. Pour ce faire, nous combinerons le contexte et la question dans un format structuré qu’un modèle de langage comme TinyLlama peut comprendre. En voici un exemple :
prompt = f"""
Context:
{context}
Question:
{query}
Answer:
"""
# Générer la réponse
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
with torch.no_grad():
outputs = model.generate(**inputs, max_new_tokens=200, pad_token_id=tokenizer.eos_token_id)
# Décoder la réponse générée
full_text = tokenizer.decode(outputs[0], skip_special_tokens=True)
answer = full_text.split("Answer:")[1].strip() if "Answer:" in full_text else full_text.strip()
Cette approche permet d’ancrer les réponses générées dans le contexte des données préalablement extraites. En épousant ce contexte, on limite considérablement les risques d’erreurs et d’hallucinations, des problèmes courants avec les modèles de langage lorsqu’ils tentent de répondre sans informations pertinentes.
Il est aussi bon de noter que si vous êtes en phase de déploiement en production, il existe plusieurs variantes possibles des modèles adaptés à vos besoins spécifiques, qu’il s’agisse de réglages fins, d’optimisation pour un type de question particulier ou même l’utilisation de modèles commerciaux qui pourraient fournir des performances optimisées selon vos ressources. Pour plus de détails sur le fonctionnement du RAG, vous pouvez jeter un œil ici.
Pourquoi intégrer un système RAG est-il un atout incontournable aujourd’hui ?
Le RAG réconcilie les faiblesses classiques des modèles de langage en leur apportant un accès direct à des données actualisées et ciblées. En combinant rigoureusement préparation, vectorisation, recherche efficace et génération adaptée, vous obtenez un système fiable et agile. Ce processus, bien que simple à coder, ouvre la porte à des applications métiers puissantes, offrant des réponses précises sans hallucinations inutiles. En maîtrisant ce pipeline, vous gagnez en contrôle sur l’IA tout en maximisant la pertinence et la confiance de ses réponses, un avantage stratégique pour tout projet impliquant des données spécifiques et évolutives.
FAQ
Qu’est-ce que le système RAG apporte aux modèles de langage ?
Pourquoi fragmenter les documents avant le traitement ?
Comment fonctionne la comparaison vectorielle dans RAG ?
Peut-on utiliser n’importe quel modèle de langage dans un pipeline RAG ?
Quels sont les cas d’usage concrets du RAG en entreprise ?
A propos de l’auteur
Franck Scandolera, expert en Analytics Engineering et formateur reconnu, accompagne depuis plus de dix ans les professionnels dans l’automatisation, la data engineering et l’intégration d’IA générative dans les systèmes métiers. Responsable de l’agence webAnalyste et formateur indépendant, il maîtrise la conception de pipelines data robustes et l’usage avancé de RAG et langages de programmation pour booster la précision des outils d’aide à la décision. Son expertise terrain lui permet d’allier technique pointue et pragmatisme pour déployer des solutions IA au plus près des besoins réels des entreprises.
⭐ Analytics engineer, Data Analyst et Automatisation IA indépendant ⭐
- Ref clients : Logis Hôtel, Yelloh Village, BazarChic, Fédération Football Français, Texdecor…
Mon terrain de jeu :
- Data Analyst & Analytics engineering : tracking avancé (GTM server, e-commerce, CAPI, RGPD), entrepôt de données (BigQuery, Snowflake, PostgreSQL, ClickHouse), modèles (Airflow, dbt, Dataform), dashboards décisionnels (Looker, Power BI, Metabase, SQL, Python).
- Automatisation IA des taches Data, Marketing, RH, compta etc : conception de workflows intelligents robustes (n8n, App Script, scraping) connectés aux API de vos outils et LLM (OpenAI, Mistral, Claude…).
- Engineering IA pour créer des applications et agent IA sur mesure : intégration de LLM (OpenAI, Mistral…), RAG, assistants métier, génération de documents complexes, APIs, backends Node.js/Python.





