Vous avez un assistant IA brillant, mais il oublie tout à chaque conversation. Frustrant, non ? C'est le problème fondamental des LLM : ils n'ont pas de mémoire persistante. Le RAG (Retrieval-Augmented Generation) est LA solution pour donner une mémoire à votre IA.
Dans ce guide, on va démystifier le RAG en termes simples, comprendre comment ça marche sous le capot, et surtout : savoir quand c'est utile et quand c'est overkill.
🧠 C'est quoi le RAG ? (En termes simples)
Le problème
Imaginez un expert brillant qui a lu des millions de livres... mais qui ne peut pas consulter VOS documents. Vous lui demandez "Quel est le chiffre d'affaires de mon entreprise au Q3 ?" et il répond "Je n'ai pas cette information."
C'est exactement ce qui se passe avec les LLM classiques (ChatGPT, Claude, Gemini) :
- Ils connaissent des connaissances générales (ce qu'ils ont appris pendant l'entraînement)
- Ils ne connaissent PAS vos données (documents, notes, base de données)
- Leur connaissance est figée dans le temps (date de coupure)
La solution RAG
RAG = Retrieval-Augmented Generation = Génération Augmentée par la Recherche.
En français simple : avant de répondre, l'IA va chercher les informations pertinentes dans VOS documents, puis les utilise pour formuler sa réponse.
Sans RAG :
Question --> LLM --> Réponse (basée sur ses connaissances générales)
Avec RAG :
Question --> Recherche dans vos docs --> Documents pertinents
|
v
Question + Documents pertinents --> LLM --> Réponse précise et sourcée
Analogie simple
Pensez à un étudiant passant un examen :
- Sans RAG : il répond de mémoire (parfois il se trompe ou invente)
- Avec RAG : il a le droit de consulter ses fiches de révision avant de répondre
Le RAG ne rend pas l'IA plus intelligente -- il lui donne accès à vos informations.
🔢 Les embeddings : transformer du texte en vecteurs
Le concept clé
Pour que l'IA puisse "chercher" dans vos documents, il faut d'abord transformer le texte en quelque chose que l'ordinateur peut comparer efficacement : des vecteurs (des listes de nombres).
# Conceptuellement, un embedding transforme du texte en nombres
"Le chat dort sur le canapé" --> [0.23, -0.45, 0.78, 0.12, ...]
"Le félin se repose au salon" --> [0.21, -0.43, 0.76, 0.14, ...]
"Python est un langage" --> [-0.67, 0.34, -0.12, 0.89, ...]
Les deux premières phrases ont des vecteurs proches (elles parlent du même sujet). La troisième est éloignée (sujet différent).
Comment ça marche ?
Un modèle d'embedding (comme text-embedding-3-small d'OpenAI) a été entraîné sur des milliards de textes pour comprendre la sémantique. Il ne compare pas les mots un par un -- il comprend le sens.
import httpx
import os
async def get_embedding(text: str) -> list[float]:
"""Transforme du texte en vecteur via OpenAI."""
async with httpx.AsyncClient() as client:
response = await client.post(
"https://api.openai.com/v1/embeddings",
headers={"Authorization": f"Bearer {os.getenv('OPENAI_API_KEY')}"},
json={
"model": "text-embedding-3-small",
"input": text,
},
)
response.raise_for_status()
return response.json()["data"][0]["embedding"]
Comparaison de similarité
Une fois que vous avez des vecteurs, comparer deux textes revient à calculer la distance entre leurs vecteurs :
import numpy as np
def cosine_similarity(vec_a: list[float], vec_b: list[float]) -> float:
"""Calcule la similarite cosinus entre deux vecteurs."""
a = np.array(vec_a)
b = np.array(vec_b)
return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))
# Exemple
sim = cosine_similarity(
embedding_chat, # "Le chat dort sur le canape"
embedding_felin # "Le felin se repose au salon"
)
# sim ≈ 0.95 (tres similaire!)
sim = cosine_similarity(
embedding_chat, # "Le chat dort sur le canape"
embedding_python # "Python est un langage"
)
# sim ≈ 0.12 (pas du tout similaire)
| Score de similarité | Signification |
|---|---|
| 0.90 - 1.00 | Quasi identique |
| 0.75 - 0.90 | Très similaire |
| 0.50 - 0.75 | Lié au sujet |
| 0.25 - 0.50 | Peu de rapport |
| 0.00 - 0.25 | Aucun rapport |
🗄️ Vector databases : où stocker les embeddings
Une fois vos textes transformés en vecteurs, il faut les stocker quelque part et pouvoir les rechercher efficacement. C'est le rôle des bases de données vectorielles.
Comparatif des solutions
| Solution | Type | Complexité | Performance | Self-hosted | Idéal pour |
|---|---|---|---|---|---|
| FAISS | Librairie Python | Facile | Excellente | Oui | Prototypage, petits datasets |
| ChromaDB | Base embarquée | Facile | Bonne | Oui | Side projects, <1M docs |
| Qdrant | Serveur dédié | Moyenne | Excellente | Oui | Production, gros volumes |
| pgvector | Extension Postgres | Moyenne | Bonne | Oui | Si vous avez déjà Postgres |
| Pinecone | Cloud managé | Facile | Excellente | Non | Si vous ne voulez pas gérer l'infra |
| Weaviate | Serveur dédié | Complexe | Excellente | Oui | Cas avancés, multimodal |
FAISS : le plus simple pour commencer
import faiss
import numpy as np
# Creer un index FAISS
dimension = 1536 # Taille des embeddings OpenAI
index = faiss.IndexFlatL2(dimension)
# Ajouter des vecteurs
vectors = np.array(embeddings_list).astype('float32')
index.add(vectors)
# Rechercher les 5 plus proches
query_vector = np.array([query_embedding]).astype('float32')
distances, indices = index.search(query_vector, k=5)
print(f"Documents les plus pertinents : {indices[0]}")
print(f"Distances : {distances[0]}")
ChromaDB : la base embarquée user-friendly
import chromadb
# Creer/ouvrir une collection
client = chromadb.PersistentClient(path="/home/deploy/data/chroma")
collection = client.get_or_create_collection(
name="mes_documents",
metadata={"hnsw:space": "cosine"}
)
# Ajouter des documents (ChromaDB genere les embeddings automatiquement)
collection.add(
documents=[
"Guide complet du self-hosting",
"Comment configurer un reverse proxy",
"Les bases de la securite serveur",
],
ids=["doc1", "doc2", "doc3"],
metadatas=[
{"source": "blog", "date": "2025-01"},
{"source": "blog", "date": "2025-02"},
{"source": "wiki", "date": "2025-01"},
],
)
# Rechercher
results = collection.query(
query_texts=["comment securiser mon serveur"],
n_results=3,
)
print(results["documents"])
# --> [["Les bases de la securite serveur", ...]]
Qdrant : pour la production
from qdrant_client import QdrantClient
from qdrant_client.models import VectorParams, Distance, PointStruct
# Connexion
client = QdrantClient(host="localhost", port=6333)
# Creer une collection
client.create_collection(
collection_name="documents",
vectors_config=VectorParams(
size=1536,
distance=Distance.COSINE,
),
)
# Ajouter des documents
client.upsert(
collection_name="documents",
points=[
PointStruct(
id=1,
vector=embedding_1,
payload={"text": "Guide du self-hosting", "source": "blog"},
),
PointStruct(
id=2,
vector=embedding_2,
payload={"text": "Config reverse proxy", "source": "blog"},
),
],
)
# Rechercher
results = client.search(
collection_name="documents",
query_vector=query_embedding,
limit=5,
)
for result in results:
print(f"Score: {result.score:.3f} | {result.payload['text']}")
🔄 Pipeline complet : de l'ingestion à la réponse
Voici le pipeline RAG complet, étape par étape :
1. INGESTION 2. CHUNKING 3. EMBEDDING
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Documents │──────>│ Découper │──────>│ Vectoriser│
│ (PDF, MD, │ │ en chunks│ │ chaque │
│ TXT...) │ │ de ~500 │ │ chunk │
└──────────┘ │ tokens │ └─────┬─────┘
└──────────┘ │
v
4. STOCKAGE 5. RETRIEVAL 6. GENERATION
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Vector DB │<──── │ Chercher │──────>│ LLM + │
│ (Qdrant, │ │ les plus │ │ contexte │
│ Chroma) │─────>│ proches │ │ = Reponse│
└──────────┘ └──────────┘ └──────────┘
Étape 1 : Ingestion
Récupérer vos documents depuis toutes les sources :
from pathlib import Path
def load_documents(directory: str) -> list[dict]:
"""Charge tous les documents d'un repertoire."""
docs = []
for path in Path(directory).rglob("*"):
if path.suffix in (".md", ".txt", ".py", ".json"):
content = path.read_text(encoding="utf-8")
docs.append({
"content": content,
"source": str(path),
"type": path.suffix,
})
return docs
documents = load_documents("/home/deploy/knowledge")
print(f"{len(documents)} documents charges")
Étape 2 : Chunking (découpage)
Les LLM ont une limite de contexte. On découpe les gros documents en morceaux digestibles :
def chunk_text(
text: str,
chunk_size: int = 500,
overlap: int = 50
) -> list[str]:
"""Decoupe un texte en chunks avec chevauchement."""
words = text.split()
chunks = []
for i in range(0, len(words), chunk_size - overlap):
chunk = " ".join(words[i:i + chunk_size])
if chunk.strip():
chunks.append(chunk)
return chunks
# Exemple
text = "Un tres long document..."
chunks = chunk_text(text, chunk_size=500, overlap=50)
print(f"{len(chunks)} chunks crees")
| Paramètre | Valeur recommandée | Impact |
|---|---|---|
| chunk_size | 300-800 tokens | Trop petit = perte de contexte, trop grand = bruit |
| overlap | 50-100 tokens | Assure la continuité entre chunks |
| Séparateur | Paragraphes > phrases > mots | Respecte la structure du texte |
Pour aller plus loin sur ce sujet, consultez notre guide Piloter ses projets depuis Telegram avec l'IA.
Étape 3 : Embedding
Pour aller plus loin sur ce sujet, consultez notre guide L'IA comme second cerveau : organiser ses idées.
Transformer chaque chunk en vecteur :
async def embed_chunks(chunks: list[str]) -> list[list[float]]:
"""Genere les embeddings pour une liste de chunks."""
embeddings = []
# Traiter par batch de 100 (limite API)
for i in range(0, len(chunks), 100):
batch = chunks[i:i+100]
async with httpx.AsyncClient() as client:
response = await client.post(
"https://api.openai.com/v1/embeddings",
headers={"Authorization": f"Bearer {os.getenv('OPENAI_API_KEY')}"},
json={
"model": "text-embedding-3-small",
"input": batch,
},
timeout=30.0,
)
response.raise_for_status()
batch_embeddings = [
item["embedding"]
for item in response.json()["data"]
]
embeddings.extend(batch_embeddings)
return embeddings
Étape 4 : Stockage
Insérer dans la vector database (ici ChromaDB) :
collection.add(
documents=chunks,
embeddings=embeddings,
ids=[f"chunk_{i}" for i in range(len(chunks))],
metadatas=[
{"source": doc["source"], "chunk_index": i}
for i, doc in enumerate(chunk_metadata)
],
)
Étape 5 : Retrieval (recherche)
Quand l'utilisateur pose une question, on cherche les chunks pertinents :
async def retrieve(question: str, n_results: int = 5) -> list[str]:
"""Retrouve les chunks les plus pertinents pour une question."""
results = collection.query(
query_texts=[question],
n_results=n_results,
)
return results["documents"][0]
# Exemple
chunks = await retrieve("Comment securiser mon VPS ?")
# --> ["Les bases de la securite serveur...", "UFW et fail2ban...", ...]
Étape 6 : Génération
On envoie la question ET les chunks pertinents au LLM :
async def rag_answer(question: str) -> str:
"""Repond a une question en utilisant le RAG."""
# 1. Retrouver les documents pertinents
relevant_chunks = await retrieve(question, n_results=5)
# 2. Construire le prompt avec le contexte
context = "\n\n---\n\n".join(relevant_chunks)
prompt = f"""Reponds a la question en te basant UNIQUEMENT
sur le contexte fourni. Si le contexte ne contient pas
l'information, dis-le clairement.
CONTEXTE :
{context}
QUESTION : {question}
REPONSE :"""
# 3. Appeler le LLM
async with httpx.AsyncClient() as client:
response = await client.post(
"https://openrouter.ai/api/v1/chat/completions",
headers={
"Authorization": f"Bearer {os.getenv('OPENROUTER_API_KEY')}",
},
json={
"model": "anthropic/claude-sonnet-4-20250514",
"messages": [{"role": "user", "content": prompt}],
"max_tokens": 1000,
},
timeout=60.0,
)
response.raise_for_status()
return response.json()["choices"][0]["message"]["content"]
# Utilisation
answer = await rag_answer("Comment securiser mon VPS ?")
print(answer)
🪶 Alternatives simples au RAG
Le RAG complet (embeddings + vector DB + pipeline) n'est pas toujours nécessaire. Voici des alternatives plus simples qui marchent étonnamment bien.
1. Fichiers Markdown dans le contexte
La méthode la plus simple : injecter directement vos fichiers dans le prompt.
# Charger le contexte depuis des fichiers
context_files = [
"MEMORY.md", # Memoire long-terme
"PROJECT.md", # Info projet
"NOTES.md", # Notes diverses
]
context = ""
for f in context_files:
with open(f) as fh:
context += f"\n## {f}\n{fh.read()}\n"
prompt = f"""Contexte :\n{context}\n\nQuestion : {question}"""
Avantages : zéro infrastructure, fonctionne immédiatement
Limites : limité par la fenêtre de contexte du LLM (~100-200k tokens)
2. MEMORY.md : la mémoire fichier
C'est l'approche utilisée par OpenClaw : un fichier Markdown qui sert de mémoire persistante.
# MEMORY.md
## Preferences utilisateur
- Prefere les reponses concises
- Utilise Python 3.11
- VPS chez Hostinger, Ubuntu 24.04
## Projets en cours
- AI-master.dev : site de guides IA en francais
- API interne : FastAPI + SQLite
## Decisions prises
- 2025-02-20 : Choisi Caddy plutot que Nginx
- 2025-02-22 : Migration vers ChromaDB pour le RAG
L'IA lit ce fichier à chaque conversation et le met à jour quand il y a de nouvelles infos à retenir.
3. Recherche par mots-clés (memory_search)
Plutôt que des embeddings, une recherche textuelle simple :
import re
def memory_search(query: str, documents: list[dict], top_k: int = 5) -> list[dict]:
"""Recherche simple par mots-cles dans les documents."""
keywords = set(query.lower().split())
scored = []
for doc in documents:
text = doc["content"].lower()
# Compter les mots-cles presents
score = sum(1 for kw in keywords if kw in text)
if score > 0:
scored.append((score, doc))
scored.sort(key=lambda x: x[0], reverse=True)
return [doc for _, doc in scored[:top_k]]
Avantages : pas besoin d'API d'embedding, gratuit, rapide
Limites : pas de compréhension sémantique ("voiture" ne matchera pas "automobile")
Tableau comparatif
| Méthode | Complexité | Coût | Précision | Échelle |
|---|---|---|---|---|
| Fichiers dans le contexte | Nulle | Gratuit | Bonne (si tout tient) | <100 pages |
| MEMORY.md | Nulle | Gratuit | Bonne | <50 pages |
| Recherche mots-clés | Faible | Gratuit | Moyenne | <10k docs |
| RAG complet (embeddings) | Moyenne | ~0.01$/1k chunks | Excellente | Illimitée |
| RAG + reranking | Élevée | ~0.02$/1k chunks | Optimale | Illimitée |
⚖️ Quand le RAG est overkill vs indispensable
Le RAG est OVERKILL quand...
| Situation | Alternative recommandée |
|---|---|
| Moins de 50 pages de contexte | Fichiers dans le prompt |
| Données qui changent rarement | MEMORY.md |
| Questions simples et prévisibles | Recherche mots-clés |
| Prototype / MVP | Context injection |
| Budget zéro | Fichiers Markdown |
Le RAG est INDISPENSABLE quand...
| Situation | Pourquoi |
|---|---|
| Des milliers de documents | Ne tient plus dans le contexte |
| Base de connaissances qui évolue | Mise à jour incrémentale |
| Questions imprévisibles | Besoin de recherche sémantique |
| Précision critique | Sources vérifiables |
| Multi-utilisateurs | Chacun ses documents |
| Données techniques denses | L'IA doit trouver l'aiguille dans la botte de foin |
Le test simple
Posez-vous cette question :
"Est-ce que TOUTES mes données tiennent dans le contexte du LLM ?"
- Oui (< 100k tokens, soit ~75k mots) --> Pas besoin de RAG, injectez directement
- Non --> RAG nécessaire
🏗️ RAG dans OpenClaw
OpenClaw intègre nativement des mécanismes de mémoire qui utilisent les principes du RAG sans la complexité :
- MEMORY.md : mémoire long-terme, injectée automatiquement
- memory_search : recherche dans les notes quotidiennes
- Fichiers de contexte : AGENTS.md, TOOLS.md, etc.
Pour la plupart des cas d'usage personnels, cette approche fichiers + recherche suffit largement. Le RAG complet avec vector database devient utile quand vous passez à l'échelle.
Pour configurer la mémoire d'OpenClaw, consultez notre guide Configurer OpenClaw.
🎯 Résumé : par où commencer ?
| Votre situation | Notre recommandation |
|---|---|
| Débutant, petit projet | MEMORY.md + fichiers contexte |
| Projet moyen, <1000 docs | ChromaDB + embeddings |
| Production, gros volume | Qdrant + pipeline complet |
| Utilisateur OpenClaw | Utilisez la mémoire native d'abord |
Les étapes concrètes
- Commencez simple : MEMORY.md et fichiers de contexte
- Quand ça ne suffit plus : ajoutez ChromaDB pour la recherche sémantique
- Quand vous passez à l'échelle : migrez vers Qdrant avec un pipeline d'ingestion
- Optimisez : ajoutez du reranking, du chunking intelligent, des métadonnées
Le RAG n'est pas magique -- c'est juste un moyen élégant de donner à votre IA accès à VOS informations. Commencez simple, complexifiez seulement quand c'est nécessaire.
📚 Articles liés
- Qu'est-ce qu'OpenClaw ? -- L'agent IA avec mémoire intégrée
- Configurer OpenClaw -- Personnaliser la mémoire et le contexte
- APIs IA : OpenRouter vs appels directs -- Les APIs pour générer les embeddings et les réponses
- Claude Anthropic -- Le LLM recommandé pour le RAG
- VPS + IA : le setup complet -- Héberger votre pipeline RAG
- Automatiser sa vie avec l'IA -- Automatiser l'ingestion de documents