Vous avez lu la théorie du RAG, vous voulez un truc qui marche en 30 minutes. Ce guide construit un RAG complet : PDF + Markdown en entrée, ChromaDB pour la base vectorielle, Mistral 7B via Ollama pour la génération, 100 % offline. Le code final tient en 80 lignes de Python lisible.
Par Mohamed Meguedmi·Màj 2026-03-22·Testé sur Windows, macOS, Linux
#Ce qu'on va construire
Un petit outil CLI qui : (1) indexe un dossier de documents au premier lancement, (2) lit une question, (3) cherche les 5 passages les plus pertinents, (4) les passe à Mistral avec la question, (5) imprime la réponse + les sources.
i
Prérequis
Ollama installé avec mistral téléchargé. Python 3.10+. Les libs Python s'installent en une commande.
#La stack
Embeddings
sentence-transformers avec BGE-M3 (multilingue, excellent en français).
Base vectorielle
ChromaDB en mode local (fichier SQLite). Zéro serveur à déployer.
LLM
Mistral 7B via Ollama en HTTP local.
Parsing
unstructured pour PDF, python-markdown pour md.
#1. Setup projet
Environnement Python
mkdir mon-rag && cd mon-rag
python3 -m venv venv
source venv/bin/activate # .\venv\Scripts\activate sur Windows
pip install chromadb sentence-transformers \
unstructured[pdf] requests markdown
Structure
mon-rag/
├── docs/ # vos PDFs et .md
├── index.py # indexation
├── ask.py # requête
└── chroma_db/ # créé automatiquement
#2. Ingestion des documents
index.py (partie ingestion)
from pathlib import Path
from unstructured.partition.auto import partition
def load_docs(folder: str):
"""Retourne une liste de (texte, metadata)."""
docs = []
for path in Path(folder).rglob('*'):
if path.suffix.lower() not in {'.pdf', '.md', '.txt'}:
continue
try:
elements = partition(str(path))
text = "\n".join(e.text for e in elements if e.text)
docs.append((text, {"source": str(path)}))
print(f" + {path.name} ({len(text)} chars)")
except Exception as e:
print(f" ! {path.name} : {e}")
return docs
#3. Indexation dans ChromaDB
index.py (chunking + indexation)
import chromadb
from sentence_transformers import SentenceTransformer
def chunk_text(text: str, size=700, overlap=100):
"""Chunking simple par caractères, avec recouvrement."""
chunks = []
start = 0
while start < len(text):
end = start + size
chunks.append(text[start:end])
start = end - overlap
return chunks
def index_documents(folder="docs"):
docs = load_docs(folder)
model = SentenceTransformer("BAAI/bge-m3")
client = chromadb.PersistentClient(path="./chroma_db")
coll = client.get_or_create_collection("mes_docs")
cid = 0
for text, meta in docs:
for chunk in chunk_text(text):
emb = model.encode(chunk).tolist()
coll.add(
ids=[str(cid)],
documents=[chunk],
embeddings=[emb],
metadatas=[meta],
)
cid += 1
print(f"✓ {cid} chunks indexés")
if __name__ == "__main__":
index_documents()
Lancer l'indexation
python index.py
# ~1 minute pour 100 pages, selon votre CPU
#4. La pipeline de requête
ask.py
import sys, json, requests
import chromadb
from sentence_transformers import SentenceTransformer
def retrieve(question: str, k: int = 5):
model = SentenceTransformer("BAAI/bge-m3")
client = chromadb.PersistentClient(path="./chroma_db")
coll = client.get_collection("mes_docs")
q_emb = model.encode(question).tolist()
res = coll.query(query_embeddings=[q_emb], n_results=k)
return list(zip(res['documents'][0], res['metadatas'][0]))
def ask_mistral(question: str, passages):
ctx = "\n\n---\n\n".join(
f"Source : {m['source']}\n{p}" for p, m in passages
)
prompt = f"""Tu es un assistant qui répond UNIQUEMENT à partir des passages fournis.
Si l'information n'est pas dans les passages, dis-le clairement.
Cite la source (nom de fichier) à la fin de chaque affirmation.
PASSAGES :
{ctx}
QUESTION : {question}
RÉPONSE :"""
r = requests.post("http://localhost:11434/api/generate", json={
"model": "mistral", "prompt": prompt, "stream": False,
"options": {"temperature": 0.2}
})
return r.json()["response"]
if __name__ == "__main__":
q = " ".join(sys.argv[1:]) or input("Question : ")
passages = retrieve(q)
answer = ask_mistral(q, passages)
print("\n" + answer)
print("\n\nSources consultées :")
for _, m in passages:
print(" -", m['source'])
#5. Interface de chat
Utilisation
python ask.py "Quels sont les points clés du contrat X ?"
→
Interface web en 10 lignes
Pour une UI propre, enrobez ask_mistral dans une API FastAPI (pip install fastapi uvicorn) et pointez dessus depuis Open WebUI ou votre propre front.
#Pour améliorer ensuite
Chunking sémantique
Découper par sections (titres Markdown, paragraphes PDF) au lieu de caractères. Gros gain en pertinence.
Reranker
Ajouter un cross-encoder (BAAI/bge-reranker-v2-m3) qui réordonne les 20 premiers candidats. +15 % de pertinence.
Recherche hybride
Combiner ChromaDB (sémantique) avec BM25 (lexical) via Reciprocal Rank Fusion. Robuste aux requêtes contenant des termes exacts.
Streaming
Passer stream=True à Ollama et yielder les tokens. Le temps de première réponse tombe à ~500 ms.
Ce guide vous a aidé ?
Un retour, une erreur, une précision ? Faites-nous signe, ça améliore le guide pour tout le monde.