Aller au contenu
Développement medium

RAG pratique : construire un assistant documentation

10 min de lecture

logo python

Les guides précédents ont posé chaque brique séparément : extraction, nettoyage, chunking, embeddings, base vectorielle. Ce guide les assemble en un RAG complet — un assistant qui répond à des questions sur une documentation, en citant ses sources. La base vectorielle est Qdrant : un vrai serveur, capable de filtrage et de montée en charge. La génération tourne sur Ollama, en local. Vous indexerez un corpus, récupérerez les passages pertinents d'une question, et construirez la réponse sourcée. À la fin, vous aurez un pipeline RAG de bout en bout, fonctionnel et testé. Public visé : développeur ayant suivi les étapes du parcours et prêt à les réunir.

  • Indexer un corpus dans Qdrant : découpe, vectorisation, stockage.
  • Rechercher les passages pertinents d'une question.
  • Construire un prompt RAG qui cadre le modèle.
  • Générer une réponse sourcée avec un modèle local.
  • Tester un pipeline RAG de bout en bout.
  • Python 3.10+.
  • Qdrant en service : docker run -d -p 6333:6333 qdrant/qdrant.
  • Une instance Ollama avec qwen2.5 et nomic-embed-text.
  • Avoir parcouru les étapes : chunking, embeddings.

FAISS et Chroma sont parfaits pour prototyper — mais ils vivent dans le processus. Dès qu'un RAG doit être interrogé par plusieurs applications, monter en charge ou filtrer finement, il faut une base vectorielle serveur.

Qdrant est cette base. Elle tourne comme un service — un conteneur Docker —, et le code s'y connecte en réseau. Plusieurs applications partagent le même index ; la base persiste indépendamment du code ; le filtrage par payload et la montée en charge sont natifs. C'est la base vectorielle de référence pour un RAG destiné à durer, et c'est elle qu'on utilise ici et dans les guides suivants.

Le principe ne change pas pour autant : vectoriser, indexer, rechercher. Seul l'outil de stockage gagne en robustesse.

L'indexation transforme un corpus de documents en collection interrogeable. Trois opérations s'enchaînent : découper, vectoriser, stocker.

On crée d'abord la collection, en la typant : la dimension des vecteurs et la distance de comparaison.

from qdrant_client import QdrantClient
from qdrant_client.models import Distance, PointStruct, VectorParams
DIMENSION = 768 # taille des vecteurs de nomic-embed-text
COLLECTION = "rag_documentation"
client = QdrantClient(host="localhost", port=6333)
if client.collection_exists(COLLECTION):
client.delete_collection(COLLECTION)
client.create_collection(
COLLECTION,
vectors_config=VectorParams(size=DIMENSION, distance=Distance.COSINE),
)

On découpe ensuite chaque document en chunks, on les vectorise en un lot, et on les range dans Qdrant sous forme de points.

chunks = []
for doc in DOCUMENTS:
for chunk in decouper(doc["texte"]):
chunks.append({"texte": chunk, "source": doc["titre"]})
vecteurs = vectoriser([c["texte"] for c in chunks])
points = [
PointStruct(id=i, vector=vecteurs[i], payload=chunks[i])
for i in range(len(chunks))
]
client.upsert(COLLECTION, points=points)

Chaque PointStruct réunit trois choses : un identifiant, un vecteur, et un payload — des données libres attachées au point. Ici, le payload porte le texte du chunk et sa source. C'est lui qui rendra la réponse traçable.

À chaque question, on cherche les chunks les plus proches. On vectorise la question avec le même modèle qu'à l'indexation, puis on interroge Qdrant.

def rechercher(question: str, k: int = 3) -> list[dict]:
"""Renvoie les k chunks les plus proches de la question."""
v_question = vectoriser([question])[0]
reponse = client.query_points(
COLLECTION, query=v_question, limit=k, with_payload=True
)
return [
{"texte": p.payload["texte"], "source": p.payload["source"],
"score": p.score}
for p in reponse.points
]

query_points renvoie les points classés par similarité décroissante. Chaque résultat ramène son score et son payload — le texte du chunk et sa source, qu'on avait rangés à l'indexation. Le paramètre k fixe combien de chunks alimenteront la réponse.

Le cœur du RAG : injecter les chunks récupérés dans le prompt, pour que le modèle réponde à partir d'eux et non de ses connaissances générales.

def construire_prompt(question: str, chunks: list[dict]) -> str:
"""Assemble le contexte récupéré et la question en un prompt."""
contexte = "\n".join(f"[{c['source']}] {c['texte']}" for c in chunks)
return f"Contexte :\n{contexte}\n\nQuestion : {question}"

Deux détails comptent. Chaque chunk est préfixé de sa source[volumes.md] — pour que le modèle puisse, s'il le faut, attribuer l'information. Et le contexte précède la question : le modèle lit d'abord ce sur quoi il doit s'appuyer.

Reste à appeler le modèle. Le prompt système est décisif : c'est lui qui interdit l'hallucination en cadrant strictement le modèle.

def repondre(question: str, k: int = 3) -> dict:
"""Récupère le contexte, génère une réponse fondée sur lui."""
chunks = rechercher(question, k)
prompt = construire_prompt(question, chunks)
reponse = CLIENT_OLLAMA.chat.completions.create(
model="qwen2.5",
messages=[
{"role": "system", "content":
"Tu réponds en français, uniquement à partir du contexte "
"fourni. Si le contexte ne suffit pas, dis-le."},
{"role": "user", "content": prompt},
],
)
return {
"reponse": reponse.choices[0].message.content,
"sources": sorted({c["source"] for c in chunks}),
}

La consigne « uniquement à partir du contexte » est la ligne la plus importante du RAG. Sans elle, le modèle complète avec ce qu'il « sait » — et invente. Avec elle, il s'en tient aux passages fournis, et reconnaît quand ils ne suffisent pas. On renvoie aussi la liste des sources : l'utilisateur peut remonter aux documents d'origine.

Question : Comment conserver les données d'un conteneur Docker ?
Réponse : Pour conserver les données d'un conteneur Docker, on peut
utiliser un volume. Un volume est un stockage géré par Docker qui
persiste indépendamment de la vie du conteneur...
Sources : reseau.md, volumes.md

Un RAG se teste à deux niveaux. Les fonctions déterministes d'abord — le découpage, la construction du prompt — se vérifient sans modèle : un prompt doit bien contenir le contexte et la question. L'intégration ensuite : on indexe le corpus, on pose une question, et on vérifie que la réponse contient l'information attendue et cite la bonne source.

Ce double niveau est la bonne pratique : la logique pure est testée vite et de façon stable, l'enchaînement complet est validé par quelques tests d'intégration. Le lab de ce guide suit exactement ce découpage — deux tests déterministes, deux tests d'intégration.

SymptômeCause probableSolution
Connection refused sur 6333Qdrant non démarréLancer le conteneur qdrant/qdrant
Erreur de dimension à l'upsertCollection et modèle d'embedding divergentRecréer la collection à la bonne dimension
Réponse hors contextePrompt système trop permissifImposer « uniquement à partir du contexte »
Le modèle ne trouve rienk trop petit ou chunks mal découpésAugmenter k, revoir le chunking
Réponse sans sourcePayload non renvoyéPasser with_payload=True à query_points
  • Un RAG assemble les briques du parcours : découpe, embeddings, base vectorielle, génération.
  • Qdrant est une base vectorielle serveur — partage, filtrage et montée en charge natifs.
  • Un point Qdrant réunit un identifiant, un vecteur et un payload — ici le texte et sa source.
  • On vectorise question et documents avec le même modèle ; les chunks alimentent le prompt.
  • Le prompt système « uniquement à partir du contexte » est ce qui bride l'hallucination.
  • Un RAG se teste à deux niveaux : logique pure déterministe, puis intégration complète.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn