flowchart LR
A[Question] --> B[Retriever]
B --> C[Documents pertinents]
C --> D[LLM + Contexte]
D --> E[Réponse enrichie]
F[(Vector DB)] --> B
Formation Udemy sur le RAG (Retrieval Augmented Generation) avec Python. Je documente ici ce que j’ai appris en suivant le cours RAG for Professionals with LangGraph, Python and OpenAI.

Section 1 : Qu’est-ce que le RAG ?
Le RAG (Retrieval Augmented Generation) permet d’enrichir les réponses d’un LLM avec des données externes. Au lieu de se limiter à ses connaissances d’entraînement, le modèle peut chercher dans vos documents.
Sans RAG : “Quelles sont les règles de mon entreprise ?” → “Je ne connais pas vos règles internes”
Avec RAG : Le LLM récupère les documents pertinents et répond avec des informations précises.
Architecture RAG complète
flowchart TD
subgraph Ingestion
A[Documents] --> B[Loader]
B --> C[Splitter]
C --> D[Embeddings]
D --> E[(Vector Store)]
end
subgraph Query
F[Question] --> G[Embed Query]
G --> H[Similarity Search]
E --> H
H --> I[Top-K Documents]
end
subgraph Generation
I --> J[Prompt + Context]
F --> J
J --> K[LLM]
K --> L[Réponse]
end
Section 4 : LangChain et OpenAI Basics
Lectures 14-26 : ChatBot simple avec LangChain
OpenAI Chat Completions API
from openai import OpenAI
client = OpenAI(api_key="sk-...")
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "Tu es un assistant utile."},
{"role": "user", "content": "Explique le RAG en une phrase."}
],
temperature=0.7
)
print(response.choices[0].message.content)| Paramètre | Rôle |
|---|---|
| model | gpt-4o-mini, gpt-4o, etc. |
| messages | Historique de conversation |
| temperature | 0 = déterministe, 1 = créatif |
| max_tokens | Limite de réponse |
ChatOpenAI avec LangChain
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
response = llm.invoke("Qu'est-ce que le RAG ?")Dynamic Prompts avec PromptTemplate
from langchain_core.prompts import PromptTemplate
prompt = PromptTemplate.from_template(
"Traduis ce texte en {language}: {text}"
)
formatted = prompt.format(language="français", text="Hello world")ChatPromptTemplate avec Roles
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_messages([
("system", "Tu es un expert en {domain}."),
("human", "{question}")
])
messages = prompt.invoke({
"domain": "IA",
"question": "Qu'est-ce que le RAG ?"
})LangChain Expression Language (LCEL)
Le pipe | permet de chaîner les composants :
chain = prompt | llm | StrOutputParser()
response = chain.invoke({
"domain": "IA",
"question": "Qu'est-ce que le RAG ?"
})Runnables personnalisés
from langchain_core.runnables import RunnableLambda
def uppercase(text: str) -> str:
return text.upper()
chain = prompt | llm | StrOutputParser() | RunnableLambda(uppercase)Output Parsers
from langchain_core.output_parsers import StrOutputParser, JsonOutputParser
from langchain_core.pydantic_v1 import BaseModel, Field
# Parser simple (string)
chain = prompt | llm | StrOutputParser()
# Parser JSON structuré
class BookRecommendation(BaseModel):
title: str = Field(description="Titre du livre")
author: str = Field(description="Auteur")
reason: str = Field(description="Pourquoi ce livre")
parser = JsonOutputParser(pydantic_object=BookRecommendation)
prompt = ChatPromptTemplate.from_messages([
("system", "Recommande un livre. {format_instructions}"),
("human", "{query}")
]).partial(format_instructions=parser.get_format_instructions())
chain = prompt | llm | parser
result = chain.invoke({"query": "livre sur l'IA"})
# result = {"title": "...", "author": "...", "reason": "..."}Section 5 : Document Summarization
Lectures 27-49 : Loaders, Splitters, Stratégies de résumé
Document Loaders
flowchart LR
subgraph Sources
A[PDF]
B[CSV]
C[Web]
D[YouTube]
E[TXT]
end
subgraph Loaders
A --> F[PyPDFLoader]
B --> G[CSVLoader]
C --> H[WebBaseLoader]
D --> I[YoutubeLoader]
E --> J[TextLoader]
end
F & G & H & I & J --> K[Documents]
from langchain_community.document_loaders import PyPDFLoader
loader = PyPDFLoader("document.pdf")
documents = loader.load()
# Chaque document a: page_content + metadata
for doc in documents:
print(f"Page {doc.metadata['page']}: {doc.page_content[:100]}...")Single Mode vs Page Mode
- Single Mode : Tout le PDF dans un seul Document
- Page Mode : Un Document par page (défaut)
Lazy Loading
Pour les gros fichiers, charger page par page :
for doc in loader.lazy_load():
process(doc) # Traite une page à la foisText Splitters
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000, # Taille max d'un chunk
chunk_overlap=200, # Chevauchement entre chunks
separators=["\n\n", "\n", " ", ""] # Priorité de découpe
)
chunks = splitter.split_documents(documents)| Splitter | Usage |
|---|---|
| CharacterTextSplitter | Découpe simple par caractère |
| RecursiveCharacterTextSplitter | Respecte la structure (paragraphes, phrases) |
| TokenTextSplitter | Découpe par tokens (pour limites API) |
TokenTextSplitter
Pour respecter les limites de tokens de l’API :
from langchain.text_splitter import TokenTextSplitter
splitter = TokenTextSplitter(
chunk_size=500, # En tokens
chunk_overlap=50
)Stratégies de résumé
flowchart TD
subgraph Stuff
A1[Doc 1] & A2[Doc 2] & A3[Doc 3] --> B1[Tout dans le prompt]
B1 --> C1[Un seul appel LLM]
end
subgraph MapReduce
D1[Doc 1] --> E1[Résumé 1]
D2[Doc 2] --> E2[Résumé 2]
D3[Doc 3] --> E3[Résumé 3]
E1 & E2 & E3 --> F1[Résumé final]
end
subgraph Refine
G1[Doc 1] --> H1[Résumé initial]
H1 --> I1[+ Doc 2 → Résumé amélioré]
I1 --> J1[+ Doc 3 → Résumé final]
end
| Stratégie | Avantage | Inconvénient |
|---|---|---|
| Stuff | Simple, rapide | Limité par context window |
| MapReduce | Parallélisable | Peut perdre le contexte global |
| Refine | Résumé progressif | Lent (séquentiel) |
Section 6 : Introduction au RAG
Lectures 50-71 : Embeddings, Vector Stores, Retrieval Strategies
Vectorization & Embedding
Les embeddings convertissent le texte en vecteurs numériques :
from langchain_openai import OpenAIEmbeddings
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
# Embed un texte
vector = embeddings.embed_query("Qu'est-ce que le RAG ?")
# vector = [0.023, -0.456, 0.789, ...] # 1536 dimensionsVector Store avec FAISS
from langchain_community.vectorstores import FAISS
# Créer et peupler
vectorstore = FAISS.from_documents(chunks, embeddings)
# Recherche par similarité
results = vectorstore.similarity_search("Comment fonctionne le RAG ?", k=3)Persistance FAISS vs Chroma
# FAISS - Persistance manuelle
vectorstore.save_local("./faiss_index")
loaded = FAISS.load_local("./faiss_index", embeddings)
# ChromaDB - Persistance intégrée
from langchain_community.vectorstores import Chroma
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_db"
)| Aspect | ChromaDB | FAISS |
|---|---|---|
| Persistance | Intégrée | Manuelle |
| Métadonnées | Oui | Limitées |
| Scalabilité | Moyenne | Haute |
| Setup | Simple | Plus complexe |
Stratégies de Retrieval
flowchart TD
A[Query] --> B{Stratégie?}
B -->|similarity| C[Top-K plus proches]
B -->|threshold| D[Score > seuil]
B -->|mmr| E[Diversité maximale]
C --> F[Résultats]
D --> F
E --> F
# Similarity simple (défaut)
retriever = vectorstore.as_retriever(
search_type="similarity",
search_kwargs={"k": 4}
)
# Avec seuil de score (filtre les résultats peu pertinents)
retriever = vectorstore.as_retriever(
search_type="similarity_score_threshold",
search_kwargs={"score_threshold": 0.7, "k": 4}
)
# MMR - Maximum Marginal Relevance (diversité)
retriever = vectorstore.as_retriever(
search_type="mmr",
search_kwargs={"k": 4, "fetch_k": 20, "lambda_mult": 0.5}
)| Stratégie | Usage | Quand l’utiliser |
|---|---|---|
| similarity | Top-K plus proches | Focused Q&A |
| similarity_score_threshold | Filtrer par qualité | Éviter les faux positifs |
| mmr | Diversité des résultats | Exploratory Q&A, Multi-Aspect |
MultiQueryRetriever
Génère plusieurs variantes de la question pour améliorer le recall :
from langchain.retrievers.multi_query import MultiQueryRetriever
retriever = MultiQueryRetriever.from_llm(
retriever=vectorstore.as_retriever(),
llm=llm
)
# Génère 3 variantes de la question et fusionne les résultatsRAG Chain complète
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
# Prompt RAG
prompt = ChatPromptTemplate.from_template("""
Réponds à la question en utilisant uniquement le contexte suivant :
Contexte : {context}
Question : {question}
Réponse :
""")
# Chain
rag_chain = (
{"context": retriever, "question": RunnablePassthrough()}
| prompt
| ChatOpenAI(model="gpt-4o-mini")
)
response = rag_chain.invoke("Quelles sont les étapes du RAG ?")Sécurité des données
flowchart TD
subgraph "Niveau 1 - OpenAI API"
A[Données] --> B[API OpenAI]
B --> C[Serveurs OpenAI]
end
subgraph "Niveau 2 - Azure OpenAI"
D[Données] --> E[Azure OpenAI]
E --> F[Votre tenant Azure]
end
subgraph "Niveau 3 - Local"
G[Données] --> H[Ollama/vLLM]
H --> I[Votre serveur]
end
| Niveau | Solution | Données |
|---|---|---|
| Basique | OpenAI API | Transitent par OpenAI |
| Entreprise | Azure OpenAI | Restent dans votre tenant |
| Maximum | Ollama, vLLM | 100% local |
Section 7 : LangGraph Basics
Lectures 72-91 : StateGraph, Reducers, Memory
TypedDict pour le State
from typing import TypedDict, Annotated
from operator import add
class RAGState(TypedDict):
question: str
context: list
messages: Annotated[list, add] # Reducer: accumule les messages
response: strStateGraph simple
flowchart TD
A[START] --> B[Retrieve]
B --> C[Generate]
C --> D[END]
from langgraph.graph import StateGraph, END
def retrieve_node(state: RAGState) -> RAGState:
docs = retriever.invoke(state["question"])
return {"context": docs}
def generate_node(state: RAGState) -> RAGState:
response = llm.invoke(f"Context: {state['context']}\nQ: {state['question']}")
return {"response": response.content}
# Build graph
workflow = StateGraph(RAGState)
workflow.add_node("retrieve", retrieve_node)
workflow.add_node("generate", generate_node)
workflow.set_entry_point("retrieve")
workflow.add_edge("retrieve", "generate")
workflow.add_edge("generate", END)
app = workflow.compile()LangGraph Reducers
Les reducers définissent comment combiner les valeurs :
from operator import add
from typing import Annotated
class State(TypedDict):
count: int # Écrasement (défaut)
messages: Annotated[list, add] # Accumulation avec addadd_messages Reducer
Pour les conversations :
from langgraph.graph import add_messages
class ChatState(TypedDict):
messages: Annotated[list, add_messages]MessagesState (raccourci)
from langgraph.graph import MessagesState
# Équivalent à:
# class State(TypedDict):
# messages: Annotated[list, add_messages]Mémoire persistante avec MemorySaver
from langgraph.checkpoint.memory import MemorySaver
memory = MemorySaver()
app = workflow.compile(checkpointer=memory)
# Utiliser avec un thread_id
config = {"configurable": {"thread_id": "user_123"}}
result = app.invoke({"question": "Qu'est-ce que le RAG?"}, config)
# Le prochain appel avec le même thread_id conserve l'historique
result2 = app.invoke({"question": "Peux-tu préciser?"}, config)Gestion de la mémoire conversationnelle
Les LLM ont une context window limitée. Stratégies :
from langchain_core.messages import trim_messages
# Truncation par tokens
trimmer = trim_messages(
max_tokens=1000,
strategy="last", # Garder les derniers
token_counter=len, # Ou tiktoken pour comptage précis
include_system=True, # Toujours garder le system prompt
allow_partial=False # Pas de messages tronqués
)
trimmed = trimmer.invoke(messages)Conditional Edges
flowchart TD
A[START] --> B[Retrieve]
B --> C[Generate]
C --> D[Validate]
D --> E{OK?}
E -->|Non| F[Refine]
F --> D
E -->|Oui| G[END]
def should_continue(state: RAGState) -> str:
return "end" if state["is_valid"] else "refine"
workflow.add_conditional_edges(
"validate",
should_continue,
{"end": END, "refine": "refine"}
)Section 8 : Multiple Documents et File Types
Lectures 92-107 : DirectoryLoader, UnstructuredLoader, types de fichiers
PyPDFDirectoryLoader
from langchain_community.document_loaders import PyPDFDirectoryLoader
loader = PyPDFDirectoryLoader("./docs/")
documents = loader.load()DirectoryLoader (flexible)
from langchain_community.document_loaders import DirectoryLoader, PyPDFLoader
# PDFs seulement
loader = DirectoryLoader(
"./docs/",
glob="**/*.pdf",
loader_cls=PyPDFLoader
)
# Avec show_progress
loader = DirectoryLoader(
"./docs/",
glob="**/*.pdf",
loader_cls=PyPDFLoader,
show_progress=True
)UnstructuredLoader (multi-format)
from langchain_community.document_loaders import UnstructuredFileLoader
# Charge PDF, DOCX, PPTX, TXT, etc.
loader = UnstructuredFileLoader("document.docx")
documents = loader.load()Loaders par type de fichier
| Type | Loader | Package |
|---|---|---|
PyPDFLoader |
pypdf |
|
| TXT | TextLoader |
- |
| DOCX | Docx2txtLoader |
docx2txt |
| PPTX | UnstructuredPowerPointLoader |
unstructured |
| CSV | CSVLoader |
- |
from langchain_community.document_loaders import (
TextLoader,
Docx2txtLoader,
UnstructuredPowerPointLoader,
CSVLoader
)
# TXT
loader = TextLoader("notes.txt")
# Word
loader = Docx2txtLoader("document.docx")
# PowerPoint
loader = UnstructuredPowerPointLoader("slides.pptx")
# CSV (un Document par ligne)
loader = CSVLoader("data.csv")Chargement mixte avec DirectoryLoader
from langchain_community.document_loaders import DirectoryLoader
# Mapping type -> loader
loaders = {
"**/*.pdf": PyPDFLoader,
"**/*.txt": TextLoader,
"**/*.docx": Docx2txtLoader,
}
all_docs = []
for glob, loader_cls in loaders.items():
loader = DirectoryLoader("./docs/", glob=glob, loader_cls=loader_cls)
all_docs.extend(loader.load())Section 9 : Dynamic Vector Database avec Chroma
Lectures 108-127 : CRUD, Metadata filtering, Query syntax
Créer une Chroma DB vide
from langchain_community.vectorstores import Chroma
# DB vide
vectorstore = Chroma(
collection_name="my_collection",
embedding_function=embeddings,
persist_directory="./chroma_db"
)
# Ajouter des documents
vectorstore.add_documents(documents, ids=["doc1", "doc2", "doc3"])Charger une DB existante
vectorstore = Chroma(
persist_directory="./chroma_db",
embedding_function=embeddings
)CRUD Operations
# Get by ID
result = vectorstore.get(ids=["doc1"])
# Delete by ID
vectorstore.delete(ids=["doc1"])
# Count
count = vectorstore._collection.count()Filtrage par métadonnées
# Ajouter des métadonnées
documents = [
Document(
page_content="Contenu du chapitre 1",
metadata={"source": "livre.pdf", "chapter": 1, "author": "Dupont"}
)
]
# Filtrer lors de la recherche
results = vectorstore.similarity_search(
"question",
k=3,
filter={"chapter": 1}
)Query Syntax avancée
# Opérateurs de comparaison
results = vectorstore.similarity_search(
"question",
filter={"chapter": {"$gte": 2}} # chapter >= 2
)
# Opérateurs logiques
results = vectorstore.similarity_search(
"question",
filter={
"$and": [
{"author": {"$eq": "Dupont"}},
{"chapter": {"$gte": 2}}
]
}
)
# $or
results = vectorstore.similarity_search(
"question",
filter={
"$or": [
{"source": "livre1.pdf"},
{"source": "livre2.pdf"}
]
}
)| Opérateur | Signification |
|---|---|
$eq |
Égal |
$ne |
Différent |
$gt, $gte |
Plus grand (ou égal) |
$lt, $lte |
Plus petit (ou égal) |
$in |
Dans la liste |
$nin |
Pas dans la liste |
$and, $or |
Combinaison logique |
Filtrage par contenu (where_document)
# Recherche dans le contenu du document
results = vectorstore._collection.query(
query_texts=["question"],
where_document={"$contains": "RAG"}
)Timestamps et dates
import time
# Ajouter un timestamp
doc.metadata["created_at"] = int(time.time())
# Filtrer par date
one_week_ago = int(time.time()) - 7*24*60*60
results = vectorstore.similarity_search(
"question",
filter={"created_at": {"$gte": one_week_ago}}
)Section 10 : Automated VectorDB Updates
Lectures 128-135 : Détection de changements, Hashing
Détection de changements avec Hash
flowchart TD
A[Nouveaux docs] --> B[Calculer hash]
B --> C{Hash existe?}
C -->|Non| D[Ajouter au vector store]
C -->|Oui| E{Hash identique?}
E -->|Non| F[Mettre à jour]
E -->|Oui| G[Ignorer]
D --> H[Sauver manifest]
F --> H
import hashlib
import json
def compute_hash(content: str) -> str:
return hashlib.md5(content.encode()).hexdigest()
def load_manifest(path: str) -> dict:
try:
with open(path, "r") as f:
return json.load(f)
except FileNotFoundError:
return {}
def save_manifest(manifest: dict, path: str):
with open(path, "w") as f:
json.dump(manifest, f)Sync incrémentale
def sync_documents(documents, vectorstore, manifest_path="manifest.json"):
manifest = load_manifest(manifest_path)
for doc in documents:
doc_id = doc.metadata.get("source", str(hash(doc.page_content)))
new_hash = compute_hash(doc.page_content)
if doc_id not in manifest:
# Nouveau document
vectorstore.add_documents([doc], ids=[doc_id])
manifest[doc_id] = new_hash
elif manifest[doc_id] != new_hash:
# Document modifié
vectorstore.delete([doc_id])
vectorstore.add_documents([doc], ids=[doc_id])
manifest[doc_id] = new_hash
# Sinon: inchangé, on skip
save_manifest(manifest, manifest_path)Détection avec métadonnées
Alternative : stocker le hash dans les métadonnées Chroma :
# Lors de l'ajout
doc.metadata["content_hash"] = compute_hash(doc.page_content)
# Pour vérifier si changé
existing = vectorstore.get(where={"source": doc.metadata["source"]})
if existing and existing["metadatas"][0]["content_hash"] != new_hash:
# Le document a changé
passSection 11 : Application RAG complète
Lectures 136-143 : TKinter, Desktop App
GUI avec TKinter
import tkinter as tk
from tkinter import scrolledtext
class RAGChatApp:
def __init__(self, rag_chain):
self.rag_chain = rag_chain
self.window = tk.Tk()
self.window.title("RAG Chat")
# Zone de chat
self.chat_area = scrolledtext.ScrolledText(self.window, wrap=tk.WORD)
self.chat_area.pack(padx=10, pady=10, fill=tk.BOTH, expand=True)
# Zone de saisie
self.input_field = tk.Entry(self.window)
self.input_field.pack(padx=10, pady=5, fill=tk.X)
self.input_field.bind("<Return>", self.send_message)
# Bouton envoyer
self.send_button = tk.Button(self.window, text="Envoyer", command=self.send_message)
self.send_button.pack(pady=5)
def send_message(self, event=None):
question = self.input_field.get()
if question:
self.chat_area.insert(tk.END, f"Vous: {question}\n")
response = self.rag_chain.invoke(question)
self.chat_area.insert(tk.END, f"RAG: {response}\n\n")
self.input_field.delete(0, tk.END)
def run(self):
self.window.mainloop()
# Usage
app = RAGChatApp(rag_chain)
app.run()Desktop App avec PyInstaller
# Installation
pip install pyinstaller
# Créer l'exécutable
pyinstaller --onefile --windowed rag_app.pyStack technique
| Composant | Rôle |
|---|---|
| OpenAI API | LLM (gpt-4o-mini) + Embeddings |
| LangChain | Orchestration, Loaders, Chains |
| ChromaDB | Vector store persistant |
| LangGraph | Workflows avec état |
| TKinter | Interface utilisateur desktop |
Comparaison avec le cours AI Agents
| Aspect | AI Agents Course | RAG Course |
|---|---|---|
| Focus | Tool Calling, Memory | Document Retrieval |
| LLM | Ollama (local) | OpenAI API |
| Workflow | LangGraph (optionnel) | LangGraph (avancé) |
| Mémoire | Chat history | Document embeddings |
Bilan
Ce cours couvre en profondeur :
| Section | Concepts clés |
|---|---|
| Section 4 | ChatOpenAI, PromptTemplate, LCEL, Runnables, Output Parsers |
| Section 5 | Loaders, Splitters, Stuff/MapReduce/Refine |
| Section 6 | Embeddings, FAISS/Chroma, Retrieval Strategies, Sécurité |
| Section 7 | TypedDict, StateGraph, Reducers, Memory, Conditional Edges |
| Section 8 | DirectoryLoader, UnstructuredLoader, multi-formats |
| Section 9 | CRUD Chroma, Metadata filtering, Query syntax |
| Section 10 | Change detection, Hashing, Dynamic sync |
| Section 11 | TKinter GUI, Desktop App avec PyInstaller |
Le RAG est essentiel pour créer des assistants qui peuvent répondre sur vos données privées sans fine-tuning coûteux.