Créer un système RAG

Guide pratique pour construire votre propre système RAG de A à Z

01

📖 Introduction

Ce tutoriel vous guide dans la création d'un système RAG (Retrieval-Augmented Generation) complet. Nous allons construire ensemble un assistant capable de répondre à des questions sur un corpus de documents en utilisant la recherche vectorielle et un LLM.

💡 Prérequis :
  • Python 3.9 ou supérieur
  • Connaissances de base en Python
  • Une clé API OpenAI (ou alternative locale)
📊 Architecture RAG :

[Documents] → [Chunking] → [Embeddings] → [Vector Store]
[Question] → [Embedding] → [Search] → [Contexte] → [LLM] → [Réponse]
02

📄 Étape 1 : Préparation des données

Installation des dépendances

pip install langchain langchain-community chromadb sentence-transformers openai tiktoken

Chargement et découpage des documents

from langchain.document_loaders import TextLoader, DirectoryLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter

# Charger les documents
loader = DirectoryLoader("./documents/", glob="**/*.txt", loader_cls=TextLoader)
documents = loader.load()

# Découpage en chunks
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=200,
    separators=["\n\n", "\n", " ", ""]
)
chunks = text_splitter.split_documents(documents)

print(f"{len(chunks)} chunks créés")
💡 Bonnes pratiques :
  • Chunk size : 500-1000 tokens pour la plupart des LLM
  • Chunk overlap : 10-20% pour conserver le contexte
  • Utilisez des séparateurs sémantiques (paragraphes, phrases)
03

🔢 Étape 2 : Génération des embeddings

from sentence_transformers import SentenceTransformer

# Modèle d'embedding (multilingue)
model = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')

# Générer les embeddings pour tous les chunks
texts = [chunk.page_content for chunk in chunks]
embeddings = model.encode(texts, show_progress_bar=True)

print(f"Shape des embeddings : {embeddings.shape}")  # (nb_chunks, 384)

Modèles d'embeddings recommandés

  • multilingue : paraphrase-multilingual-MiniLM-L12-v2 (384 dims)
  • Français : camembert-base (768 dims)
  • OpenAI : text-embedding-3-small (1536 dims)

Avec LangChain

from langchain.embeddings import HuggingFaceEmbeddings

embeddings = HuggingFaceEmbeddings(
    model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
)
04

🗄️ Étape 3 : Création de la base vectorielle

Avec FAISS

import faiss
import numpy as np

# Créer l'index FAISS
dimension = 384
index = faiss.IndexFlatIP(dimension)

# Normaliser les embeddings pour similarité cosinus
faiss.normalize_L2(embeddings)
index.add(embeddings)

# Sauvegarder l'index
faiss.write_index(index, "faiss_index.bin")

Avec Chroma (plus simple)

from langchain.vectorstores import Chroma

vectorstore = Chroma.from_documents(
    documents=chunks,
    embedding=embeddings,
    persist_directory="./chroma_db"
)

# Persistance automatique
vectorstore.persist()
💡 Choix de la base vectorielle :
  • FAISS : Ultra-rapide, pour production (recommandé)
  • Chroma : Simple, pour prototypage
  • Pinecone : Cloud managé
  • Qdrant : Open source, performances élevées
05

🤖 Étape 4 : Configuration du LLM

Avec OpenAI

from langchain_openai import ChatOpenAI

llm = ChatOpenAI(
    model="gpt-4",
    temperature=0.2,  # Faible pour des réponses précises
    api_key="votre_clé_api"
)

Avec un modèle local (Mistral via Ollama)

from langchain_community.llms import Ollama

llm = Ollama(
    model="mistral",
    temperature=0.2,
    base_url="http://localhost:11434"
)

LLM recommandés

  • OpenAI GPT-4 : Haute qualité (payant)
  • Mistral : Excellent rapport qualité/prix
  • Llama 3 : Open source, performant
  • Claude 3 : Très bonne compréhension du français

Température recommandée

  • RAG (précision) : 0.1 - 0.3
  • Créativité : 0.7 - 1.0
  • Équilibre : 0.5 - 0.7
06

🔗 Étape 5 : Construction de la chaîne RAG

from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate

# Template de prompt
template = """Tu es un assistant expert. Utilise le contexte suivant pour répondre à la question. Si la réponse n'est pas dans le contexte, dis que tu ne sais pas.

Contexte: {context}

Question: {question}

Réponse:"""

prompt = PromptTemplate(
    template=template,
    input_variables=["context", "question"]
)

# Créer le retriever
retriever = vectorstore.as_retriever(
    search_type="similarity",
    search_kwargs={"k": 4}
)

# Chaîne RAG complète
qa_chain = RetrievalQA.from_chain_type(
    llm=llm,
    chain_type="stuff",
    retriever=retriever,
    chain_type_kwargs={"prompt": prompt},
    return_source_documents=True
)
📝 Test de la chaîne :
response = qa_chain.invoke({"query": "Quels sont les discours de Charles de Gaulle sur la résistance ?"})

print(f"Réponse: {response['result']}")
print(f"Sources: {[doc.metadata for doc in response['source_documents']]}")
07

🧪 Étape 6 : Test et optimisation

Métriques d'évaluation

def evaluate_rag(chain, questions, ground_truth):
    results = []
    for q, gt in zip(questions, ground_truth):
        response = chain.invoke({"query": q})
        results.append({
            "question": q,
            "expected": gt,
            "actual": response["result"],
            "sources": len(response["source_documents"])
        })
    return results

Optimisations possibles

  • Chunk size : Ajuster pour capturer plus de contexte
  • k (nombre de chunks) : 4-10 chunks pour un bon équilibre
  • Re-ranking : Utiliser un second modèle pour réordonner les résultats
  • Hybrid search : Combiner vectoriel + mots-clés (BM25)
  • Prompt engineering : Instructions plus précises
💡 Astuce : Utilisez LangSmith ou LangFuse pour tracer et debugger vos chaînes RAG.
08

🚀 Étape 7 : Déploiement

API FastAPI simple

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Query(BaseModel):
    question: str
    k: int = 4

@app.post("/rag")
async def rag_endpoint(query: Query):
    response = qa_chain.invoke({"query": query.question})
    return {
        "answer": response["result"],
        "sources": [doc.metadata for doc in response["source_documents"]]
    }

# Lancer : uvicorn main:app --reload