TP : Créer un Chatbot RAG de A à Z
Construisez un chatbot intelligent qui répond en se basant sur vos documents : chunking, embeddings, vector store, prompt engineering et interface web.
Objectif du TP
Nous allons construire un chatbot RAG complet qui peut répondre aux questions en se basant sur vos propres documents (PDF, texte, markdown).
Ce que nous allons construire
Stack technique
- Python 3.11+
- OpenAI API (embeddings + LLM)
- ChromaDB (vector store local)
- Streamlit (interface web)
Étape 1 : Setup du Projet
Installation
mkdir mon-chatbot-rag && cd mon-chatbot-rag
python -m venv venv
source venv/bin/activate # Linux/Mac
# venv\Scripts\activate # Windows
pip install openai chromadb streamlit pypdf2 tiktoken
Structure du projet
mon-chatbot-rag/
├── documents/ # Vos fichiers source (PDF, txt, md)
├── ingest.py # Script d''ingestion des documents
├── chat.py # Logique du chatbot RAG
├── app.py # Interface Streamlit
└── requirements.txt
Étape 2 : Ingestion des Documents
La première étape est de transformer vos documents en vecteurs stockés dans ChromaDB.
Le processus d''ingestion
Code : ingest.py
import os
from pathlib import Path
import chromadb
from openai import OpenAI
from PyPDF2 import PdfReader
client = OpenAI() # Utilise OPENAI_API_KEY env var
# --- 1. Lecture des documents ---
def load_documents(folder: str) -> list[dict]:
"""Charge tous les documents d''un dossier."""
docs = []
for file_path in Path(folder).rglob("*"):
if file_path.suffix == ".pdf":
reader = PdfReader(str(file_path))
text = "\n".join(page.extract_text() for page in reader.pages)
elif file_path.suffix in (".txt", ".md"):
text = file_path.read_text(encoding="utf-8")
else:
continue
docs.append({
"text": text,
"source": file_path.name,
})
print(f"Chargé : {file_path.name} ({len(text)} caractères)")
return docs
# --- 2. Chunking ---
def chunk_text(text: str, chunk_size: int = 500, overlap: int = 50) -> list[str]:
"""Découpe un texte en morceaux avec chevauchement."""
words = text.split()
chunks = []
start = 0
while start < len(words):
end = start + chunk_size
chunk = " ".join(words[start:end])
chunks.append(chunk)
start = end - overlap # Chevauchement pour ne pas couper le contexte
return chunks
# --- 3. Embedding + Stockage ---
def embed_and_store(docs: list[dict]):
"""Transforme les documents en vecteurs et les stocke."""
# Initialiser ChromaDB
chroma = chromadb.PersistentClient(path="./chroma_db")
collection = chroma.get_or_create_collection(
name="mes_documents",
metadata={"hnsw:space": "cosine"},
)
all_chunks = []
all_ids = []
all_metadata = []
for doc in docs:
chunks = chunk_text(doc["text"])
for i, chunk in enumerate(chunks):
chunk_id = f"{doc[''source'']}_{i}"
all_chunks.append(chunk)
all_ids.append(chunk_id)
all_metadata.append({"source": doc["source"], "chunk_index": i})
# Embedding par batch de 100
batch_size = 100
for i in range(0, len(all_chunks), batch_size):
batch_texts = all_chunks[i:i + batch_size]
batch_ids = all_ids[i:i + batch_size]
batch_meta = all_metadata[i:i + batch_size]
# Appel API OpenAI pour les embeddings
response = client.embeddings.create(
model="text-embedding-3-small",
input=batch_texts,
)
embeddings = [item.embedding for item in response.data]
collection.upsert(
ids=batch_ids,
embeddings=embeddings,
documents=batch_texts,
metadatas=batch_meta,
)
print(f"Indexé {i + len(batch_texts)}/{len(all_chunks)} chunks")
print(f"\nTotal : {len(all_chunks)} chunks indexés dans ChromaDB")
# --- Exécution ---
if __name__ == "__main__":
docs = load_documents("./documents")
embed_and_store(docs)
Chunk size de 500 mots avec un overlap de 50 mots est un bon point de départ. Si vos documents sont très structurés (articles, docs techniques), vous pouvez chunker par section/paragraphe.
Étape 3 : Le Chatbot RAG
Code : chat.py
import chromadb
from openai import OpenAI
client = OpenAI()
def get_collection():
"""Connecte à la collection ChromaDB existante."""
chroma = chromadb.PersistentClient(path="./chroma_db")
return chroma.get_collection("mes_documents")
def search_context(query: str, n_results: int = 5) -> list[dict]:
"""Recherche les chunks les plus pertinents."""
collection = get_collection()
# Embedding de la question
response = client.embeddings.create(
model="text-embedding-3-small",
input=[query],
)
query_embedding = response.data[0].embedding
# Recherche dans ChromaDB
results = collection.query(
query_embeddings=[query_embedding],
n_results=n_results,
)
contexts = []
for i in range(len(results["documents"][0])):
contexts.append({
"text": results["documents"][0][i],
"source": results["metadatas"][0][i]["source"],
"distance": results["distances"][0][i],
})
return contexts
def chat(query: str, history: list[dict] = None) -> str:
"""Génère une réponse RAG."""
# 1. Rechercher le contexte pertinent
contexts = search_context(query)
# 2. Construire le prompt enrichi
context_text = "\n\n---\n\n".join(
f"[Source: {c[''source'']}]\n{c[''text'']}" for c in contexts
)
system_prompt = f"""Tu es un assistant qui répond aux questions
en te basant UNIQUEMENT sur le contexte fourni ci-dessous.
Si le contexte ne contient pas l''information, dis-le clairement.
Cite toujours la source de tes informations.
--- CONTEXTE ---
{context_text}
--- FIN CONTEXTE ---"""
messages = [{"role": "system", "content": system_prompt}]
if history:
messages.extend(history)
messages.append({"role": "user", "content": query})
# 3. Appel au LLM
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
temperature=0.3, # Basse pour des réponses factuelles
)
return response.choices[0].message.content
Étape 4 : Interface Web avec Streamlit
Code : app.py
import streamlit as st
from chat import chat
st.set_page_config(page_title="Mon Chatbot RAG", page_icon="🤖")
st.title("Mon Chatbot RAG")
st.caption("Posez des questions sur vos documents")
# Historique de conversation
if "messages" not in st.session_state:
st.session_state.messages = []
# Afficher l''historique
for msg in st.session_state.messages:
with st.chat_message(msg["role"]):
st.markdown(msg["content"])
# Input utilisateur
if prompt := st.chat_input("Votre question..."):
# Afficher la question
st.session_state.messages.append({"role": "user", "content": prompt})
with st.chat_message("user"):
st.markdown(prompt)
# Générer la réponse
with st.chat_message("assistant"):
with st.spinner("Recherche en cours..."):
response = chat(
prompt,
history=st.session_state.messages[:-1],
)
st.markdown(response)
st.session_state.messages.append({"role": "assistant", "content": response})
Lancer l''application
En moins de 100 lignes de code, vous avez un chatbot RAG fonctionnel avec une interface web ! L''application est accessible sur http://localhost:8501.
Étape 5 : Améliorations
Amélioration 1 : Reranking
Le reranking améliore la pertinence des résultats en reclassant les chunks après la recherche vectorielle :
# Après la recherche vectorielle, reranker les résultats
from cohere import Client
cohere = Client(api_key="...")
def rerank_results(query: str, contexts: list[dict], top_n: int = 3):
"""Reranke les résultats avec Cohere."""
docs = [c["text"] for c in contexts]
response = cohere.rerank(
model="rerank-v3.5",
query=query,
documents=docs,
top_n=top_n,
)
return [contexts[r.index] for r in response.results]
Amélioration 2 : Chunking par en-têtes
Pour les documents structurés, un chunking par sections est plus efficace :
import re
def chunk_by_headers(text: str) -> list[str]:
"""Découpe par titres markdown (## ou ###)."""
sections = re.split(r'\n(?=#{2,3} )', text)
return [s.strip() for s in sections if len(s.strip()) > 50]
Amélioration 3 : Citations
Ajouter les sources exactes dans la réponse :
system_prompt = """...
IMPORTANT : Pour chaque information que tu donnes, cite la source
entre crochets comme ceci : [Source: nom_du_fichier.pdf]
Si plusieurs sources se contredisent, mentionne-le."""
Architecture Finale
Résumé
| Étape | Outil | Rôle |
|---|---|---|
| Ingestion | PyPDF2 + chunk_text | Lecture et découpage des documents |
| Embedding | OpenAI text-embedding-3-small | Texte → vecteurs |
| Stockage | ChromaDB | Base vectorielle locale |
| Recherche | ChromaDB query | Trouver les chunks pertinents |
| Génération | GPT-4o-mini | Réponse basée sur le contexte |
| Interface | Streamlit | Chat web interactif |
Pour aller plus loin : ajoutez le reranking, le chunking intelligent, les citations automatiques, et un système de feedback utilisateur pour améliorer la qualité.
Specialiste IA — Master Intelligence Artificielle
Diplome d'un Master en Intelligence Artificielle, je travaille au quotidien sur des projets IA en entreprise. J'ai cree IwanttolearnAI pour rendre l'apprentissage de l'IA accessible a tous, gratuitement.
Continuer a apprendre
TP : Fine-tuner un LLM sur vos Données
Apprenez à fine-tuner un LLM (Llama, Mistral) avec LoRA/QLoRA sur vos données métier : préparation du dataset, entraînement, évaluation et déploiement avec Ollama.
Token Trainer
Prédisez comment un tokenizer découpe les phrases en tokens ! Cliquez entre les caractères pour placer vos séparations et découvrez les règles de tokenization.
Embedding Explorer
Classez des phrases par similarité sémantique ! Découvrez comment les embeddings capturent le sens des mots grâce à une visualisation 2D interactive.