Créer un chatbot RAG de A à Z [TUTORIEL]

tl;dr: Tutoriel pratique pour construire un chatbot RAG professionnel : chargement documents (PDF, web), chunking intelligent, embeddings OpenAI, base vectorielle Chroma, mémoire conversationnelle, interface Gradio, et déploiement. Code complet inclus.

Vous voulez créer un chatbot qui répond avec vos propres données ? Un assistant qui connaît votre documentation, vos produits, votre base de connaissances ?

La technique : RAG (Retrieval-Augmented Generation) - combiner recherche sémantique + LLM génératif.

Dans ce tutoriel 100% pratique, vous allez construire de zéro un chatbot RAG professionnel capable de :

  • 📄 Ingérer documents (PDF, Markdown, sites web)
  • 🧠 Comprendre les questions en langage naturel
  • 🔍 Retrouver les informations pertinentes
  • 💬 Répondre avec contexte et citer ses sources
  • 🗣️ Maintenir une conversation cohérente
  • 🌐 Fonctionner via interface web élégante

Stack technique :

  • LangChain : orchestration RAG
  • OpenAI GPT-4o : LLM génératif
  • Chroma : base vectorielle locale
  • Gradio : interface web
  • Python 3.10+
💡 À la fin de ce tutoriel, vous aurez un chatbot RAG fonctionnel que vous pourrez adapter à vos propres données. Temps estimé : 2-3 heures.

Prérequis :

  • Python 3.10+
  • Clé API OpenAI
  • Connaissances basiques Python
  • 8 GB RAM minimum

Tutoriel pratique étape par étape : la création d’un chatbot avec RAG avec exemples de code

Table des matières

  1. Architecture du chatbot RAG
  2. Setup de l’environnement
  3. Ingestion des documents
  4. Logique du chatbot RAG
  5. Interface Gradio
  6. Optimisations et améliorations
  7. Tests et évaluation
  8. Déploiement
  9. Monitoring et maintenance
  10. Conclusion
  11. Pour aller plus loin

Architecture du chatbot RAG

Vue d’ensemble

┌─────────────────────────────────────────────────────────────┐
                    PIPELINE RAG COMPLET                      
└─────────────────────────────────────────────────────────────┘

1. PRÉPARATION (offline)
   Documents (PDF, MD, Web)
         
   Loading (LangChain loaders)
         
   Chunking (RecursiveCharacterTextSplitter)
         
   Embeddings (OpenAI text-embedding-3-small)
         
   Vector DB (Chroma)

2. CONVERSATION (runtime)
   User Query  Embedding  Similarity Search  Top-K docs
                                                       
                                            Contexte + History
                                                       
                                     GPT-4o (avec prompt + mémoire)
                                                       
                                                  Réponse

Composants clés

ComposantRôleTechnologie
Document LoadersCharger PDFs, web, markdownLangChain loaders
Text SplitterDécouper en chunks 1000 tokensRecursiveCharacterTextSplitter
EmbeddingsTransformer texte → vecteursOpenAI text-embedding-3-small
Vector StoreStockage + recherche similaritéChroma
RetrieverRécupérer top-K documentsMMR retriever
LLMGénération réponse contextuelleGPT-4o
MemoryHistorique conversationConversationBufferMemory
InterfaceChat webGradio
💡 Ce tutoriel utilise le pattern RAG classique. Pour aller plus loin, voir Agents LangChain pour un chatbot autonome multi-outils.

Setup de l’environnement

Installation des dépendances

Créer un nouveau projet :

mkdir chatbot-rag
cd chatbot-rag
python -m venv venv
source venv/bin/activate  # Windows: venv\Scripts\activate

Installer les packages :

pip install langchain==0.1.0 \
            langchain-openai==0.0.5 \
            langchain-community==0.0.13 \
            chromadb==0.4.22 \
            pypdf==4.0.0 \
            gradio==4.16.0 \
            python-dotenv==1.0.0 \
            beautifulsoup4==4.12.0 \
            lxml==5.1.0

Explication des packages :

  • langchain : framework RAG
  • langchain-openai : intégration OpenAI
  • chromadb : base vectorielle
  • pypdf : lecture PDFs
  • gradio : interface web
  • beautifulsoup4 : scraping web

Configuration

Créer .env avec vos clés API :

# .env
OPENAI_API_KEY=sk-proj-...
LANGCHAIN_TRACING_V2=true
LANGCHAIN_API_KEY=ls__...  # Optionnel : monitoring LangSmith

Créer structure du projet :

chatbot-rag/
├── .env
├── requirements.txt
├── data/               # Documents source
│   ├── pdfs/
│   ├── markdown/
│   └── urls.txt
├── chroma_db/          # Base vectorielle (généré)
├── src/
│   ├── 1_ingest.py     # Ingestion documents
│   ├── 2_chatbot.py    # Logique RAG
│   └── 3_interface.py  # Interface Gradio
└── README.md
⚠️ Warning
Ne committez JAMAIS votre .env ! Ajoutez-le au .gitignore. Votre clé API OpenAI donne accès à votre compte.

Ingestion des documents

Script d’ingestion complet

Créer src/1_ingest.py :

"""
Ingestion de documents dans Chroma
Supporte : PDF, Markdown, URLs
"""

import os
from dotenv import load_dotenv
from langchain.document_loaders import (
    PyPDFLoader,
    DirectoryLoader,
    TextLoader,
    WebBaseLoader
)
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import Chroma

# Charger variables d'environnement
load_dotenv()

# Configuration
CHROMA_PATH = "./chroma_db"
DATA_PATH = "./data"

def load_documents():
    """Charge tous les documents depuis data/"""
    documents = []

    # 1. Charger PDFs
    print("📄 Chargement des PDFs...")
    pdf_loader = DirectoryLoader(
        f"{DATA_PATH}/pdfs",
        glob="**/*.pdf",
        loader_cls=PyPDFLoader,
        show_progress=True
    )
    documents.extend(pdf_loader.load())

    # 2. Charger Markdown
    print("📝 Chargement des fichiers Markdown...")
    md_loader = DirectoryLoader(
        f"{DATA_PATH}/markdown",
        glob="**/*.md",
        loader_cls=TextLoader,
        loader_kwargs={"encoding": "utf-8"}
    )
    documents.extend(md_loader.load())

    # 3. Charger URLs (depuis urls.txt)
    urls_file = f"{DATA_PATH}/urls.txt"
    if os.path.exists(urls_file):
        print("🌐 Chargement des URLs...")
        with open(urls_file, "r") as f:
            urls = [line.strip() for line in f if line.strip()]

        if urls:
            web_loader = WebBaseLoader(urls)
            web_loader.requests_per_second = 2  # Rate limiting
            documents.extend(web_loader.load())

    print(f"✅ Total: {len(documents)} documents chargés")
    return documents

def split_documents(documents):
    """Découpe les documents en chunks optimaux"""
    print("✂️ Découpage en chunks...")

    text_splitter = RecursiveCharacterTextSplitter(
        chunk_size=1000,       # Taille cible par chunk
        chunk_overlap=200,     # Overlap 20% pour continuité
        length_function=len,
        separators=["\n\n", "\n", ". ", " ", ""]
    )

    chunks = text_splitter.split_documents(documents)
    print(f"✅ {len(chunks)} chunks créés")

    return chunks

def create_vector_store(chunks):
    """Crée la base vectorielle Chroma"""
    print("🧠 Création des embeddings et indexation...")

    # Initialiser embeddings OpenAI
    embeddings = OpenAIEmbeddings(
        model="text-embedding-3-small",  # 1536 dimensions
        chunk_size=1000  # Batch size pour API
    )

    # Créer/mettre à jour Chroma
    # Si chroma_db existe déjà, il sera mis à jour
    vectorstore = Chroma.from_documents(
        documents=chunks,
        embedding=embeddings,
        persist_directory=CHROMA_PATH,
        collection_name="knowledge_base"
    )

    print(f"✅ Base vectorielle créée : {CHROMA_PATH}")
    print(f"📊 Nombre de vecteurs : {vectorstore._collection.count()}")

    return vectorstore

def main():
    """Pipeline complet d'ingestion"""
    print("=" * 50)
    print("🚀 INGESTION DE DOCUMENTS")
    print("=" * 50)

    # 1. Charger documents
    documents = load_documents()

    if not documents:
        print("❌ Aucun document trouvé dans ./data/")
        print("   Ajoutez des PDFs dans ./data/pdfs/")
        print("   Ajoutez des .md dans ./data/markdown/")
        print("   Ajoutez des URLs dans ./data/urls.txt")
        return

    # 2. Découper en chunks
    chunks = split_documents(documents)

    # 3. Créer base vectorielle
    vectorstore = create_vector_store(chunks)

    # 4. Test de recherche
    print("\n🔍 Test de recherche...")
    results = vectorstore.similarity_search("RAG", k=3)
    print(f"Top résultat : {results[0].page_content[:200]}...")

    print("\n✅ Ingestion terminée avec succès!")

if __name__ == "__main__":
    main()

Préparer les données

Créer quelques documents de test :

data/markdown/intro_rag.md :

# Introduction au RAG

Le RAG (Retrieval-Augmented Generation) est une technique qui combine :
1. Recherche sémantique dans une base de connaissances
2. Génération de texte par un LLM

Cette approche permet aux LLMs d'accéder à des informations spécifiques
sans fine-tuning, réduisant les hallucinations et améliorant la précision.

## Avantages {#avantages}
- Réponses basées sur vos données
- Pas de fine-tuning nécessaire
- Sources citées
- Mise à jour facile des connaissances

data/urls.txt :

https://naileru.com/ia/rag/
https://naileru.com/ia/embedding/

Exécuter l’ingestion

python src/1_ingest.py

Sortie attendue :

==================================================
🚀 INGESTION DE DOCUMENTS
==================================================
📄 Chargement des PDFs...
📝 Chargement des fichiers Markdown...
🌐 Chargement des URLs...
✅ Total: 15 documents chargés
✂️ Découpage en chunks...
✅ 247 chunks créés
🧠 Création des embeddings et indexation...
✅ Base vectorielle créée : ./chroma_db
📊 Nombre de vecteurs : 247
🔍 Test de recherche...
Top résultat : Le RAG (Retrieval-Augmented Generation) est une technique...
✅ Ingestion terminée avec succès!
💡 L’ingestion est un processus offline. Lancez-le à chaque fois que vous ajoutez/modifiez des documents. Coût : ~$0.02 pour 1000 pages.

Logique du chatbot RAG

Script principal

Créer src/2_chatbot.py :

"""
Logique principale du chatbot RAG avec mémoire conversationnelle
"""

from dotenv import load_dotenv
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import Chroma
from langchain.chat_models import ChatOpenAI
from langchain.chains import ConversationalRetrievalChain
from langchain.memory import ConversationBufferMemory
from langchain.prompts import PromptTemplate

load_dotenv()

CHROMA_PATH = "./chroma_db"

class RAGChatbot:
    """Chatbot RAG avec mémoire et sources"""

    def __init__(self):
        self.vectorstore = None
        self.chain = None
        self.chat_history = []
        self._setup()

    def _setup(self):
        """Initialise tous les composants"""
        print("🔧 Initialisation du chatbot...")

        # 1. Charger base vectorielle
        embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
        self.vectorstore = Chroma(
            persist_directory=CHROMA_PATH,
            embedding_function=embeddings,
            collection_name="knowledge_base"
        )
        print(f"✅ Base vectorielle chargée ({self.vectorstore._collection.count()} vecteurs)")

        # 2. Créer retriever avec MMR
        self.retriever = self.vectorstore.as_retriever(
            search_type="mmr",  # Maximum Marginal Relevance
            search_kwargs={
                "k": 5,           # Top 5 résultats finaux
                "fetch_k": 20     # Récupère 20, garde les 5 plus divers
            }
        )

        # 3. LLM
        self.llm = ChatOpenAI(
            model="gpt-4o",
            temperature=0.3,  # Un peu de créativité
            max_tokens=1000
        )

        # 4. Mémoire conversationnelle
        self.memory = ConversationBufferMemory(
            memory_key="chat_history",
            return_messages=True,
            output_key="answer"
        )

        # 5. Prompt personnalisé
        prompt_template = """Tu es un assistant expert qui répond aux questions en te basant sur le contexte fourni.

Contexte pertinent :
{context}

Historique de conversation :
{chat_history}

Question actuelle : {question}

Instructions :
1. Réponds en te basant UNIQUEMENT sur le contexte fourni
2. Si l'information n'est pas dans le contexte, dis "Je n'ai pas cette information dans ma base de connaissances"
3. Cite tes sources en mentionnant les documents pertinents
4. Sois concis mais complet
5. Utilise un ton professionnel et amical
6. Si la question fait référence à la conversation précédente, utilise l'historique

Réponse :"""

        PROMPT = PromptTemplate(
            template=prompt_template,
            input_variables=["context", "chat_history", "question"]
        )

        # 6. Créer la chaîne RAG
        self.chain = ConversationalRetrievalChain.from_llm(
            llm=self.llm,
            retriever=self.retriever,
            memory=self.memory,
            return_source_documents=True,
            combine_docs_chain_kwargs={"prompt": PROMPT},
            verbose=False
        )

        print("✅ Chatbot prêt!\n")

    def ask(self, question: str) -> dict:
        """
        Pose une question au chatbot

        Returns:
            dict avec 'answer', 'sources', 'chat_history'
        """
        # Appeler la chaîne
        result = self.chain({"question": question})

        # Extraire sources
        sources = []
        for doc in result.get("source_documents", []):
            source_info = {
                "content": doc.page_content[:200] + "...",
                "metadata": doc.metadata
            }
            sources.append(source_info)

        return {
            "answer": result["answer"],
            "sources": sources,
            "chat_history": self.chat_history
        }

    def reset_memory(self):
        """Efface l'historique de conversation"""
        self.memory.clear()
        self.chat_history = []
        print("🔄 Mémoire effacée")

# Test du chatbot
if __name__ == "__main__":
    chatbot = RAGChatbot()

    # Conversation de test
    questions = [
        "Qu'est-ce que le RAG ?",
        "Quels sont ses avantages ?",
        "Comment l'implémenter ?"
    ]

    for q in questions:
        print(f"\n{'='*60}")
        print(f"❓ {q}")
        print('='*60)

        response = chatbot.ask(q)

        print(f"\n💬 {response['answer']}\n")

        if response['sources']:
            print("📚 Sources :")
            for i, source in enumerate(response['sources'][:3], 1):
                print(f"   {i}. {source['content']}")

Tester le chatbot

python src/2_chatbot.py

Sortie attendue :

🔧 Initialisation du chatbot...
✅ Base vectorielle chargée (247 vecteurs)
✅ Chatbot prêt!

============================================================
❓ Qu'est-ce que le RAG ?
============================================================

💬 Le RAG (Retrieval-Augmented Generation) est une technique qui combine
la recherche sémantique dans une base de connaissances avec la génération
de texte par un LLM. Cette approche permet aux modèles d'accéder à des
informations spécifiques sans nécessiter de fine-tuning.

📚 Sources :
   1. Le RAG (Retrieval-Augmented Generation) est une technique...
   2. Cette approche permet aux LLMs d'accéder à des informations...
💡 La mémoire ConversationBufferMemory garde tout l’historique en contexte. Pour de longues conversations, utilisez ConversationSummaryMemory pour éviter de dépasser le context window.

Interface Gradio

Script interface web

Créer src/3_interface.py :

"""
Interface Gradio pour le chatbot RAG
"""

import gradio as gr
from chatbot import RAGChatbot

# Initialiser chatbot global
chatbot = RAGChatbot()

def respond(message, history):
    """
    Fonction appelée par Gradio pour chaque message

    Args:
        message: Question de l'utilisateur
        history: Historique Gradio (liste de tuples)

    Returns:
        str: Réponse du chatbot
    """
    # Obtenir réponse
    response = chatbot.ask(message)

    # Formatter réponse avec sources
    answer = response["answer"]

    if response["sources"]:
        answer += "\n\n---\n**📚 Sources :**\n"
        for i, source in enumerate(response["sources"][:3], 1):
            # Extraire nom de fichier si disponible
            source_name = source["metadata"].get("source", "Document")
            if "/" in source_name:
                source_name = source_name.split("/")[-1]

            answer += f"\n{i}. *{source_name}*"
            if "page" in source["metadata"]:
                answer += f" (page {source['metadata']['page']})"

    return answer

def reset():
    """Reset la conversation"""
    chatbot.reset_memory()
    return None

# Créer interface Gradio
with gr.Blocks(theme=gr.themes.Soft(), title="Chatbot RAG") as demo:
    gr.Markdown(
        """
        # 🤖 Chatbot RAG Intelligent

        Posez vos questions sur la base de connaissances.
        Le chatbot recherche les informations pertinentes et cite ses sources.
        """
    )

    with gr.Row():
        with gr.Column(scale=4):
            chatbot_ui = gr.Chatbot(
                label="Conversation",
                height=500,
                show_copy_button=True
            )

            with gr.Row():
                msg = gr.Textbox(
                    label="Votre question",
                    placeholder="Tapez votre question ici...",
                    lines=2,
                    scale=4
                )
                submit_btn = gr.Button("Envoyer", variant="primary", scale=1)

            with gr.Row():
                clear_btn = gr.Button("🔄 Nouvelle conversation", size="sm")

        with gr.Column(scale=1):
            gr.Markdown(
                """
                ### 💡 Exemples de questions

                - Qu'est-ce que le RAG ?
                - Comment fonctionne l'embedding ?
                - Quels sont les avantages du RAG ?
                - Compare RAG et fine-tuning

                ### ℹ️ Informations

                - **Modèle** : GPT-4o
                - **Embeddings** : text-embedding-3-small
                - **Base vectorielle** : Chroma
                - **Mémoire** : Conversation complète
                """
            )

    # Events
    msg.submit(respond, [msg, chatbot_ui], [chatbot_ui])
    submit_btn.click(respond, [msg, chatbot_ui], [chatbot_ui])
    clear_btn.click(reset, None, chatbot_ui, queue=False)

# Lancer l'application
if __name__ == "__main__":
    demo.launch(
        server_name="0.0.0.0",  # Accessible depuis réseau local
        server_port=7860,
        share=False,  # True pour créer lien public temporaire
        show_error=True
    )

Lancer l’interface

python src/3_interface.py

Ouvrir dans le navigateur : http://localhost:7860

Vous verrez une interface moderne avec :

  • Zone de chat avec historique
  • Zone de saisie
  • Bouton pour nouvelle conversation
  • Exemples de questions
  • Informations système
💡 Pour partager votre chatbot temporairement (ex: démo client), ajoutez share=True lors du lancement. Gradio créera un lien public valide 72h.

Optimisations et améliorations

Reranking pour meilleure précision

Ajouter un reranker Cohere pour améliorer la pertinence :

pip install cohere

Modifier src/2_chatbot.py :

from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import CohereRerank

# Dans _setup(), après création du retriever de base
base_retriever = self.vectorstore.as_retriever(
    search_type="mmr",
    search_kwargs={"k": 20}  # Récupérer plus de candidats
)

# Ajouter reranker
compressor = CohereRerank(
    cohere_api_key=os.getenv("COHERE_API_KEY"),
    model="rerank-multilingual-v2.0",  # Support français
    top_n=5  # Garder top 5 après reranking
)

self.retriever = ContextualCompressionRetriever(
    base_compressor=compressor,
    base_retriever=base_retriever
)

Gain : +15-25% de précision sur retrieval (selon benchmarks)

Chunking intelligent par document

Pour mieux préserver la structure :

from langchain.text_splitter import MarkdownHeaderTextSplitter, RecursiveCharacterTextSplitter

def smart_split_documents(documents):
    """Chunking adapté au type de document"""
    all_chunks = []

    for doc in documents:
        # Markdown : découper par headers
        if doc.metadata.get("source", "").endswith(".md"):
            headers_to_split_on = [
                ("#", "Header 1"),
                ("##", "Header 2"),
                ("###", "Header 3"),
            ]

            md_splitter = MarkdownHeaderTextSplitter(
                headers_to_split_on=headers_to_split_on
            )
            md_chunks = md_splitter.split_text(doc.page_content)

            # Découper davantage si chunks trop gros
            text_splitter = RecursiveCharacterTextSplitter(
                chunk_size=1000,
                chunk_overlap=200
            )
            chunks = text_splitter.split_documents(md_chunks)

        # PDF : chunking standard
        else:
            text_splitter = RecursiveCharacterTextSplitter(
                chunk_size=1000,
                chunk_overlap=200
            )
            chunks = text_splitter.split_documents([doc])

        all_chunks.extend(chunks)

    return all_chunks

Filtrage par métadonnées

Ajouter filtres à la recherche :

# Dans chatbot.ask(), avant d'appeler la chaîne
# Détecter si question contient filtre temporel
import re
from datetime import datetime, timedelta

def extract_time_filter(question):
    """Extrait filtre temporel de la question"""
    # Exemples: "cette semaine", "ce mois", "2024"

    if "cette semaine" in question.lower():
        week_ago = datetime.now() - timedelta(days=7)
        return {"date": {"$gte": week_ago.isoformat()}}

    if "ce mois" in question.lower():
        month_ago = datetime.now() - timedelta(days=30)
        return {"date": {"$gte": month_ago.isoformat()}}

    # Extraction année
    year_match = re.search(r'\b(20\d{2})\b', question)
    if year_match:
        year = year_match.group(1)
        return {"year": year}

    return None

# Utilisation
time_filter = extract_time_filter(question)
if time_filter:
    # Récupérer avec filtre
    docs = self.vectorstore.similarity_search(
        question,
        k=5,
        filter=time_filter
    )

Cache des embeddings

Pour éviter de recalculer les mêmes embeddings :

pip install diskcache
from langchain.embeddings import CacheBackedEmbeddings
from langchain.storage import LocalFileStore

# Dans _setup()
store = LocalFileStore("./cache/embeddings")

cached_embeddings = CacheBackedEmbeddings.from_bytes_store(
    underlying_embeddings=OpenAIEmbeddings(model="text-embedding-3-small"),
    document_embedding_cache=store,
    namespace="openai-embeddings"
)

# Utiliser cached_embeddings au lieu de embeddings

Gain : Économie de ~$0.10 par 1M tokens sur re-indexations

💡 Ces optimisations réduisent coûts et améliorent qualité. Le reranking est particulièrement efficace (+20% précision) pour un coût marginal ($1/1000 requêtes).

Tests et évaluation

Créer un dataset de test

tests/eval_dataset.json :

[
  {
    "question": "Qu'est-ce que le RAG ?",
    "expected_keywords": ["retrieval", "augmented", "generation", "recherche", "llm"],
    "should_have_sources": true
  },
  {
    "question": "Quels sont les avantages du RAG ?",
    "expected_keywords": ["hallucinations", "connaissances", "précision", "sources"],
    "should_have_sources": true
  },
  {
    "question": "Qui a inventé la relativité ?",
    "expected_keywords": ["pas cette information", "base de connaissances"],
    "should_have_sources": false
  }
]

Script d’évaluation

tests/evaluate.py :

"""Évaluation du chatbot RAG"""

import json
from src.chatbot import RAGChatbot

def evaluate():
    """Évalue le chatbot sur dataset de test"""

    # Charger dataset
    with open("tests/eval_dataset.json", "r") as f:
        dataset = json.load(f)

    chatbot = RAGChatbot()
    results = []

    print("🧪 ÉVALUATION DU CHATBOT")
    print("=" * 60)

    for i, test_case in enumerate(dataset, 1):
        question = test_case["question"]
        expected_keywords = test_case["expected_keywords"]
        should_have_sources = test_case["should_have_sources"]

        print(f"\n{i}. {question}")

        # Obtenir réponse
        response = chatbot.ask(question)
        answer = response["answer"].lower()
        has_sources = len(response["sources"]) > 0

        # Vérifier keywords
        keywords_found = sum(1 for kw in expected_keywords if kw.lower() in answer)
        keyword_score = keywords_found / len(expected_keywords)

        # Vérifier sources
        source_check = has_sources == should_have_sources

        # Score global
        score = (keyword_score + (1 if source_check else 0)) / 2

        result = {
            "question": question,
            "answer": response["answer"],
            "keyword_score": keyword_score,
            "source_check": source_check,
            "score": score
        }
        results.append(result)

        print(f"   Keywords: {keywords_found}/{len(expected_keywords)}")
        print(f"   Sources: {'✅' if source_check else '❌'}")
        print(f"   Score: {score:.1%}")

    # Résumé
    avg_score = sum(r["score"] for r in results) / len(results)
    print(f"\n{'='*60}")
    print(f"📊 RÉSULTATS")
    print(f"   Score moyen: {avg_score:.1%}")
    print(f"   Tests passés: {sum(1 for r in results if r['score'] >= 0.7)}/{len(results)}")

    # Sauvegarder résultats
    with open("tests/results.json", "w") as f:
        json.dump(results, f, indent=2, ensure_ascii=False)

    print(f"\n✅ Résultats sauvegardés dans tests/results.json")

if __name__ == "__main__":
    evaluate()

Exécuter :

python tests/evaluate.py
💡 Un bon chatbot RAG devrait scorer >80% sur un dataset représentatif. Itérez sur prompt, chunking et retrieval pour améliorer.

Déploiement

Hugging Face Spaces (gratuit)

Étapes :

  1. Créer requirements.txt :
langchain==0.1.0
langchain-openai==0.0.5
chromadb==0.4.22
gradio==4.16.0
pypdf==4.0.0
python-dotenv==1.0.0
  1. Créer app.py (point d’entrée) :
from src.interface import demo

if __name__ == "__main__":
    demo.launch()
  1. Créer compte sur https://huggingface.co/spaces
  2. Nouveau Space → Gradio
  3. Upload fichiers :
    • app.py
    • requirements.txt
    • src/ (dossier)
    • chroma_db/ (dossier)
  4. Ajouter secret OPENAI_API_KEY dans Settings

Lien public : https://huggingface.co/spaces/USERNAME/chatbot-rag

Docker (self-hosted)

Dockerfile :

FROM python:3.10-slim

WORKDIR /app

# Installer dépendances
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copier application
COPY . .

# Exposer port Gradio
EXPOSE 7860

# Lancer interface
CMD ["python", "src/3_interface.py"]

Build et run :

docker build -t chatbot-rag .
docker run -p 7860:7860 \
  -e OPENAI_API_KEY=sk-... \
  -v $(pwd)/chroma_db:/app/chroma_db \
  chatbot-rag

Serverless (modal)

pip install modal

deploy_modal.py :

import modal

stub = modal.Stub("chatbot-rag")

@stub.function(
    image=modal.Image.debian_slim().pip_install(
        "langchain", "langchain-openai", "chromadb", "gradio"
    ),
    secrets=[modal.Secret.from_name("openai-secret")],
    mounts=[modal.Mount.from_local_dir("./chroma_db", remote_path="/chroma_db")]
)
@modal.web_endpoint(method="POST")
def ask(question: str):
    from src.chatbot import RAGChatbot
    chatbot = RAGChatbot()
    response = chatbot.ask(question)
    return response

@stub.function()
@modal.asgi_app()
def app():
    from src.interface import demo
    return demo.app

Déployer :

modal deploy deploy_modal.py
💡 Hugging Face Spaces est idéal pour démos. Docker pour self-hosting. Modal pour production serverless avec scaling automatique.

Monitoring et maintenance

LangSmith pour observabilité

Activer tracing dans .env :

LANGCHAIN_TRACING_V2=true
LANGCHAIN_API_KEY=ls__...
LANGCHAIN_PROJECT=chatbot-rag-prod

Métriques trackées automatiquement :

  • Latence par composant (retrieval, LLM, total)
  • Coûts par requête
  • Tokens utilisés (input/output)
  • Erreurs et stack traces
  • Feedback utilisateurs

Dashboard : https://smith.langchain.com/

Logs structurés

Ajouter logging :

import logging
import json
from datetime import datetime

# Configuration
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('chatbot.log'),
        logging.StreamHandler()
    ]
)

logger = logging.getLogger(__name__)

# Dans chatbot.ask()
def ask(self, question: str) -> dict:
    start_time = datetime.now()

    logger.info(json.dumps({
        "event": "question_received",
        "question": question,
        "timestamp": start_time.isoformat()
    }))

    try:
        result = self.chain({"question": question})

        duration = (datetime.now() - start_time).total_seconds()

        logger.info(json.dumps({
            "event": "response_generated",
            "duration_sec": duration,
            "num_sources": len(result.get("source_documents", [])),
            "timestamp": datetime.now().isoformat()
        }))

        return result

    except Exception as e:
        logger.error(json.dumps({
            "event": "error",
            "error": str(e),
            "question": question,
            "timestamp": datetime.now().isoformat()
        }))
        raise

Alertes sur coûts

Script de monitoring :

import os
from openai import OpenAI

client = OpenAI()

def check_usage():
    """Vérifie utilisation quotidienne OpenAI"""
    # Note: API usage nécessite org admin access

    # Estimer depuis logs LangSmith
    # Ou implémenter compteur local

    DAILY_BUDGET = 10.00  # $10/jour
    current_spend = get_daily_spend()  # À implémenter

    if current_spend > DAILY_BUDGET * 0.8:
        send_alert(f"⚠️ 80% du budget quotidien atteint : ${current_spend:.2f}")

    if current_spend > DAILY_BUDGET:
        send_alert(f"🚨 BUDGET DÉPASSÉ : ${current_spend:.2f}")
        # Optionnel : désactiver temporairement

def send_alert(message):
    """Envoie alerte (email, Slack, etc.)"""
    # Implémenter selon votre stack
    print(message)

Conclusion

Félicitations ! Vous avez construit un chatbot RAG production-ready de A à Z.

Ce que vous avez appris :

  • ✅ Ingestion multi-sources (PDF, Markdown, web)
  • ✅ Chunking intelligent et embeddings
  • ✅ Base vectorielle Chroma
  • ✅ RAG avec LangChain + GPT-4o
  • ✅ Mémoire conversationnelle
  • ✅ Interface Gradio professionnelle
  • ✅ Optimisations (reranking, cache, filtres)
  • ✅ Tests et évaluation
  • ✅ Déploiement multi-options

Prochaines étapes :

  1. Adapter à vos données : Remplacez les documents de test par votre vraie documentation
  2. Optimiser le chunking : Expérimentez avec tailles/overlaps selon vos documents
  3. Personnaliser le prompt : Ajustez ton, format, instructions spécifiques
  4. Ajouter des filtres : Par catégorie, date, auteur selon vos métadonnées
  5. Améliorer le reranking : Tester Cohere vs cross-encoders
  6. Implémenter feedback : Boutons 👍/👎 pour amélioration continue
  7. Scaling : Passer à Pinecone ou Qdrant si >1M documents

Ressources pour aller plus loin :

💡 Vous maîtrisez maintenant le RAG end-to-end ! Partagez votre création sur LinkedIn et taguez @NaileruTech pour être featured.

Pour aller plus loin

Articles connexes :

Ressources externes :

Communauté :