📑 Table des matières

RAG pour les nuls : donner de la mémoire à son IA

Productivité IA 🟡 Intermédiaire ⏱️ 16 min de lecture 📅 2026-02-24

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.

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é :

  1. MEMORY.md : mémoire long-terme, injectée automatiquement
  2. memory_search : recherche dans les notes quotidiennes
  3. 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

  1. Commencez simple : MEMORY.md et fichiers de contexte
  2. Quand ça ne suffit plus : ajoutez ChromaDB pour la recherche sémantique
  3. Quand vous passez à l'échelle : migrez vers Qdrant avec un pipeline d'ingestion
  4. 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