Wiki IA
LLM et IA Générative

RAG - Retrieval Augmented Generation

Augmenter les LLM avec des sources externes

Le RAG (Retrieval Augmented Generation) combine la puissance des LLM avec la recherche d'information pour fournir des réponses précises basées sur des sources spécifiques.

Pourquoi le RAG ?

Les limites des LLM "nus"

ProblèmeDescription
Connaissances figéesDonnées d'entraînement avec une date limite
Pas de données privéesNe connaît pas vos documents internes
HallucinationsPeut inventer des informations
Pas de sourcesDifficile de vérifier les réponses

La solution RAG

Question de l'utilisateur

┌─────────────────────────┐
│   RETRIEVAL (Recherche) │  ← Chercher dans vos documents
└───────────┬─────────────┘

     Documents pertinents

┌─────────────────────────┐
│   AUGMENTED (Contexte)  │  ← Enrichir le prompt
└───────────┬─────────────┘

┌─────────────────────────┐
│  GENERATION (LLM)       │  ← Générer avec contexte
└───────────┬─────────────┘

     Réponse + Sources

Architecture RAG

Pipeline complet

┌─────────────────────────────────────────────────────────────┐
│                    PIPELINE RAG                              │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  INDEXATION (offline)                                       │
│  ───────────────────                                        │
│  Documents → Chunking → Embedding → Vector DB               │
│                                                              │
│  INFÉRENCE (online)                                         │
│  ──────────────────                                         │
│  Query → Embedding → Similarity Search → Top-K docs         │
│           ↓                                                  │
│  [Query + Retrieved Docs] → LLM → Réponse                   │
│                                                              │
└─────────────────────────────────────────────────────────────┘

Étape 1 : Chunking

Découper les documents en morceaux (chunks) de taille appropriée.

Stratégies de découpage

# Chunking simple par caractères
from langchain.text_splitter import CharacterTextSplitter

splitter = CharacterTextSplitter(
    chunk_size=1000,      # Taille max
    chunk_overlap=200,    # Chevauchement
    separator="\n"
)
chunks = splitter.split_text(document)

Types de splitters

TypeDescriptionUsage
CharacterCoupe à N caractèresSimple, rapide
RecursiveEssaie plusieurs séparateursTexte général
SentencePar phrasesPréserve le sens
SemanticPar similaritéPlus précis
CodePar fonctions/classesCode source
MarkdownPar sectionsDocumentation

Taille optimale

Trop petit (100 tokens) : Perd le contexte
Trop grand (2000 tokens) : Noie l'information

Recommandation : 500-1000 tokens avec 10-20% overlap

Le chevauchement (overlap) évite de couper au milieu d'une idée importante.

Étape 2 : Embeddings

Convertir le texte en vecteurs numériques pour la recherche sémantique.

Modèles d'embedding

ModèleDimensionContextePerformance
OpenAI text-embedding-3-small15368191Très bon
OpenAI text-embedding-3-large30728191Excellent
Cohere embed-v31024512Très bon
BGE-large1024512Open source, bon
E5-large1024512Open source
Sentence-transformers384-768512Gratuit

Implémentation

from openai import OpenAI

client = OpenAI()

def get_embedding(text):
    response = client.embeddings.create(
        model="text-embedding-3-small",
        input=text
    )
    return response.data[0].embedding

# Embedder tous les chunks
embeddings = [get_embedding(chunk) for chunk in chunks]

Avec Sentence Transformers (gratuit)

from sentence_transformers import SentenceTransformer

model = SentenceTransformer('all-MiniLM-L6-v2')
embeddings = model.encode(chunks)

Étape 3 : Vector Database

Stocker et rechercher efficacement les embeddings.

Options populaires

BaseTypeCaractéristique
PineconeCloudManaged, facile
WeaviateCloud/SelfHybride, GraphQL
ChromaLocalSimple, léger
QdrantCloud/SelfPerformant
MilvusSelf-hostedScalable
pgvectorPostgreSQLIntégré à PG
FAISSLibraryFacebook, rapide

Exemple avec Chroma

import chromadb
from chromadb.utils import embedding_functions

# Créer le client
client = chromadb.Client()

# Fonction d'embedding
openai_ef = embedding_functions.OpenAIEmbeddingFunction(
    api_key="...",
    model_name="text-embedding-3-small"
)

# Créer la collection
collection = client.create_collection(
    name="my_documents",
    embedding_function=openai_ef
)

# Ajouter les documents
collection.add(
    documents=chunks,
    ids=[f"chunk_{i}" for i in range(len(chunks))],
    metadatas=[{"source": "doc1.pdf", "page": i} for i in range(len(chunks))]
)

# Rechercher
results = collection.query(
    query_texts=["Comment fonctionne le RAG ?"],
    n_results=5
)

Étape 4 : Retrieval

Rechercher les documents les plus pertinents.

Similarité cosinus

similarity = cos(query_embedding, doc_embedding)
           = (A · B) / (||A|| × ||B||)

Valeur entre -1 et 1 (1 = très similaire)

Recherche hybride

Combiner recherche sémantique et recherche par mots-clés.

# Résultats sémantiques (embeddings)
semantic_results = vector_db.search(query_embedding, k=10)

# Résultats lexicaux (BM25)
keyword_results = bm25_search(query, k=10)

# Fusion des résultats (RRF - Reciprocal Rank Fusion)
final_results = reciprocal_rank_fusion(semantic_results, keyword_results)

Reranking

Affiner les résultats avec un modèle de reranking.

from cohere import Client

co = Client("api-key")

# Reranker les résultats
reranked = co.rerank(
    query="Question de l'utilisateur",
    documents=retrieved_docs,
    top_n=5,
    model="rerank-english-v2.0"
)

Étape 5 : Augmentation du prompt

Intégrer les documents récupérés dans le prompt.

def build_rag_prompt(query, retrieved_docs):
    context = "\n\n".join([
        f"[Source {i+1}]: {doc}"
        for i, doc in enumerate(retrieved_docs)
    ])

    prompt = f"""Utilise les sources suivantes pour répondre à la question.
Si l'information n'est pas dans les sources, dis-le clairement.
Cite les sources utilisées.

SOURCES :
{context}

QUESTION : {query}

RÉPONSE :"""

    return prompt

Étape 6 : Génération

Générer la réponse avec le LLM.

from anthropic import Anthropic

client = Anthropic()

def generate_answer(query, retrieved_docs):
    prompt = build_rag_prompt(query, retrieved_docs)

    response = client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=1024,
        messages=[{"role": "user", "content": prompt}]
    )

    return response.content[0].text

Pipeline complet

class RAGPipeline:
    def __init__(self):
        self.embedder = SentenceTransformer('all-MiniLM-L6-v2')
        self.vector_db = chromadb.Client()
        self.collection = None
        self.llm = Anthropic()

    def index_documents(self, documents):
        # Chunking
        chunks = self.chunk_documents(documents)

        # Créer collection
        self.collection = self.vector_db.create_collection("docs")

        # Indexer
        embeddings = self.embedder.encode(chunks)
        self.collection.add(
            embeddings=embeddings.tolist(),
            documents=chunks,
            ids=[f"c{i}" for i in range(len(chunks))]
        )

    def query(self, question, top_k=5):
        # Rechercher
        results = self.collection.query(
            query_embeddings=[self.embedder.encode(question).tolist()],
            n_results=top_k
        )

        retrieved_docs = results['documents'][0]

        # Générer
        prompt = self.build_prompt(question, retrieved_docs)
        response = self.llm.messages.create(
            model="claude-sonnet-4-20250514",
            max_tokens=1024,
            messages=[{"role": "user", "content": prompt}]
        )

        return {
            "answer": response.content[0].text,
            "sources": retrieved_docs
        }

Techniques avancées

Hypothetical Document Embeddings (HyDE)

Générer un document hypothétique pour améliorer la recherche.

# Au lieu de chercher avec la question...
# Demander au LLM de générer une réponse hypothétique
hypothetical = llm.generate(f"Réponds brièvement : {query}")

# Utiliser cette réponse pour la recherche
results = vector_db.search(embed(hypothetical))

Multi-Query Retrieval

Reformuler la question de plusieurs façons.

queries = llm.generate(f"""
Génère 3 reformulations de cette question :
{original_query}
""")

# Rechercher avec chaque reformulation
all_results = []
for q in queries:
    all_results.extend(vector_db.search(q))

# Dédupliquer et fusionner
final_results = deduplicate(all_results)

Parent Document Retriever

Indexer des petits chunks mais retourner le document parent.

Document complet
├── Chunk 1 (indexé)  ─┐
├── Chunk 2 (indexé)   ├→ Si match → retourner document complet
├── Chunk 3 (indexé)  ─┘

Self-RAG

Le modèle décide lui-même s'il a besoin de chercher.

1. Question → LLM évalue si recherche nécessaire
2. Si oui → Retrieval → Génération avec contexte
3. Si non → Génération directe
4. LLM vérifie sa réponse et peut relancer une recherche

Frameworks

LangChain

from langchain.chains import RetrievalQA
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_community.vectorstores import Chroma

# Créer le retriever
vectorstore = Chroma.from_documents(documents, OpenAIEmbeddings())
retriever = vectorstore.as_retriever(search_kwargs={"k": 5})

# Créer la chaîne RAG
qa_chain = RetrievalQA.from_chain_type(
    llm=ChatOpenAI(),
    retriever=retriever,
    return_source_documents=True
)

result = qa_chain.invoke({"query": "Ma question"})

LlamaIndex

from llama_index.core import VectorStoreIndex, SimpleDirectoryReader

# Charger les documents
documents = SimpleDirectoryReader("./data").load_data()

# Créer l'index
index = VectorStoreIndex.from_documents(documents)

# Créer le query engine
query_engine = index.as_query_engine()

# Requête
response = query_engine.query("Ma question")

Métriques d'évaluation

Pour le Retrieval

MétriqueDescription
Recall@K% de docs pertinents dans top-K
Precision@K% de top-K qui sont pertinents
MRRPosition moyenne du premier résultat pertinent
NDCGPrend en compte l'ordre des résultats

Pour la génération

MétriqueDescription
FaithfulnessRéponse fidèle aux sources
Answer RelevancyRéponse pertinente à la question
Context RelevancyContexte récupéré pertinent
from ragas import evaluate
from ragas.metrics import faithfulness, answer_relevancy

results = evaluate(
    dataset,
    metrics=[faithfulness, answer_relevancy]
)

Résumé

RAG = Retrieval + Augmentation + Generation

Pipeline :
1. Chunking : Découper les documents
2. Embedding : Vectoriser les chunks
3. Indexation : Stocker dans vector DB
4. Retrieval : Chercher les docs pertinents
5. Augmentation : Enrichir le prompt
6. Generation : Produire la réponse

Avantages :
├── Connaissances à jour
├── Données privées
├── Moins d'hallucinations
├── Sources vérifiables
└── Pas de fine-tuning nécessaire

On this page