La semaine dernière, nous avons fait tourner un LLM sur votre machine. Il répond bien, mais il ne connaît rien à votre entreprise. Vos procédures, vos contrats, vos comptes rendus de réunion : pour lui, tout cela n’existe pas.
C’est le rôle du RAG, pour Retrieval-Augmented Generation. On indexe vos documents, et à chaque question on va chercher les passages pertinents pour les fournir au modèle avant qu’il réponde. Le principe tient en trois lignes. La mise en œuvre correcte, beaucoup moins.
Ce TP construit une chaîne complète, en local, avec Qdrant et Ollama. Il insiste surtout sur la partie que la plupart des tutoriels sautent : mesurer si votre RAG retrouve vraiment les bons passages. Sans cette étape, vous avez un système qui se trompe avec assurance, ce qui est bien pire qu’un système qui ne répond pas.
Faits clés
- Le RAG se joue à la récupération, pas à la génération. Si le bon passage n’est pas retrouvé, aucun modèle, si puissant soit-il, ne rattrapera l’erreur.
- La limite de contexte du modèle d’embeddings dicte la taille de découpage.
mxbai-embed-largeplafonne à 512 tokens quandbge-m3en accepte 8 194. Un bloc trop long est tronqué en silence. - Dimensions des vecteurs : 768 pour
nomic-embed-text, 1 024 pourbge-m3etmxbai-embed-large. Cette valeur est à déclarer à la création de la collection et ne se change pas après coup. - Qdrant 1.19 et pgvector 0.8.6 au 17 août 2026. Qdrant pour démarrer vite, pgvector si vous avez déjà PostgreSQL.
- Mesurez le rappel sur une vingtaine de questions dont vous connaissez la réponse. C’est le seul garde-fou contre un RAG qui a l’air de marcher.
Ce qu’on construit exactement
Un RAG, ce sont deux chaînes distinctes qui ne tournent pas au même moment.
À l’indexation, une fois pour toutes : on lit les documents, on les découpe en blocs, on transforme chaque bloc en vecteur numérique via un modèle d’embeddings, et on stocke ces vecteurs dans une base vectorielle.
À l’interrogation, à chaque question : on transforme la question en vecteur avec le même modèle, on cherche les vecteurs les plus proches dans la base, et on injecte les blocs correspondants dans le prompt du LLM.
La règle qu’on ne transgresse pas
Le modèle d’embeddings utilisé à l’indexation et celui utilisé à l’interrogation doivent être strictement le même. Deux modèles différents produisent des espaces vectoriels incomparables, et votre recherche remontera des résultats parfaitement aléatoires, sans la moindre erreur affichée. Si vous changez de modèle, il faut ré-indexer l’intégralité du corpus.
Étape 1 : choisir le modèle d’embeddings
C’est le choix le plus structurant du TP, et celui qu’on fait généralement à la légère. Deux critères comptent : la longueur de contexte, qui détermine la taille maximale d’un bloc, et la qualité en français.
| Modèle | Dimensions | Contexte max | Quand le choisir |
|---|---|---|---|
bge-m31,2 Go |
1 024 | 8 194 tokens | Le choix par défaut pour du français. Conçu pour le multilingue, et sa longue fenêtre autorise de gros blocs qui préservent le contexte. |
nomic-embed-text274 Mo |
768 | 2 048 tokens | Très léger et rapide. Excellent rapport qualité et ressources si votre corpus est majoritairement en anglais. |
mxbai-embed-large670 Mo |
1 024 | 512 tokens | Bon en anglais, mais cette fenêtre de 512 tokens impose des blocs très courts. Piège classique : au-delà, le texte est tronqué sans avertissement. |
Dimensions et longueurs de contexte relevées dans les fichiers de configuration officiels des modèles le 17 août 2026.
Pour un corpus francophone, bge-m3 est le choix raisonnable. Il coûte 1,2 Go de disque, ce qui reste modeste, et sa fenêtre de 8 194 tokens vous évite le piège du bloc tronqué.
Étape 2 : lancer Qdrant
Qdrant est une base vectorielle open source, écrite en Rust, qui se lance en un conteneur et expose une API HTTP. C’est le chemin le plus court pour un TP. Si vous avez déjà PostgreSQL en production, l’extension pgvector fait le même travail sans ajouter de brique à superviser.
# Qdrant, avec un volume pour ne pas tout perdre au redemarrage
docker run -d --name qdrant \
-p 6333:6333 \
-v "$(pwd)/qdrant_storage:/qdrant/storage" \
qdrant/qdrant
# Verifier qu'il repond
curl http://localhost:6333/healthz
# Interface web d'exploration : http://localhost:6333/dashboard
# Le modele d'embeddings, cote Ollama
ollama pull bge-m3
# Le client Python
pip install qdrant-client requests
Étape 3 : découper les documents
C’est ici que se joue la qualité de votre RAG, bien plus que dans le choix du LLM. Un bloc trop court perd son contexte : « il doit être renouvelé tous les 90 jours » ne sert à rien si le mot « certificat » est resté dans le bloc précédent. Un bloc trop long dilue l’information : le vecteur devient une moyenne floue de plusieurs sujets et ne ressort sur aucune question précise.
| Type de document | Taille de bloc | Pourquoi |
|---|---|---|
| Procédures, documentation technique | 800 à 1 200 caractères | Une étape ou une notion par bloc, c’est la granularité des questions posées. |
| Contrats, textes juridiques | 1 500 à 2 000 caractères | Un article se comprend entier. Le couper au milieu rend les deux moitiés inexploitables. |
| Comptes rendus, échanges | 500 à 800 caractères | Les sujets changent vite. Des blocs courts évitent de mélanger deux décisions sans rapport. |
| FAQ, questions et réponses | 1 paire par bloc | La structure fait déjà le découpage. Ne la cassez pas avec un découpage aveugle. |
Ajoutez un chevauchement de 10 à 15 % entre blocs consécutifs. Cela coûte un peu de stockage et évite qu’une phrase coupée en deux ne devienne introuvable. Et découpez de préférence sur les frontières naturelles, un double saut de ligne ou un titre, plutôt qu’au caractère près.
Étape 4 : indexer
Le script ci-dessous lit des fichiers texte, les découpe en respectant les paragraphes, calcule les vecteurs via Ollama et les envoie dans Qdrant. Il tient en une cinquantaine de lignes.
import glob, uuid, requests
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct
MODELE, DIMENSIONS = "bge-m3", 1024
COLLECTION = "documentation"
TAILLE_BLOC, CHEVAUCHEMENT = 1000, 150
def vectoriser(textes):
"""Un seul appel Ollama pour une liste de textes."""
r = requests.post("http://localhost:11434/api/embed",
json={"model": MODELE, "input": textes}, timeout=180)
r.raise_for_status()
return r.json()["embeddings"]
def decouper(texte):
"""Decoupe sur les paragraphes, sans depasser TAILLE_BLOC."""
blocs, courant = [], ""
for para in texte.split("\n\n"):
para = para.strip()
if not para:
continue
if len(courant) + len(para) + 2 <= TAILLE_BLOC:
courant = f"{courant}\n\n{para}" if courant else para
else:
if courant:
blocs.append(courant)
# on repart avec la fin du bloc precedent : le chevauchement
courant = (courant[-CHEVAUCHEMENT:] + "\n\n" + para) if courant else para
if courant:
blocs.append(courant)
return blocs
client = QdrantClient(url="http://localhost:6333")
client.recreate_collection(
collection_name=COLLECTION,
vectors_config=VectorParams(size=DIMENSIONS, distance=Distance.COSINE),
)
points = []
for chemin in glob.glob("documents/**/*.txt", recursive=True):
blocs = decouper(open(chemin, encoding="utf-8").read())
for bloc, vecteur in zip(blocs, vectoriser(blocs)):
points.append(PointStruct(
id=str(uuid.uuid4()),
vector=vecteur,
payload={"texte": bloc, "source": chemin},
))
print(f"{chemin} : {len(blocs)} blocs")
client.upsert(collection_name=COLLECTION, points=points)
print(f"Total indexe : {len(points)} blocs")
Notez le champ source dans le payload. Il ne sert pas à la recherche, mais il vous permettra d’afficher d’où vient chaque réponse. Sans cette traçabilité, vous ne pourrez jamais vérifier ce que raconte votre système.
Étape 5 : mesurer la qualité
C’est l’étape que presque tout le monde saute, et c’est pourtant la seule qui vous dira si votre RAG fonctionne. La méthode est simple : constituez une vingtaine de questions dont vous connaissez déjà la réponse, et vérifiez que le bon document remonte dans les premiers résultats.
import requests
from qdrant_client import QdrantClient
MODELE, COLLECTION, K = "bge-m3", "documentation", 5
client = QdrantClient(url="http://localhost:6333")
# (question, fragment du chemin du document qui DOIT remonter)
JEU_DE_TEST = [
("Quelle est la duree de retention des sauvegardes ?", "sauvegarde"),
("Qui contacter en cas d'incident hors heures ouvrees ?", "astreinte"),
("Comment demander un acces VPN ?", "acces-distant"),
# ... visez une vingtaine de cas couvrant vos differents documents
]
def vectoriser(texte):
r = requests.post("http://localhost:11434/api/embed",
json={"model": MODELE, "input": texte}, timeout=60)
return r.json()["embeddings"][0]
trouves_top1 = trouves_topk = 0
for question, attendu in JEU_DE_TEST:
res = client.query_points(collection_name=COLLECTION,
query=vectoriser(question), limit=K).points
sources = [p.payload["source"] for p in res]
rang = next((i + 1 for i, s in enumerate(sources) if attendu in s), None)
if rang == 1:
trouves_top1 += 1
if rang:
trouves_topk += 1
else:
print(f" ECHEC : {question}")
print(f" remonte a la place : {sources[:3]}")
n = len(JEU_DE_TEST)
print(f"\nRappel@1 : {trouves_top1}/{n} ({100*trouves_top1//n} %)")
print(f"Rappel@{K} : {trouves_topk}/{n} ({100*trouves_topk//n} %)")
Ce petit script vaut de l’or, parce qu’il transforme une impression en chiffre. Quand vous modifierez la taille des blocs ou le modèle d’embeddings, vous saurez immédiatement si vous avez amélioré ou dégradé les choses, au lieu de vous fier à deux ou trois essais au hasard.
Étape 6 : brancher le modèle
La récupération fonctionne et vous l’avez mesurée. Il ne reste qu’à passer les blocs retrouvés au LLM, avec une consigne stricte.
import sys, requests
from qdrant_client import QdrantClient
EMB, LLM, COLLECTION = "bge-m3", "mistral", "documentation"
client = QdrantClient(url="http://localhost:6333")
question = " ".join(sys.argv[1:])
vec = requests.post("http://localhost:11434/api/embed",
json={"model": EMB, "input": question}).json()["embeddings"][0]
blocs = client.query_points(collection_name=COLLECTION, query=vec, limit=4).points
contexte = "\n\n---\n\n".join(
f"[Source : {p.payload['source']}]\n{p.payload['texte']}" for p in blocs)
prompt = f"""Tu reponds uniquement a partir du CONTEXTE fourni.
Si le contexte ne contient pas la reponse, dis-le explicitement
au lieu d'inventer. Cite systematiquement la source utilisee.
CONTEXTE :
{contexte}
QUESTION : {question}"""
r = requests.post("http://localhost:11434/api/generate",
json={"model": LLM, "prompt": prompt, "stream": False}, timeout=300)
print(r.json()["response"])
print("\nSources consultees :")
for p in blocs:
print(f" - {p.payload['source']} (score {p.score:.3f})")
Ce qui rate en vrai
| Symptôme | Cause | Ce qu’on fait |
|---|---|---|
| Réponses hors sujet et assurées | La récupération remonte les mauvais blocs. Le modèle fait ensuite son travail sur une mauvaise matière. | Mesurer le rappel. Le problème est presque toujours en amont de la génération. |
| Rien ne remonte correctement après un changement | Le modèle d’embeddings a changé sans ré-indexation. | Ré-indexer tout le corpus. Les anciens vecteurs sont inexploitables. |
| Les PDF ne donnent rien | PDF scannés, donc images sans couche texte. | Vérifier l’extraction avant d’indexer. Passer par un OCR si nécessaire. |
| Bonnes réponses sur les débuts de documents seulement | Blocs plus longs que la fenêtre du modèle d’embeddings, tronqués en silence. | Comparer la taille des blocs à la limite du modèle. C’est le piège de mxbai-embed-large et ses 512 tokens. |
| Le système cite une procédure périmée | L’index contient encore l’ancienne version du document. | Prévoir la mise à jour dès le départ. Un index qui dérive est pire que pas d’index. |
Le RAG n’est pas un dispositif de confidentialité
Une base vectorielle ne gère aucun droit d’accès par défaut. Si vous indexez pêle-mêle les documents RH et la documentation technique, toute personne interrogeant le système peut faire ressortir un extrait de fiche de paie. La séparation se construit : une collection par périmètre de confidentialité, ou un filtrage sur les métadonnées appliqué à la requête. Ne remettez pas ce sujet à plus tard, la reprise coûte bien plus cher que la conception initiale.
Passer à l’échelle
Le montage ci-dessus tourne très bien sur un poste de travail avec quelques milliers de blocs. Les choses changent quand :
- Le corpus dépasse la centaine de milliers de blocs. Il faut alors s’intéresser sérieusement au paramétrage de l’index et à la mémoire de la base.
- L’index devient un actif. Réindexer un gros corpus prend des heures. À ce stade, la base vectorielle a besoin de sauvegardes, comme n’importe quelle base de données.
- Plusieurs personnes interrogent en même temps. La recherche vectorielle est gourmande en mémoire, et un poste de travail ne tient pas la charge.
- Les documents indexés sont sensibles. Vos vecteurs contiennent le texte source dans leur payload : l’index mérite exactement le même niveau de protection que les documents d’origine.
Et Datacampus dans tout ça ?
Comme dit dans le TP précédent, nous ne vendons pas encore de serveurs d’inférence, et le modèle peut parfaitement rester chez vous.
En revanche, la base vectorielle est exactement le genre de brique que nous hébergeons : Qdrant, Milvus ou pgvector sur une infrastructure infogérée au Futuroscope, avec sauvegardes, supervision et données qui ne quittent pas la France. Ajoutez-y un n8n pour tenir l’index à jour automatiquement quand un document change, et un serveur MCP pour exposer cette base à vos assistants. C’est la partie qui demande de l’exploitation sérieuse, et c’est notre métier.
TL;DR, ce qu’il faut retenir
- Le RAG se joue à la récupération. Un mauvais passage retrouvé ne se rattrape pas à la génération.
- Le même modèle d’embeddings à l’indexation et à l’interrogation, sans exception.
- Pour du français,
bge-m3est le choix par défaut, avec ses 8 194 tokens de contexte. - Vérifiez que vos blocs tiennent dans la fenêtre du modèle, la troncature est silencieuse.
- Adaptez la taille des blocs au type de document, avec 10 à 15 % de chevauchement.
- Mesurez le rappel sur une vingtaine de questions connues. Visez plus de 90 % au rappel@5.
- Une base vectorielle n’a pas de gestion de droits : séparez les périmètres de confidentialité dès la conception.
FAQ — RAG sur ses propres documents
Quelle différence entre RAG et fine-tuning ?
Le RAG fournit des documents au modèle au moment de la question, sans le modifier. Le fine-tuning réentraîne le modèle pour changer son comportement. Pour de la connaissance métier qui évolue, le RAG est presque toujours le bon choix : mettre à jour un document coûte quelques secondes de réindexation, là où un fine-tuning demande de tout recommencer.
Quelle taille de bloc choisir pour le découpage ?
Entre 800 et 1 200 caractères pour de la documentation technique, 1 500 à 2 000 pour des contrats où un article doit rester entier, 500 à 800 pour des comptes rendus où les sujets changent vite. Ajoutez 10 à 15 % de chevauchement et découpez sur les paragraphes plutôt qu’au caractère près.
Quel modèle d’embeddings pour des documents en français ?
bge-m3 est le choix par défaut : conçu pour le multilingue, vecteurs de 1 024 dimensions et fenêtre de 8 194 tokens qui autorise de gros blocs. nomic-embed-text est plus léger mais plafonne à 2 048 tokens. Méfiez-vous de mxbai-embed-large, limité à 512 tokens : au-delà, vos blocs sont tronqués sans le moindre avertissement.
Qdrant ou pgvector ?
Qdrant si vous partez de zéro : il se lance en un conteneur et propose une interface d’exploration. pgvector si vous exploitez déjà PostgreSQL, car vous réutilisez alors vos sauvegardes, votre supervision et vos compétences existantes plutôt que d’ajouter une brique à exploiter.
Comment savoir si mon RAG fonctionne vraiment ?
En mesurant le rappel. Constituez une vingtaine de questions dont vous connaissez la réponse, puis vérifiez que le bon document remonte dans les premiers résultats. Visez plus de 90 % de rappel@5. En dessous de 70 %, le problème vient du découpage bien plus souvent que du modèle.
Un RAG peut-il exposer des documents confidentiels ?
Oui, et c’est un risque réel. Une base vectorielle n’implémente aucun contrôle d’accès par défaut : tout ce qui est indexé devient interrogeable par quiconque atteint le système. Séparez les périmètres avec une collection par niveau de confidentialité, ou filtrez sur les métadonnées à chaque requête. Cette séparation se conçoit dès le départ.
Datacampus héberge-t-il des bases vectorielles ?
Oui, c’est même l’une de nos briques IA : Qdrant, Milvus, pgvector ou ChromaDB hébergés et infogérés au Futuroscope, avec sauvegardes et supervision. Le modèle de langage, lui, peut rester chez vous : nous ne proposons pas encore de serveurs d’inférence.
Pour aller plus loin :
• Documentation Qdrant · pgvector
• Sur datacampus.fr : Faire tourner un LLM sur votre machine · Héberger un LLM en local · Qu’est-ce que MCP ? · n8n et l’automatisation souveraine · Hébergement IA