Talki Academy
Technique12 min de lecture

Construire des Systèmes RAG avec LangChain : Tutoriel Pratique avec Code Fonctionnel

La Génération Augmentée par Récupération (RAG) permet aux LLM de répondre à des questions sur vos documents privés sans fine-tuning coûteux. Ce tutoriel pratique vous guide dans la construction d'un système RAG prêt pour la production avec LangChain : du chargement de documents et de la création d'embeddings, jusqu'aux requêtes avec attribution des sources. Inclut du code fonctionnel complet, les bonnes pratiques pour le chunking et la récupération, et des conseils pour utiliser à la fois les API cloud et les modèles open-source locaux.

Par Talki Academy·Publié le 5 avril 2026

Qu'est-ce que RAG et Pourquoi Est-ce Important ?

La Génération Augmentée par Récupération (RAG) résout une limitation fondamentale des Large Language Models : ils ne connaissent pas les informations qui n'existaient pas dans leurs données d'entraînement. Si vous demandez à ChatGPT des informations sur la documentation interne de votre entreprise, un article de recherche récent ou les données de ventes du dernier trimestre, il ne peut pas répondre car il n'a jamais été entraîné sur ces informations.

RAG comble ce fossé en récupérant les documents pertinents depuis votre base de connaissances et en les incluant dans le prompt envoyé au LLM. Le LLM génère ensuite une réponse basée sur le contexte fourni. Cette approche offre plusieurs avantages clés :

  • Pas de fine-tuning requis : Mettez à jour votre base de connaissances à tout moment sans réentraîner les modèles
  • Attribution des sources : Sachez exactement quels documents ont été utilisés pour générer chaque réponse
  • Rentable : Moins cher que le fine-tuning et la maintenance de modèles personnalisés
  • Respectueux de la vie privée : Peut fonctionner entièrement sur site avec des modèles locaux
  • Toujours à jour : Les réponses reflètent vos derniers documents, pas des données d'entraînement obsolètes

Vue d'Ensemble de l'Architecture RAG

Un système RAG se compose de deux phases principales : l'indexation (prétraitement) et la récupération (au moment de la requête).

Phase 1 : Indexation (Configuration Unique)

  1. Charger les documents : Lire les PDF, fichiers Word, pages web, etc.
  2. Découper en chunks : Diviser les documents en morceaux plus petits (500-1000 caractères)
  3. Générer les embeddings : Convertir les chunks de texte en vecteurs numériques
  4. Stocker dans une base vectorielle : Indexer les embeddings pour une recherche de similarité rapide

Phase 2 : Récupération (Chaque Requête)

  1. Embedder la requête : Convertir la question utilisateur en vecteur
  2. Rechercher dans la base vectorielle : Trouver les chunks de documents les plus similaires
  3. Formater le prompt : Combiner la requête + chunks récupérés
  4. Générer la réponse : Le LLM répond en se basant sur le contexte fourni
  5. Retourner avec les sources : Inclure les références aux documents sources

Implémentation Étape par Étape

Étape 1 : Configuration de l'Environnement

D'abord, installez les dépendances requises. Nous utiliserons ChromaDB comme base vectorielle (gratuite, open-source, fonctionne en local).

# Créer un environnement virtuel python -m venv venv source venv/bin/activate # Sur Windows : venv\Scripts\activate # Installer les dépendances principales pip install langchain langchain-openai langchain-community # Installer les chargeurs de documents et le découpage de texte pip install pypdf unstructured # Installer la base vectorielle pip install chromadb # Installer le client OpenAI pip install openai # Pour les modèles locaux (optionnel - nécessite Ollama installé) pip install ollama

Définissez votre clé API OpenAI (ou ignorez si vous utilisez des modèles locaux) :

export OPENAI_API_KEY="votre-clé-api-ici" # Ou créez un fichier .env : # OPENAI_API_KEY=votre-clé-api-ici # En Python, chargez avec : from dotenv import load_dotenv load_dotenv()

Étape 2 : Charger et Traiter les Documents

Commençons par charger un document PDF. LangChain fournit des chargeurs spécialisés pour différents formats.

from langchain_community.document_loaders import PyPDFLoader from langchain.text_splitter import RecursiveCharacterTextSplitter # Charger le PDF loader = PyPDFLoader("manuel_employe.pdf") documents = loader.load() print(f"Chargé {len(documents)} pages du PDF") print(f"Aperçu première page : {documents[0].page_content[:200]}...") # Sortie attendue : # Chargé 45 pages du PDF # Aperçu première page : Manuel de l'Employé # # Bienvenue chez Acme Corporation # # Ce manuel contient les politiques et procédures importantes...

Les documents sont maintenant chargés, mais les pages complètes sont trop volumineuses pour être utilisées comme contexte. Nous devons les diviser en chunks plus petits.

# Initialiser le diviseur de texte text_splitter = RecursiveCharacterTextSplitter( chunk_size=1000, # Nombre max de caractères par chunk chunk_overlap=200, # Chevauchement entre chunks pour préserver le contexte length_function=len, separators=["\n\n", "\n", " ", ""] # Essayer de diviser d'abord par paragraphes ) # Diviser les documents en chunks chunks = text_splitter.split_documents(documents) print(f"Divisé en {len(chunks)} chunks") print(f"\nExemple de chunk :") print(f"Contenu : {chunks[10].page_content}") print(f"Métadonnées : {chunks[10].metadata}") # Sortie attendue : # Divisé en 287 chunks # # Exemple de chunk : # Contenu : Politique de Congés # # Les employés à temps plein accumulent 15 jours de congés payés par an... # Métadonnées : {'source': 'manuel_employe.pdf', 'page': 12}

Étape 3 : Créer les Embeddings et la Base Vectorielle

Maintenant, nous convertissons les chunks de texte en vecteurs numériques (embeddings) et les stockons dans ChromaDB pour une récupération rapide.

from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import Chroma # Initialiser le modèle d'embeddings embeddings = OpenAIEmbeddings( model="text-embedding-3-small" # Coût : 0,02 € par 1M de tokens ) # Créer la base vectorielle et indexer les documents vectorstore = Chroma.from_documents( documents=chunks, embedding=embeddings, persist_directory="./chroma_db" # Sauvegarder sur disque pour réutilisation ) print(f"✅ Indexé {len(chunks)} chunks dans la base vectorielle") # Pour les exécutions suivantes, charger la base existante : # vectorstore = Chroma( # persist_directory="./chroma_db", # embedding_function=embeddings # )

Utiliser des modèles locaux à la place (pas de coûts d'API, confidentialité complète) :

# Nécessite Ollama installé : https://ollama.ai/ # Puis exécuter : ollama pull nomic-embed-text from langchain_community.embeddings import OllamaEmbeddings embeddings = OllamaEmbeddings( model="nomic-embed-text" # Embeddings 768-dim, gratuits, fonctionnent en local ) # Le reste du code est identique - LangChain abstrait l'implémentation

Étape 4 : Créer le Retriever et Tester les Requêtes

Le retriever recherche dans la base vectorielle les chunks les plus similaires à la requête de l'utilisateur.

# Créer le retriever retriever = vectorstore.as_retriever( search_type="similarity", # Autres options : "mmr" (diversité), "similarity_score_threshold" search_kwargs={"k": 4} # Récupérer les 4 chunks les plus pertinents ) # Tester la récupération query = "Quelle est la politique de congés ?" relevant_docs = retriever.invoke(query) print(f"Requête : {query}") print(f"\nTrouvé {len(relevant_docs)} chunks pertinents :") for i, doc in enumerate(relevant_docs): print(f"\n[{i+1}] Page {doc.metadata.get('page', 'N/A')}") print(f"Contenu : {doc.page_content[:200]}...") # Sortie attendue : # Requête : Quelle est la politique de congés ? # # Trouvé 4 chunks pertinents : # # [1] Page 12 # Contenu : Politique de Congés # # Les employés à temps plein accumulent 15 jours de congés payés par an...

Étape 5 : Construire la Chaîne RAG

Maintenant, combinez le retriever avec un LLM pour créer un système RAG complet. Nous utiliserons la syntaxe LCEL moderne de LangChain pour plus de clarté.

from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser from langchain_core.runnables import RunnablePassthrough # Initialiser le LLM llm = ChatOpenAI( model="gpt-4o-mini", # Coût : 0,15 € par 1M de tokens en entrée temperature=0 # Réponses déterministes pour les requêtes factuelles ) # Créer le template de prompt template = """Vous êtes un assistant utile répondant à des questions basées sur le contexte fourni. Utilisez les éléments de contexte suivants pour répondre à la question à la fin. Si vous ne connaissez pas la réponse basée sur le contexte, dites "Je n'ai pas assez d'informations pour répondre" - n'inventez pas d'informations. Contexte : {context} Question : {question} Réponse : Fournissez une réponse claire et concise basée uniquement sur le contexte ci-dessus. Si vous référencez des politiques ou procédures spécifiques, mentionnez d'où elles viennent.""" prompt = ChatPromptTemplate.from_template(template) # Fonction d'aide pour formater les documents récupérés def format_docs(docs): return "\n\n".join(doc.page_content for doc in docs) # Créer la chaîne RAG en utilisant LCEL rag_chain = ( {"context": retriever | format_docs, "question": RunnablePassthrough()} | prompt | llm | StrOutputParser() ) # Interroger le système RAG question = "Combien de jours de congés ont les employés ?" answer = rag_chain.invoke(question) print(f"Question : {question}") print(f"\nRéponse : {answer}") # Sortie attendue : # Question : Combien de jours de congés ont les employés ? # # Réponse : Selon la Politique de Congés en page 12, les employés à temps plein # accumulent 15 jours de congés payés par an. Les employés à temps partiel accumulent # des congés au prorata selon leurs heures programmées.

Étape 6 : Ajouter l'Attribution des Sources

Pour les systèmes de production, vous voulez retourner les documents sources avec la réponse pour que les utilisateurs puissent vérifier les informations.

from langchain.chains import RetrievalQA # Alternative : utiliser RetrievalQA pour le suivi automatique des sources qa_chain = RetrievalQA.from_chain_type( llm=llm, chain_type="stuff", # "stuff" = inclure tous les docs dans un seul prompt retriever=retriever, return_source_documents=True, chain_type_kwargs={ "prompt": prompt, } ) # Requête avec sources result = qa_chain.invoke({"query": "Quelle est la politique de télétravail ?"}) print(f"Question : {result['query']}") print(f"\nRéponse : {result['result']}") print(f"\nSources :") for i, doc in enumerate(result['source_documents']): print(f" [{i+1}] Page {doc.metadata.get('page', 'N/A')} - {doc.metadata.get('source', 'Inconnu')}") print(f" Aperçu : {doc.page_content[:150]}...") # Sortie attendue : # Question : Quelle est la politique de télétravail ? # # Réponse : Les employés peuvent télétravailler jusqu'à 2 jours par semaine avec # l'approbation du manager. Les arrangements de télétravail doivent être documentés # par écrit et révisés trimestriellement. # # Sources : # [1] Page 18 - manuel_employe.pdf # Aperçu : Politique de Télétravail # # Pour soutenir l'équilibre vie professionnelle-vie privée, Acme permet le # télétravail sous les conditions suivantes...

Exemple Complet Fonctionnel

Voici une implémentation RAG complète et exécutable que vous pouvez utiliser comme point de départ :

# rag_system.py from langchain_community.document_loaders import PyPDFLoader from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_openai import OpenAIEmbeddings, ChatOpenAI from langchain_community.vectorstores import Chroma from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser from langchain_core.runnables import RunnablePassthrough import os class RAGSystem: def __init__(self, pdf_path: str, persist_dir: str = "./chroma_db"): """Initialiser le système RAG avec un document PDF.""" self.pdf_path = pdf_path self.persist_dir = persist_dir # Initialiser les composants self.embeddings = OpenAIEmbeddings(model="text-embedding-3-small") self.llm = ChatOpenAI(model="gpt-4o-mini", temperature=0) # Charger ou créer la base vectorielle if os.path.exists(persist_dir): print("Chargement de la base vectorielle existante...") self.vectorstore = Chroma( persist_directory=persist_dir, embedding_function=self.embeddings ) else: print("Création d'une nouvelle base vectorielle...") self._index_documents() # Créer le retriever self.retriever = self.vectorstore.as_retriever( search_kwargs={"k": 4} ) # Créer la chaîne RAG self._create_chain() def _index_documents(self): """Charger, diviser et indexer les documents.""" # Charger le PDF loader = PyPDFLoader(self.pdf_path) documents = loader.load() print(f"Chargé {len(documents)} pages") # Diviser en chunks text_splitter = RecursiveCharacterTextSplitter( chunk_size=1000, chunk_overlap=200, separators=["\n\n", "\n", " ", ""] ) chunks = text_splitter.split_documents(documents) print(f"Divisé en {len(chunks)} chunks") # Créer et persister la base vectorielle self.vectorstore = Chroma.from_documents( documents=chunks, embedding=self.embeddings, persist_directory=self.persist_dir ) print(f"✅ Indexé {len(chunks)} chunks") def _create_chain(self): """Créer la chaîne RAG.""" template = """Répondez à la question en vous basant sur le contexte suivant. Si vous ne savez pas, dites "Je n'ai pas assez d'informations" - n'inventez pas d'informations. Contexte : {context} Question : {question} Réponse :""" prompt = ChatPromptTemplate.from_template(template) def format_docs(docs): return "\n\n".join(doc.page_content for doc in docs) self.chain = ( {"context": self.retriever | format_docs, "question": RunnablePassthrough()} | prompt | self.llm | StrOutputParser() ) def query(self, question: str) -> dict: """Interroger le système RAG et retourner la réponse avec les sources.""" # Obtenir la réponse answer = self.chain.invoke(question) # Obtenir les documents sources sources = self.retriever.invoke(question) return { "question": question, "answer": answer, "sources": [ { "page": doc.metadata.get("page", "N/A"), "content": doc.page_content[:200] + "..." } for doc in sources ] } # Exemple d'utilisation if __name__ == "__main__": # Initialiser le système RAG rag = RAGSystem("manuel_employe.pdf") # Poser des questions questions = [ "Quelle est la politique de congés ?", "Combien de jours de maladie ont les employés ?", "Quelle est la politique de télétravail ?" ] for q in questions: result = rag.query(q) print(f"\nQ : {result['question']}") print(f"R : {result['answer']}") print(f"Sources : {len(result['sources'])} documents") print("-" * 80)

Bonnes Pratiques pour la Production

1. Optimiser la Taille et le Chevauchement des Chunks

La bonne taille de chunk dépend de votre cas d'usage. Testez avec différentes valeurs :

  • Documentation technique : 500-1000 caractères, 100-200 de chevauchement
  • Documents juridiques : 1500-2000 caractères (préserver le contexte des clauses)
  • Logs de chat : Diviser par message ou timestamp
  • Fichiers de code : Diviser par définition de fonction ou de classe

2. Implémenter le Caching pour les Embeddings

Évitez de recalculer les embeddings pour les mêmes requêtes :

from langchain.embeddings import CacheBackedEmbeddings from langchain.storage import LocalFileStore # Créer le cache store = LocalFileStore("./embedding_cache/") # Envelopper les embeddings avec le cache cached_embeddings = CacheBackedEmbeddings.from_bytes_store( underlying_embeddings=OpenAIEmbeddings(), document_embedding_cache=store, namespace="openai_embeddings" ) # Utiliser cached_embeddings dans vectorstore # Réduit les coûts de 60% pour les requêtes répétées

3. Ajouter le Filtrage par Métadonnées

Filtrer la récupération par type de document, date, auteur, etc :

# Lors du chargement des documents, ajouter des métadonnées for doc in documents: doc.metadata["departement"] = "RH" doc.metadata["derniere_mise_a_jour"] = "2026-01-15" # Filtrer la récupération retriever = vectorstore.as_retriever( search_kwargs={ "k": 4, "filter": {"departement": "RH"} # Récupérer uniquement les documents RH } )

4. Surveiller et Logger les Performances

import time from datetime import datetime def query_with_metrics(rag_system, question): start = time.time() result = rag_system.query(question) elapsed = time.time() - start # Logger les métriques print(f"[{datetime.now()}] Requête : {question[:50]}...") print(f" Temps de réponse : {elapsed:.2f}s") print(f" Sources récupérées : {len(result['sources'])}") print(f" Longueur réponse : {len(result['answer'])} caractères") return result

Utiliser des Modèles Locaux pour une Confidentialité Complète

Pour des documents sensibles, faites tout fonctionner en local avec Ollama :

# Installer Ollama : https://ollama.ai/ # Télécharger les modèles : # ollama pull llama3.3:70b # ollama pull nomic-embed-text from langchain_community.llms import Ollama from langchain_community.embeddings import OllamaEmbeddings # Utiliser les modèles locaux embeddings = OllamaEmbeddings(model="nomic-embed-text") llm = Ollama(model="llama3.3:70b", temperature=0) # Tout le reste reste identique # La base vectorielle, le retriever, la logique de chaîne sont identiques # Fonctionne maintenant 100% en local avec zéro coût d'API

Problèmes Courants et Solutions

ProblèmeCauseSolution
Réponses manquent de contexteChunks trop petits ou k trop basAugmenter chunk_size à 1500 ou k à 6-8
Info non pertinente dans réponsesChunks trop grands, mauvaise récupérationDiminuer chunk_size à 500, ajouter filtres métadonnées
Réponses lentesPas de cache embeddings, k trop grandImplémenter CacheBackedEmbeddings, réduire k à 3-4
Coûts API élevésUtilisation de gpt-4, pas de cachingPasser à gpt-4o-mini, cacher les embeddings, considérer modèles locaux
"Je ne sais pas" pour info connueMauvais chunking ou embeddingsAjuster separators, essayer autre modèle embedding, vérifier qualité documents

Prochaines Étapes

Maintenant que vous avez un système RAG fonctionnel, considérez ces améliorations :

  • Récupération avancée : Implémenter la recherche hybride (mot-clé + sémantique), le reranking ou la récupération de documents parents
  • Sources de données multiples : Combiner PDF, pages web, bases de données, API dans une seule base de connaissances
  • Mémoire conversationnelle : Ajouter ConversationBufferMemory pour des sessions Q&A multi-tours
  • Interface web : Construire un frontend Streamlit ou FastAPI pour votre système RAG
  • Déploiement en production : Déployer sur AWS Lambda ou conteneurs Docker avec scaling horizontal

Pour une formation professionnelle complète sur les systèmes RAG et les applications LLM :

  • RAG et Agents en Production (3 jours intensifs) : Techniques RAG avancées, LangChain, LlamaIndex, optimisation bases vectorielles, patterns de déploiement
  • API Claude pour Développeurs (2 jours) : Maîtriser Claude 4.5 pour les applications RAG, fenêtres de contexte étendues (200K tokens), optimisation des prompts

Questions Fréquemment Posées

Pourquoi utiliser RAG plutôt que simplement prompter un LLM directement ?

Les LLM sont entraînés sur des données avec une date de coupure et ne connaissent pas vos documents privés. RAG récupère du contexte pertinent depuis vos documents et l'inclut dans le prompt, permettant au LLM de répondre sur des informations qu'il n'a jamais apprises. Cela élimine le besoin de fine-tuning coûteux et fournit l'attribution des sources pour les réponses.

Puis-je utiliser RAG avec des modèles locaux/open-source au lieu d'OpenAI ?

Absolument. LangChain supporte Ollama pour l'inférence locale (utilisez ChatOllama et OllamaEmbeddings). Vous pouvez faire tourner Llama 3.3 70B ou Mistral en local avec zéro coût d'API. Pour les embeddings, nomic-embed-text via Ollama fonctionne excellemment. Cela vous donne une confidentialité complète des données et élimine les coûts d'API récurrents.

Quelle est la meilleure taille de chunk pour le découpage de documents dans RAG ?

Il n'y a pas de réponse universelle - cela dépend de votre cas d'usage. Pour la documentation technique : 500-1000 caractères avec 100-200 de chevauchement. Pour les documents juridiques : 1500-2000 caractères (préserver le contexte des clauses). Pour les transcriptions de chat : diviser par tour ou timestamp. La clé est de tester la qualité de récupération - si les réponses manquent de contexte, augmentez la taille de chunk ; si du contenu non pertinent apparaît, diminuez-la.

Comment gérer plusieurs formats de documents (PDF, Word, HTML) ?

LangChain fournit des chargeurs spécialisés : PyPDFLoader pour les PDF, UnstructuredWordDocumentLoader pour Word, WebBaseLoader pour HTML. Pour la production, utilisez UnstructuredFileLoader qui détecte automatiquement le format. Tous les chargeurs retournent le même format Document, donc votre pipeline RAG reste cohérent quel que soit le format source.

Quel est le temps de réponse et le coût typique pour une requête RAG ?

Avec OpenAI gpt-4o-mini et text-embedding-3-small : ~1-2 secondes par requête, coûtant ~0,002 € par requête (0,5 centimes embeddings + 1,5 centimes génération pour 4 chunks récupérés). Avec Llama local via Ollama : 2-5 secondes (selon le GPU), 0 € par requête. Pour la production, ajoutez du caching (Redis) pour réduire les coûts d'embeddings de 60% pour les requêtes répétées.

Maîtriser RAG et les Applications LLM

Programmes de formation professionnelle pour développeurs construisant des applications IA.

Voir les Programmes de FormationNous Contacter