Skip to Content
IAWorkflow RAG

Workflow RAG — Architecture & mise en œuvre

Le Retrieval-Augmented Generation (RAG) permet d’enrichir une réponse LLM avec des documents pertinents sans re-entraîner le modèle. C’est le pattern de référence quand on veut doter une application AI d’une base de connaissances sans engineering de données massif.

Ce guide couvre l’architecture complète : de l’indexation d’un document brut à la génération finale, en passant par l’embedding, le stockage vectoriel et le retrieval.

Architecture d’un pipeline RAG

Un pipeline RAG fonctionne en deux phases : indexation (one-shot ou incrémentale) et inférence (répétée par requête).

┌─────────────── INDEXATION ───────────────┐ │ │ │ Document brut │ │ │ │ │ ▼ │ │ Text splitter (chunks) │ │ │ │ │ ▼ │ │ Embedding model → vecteurs │ │ │ │ │ ▼ │ │ Stockage vectoriel (Chroma / Qdrant ...) │ │ │ └─────────────── INFÉRENCE ────────────────┘ │ │ │ Requête utilisateur │ │ │ │ │ ▼ │ │ Embedding de la requête │ │ │ │ │ ▼ │ │ Similarity search → top-k chunks │ │ │ │ │ ▼ │ │ Prompt enrichi (requête + contexte) │ │ │ │ │ ▼ │ │ LLM → réponse │

Règle n°1 : la qualité de la sortie RAG dépend 80 % de la qualité du retrieval, pas du modèle. Un mauvais chunk ne se rattrape pas avec un bon prompt.

Choix d’un embedding model

Le choix de l’embedding model impacte directement la pertinence du retrieval. Trois options majeures :

ModèleTaillePrécision (MTEB)CoûtUsage recommandé
text-embedding-3-small (OpenAI)1536 dims62,3 %~0,02 USD / 1M tokensProduction, multi-langue
text-embedding-3-large (OpenAI)3072 dims64,6 %~0,13 USD / 1M tokensCas critiques, précision maximale
all-MiniLM-L6-v2 (sentence-transformers)384 dims56,5 %Gratuit (local)Prototypage, contraintes de latence, offline
bge-large-en-v1.5 (BAAI)1024 dims63,1 %Gratuit (local)Recherche anglophone, équilibre vitesse/précision

Pour un projet en production, démarrer avec text-embedding-3-small. Pour un prototype ou une contrainte d’offline, utiliser all-MiniLM-L6-v2 en local.

Initialisation des deux approches

# OpenAI (API call) from openai import OpenAI client = OpenAI() embedding = client.embeddings.create( input="Document à indexer", model="text-embedding-3-small" ).data[0].embedding
# sentence-transformers (local, gratuit) from sentence_transformers import SentenceTransformer model = SentenceTransformer("all-MiniLM-L6-v2") embedding = model.encode("Document à indexer").tolist()

Découpage en chunks — La clé du retrieval

Un document brut doit être découpé en fragments (chunks) avant embedding. La taille et le chevauchement (overlap) sont les deux paramètres à régler.

TechniqueTaille chunkOverlapQuand l’utiliser
Fixed-size256–512 tokens50–100 tokensTextes longs non structurés (articles, logs)
Recursive (hiérarchique)512–1024 tokens100–200 tokensDocuments structurés (code, docs Markdown)
Semantic (par paragraphe)VariableN/AQuand les paragraphes sont naturellement délimités

Règle n°2 : un chunk de 256 tokens est généralement le sweet spot. Au-delà de 512, on dilue la pertinence. En dessous de 128, on perd le contexte.

Exemple avec LangChain (recursive splitter)

from langchain.text_splitter import RecursiveCharacterTextSplitter splitter = RecursiveCharacterTextSplitter( chunk_size=512, chunk_overlap=100, separators=["\n\n", "\n", ". ", " ", ""], ) chunks = splitter.split_text( "Contenu long du document à indexer ici..." ) # chunks : liste de strings de max 512 tokens, overlap de 100

Stockage vectoriel

Trois options majeures, classées par complexité d’installation :

SolutionInstallationPersistanceScaleIdéal pour
Chromapip install chromadbFile système locale10K–500K docsPrototypage, solo dev
QdrantDocker / cloudRedis/MQ memory100K–10M docsProduction, scale
pgvectorExtension PostgresPostgreSQL1M+ docsQuand Postgres est déjà la BDD principale

Exemple complet : Chroma (prototypage)

import chromadb from chromadb.utils import embedding_functions # 1. Configurer l'embedding function ef = embedding_functions.OpenAIEmbeddingFunction( api_key="sk-...", model_name="text-embedding-3-small" ) # 2. Créer ou ouvrir la collection client = chromadb.Client() collection = client.get_or_create_collection( name="documents", embedding_function=ef ) # 3. Indexation collection.add( documents=[ "Premier document chunké...", "Deuxième document chunké...", ], ids=["doc_001", "doc_002"], metadatas=[ {"source": "doc1.pdf", "page": 1}, {"source": "doc1.pdf", "page": 2}, ] )

Exemple complet : pgvector (production avec Postgres)

# SQL : créer l'extension et la table # CREATE EXTENSION IF NOT EXISTS vector; # CREATE TABLE documents ( # id SERIAL PRIMARY KEY, # content TEXT, # embedding vector(1536), # metadata JSONB # ); # Python : indexation import psycopg2 import psycopg2.extras conn = psycopg2.connect("dbname=rag_dev user=postgres") cur = conn.cursor() cur.execute( "INSERT INTO documents (content, embedding, metadata) " "VALUES (%s, %s, %s)", ("Chunk de document...", embedding, {"source": "doc.pdf"}) ) conn.commit() # Python : recherche cur.execute( """SELECT content, metadata, 1 - (embedding <=> %s::vector) AS similarity FROM documents ORDER BY embedding <=> %s::vector LIMIT 5""", (query_embedding, query_embedding) ) results = cur.fetchall()

Le fallback Pgvector est pertinent quand l’application a déjà Postgres. Éviter Chroma en production multi-instance — la persistance file system ne se scale pas.

Inférence RAG — Du retrieval à la réponse

La phase d’inférence suit ce schéma pour chaque requête utilisateur :

import openai import chromadb from chromadb.utils import embedding_functions client = openai.OpenAI() client_chroma = chromadb.Client() ef = embedding_functions.OpenAIEmbeddingFunction( api_key="sk-...", model_name="text-embedding-3-small" ) collection = client_chroma.get_or_create_collection( name="documents", embedding_function=ef ) def rag_query(user_question: str, top_k: int = 3) -> str: """Pipeline RAG complet : retrieve → augment → generate.""" # 1. Embedder la requête query_embedding = client.embeddings.create( input=user_question, model="text-embedding-3-small" ).data[0].embedding # 2. Retrieval : recherche similarity results = collection.query( query_embeddings=[query_embedding], n_results=top_k ) # 3. Construire le contexte à injecter dans le prompt context = "\n\n".join(results["documents"][0]) # 4. Génération avec contexte response = client.chat.completions.create( model="gpt-4o", messages=[ { "role": "system", "content": ( "Tu es un assistant qui répond uniquement à partir " "des documents fournis. Cite tes sources. " "Si la réponse n'est pas dans les documents, dis-le." ) }, { "role": "user", "content": ( "Documents de référence :\n---\n" f"{context}\n---\n\n" f"Question : {user_question}" ) } ], max_tokens=1024 ) return response.choices[0].message.content

Règle n°3 : toujours demander au LLM de citer ses sources. Sans citation, impossible de vérifier que la réponse vient bien du retrieval et pas d’une hallucination.

Gestion du token limit

Le contexte LLM a une taille finie. Un chunk trop long + une requête longue peut dépasser le max_context_length (typiquement 128K tokens pour GPT-4o, 200K pour Gemini).

Stratégies de protection

StratégieMécanismeQuand
Truncation contrôléeCouper le contexte à une taille max avant envoiToggle rapide, perte d’information
RerankingReranker le contexte pour garder uniquement les chunks les plus pertinentsQuand top-k est trop large
Résumé hiérarchiqueRésumer les chunks non retenus en un seul blocDocuments très longs avec contexte secondaire
Chunking adaptatifAjuster la taille du chunk selon le budget token restantSettings dynamiques

Exemple de truncation sécurisée

MAX_CONTEXT_TOKENS = 4096 # Budget réservé au contexte def build_context_safely(contexts: list[str], max_tokens: int = MAX_CONTEXT_TOKENS) -> str: """Assembler le contexte en respectant le budget token.""" from langchain.text_splitter import CharacterTextSplitter splitter = CharacterTextSplitter(chunk_size=1, chunk_overlap=0) total = "" for chunk in contexts: chunk_tokens = len(splitter.split_text(chunk)) if len(splitter.split_text(total + "\n" + chunk)) > max_tokens: break total = (total + "\n" + chunk).strip() return total

Fallback si le retrieval est insuffisant

Parfois, les documents indexés ne contiennent pas l’information demandée. Un bon pipeline RAG gère ce cas explicitement.

Pattern de fallback à trois niveaux

Niveau 1 : RAG (retrieval + génération) ├── Si confiance > 0.7 → réponse avec sources ├── Si confiance basse OU "ne pas trouver" détecté │ │ │ ▼ │ Niveau 2 : RAG élargi (top-k=10, threshold relaxé) │ │ │ ├── Si nouvelle confiance acceptable → réponse élargie │ │ │ └── Si toujours insuffisant │ │ │ ▼ │ Niveau 3 : Réponse générale (sans documents) │ avec avertissement : "Cette réponse n'est pas issue de votre base de connaissances."

Détection de “ne pas trouver”

def has_relevant_answer(response_text: str, confidence_threshold: float = 0.7) -> dict: """Analyser si la réponse du LLM est vraiment ancrée dans le contexte.""" vague_markers = [ "je ne suis pas sûr", "je ne sais pas", "les documents", "ne contient pas", "pas dans les informations fournies" ] flagged = any(marker in response_text.lower() for marker in vague_markers) return { "vague": flagged, "confidence": 1.0 if flagged else confidence_threshold, "fallback_needed": flagged, }

Pièges courants

PiègeSymptômeSolution
Chunks trop grandsLe LLM noie dans du bruit, retrieval peu pertinentRéduire à 256–512 tokens, augmenter l’overlap
Embedding model inadaptéRésultats de recherche aléatoiresTester 2 modèles, choisir selon MTEB ou benchmark interne
Pas de metadataImpossible de filtrer ou comprendre l’origineToujours stocker source, page, date
Embedding non mis à jourNouveaux documents pas indexésPipeline d’indexation incrémental + versionning
Token limit violéErreur API ou réponse coupéeBudget token strict + truncation
Hallucination malgré retrievalLe LLM invente des détails absents des chunksPrompt system strict + détection de vague
Stockage non persistéPerte des données entre redémarragesChroma persistant (persist_directory) ou Qdrant/Pgvector

Liaisons


Références : LangChain Text Splitters , ChromaDB Docs , pgvector , MTEB Leaderboard