Un système RAG (Retrieval-Augmented Generation) combine recherche documentaire et intelligence artificielle pour fournir des réponses précises et à jour. Cet article détaille en 7 étapes pratiques comment bâtir un RAG efficace, de la préparation des données à la génération finale par un LLM.
3 principaux points à retenir.
- RAG associe recherche de contenu et génération d’IA pour limiter les hallucinations et garantir des réponses contextualisées.
- Le découpage en chunks et embeddings sémantiques sont essentiels pour trouver rapidement l’information pertinente.
- L’intégration avec un LLM open-source transforme les documents internes en réponses dynamiques et fiables.
Qu’est-ce qu’un système RAG et comment fonctionne-t-il ?
Un système RAG, ou Retrieval-Augmented Generation, est une architecture révolutionnaire qui enrichit la capacité des modèles de langage en intégrant des données externes à jour. Imaginez un ordinateur qui non seulement se souvient de toutes les informations qu’il a déjà intégrées, mais qui peut aussi se tourner vers des documents récents ou des données spécifiques lors d’une question complexe. C’est de cette manière que fonctionne un système RAG. Il repose sur deux composants fondamentaux : le « retriever » et le « générateur ».
Le rôle du retriever est crucial. Il scanne une base de données contenant des passages de texte pour extraire des informations pertinentes. Ce processus n’est pas aléatoire : le système analyse le contexte de la question posée pour trouver les réponses les plus adaptées. Par exemple, si vous demandez des informations sur les propriétés des arbres, le retriever cherchera des extraits spécifiques des documents en lien avec le sujet, tel que des études sur la photosynthèse ou des informations sur la lutte contre la déforestation.
Une fois cette extraction faite, c’est au tour du générateur de jouer son rôle. Ce composant est un modèle de langage entraîné qui utilise les extraits fournis par le retriever. Ce dernier va ensuite reformuler ces informations pour créer une réponse cohérente et naturelle. Ainsi, la réponse à la question initiale est fondée non seulement sur la connaissance ancienne du modèle, mais aussi sur des données actuelles et pertinentes. Ce processus assure que les réponses sont non seulement précises mais aussi à jour.
Dans le contexte d’une interaction avec un utilisateur, cela fonctionne comme suit : lorsque vous posez une question, le système active d’abord le retriever pour extraire les passages les plus proches en termes de signification. Ces passages sont ensuite présentés au générateur, qui élabore une réponse finale. Ce mariage entre recherche d’informations et génération de texte offre un niveau de précision hors pair. La prochaine fois que vous utiliserez cette technologie, pensez à cette dynamique entre ces deux acteurs essentiels. Pour plus d’informations sur le fonctionnement de RAG, consultez également cet article ici.
Comment préparer et segmenter les données pour un RAG ?
Pour construire un système RAG efficace, la première étape cruciale consiste à charger et nettoyer vos documents textuels. Imaginez : si vos données sont en désordre, le modèle risque de récupérer des passages erronés, augmentant ainsi le risque d’hallucination. Pour pallier cela, voici comment procéder efficacement : commencez par créer un script Python qui va aller chercher vos fichiers textes et les encapsuler dans un tableau. Voici un exemple de code à utiliser :
import os
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
Ce code permet de récupérer tous vos fichiers .txt situés dans un répertoire spécifique. Une fois les données chargées, il est crucial de les nettoyer. Par exemple, vous pourriez vouloir éliminer les sauts de ligne superflus ou les caractères non désirés. Cela permet d’avoir un texte propre et prêt à être utilisé pour l’apprentissage. Voici un autre exemple de script pour nettoyer le texte :
import re
def clean_text(text: str) -> str:
text = re.sub(r'\s+', ' ', text)
text = re.sub(r'[^\x00-\x7F]+', ' ', text)
return text.strip()
Cela va considérablement améliorer la qualité de vos données. Ensuite, il est temps de segmenter vos documents en morceaux courts, appelés « chunks ». C’est essentiel, car un modèle de langage a une fenêtre contextuelle limitée pour traiter des textes. Pour cette étape, utilisez un splitter comme le LangChain RecursiveCharacterTextSplitter. Voici comment procéder :
from langchain.text_splitter import RecursiveCharacterTextSplitter
def split_docs(documents, chunk_size=500, chunk_overlap=100):
splitter = RecursiveCharacterTextSplitter(
chunk_size=chunk_size,
chunk_overlap=chunk_overlap
)
chunks = splitter.create_documents(documents)
return chunks
Le chevauchement entre les chunks est primordial, car il aide à conserver la cohérence du contenu, surtout à la frontière des morceaux. Avec une taille de chunk typique entre 300 et 500 mots et un chevauchement ajusté, le modèle peut mieux saisir le sens des passages. Si vous voulez en découvrir davantage sur l’utilisation de RAG, n’hésitez pas à jeter un œil à cet article.
Comment créer et stocker des embeddings pour la recherche rapide ?
Pour que notre modèle comprenne le texte, il faut impérativement transformer ces mots en chiffres. C’est là qu’entrent en scène les embeddings, qui sont des représentations numériques permettant de capturer les nuances sémantiques des textes. Grâce à des modèles comme SentenceTransformers, on peut générer ces embeddings à partir de nos chunks de texte, rendant la machine capable d’appréhender le sens et les relations entre les contenus.
Dans un premier temps, nous allons créer nos embeddings à l’aide de la bibliothèque SentenceTransformers. Pour ce faire, sauvegardons ce code dans un fichier nommé create_embeddings.py :
from sentence_transformers import SentenceTransformer
import numpy as np
def get_embeddings(text_chunks):
# Charger le modèle d'embeddings
model = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')
print(f"Création d'embeddings pour {len(text_chunks)} chunks :")
embeddings = model.encode(text_chunks, show_progress_bar=True)
print(f"Dimensions des embeddings : {embeddings.shape}")
return np.array(embeddings)
Une fois les embeddings créés, l’étape suivante consiste à les indexer dans une base dédiée à la recherche rapide, comme FAISS (Facebook AI Similarity Search). À travers FAISS, nous allons pouvoir effectuer des recherches par similarité à la vitesse de la lumière en interrogeant nos données vectorielles. Voici le code pour créer et sauvegarder un index FAISS, que vous allez placer dans un fichier nommé store_faiss.py :
import faiss
import numpy as np
import pickle
def build_faiss_index(embeddings, save_path="faiss_index"):
"""
Construit l'index FAISS et le sauvegarde.
"""
dim = embeddings.shape[1]
print(f"Construction de l'index FAISS avec dimension : {dim}")
# Utiliser un index L2 simple
index = faiss.IndexFlatL2(dim)
index.add(embeddings.astype('float32'))
# Sauvegarder l'index FAISS
faiss.write_index(index, f"{save_path}.index")
print(f"Index FAISS sauvegardé à {save_path}.index")
return index
Pour finaliser, il est également essentiel de conserver un lien entre les embeddings et le texte original, permettant ainsi de retracer n’importe quelle information. Ce lien est stocké par le biais d’un fichier de métadonnées. Voici comment procéder :
def save_metadata(text_chunks, path="faiss_metadata.pkl"):
"""
Sauvegarde la correspondance des positions vectorielles aux chunks de texte.
"""
with open(path, "wb") as f:
pickle.dump(text_chunks, f)
print(f"Métadonnées du texte sauvegardées à {path}")
Avec ces morceaux de code, nous avons jeté les bases d’un système robuste et efficace de recherche rapide basé sur des embeddings. Après tout, dans notre quête de données intelligentes, chaque détail compte, et la bonne indexation peut faire toute la différence dans des systèmes RAG performants. N’hésitez pas à explorer des solutions avancées sur Milvus pour élargir vos horizons !
Comment récupérer les informations pertinentes à une requête ?
Une fois que l’utilisateur soumet sa requête, il est temps d’entrer dans l’arène du traitement des données. La première étape consiste à transformer cette question en un vecteur, tout comme nous l’avons fait précédemment avec les chunks de texte. Pourquoi ? Parce que, soyons clairs, un ordinateur ne se contente pas d’écouter, il a besoin de comprendre numériquement. C’est ici que le modèle d’embedding entre en jeu, garantissant que la question est représentée dans un espace vectoriel où chaque nuance de sens peut être capturée.
Pour commencer, il faut charger l’index FAISS que nous avons créé lors des étapes précédentes. Cela nous permet d’accéder rapidement à nos données sans avoir à les réanalyser chaque fois. Ensuite, nous devons également charger les métadonnées, ces précieux morceaux de texte que nous avons stockés lors de la création de nos embeddings. Ces métadonnées nous fournissent le contenu réel qui sera consulté lorsque nous retrouverons des chunks similaires.
Voici comment cela fonctionne en pratique :
import faiss
import pickle
import numpy as np
from sentence_transformers import SentenceTransformer
def load_faiss_index(index_path="faiss_index.index"):
print("Loading FAISS index.")
return faiss.read_index(index_path)
def load_metadata(metadata_path="faiss_metadata.pkl"):
print("Loading text metadata.")
with open(metadata_path, "rb") as f:
return pickle.load(f)
def retrieve_similar_chunks(query, index, text_chunks, top_k=3):
model = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')
query_vector = model.encode([query]).astype('float32')
distances, indices = index.search(query_vector, top_k)
print(f"Retrieved top {top_k} similar chunks.")
return [text_chunks[i] for i in indices[0]]
Dans cet extrait de code, nous avons le chargement de l’index FAISS et des métadonnées. Ensuite, lors de l’appel à retrieve_similar_chunks, nous passons notre requête, l’index, les chunks de texte, ainsi que le nombre d’éléments les plus proches que nous souhaitons récupérer.
Cette technique nous permet d’extraire uniquement les passages utiles pour le contexte, minimisant ainsi le bruit et augmentant la pertinence des réponses générées. En procédant de cette manière, nos modèles de langage sont ancrés dans des données réelles et précises, ce qui réduit considérablement le risque de « hallucinations » que de nombreux outils d’IA souffrent actuellement.
N’oubliez pas, l’acquisition d’informations pertinentes ne sera qu’un récital de chiffres et de texto sans le bon modèle d’embedding et une recherche efficace. Si vous voulez approfondir davantage ce sujet, vous pourriez vouloir explorer des liens externes pour élever votre compréhension. Par exemple, cette ressource peut fournir des éclaircissements précieux sur l’implémentation des systèmes RAG.
Comment générer une réponse à partir des passages récupérés ?
Une fois que vous avez assemblé les passages les plus pertinents en un contexte unique, il est temps de passer à l’étape suivante : générer une réponse à partir de ce contexte! Ce processus se repose sur l’ajout de ce contexte à la requête utilisateur pour former un prompt complet. Ce prompt est ensuite envoyé à un modèle de langage (LLM) open-source, tel que ceux disponibles via Hugging Face, permettant de concevoir une réponse cohérente, pertinente et contextualisée.
Imaginons que nous ayons une requête utilisateur : « Quel est le rôle de l’apprentissage non supervisé? ». Après avoir récupéré les trois passages les plus pertinents concernant l’apprentissage non supervisé, nous allons les combiner pour créer un contexte :
context = """
Dans l'apprentissage non supervisé, le modèle est entraîné sur des données non étiquetées.
Les algorithmes populaires incluent K-Means et les autoencodeurs.
Il détecte automatiquement les structures sous-jacentes dans les données, rendant son utilisation utile pour la détection d'anomalies et le clustering clients.
"""
Nous formons maintenant le prompt en intégrant ce contexte et la requête :
prompt = f"""
Contexte:
{context}
Question:
Quel est le rôle de l'apprentissage non supervisé ?
Réponse:
"""
Nous chargeons ensuite le modèle et générons la réponse :
from transformers import AutoTokenizer, AutoModelForCausalLM
import torch
model_name = "TinyLlama/TinyLlama-1.1B-Chat-v1.0"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(model_name)
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)
response = tokenizer.decode(outputs[0], skip_special_tokens=True).split("Réponse:")[1].strip()
print(response)
Ce code permet de transformer un prompt en une réponse structurée, en veillant à bien retirer tout ce qui précède « Réponse: » pour obtenir un résultat propre. La génération de réponses à partir de passages pertinents garantit non seulement la précision mais aussi la pertinence des informations fournies.
Pour illustrer le comportement du pipeline dans son ensemble, considérons le schéma ci-dessous qui résume l’ensemble du processus : préparation, découpage, embeddings, indexation, recherche et génération.
- Préparation des données
- Découpage en morceaux
- Création d’embeddings
- Indexation des vecteurs
- Recherche de passages pertinents
- Génération de réponse
À ce stade, vous avez un système RAG en place et fonctionnel, capable de fournir des réponses fondées sur des données pertinentes et contextualisées. Pour une exploration approfondie des systèmes RAG et de leur application, vous pouvez consulter une ressource précieuse ici : Building a RAG System with LangChain and FastAPI.
Prêt à déployer votre propre système RAG performant ?
Construire un système RAG simple en 7 étapes, de la préparation de données jusqu’à la génération de réponses contextualisées avec un LLM, est aujourd’hui accessible. Cette approche hybride corrige les limites des grands modèles isolés, comme leurs hallucinations, en intégrant vos contenus privés et actualisés. L’apprentissage progressif et la modularité de ce pipeline facilitent l’adaptation à vos besoins métiers ou projets. En maîtrisant ces fondations, vous bénéficiez d’une IA enrichie, fiable et personnalisée qui valorise vos données internes et améliore la pertinence des réponses à vos utilisateurs.
FAQ
Qu’est-ce qu’un système RAG ?
Pourquoi découper les documents en chunks ?
Quel rôle jouent les embeddings dans un RAG ?
Peut-on utiliser n’importe quel LLM avec un système RAG ?
Quels bénéfices apporte le RAG en entreprise ?
A propos de l’auteur
Franck Scandolera, fondateur de l’agence webAnalyste et formateur chez Formations Analytics, est expert en intégration et automatisation data, IA générative et RAG depuis plusieurs années. Fort de son expérience en architecture data cloud, tracking RGPD-compliant et développement de pipelines intelligents, il accompagne professionnellement entreprises et agences en France, Suisse et Belgique pour transformer données et IA en outils business concrets et performants.
⭐ Expert et formateur en Tracking avancé, Analytics Engineering et Automatisation IA (n8n, Make) ⭐
Ref clients : Logis Hôtel, Yelloh Village, BazarChic, Fédération Football Français, Texdecor…
Mon terrain de jeu :
Data & Analytics engineering : tracking propre RGPD, entrepôt de données (GTM server, BigQuery…), modèles (dbt/Dataform), dashboards décisionnels (Looker, SQL, Python).
Automatisation IA des taches Data, Marketing, RH, compta etc : conception de workflows intelligents robustes (n8n, Make, 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.






