Intelligence Artificielle

Mettre en place un chatbot interne capable de citer ses sources pas à pas

13 min de lecture

Dans beaucoup d’entreprises, l’information existe déjà. Elle se trouve dans des guides RH, des procédures qualité, des documents techniques, des PDF commerciaux, des notes de service, des comptes rendus, des FAQ internes ou des documentations produit. Le vrai problème n’est donc pas toujours l’absence de connaissance. Le problème, c’est l’accès rapide à cette connaissance.

Un chatbot interne capable de citer ses sources change fondamentalement la manière dont les équipes interagissent avec l’information. Au lieu de chercher manuellement dans plusieurs dossiers partagés, l’utilisateur pose une question en langage naturel et reçoit une réponse précise, accompagnée des passages qui la justifient.

Ce tutoriel vous guide, pas à pas, dans la construction d’un chatbot documentaire basé sur RAG (Retrieval-Augmented Génération), capable de citer ses sources et déployable localement sans infrastructure complexe.

Comprendre pourquoi la citation des sources est cruciale

Un chatbot qui répond sans dire d’où vient l’information crée un problème de confiance. Dans un contexte professionnel, un collaborateur ou un client ne peut pas vérifier si la réponse est correcte, récente ou bien interprétée. La confiance dans l’outil s’érode rapidement si des erreurs apparaissent sans traçabilité.

Les trois guide général d’un chatbot documentaire fiable

  • La précision : le chatbot ne doit répondre qu’à partir des documents fournis
  • La traçabilité : chaque réponse doit mentionner le ou les documents sources et leur page
  • L’honnêteté : si l’information n’est pas dans les documents, le chatbot doit le dire explicitement

Ces trois guide général sont non négociables dans un déploiement sérieux.

Ce qui distingue un chatbot documentaire d’un chatbot généraliste

Un chatbot généraliste (comme ChatGPT sans contexte) répond à partir de sa mémoire d’entraînement. Il peut être fluide et convaincant, mais il ne connaît pas vos procédures internes, vos tarifs, vos politiques RH ou vos spécifications techniques. Un chatbot documentaire ancré sur vos fichiers répond uniquement à partir de vos données. Il est moins impressionnant dans l’absolu, mais beaucoup plus utile et fiable dans votre contexte précis.

Architecture technique du projet

Notre chatbot interne reposera sur la stack suivante :

  • PyMuPDF : extraction du texte des PDF
  • FAISS : index vectoriel pour la recherche sémantique
  • Ollama : modèles locaux pour les embeddings et le chat
  • Streamlit : interface conversationnelle dans le navigateur

Structure du projet

chatbot-interne/
├── app.py               # Interface Streamlit
├── indexer.py           # Extraction et indexation des documents
├── retriever.py         # Recherche de passages pertinents
├── generator.py         # Génération de réponse avec citations
├── requirements.txt
└── docs/                # Vos documents PDF

Installation des dépendances

pip install pymupdf faiss-cpu numpy requests streamlit

# Installer Ollama et télécharger les modèles
ollama pull gemma3
ollama pull embeddinggemma

💡 Conseil : pour un chatbot documentaire, séparez toujours la fonction « retrouver le bon passage » de la fonction « rédiger la réponse ». Cette séparation évite beaucoup de confusion lors du débogage.

Extraire et préparer les documents

Module d’extraction (indexer.py)

import os, json, fitz, requests, numpy as np, faiss

def extract_chunks_from_pdf(pdf_path: str, chunk_size=800, overlap=100) -> list[dict]:
    doc = fitz.open(pdf_path)
    chunks = []
    for page_num, page in enumerate(doc):
        text = page.get_text("text", sort=True).strip()
        if not text:
            continue
        # Découper en chunks avec chevauchement
        for i in range(0, len(text), chunk_size - overlap):
            chunk = text[i:i + chunk_size].strip()
            if len(chunk) > 100:  # ignorer les chunks trop petits
                chunks.append({
                    "text": chunk,
                    "source": os.path.basename(pdf_path),
                    "page": page_num + 1
                })
    return chunks

def build_index(docs_dir: str):
    all_chunks = []
    for filename in os.listdir(docs_dir):
        if filename.endswith(".pdf"):
            path = os.path.join(docs_dir, filename)
            all_chunks.extend(extract_chunks_from_pdf(path))
    
    # Générer les embeddings
    texts = [c["text"] for c in all_chunks]
    resp = requests.post("http://localhost:11434/api/embed",
        json={"model": "embeddinggemma", "input": texts}, timeout=300)
    vectors = np.array(resp.json()["embeddings"], dtype="float32")
    
    # Normaliser et indexer
    norms = np.linalg.norm(vectors, axis=1, keepdims=True)
    vectors = vectors / np.clip(norms, 1e-12, None)
    
    index = faiss.IndexFlatIP(vectors.shape[1])
    index.add(vectors)
    
    faiss.write_index(index, "storage/index.faiss")
    with open("storage/metadata.json", "w", encoding="utf-8") as f:
        json.dump(all_chunks, f, ensure_ascii=False, indent=2)
    
    print(f"✅ {len(all_chunks)} chunks indexés depuis {docs_dir}")

Construire le moteur de récupération avec citations

Module de retrieval (retriever.py)

import json, requests, numpy as np, faiss

INDEX = faiss.read_index("storage/index.faiss")
with open("storage/metadata.json", encoding="utf-8") as f:
    METADATA = json.load(f)

def retrieve_with_citations(query: str, top_k=4) -> list[dict]:
    """Retrouve les passages les plus pertinents et retourne leurs métadonnées."""
    resp = requests.post("http://localhost:11434/api/embed",
        json={"model": "embeddinggemma", "input": query}, timeout=60)
    vector = np.array(resp.json()["embeddings"][0], dtype="float32")
    vector = vector / max(np.linalg.norm(vector), 1e-12)
    
    scores, indices = INDEX.search(vector.reshape(1, -1), top_k)
    
    results = []
    for score, idx in zip(scores[0], indices[0]):
        if idx >= 0:
            item = METADATA[idx].copy()
            item["relevance_score"] = float(score)
            results.append(item)
    
    return results

Générer des réponses avec citations explicites

Module de génération (generator.py)

import requests
from retriever import retrieve_with_citations

SYSTEM_PROMPT = """Tu es un assistant documentaire interne.
Règles absolues :
- Tu réponds UNIQUEMENT à partir des extraits fournis
- Tu DOIS citer les sources à la fin de chaque réponse sous la forme : [Source: nom_fichier.pdf, page X]
- Si l'information n'est pas dans les extraits, tu réponds : "Je n'ai pas trouvé cette information dans les documents disponibles."
- Tu n'inventes jamais, tu ne complètes jamais avec des connaissances générales"""

def generate_answer_with_citations(question: str) -> dict:
    contexts = retrieve_with_citations(question)
    
    # Construire le contexte avec indicateurs de source
    context_parts = []
    for ctx in contexts:
        context_parts.append(
            f"[{ctx['source']} - Page {ctx['page']}]
{ctx['text']}"
        )
    context_block = "

---

".join(context_parts)
    
    prompt = f"""{SYSTEM_PROMPT}

Question : {question}

Extraits disponibles :
{context_block}

Réponse (avec citations obligatoires) :"""

    resp = requests.post("http://localhost:11434/api/chat", json={
        "model": "gemma3",
        "messages": [{"rôle": "user", "content": prompt}],
        "stream": False
    }, timeout=120)
    
    answer = resp.json()["message"]["content"]
    
    return {
        "answer": answer,
        "sources": [{"file": c["source"], "page": c["page"], "score": c["relevance_score"]} for c in contexts]
    }

💡 Conseil : le fait d’inclure les citations dans le prompt système comme une « règle absolue » réduit significativement les cas où le modèle oublie de citer ses sources. Testez systématiquement avec des questions auxquelles vous connaissez la réponse.

Construire l’interface conversationnelle Streamlit

Application principale (app.py)

import streamlit as st
from indexer import build_index
from generator import generate_answer_with_citations
import os

st.set_page_config(page_title="Chatbot Documentaire Interne", page_icon="💬", layout="wide")
st.title("💬 Chatbot Interne — Réponses avec sources")

# Sidebar pour l'indexation
with st.sidebar:
    st.header("📂 Documents")
    if st.button("🔄 Réindexer les documents"):
        with st.spinner("Indexation en cours..."):
            os.makedirs("storage", exist_ok=True)
            build_index("docs")
            st.success("Indexation terminée !")
    
    st.markdown("---")
    st.caption("Déposez vos PDF dans le dossier 'docs/' puis réindexez.")

# Zone de conversation
if "messages" not in st.session_state:
    st.session_state.messages = []

for msg in st.session_state.messages:
    with st.chat_message(msg["rôle"]):
        st.markdown(msg["content"])
        if msg.get("sources"):
            with st.expander("📎 Sources utilisées"):
                for s in msg["sources"]:
                    st.markdown(f"- **{s['file']}** — Page {s['page']} (pertinence: {s['score']:.3f})")

question = st.chat_input("Posez votre question sur les documents internes...")

if question:
    st.session_state.messages.append({"rôle": "user", "content": question})
    with st.chat_message("user"):
        st.markdown(question)
    
    with st.chat_message("assistant"):
        with st.spinner("Recherche dans les documents..."):
            result = generate_answer_with_citations(question)
        
        st.markdown(result["answer"])
        
        with st.expander("📎 Sources utilisées"):
            for s in result["sources"]:
                st.markdown(f"- **{s['file']}** — Page {s['page']} (pertinence: {s['score']:.3f})")
    
    st.session_state.messages.append({
        "rôle": "assistant",
        "content": result["answer"],
        "sources": result["sources"]
    })
# Lancer l'application
streamlit run app.py

Améliorer la fiabilité des citations

Technique 1 : validation automatique des citations

Après génération, vous pouvez vérifier programmatiquement que la réponse contient bien une référence de source :

import re

def validate_citations(answer: str, expected_sources: list) -> bool:
    """Vérifie que la réponse contient au moins une citation valide."""
    citation_pattern = r'[Source:.*?]|[.*?.pdf.*?]'
    citations_found = re.findall(citation_pattern, answer)
    return len(citations_found) > 0

Technique 2 : mode strict vs mode flexible

Proposez deux modes à l’utilisateur :

  • Mode strict : répond uniquement si un passage très pertinent est trouvé (score > 0.7)
  • Mode flexible : répond même avec des passages moyennement pertinents en indiquant l’incertitude

Technique 3 : historique des questions sans réponse

Journalisez les questions pour lesquelles le chatbot répond « Je n’ai pas trouvé ». Ces questions révèlent les lacunes de votre base documentaire et permettent de prioriser les documents à ajouter.

Déployer dans un environnement professionnel

Points de vigilance en production

  • Confidentialité : assurez-vous que votre modèle Ollama tourne en local et que rien n’est envoyé vers l’extérieur
  • Mise à jour des documents : planifiez une réindexation régulière quand les documents changent
  • Gestion des accès : si différents utilisateurs ne doivent pas voir tous les documents, gérez la segmentation au niveau de l’index

💡 Conseil : en entreprise, la valeur d’un chatbot documentaire ne se mesure pas à sa sophistication technique, mais à la qualité et à l’actualité des documents qu’il consulte. Un bon contenu vaut mieux qu’un bon algorithme.

Conclusion

Mettre en place un chatbot interne capable de citer ses sources est aujourd’hui à la portée de n’importe quelle équipe technique, sans infrastructure cloud coûteuse. En combinant PyMuPDF pour l’extraction, FAISS pour la recherche vectorielle, Ollama pour les modèles locaux et Streamlit pour l’interface, vous obtenez un outil professionnel, traçable et maintenu en interne.

La force de cette approche réside dans sa rigueur : chaque réponse est ancrée dans vos documents, chaque source est citée, et le système refuse d’inventer ce qu’il ne sait pas. C’est exactement ce qu’on attend d’un assistant documentaire en contexte professionnel.

Prêt à aller plus loin ? Consultez les autres tutoriels sur ITSkillsCenter.io pour approfondir vos compétences en IA appliquée, automatisation et développement d’outils concrets pour les professionnels francophones.

Évaluation, conformité et passage à l’échelle

Un chatbot RAG capable de citer ses sources change la donne par rapport à un LLM seul, mais il introduit deux questions nouvelles que le tutoriel initial n’aborde qu’en surface : comment mesurer objectivement la qualité des réponses, et comment rester conforme aux régulations sur les données personnelles. Ces deux sujets deviennent critiques dès que le chatbot quitte la phase de prototype pour servir de vraies équipes.

Mesurer la qualité d’un RAG

La métrique intuitive — taux de réponses jugées satisfaisantes par les utilisateurs — est trop bruitée et trop tardive pour piloter le développement. Le framework Ragas propose quatre métriques automatiques qui se calculent avec un LLM juge sur un jeu de questions de référence : la faithfulness mesure si la réponse est effectivement supportée par les passages cités (pas d’hallucination), le context relevance mesure si les passages récupérés sont pertinents pour la question, le context recall mesure si tous les passages utiles ont été récupérés, et l’answer relevance mesure si la réponse adresse réellement la question posée. En pratique, on construit un jeu de 50 à 100 questions représentatives, idéalement co-rédigé avec les utilisateurs métiers, et on lance Ragas avant chaque modification du retriever ou du prompt. Une baisse de cinq points sur la faithfulness signale un régression à investiguer avant déploiement.

RGPD et lois africaines sur les données

Quand le corpus indexé contient des documents internes — rapports RH, comptes-rendus de réunions, échanges clients — le chatbot devient un système de traitement de données personnelles soumis au RGPD pour toute entreprise ayant des activités en Europe, mais aussi aux lois nationales africaines de plus en plus contraignantes. La loi sénégalaise n. 2008-12 du 25 janvier 2008 sur la protection des données personnelles impose une déclaration préalable à la Commission de protection des données personnelles (CDP). La Côte d’Ivoire dispose depuis 2013 de la loi n. 2013-450 et de l’Autorité de régulation des télécommunications (ARTCI) qui supervise. Le Nigeria a sa Nigeria Data Protection Act 2023 et le Kenya son Data Protection Act 2019. Concrètement, le RGPD article 5 paragraphe 1 (limitation de durée), la loi sénégalaise 2008-12 article 35 (information préalable de la personne concernée) et la doctrine ARTCI 2023 sur les traitements algorithmiques convergent sur trois exigences précises. D’abord, la journalisation des requêtes et réponses avec consentement explicite collecté avant la première interaction, avec rétention bornée à 12 mois maximum (6 mois recommandés). Ensuite, le filtrage du corpus en amont par classification automatique pour exclure les documents marqués sensibles au sens RGPD article 9 (santé, opinions politiques, données biométriques) — Microsoft Presidio en français reste l’outil open source le plus efficace pour cette détection en 2025. Enfin, une procédure documentée de droit à l’oubli qui retire un document de l’index et purge les embeddings correspondants dans un délai contractuel inférieur à 72 heures — la CDP sénégalaise tolère jusqu’à 30 jours sur les délais opérationnels mais sanctionne au-delà.

Passage à l’échelle technique

Au-delà de quelques milliers de documents, la base vectorielle FAISS en mémoire devient un point de fragilité — toute redémarrage rebuild l’index, et la mémoire RAM explose. Trois solutions activement maintenues dominent le marché vector store fin 2024 selon le State of Open Source AI Report d’Ai Insider. Qdrant (open source, écrit en Rust, version 1.12 en novembre 2024) supporte le filtrage métadata efficient et la quantization scalaire qui divise la RAM par 4. Weaviate (1.27, octobre 2024) intègre des modules ML embarqués et expose une API GraphQL native. pgvector (extension PostgreSQL 0.8.0, octobre 2024) transforme PostgreSQL en base vectorielle — solution idéale quand on a déjà PostgreSQL en production car elle économise une dépendance. Pour 10 000 documents, pgvector tient sur un VPS 4 Go de RAM avec des temps de récupération sous 50 millisecondes en moyenne. Au-delà du million de vecteurs, Qdrant ou Weaviate hébergés sur leur cloud managé deviennent économiquement pertinents — environ 30 dollars par mois pour un million de vecteurs avec leur offre de base.

Service ITSkillsCenter

Application mobile Android et iOS

Création d'application mobile Android et iOS. À partir de 350 000 FCFA.

Démarrer mon projet
Publicité