ITSkillsCenter
Intelligence Artificielle

RAG self-hosted Ollama et Qdrant : mémoire long terme pour agents IA

14 دقائق للقراءة

📍 Guide principal : Agents IA pour PME : architecture, déploiement et opérations en 2026

Introduction

Un agent IA qui ne dispose que d’une FAQ plate stockée en colonnes de Sheets ou Postgres se débrouille tant qu’il y a moins de cinquante questions. Au-delà, ou dès que la base contient des paragraphes longs, des PDF de documentation produit, des extraits de contrats, la recherche par mots-clés stricte rate la moitié des intentions formulées différemment. Le RAG (Retrieval Augmented Generation) résout cela : transformer chaque morceau de texte en vecteur de nombres flottants qui capture son sens, puis chercher par proximité sémantique plutôt que par correspondance lexicale.

Ce tutoriel couvre la mise en place complète : installation d’Ollama pour les embeddings et de Qdrant pour le stockage vectoriel, ingestion d’un corpus mixte (Markdown, PDF, Sheets), exposition de la recherche comme outil consommable par l’agent n8n, et stratégie de ré-indexation quand la base évolue. La pile reste auto-hébergée — aucune donnée sensible ne sort du serveur.

Prérequis

  • Une instance n8n version 2.0 ou ultérieure, en self-hosted Docker. Voir n8n self-hosted 2026 : guide complet.
  • Un VPS Linux 4 vCPU, 8 Go de RAM minimum. Pour faire tourner Ollama avec un modèle d’embedding sur CPU, 8 Go suffisent ; au-delà de 200 000 documents, prévoir 16 Go.
  • Docker et Docker Compose installés et fonctionnels.
  • L’agent n8n du tutoriel précédent — Agent support client n8n et LLM — fonctionnel avec son outil rechercher_faq à remplacer.
  • Niveau attendu : intermédiaire — Docker, ligne de commande, notions de Python pour le script d’ingestion.
  • Temps estimé : 4 à 6 heures pour la première mise en route, puis itérations courtes pour ajuster les paramètres.

Étape 1 — Comprendre la chaîne RAG avant de coder

Avant de lancer des conteneurs, fixer mentalement les six étapes du pipeline. Mal comprises, elles produisent un système qui semble marcher mais retourne du bruit.

D’abord, l’ingestion lit les documents source (PDF, Markdown, lignes de Sheets, articles de blog). Ensuite, le chunking découpe chaque document en morceaux de taille gérable, typiquement 300 à 800 tokens, avec un chevauchement de 10 à 20 %. Ce chevauchement évite qu’une phrase importante soit coupée en deux et perde son sens. Troisièmement, l’embedding convertit chaque chunk en un vecteur de 768 à 1 536 dimensions selon le modèle. Quatrièmement, le stockage insère ces vecteurs dans Qdrant avec leurs métadonnées (source, date, tags). Cinquièmement, lors d’une question utilisateur, la requête d’embedding transforme la question en vecteur ; le retrieval retourne les chunks les plus proches. Enfin, ces chunks sont injectés dans le contexte du modèle qui synthétise la réponse.

Chaque maillon peut casser. Un chunking trop large noie le signal ; trop fin, il fragmente les idées. Un mauvais modèle d’embedding mélange les domaines. Une recherche sans seuil de pertinence retourne des chunks sans rapport. La qualité finale du RAG est le produit de la qualité de chaque maillon.

Étape 2 — Installer Ollama et tirer un modèle d’embedding

Ollama est un runtime simple pour modèles open-weight. Sa version 0.22 d’avril 2026 ajoute la commande ollama launch et améliore les performances sur CPU.

Ajouter le service Ollama au docker-compose.yml existant :

services:
  ollama:
    image: ollama/ollama:0.22.1
    container_name: ollama
    volumes:
      - ollama_data:/root/.ollama
    ports:
      - "11434:11434"
    restart: unless-stopped

volumes:
  ollama_data:

Lancer le conteneur avec docker compose up -d ollama, puis tirer un modèle d’embedding spécialisé. Le modèle nomic-embed-text produit des vecteurs de 768 dimensions, équilibre vitesse et qualité, et tient en mémoire avec moins de 1 Go.

docker exec -it ollama ollama pull nomic-embed-text:latest

La commande télécharge environ 270 Mo. Tester l’API embedding directement avec curl pour vérifier que tout répond :

curl http://localhost:11434/api/embeddings \
  -d '{"model": "nomic-embed-text", "prompt": "Quels sont vos horaires ?"}'

La réponse JSON contient un champ embedding qui est un tableau de 768 nombres flottants. Si l’API retourne 404, le modèle n’a pas été téléchargé correctement — relancer ollama pull. Pour un modèle d’embedding multilingue plus puissant mais plus lourd (1 Go en mémoire), mxbai-embed-large est une bonne alternative.

Étape 3 — Installer Qdrant et créer la collection

Qdrant 1.17 introduit le Relevance Feedback et améliore les latences. Ajouter au docker-compose.yml :

services:
  qdrant:
    image: qdrant/qdrant:v1.17.1
    container_name: qdrant
    volumes:
      - qdrant_data:/qdrant/storage
    ports:
      - "6333:6333"
    restart: unless-stopped

volumes:
  qdrant_data:

Démarrer le service avec docker compose up -d qdrant. Le tableau de bord est disponible sur http://localhost:6333/dashboard — c’est l’interface graphique pour explorer collections et points.

Créer une collection adaptée aux embeddings 768 dimensions de nomic-embed-text :

curl -X PUT http://localhost:6333/collections/connaissances_pme \
  -H 'Content-Type: application/json' \
  -d '{
    "vectors": {
      "size": 768,
      "distance": "Cosine"
    }
  }'

La distance cosine est le standard pour les embeddings de texte — elle mesure l’angle entre les vecteurs, ignorant leur magnitude. La réponse {"result": true, "status": "ok", "time": ...} confirme la création. Tester la persistance en redémarrant le conteneur Qdrant : la collection doit toujours apparaître dans le dashboard. Si elle disparaît, c’est que le volume qdrant_data n’est pas correctement monté.

Étape 4 — Ingérer un premier corpus depuis Markdown ou PDF

Le script d’ingestion peut tourner depuis n8n via un nœud Code, mais pour des volumes au-delà du test (plus de 100 documents), un script Python autonome lancé périodiquement est plus robuste. Voici la structure minimale du script ingest.py.

import os
import requests
import uuid
from pathlib import Path
from pypdf import PdfReader

OLLAMA_URL = "http://localhost:11434/api/embeddings"
QDRANT_URL = "http://localhost:6333"
COLLECTION = "connaissances_pme"
EMBED_MODEL = "nomic-embed-text"
CHUNK_SIZE = 600
CHUNK_OVERLAP = 100

def lire_pdf(path):
    return "\n".join(p.extract_text() or "" for p in PdfReader(path).pages)

def lire_markdown(path):
    return Path(path).read_text(encoding="utf-8")

def chunker(texte, taille=CHUNK_SIZE, recouvrement=CHUNK_OVERLAP):
    mots = texte.split()
    pas = taille - recouvrement
    return [" ".join(mots[i:i+taille]) for i in range(0, len(mots), pas) if mots[i:i+taille]]

def embedder(texte):
    r = requests.post(OLLAMA_URL, json={"model": EMBED_MODEL, "prompt": texte}, timeout=60)
    r.raise_for_status()
    return r.json()["embedding"]

def upsert(points):
    r = requests.put(
        f"{QDRANT_URL}/collections/{COLLECTION}/points",
        json={"points": points},
        timeout=60,
    )
    r.raise_for_status()

if __name__ == "__main__":
    points = []
    for path in Path("docs/").rglob("*"):
        if path.suffix == ".md":
            texte = lire_markdown(path)
        elif path.suffix == ".pdf":
            texte = lire_pdf(path)
        else:
            continue
        for chunk in chunker(texte):
            points.append({
                "id": str(uuid.uuid4()),
                "vector": embedder(chunk),
                "payload": {"source": str(path), "texte": chunk},
            })
            if len(points) >= 50:
                upsert(points)
                points = []
    if points:
        upsert(points)

Lancer avec python ingest.py depuis un dossier docs/ rempli de Markdown et PDF. Le script lit chaque fichier, le découpe en chunks de 600 mots avec 100 mots de chevauchement, demande un embedding à Ollama pour chaque chunk, et insère par lots de 50 dans Qdrant. Sur 100 documents Markdown moyens, l’opération prend dix à trente minutes selon le CPU.

Vérifier le résultat dans le dashboard Qdrant — la collection connaissances_pme doit afficher un nombre de points cohérent avec le nombre de chunks attendus. Une commande SQL équivalente côté Qdrant n’existe pas directement, mais l’API /collections/{name} retourne les statistiques.

Étape 5 — Brancher la recherche au nœud AI Agent

L’agent du tutoriel précédent utilisait un Postgres Tool nommé rechercher_faq. On le remplace par un Workflow Tool qui appelle un sous-workflow recherche_rag. Cette indirection permet de centraliser la logique RAG et la réutiliser pour d’autres agents.

Créer un nouveau workflow recherche_rag avec un trigger Execute Workflow et trois nœuds.

Le premier nœud est un HTTP Request configuré ainsi :

  • Méthode : POST
  • URL : http://ollama:11434/api/embeddings
  • Body JSON : {"model": "nomic-embed-text", "prompt": "{{ $json.question }}"}

Il transforme la question reçue en vecteur. La sortie contient le champ embedding à 768 dimensions.

Le deuxième nœud est un autre HTTP Request :

  • Méthode : POST
  • URL : http://qdrant:6333/collections/connaissances_pme/points/search
  • Body JSON :
{
  "vector": {{ JSON.stringify($json.embedding) }},
  "limit": 4,
  "with_payload": true,
  "score_threshold": 0.55
}

Il interroge Qdrant et retourne au maximum les 4 chunks les plus proches dont la similarité dépasse 0.55. Ce seuil filtre les non-pertinents — sans lui, l’agent reçoit du bruit. La valeur 0.55 est un point de départ ; ajuster en fonction des résultats observés (plus haut = plus restrictif).

Le troisième nœud est un Set qui formate la sortie pour l’agent :

return $input.first().json.result.map(r => ({
  source: r.payload.source,
  texte: r.payload.texte,
  score: r.score
}));

Côté agent principal, remplacer le Postgres Tool par un Workflow Tool pointant vers recherche_rag. Adapter la description :

Recherche sémantique dans la base de connaissances de l’entreprise. Utilise une question en langage naturel. Retourne les passages les plus pertinents avec leur source et leur score de similarité. Cite toujours la source dans ta réponse.

L’instruction de citation est cruciale : sans elle, l’agent reformule sans tracer ce qui rend impossible la vérification a posteriori. Tester en envoyant une question dont la réponse est dans le corpus — l’agent doit retrouver le passage et citer le fichier source.

Étape 6 — Tester la qualité du retrieval indépendamment

Le piège fréquent est de tester le RAG via l’agent uniquement. Quand quelque chose ne marche pas, on ne sait pas si c’est l’embedding, le retrieval, ou la synthèse du modèle qui pose problème. Construire un script de test isolé pour le retrieval.

import requests

QUESTIONS = [
    "Quels sont vos horaires ?",
    "Comment retourner un produit défectueux ?",
    "Quel est le délai de livraison ?",
    "Acceptez-vous les paiements en plusieurs fois ?",
]

for question in QUESTIONS:
    emb = requests.post(
        "http://localhost:11434/api/embeddings",
        json={"model": "nomic-embed-text", "prompt": question},
        timeout=60,
    ).json()["embedding"]
    res = requests.post(
        "http://localhost:6333/collections/connaissances_pme/points/search",
        json={"vector": emb, "limit": 3, "with_payload": True, "score_threshold": 0.5},
        timeout=60,
    ).json()
    print(f"\n=== {question} ===")
    for r in res.get("result", []):
        print(f"  [{r['score']:.3f}] {r['payload']['source']} :: {r['payload']['texte'][:80]}...")

Lancer ce script donne immédiatement une vue de ce que le retrieval retourne pour chaque question. Si les scores sont tous en dessous de 0.4, soit la question n’a pas de correspondance dans la base (la base est incomplète), soit le modèle d’embedding est mal adapté à la langue (essayer mxbai-embed-large qui supporte mieux le français).

Si les bons documents apparaissent mais l’agent invente quand même, le problème est côté prompt système — il faut renforcer l’instruction « réponds UNIQUEMENT à partir des passages fournis, sinon escalade ».

Étape 7 — Stratégie de ré-indexation

La base de connaissances évolue. Réindexer entièrement chaque jour est coûteux ; ne pas réindexer du tout produit des réponses obsolètes. Stratégie pragmatique en trois axes.

Indexation incrémentale par hash. Calculer un hash MD5 ou SHA-256 du contenu de chaque fichier. Stocker ce hash dans la payload de chaque point Qdrant. Au prochain lancement de ingest.py, ne réindexer que les fichiers dont le hash a changé. Cette stratégie traite les ajouts et modifications mais pas les suppressions.

Suppression par filtrage. Avant la nouvelle ingestion, supprimer tous les points Qdrant dont le source n’apparaît plus dans le dossier source. Implémentation simple via l’API Qdrant /collections/{name}/points/delete avec un filtre négatif.

Cron quotidien. Lancer le script ingestion une fois par nuit via cron ou via un workflow n8n planifié. Pour des bases qui changent en temps réel (par exemple un Sheets de FAQ édité plusieurs fois par jour), une approche événementielle s’impose : un trigger n8n sur modification du Sheets qui réindexe uniquement les lignes modifiées.

Erreurs fréquentes

Erreur Cause Solution
Tous les scores de retrieval sont bas (< 0.3) Modèle d’embedding mal adapté à la langue Tester mxbai-embed-large ou bge-m3 qui supportent mieux le multilingue
L’agent ignore les chunks retournés Description du tool ne précise pas que c’est de la connaissance fiable Ajouter explicitement « passages issus de la documentation officielle, à utiliser comme source de vérité »
La recherche prend plus de 5 secondes Pas d’index HNSW pour la collection ou trop de points sans optimisation Lancer POST /collections/{name}/points/optimizer ou recréer la collection avec les bons paramètres HNSW
Les PDF scannés produisent du texte vide pypdf ne fait pas d’OCR Pré-traiter avec tesseract ou ocrmypdf avant ingestion
Doublons dans Qdrant après plusieurs ingestions Pas de déduplication Utiliser un id déterministe (hash du contenu du chunk + source) à la place d’un UUID aléatoire
Le score_threshold filtre tout Valeur trop haute pour le modèle Logger les scores réels sur 50 questions, ajuster en bas du quartile inférieur

FAQ

Quelle taille de chunk choisir ?
Pour des FAQ ou des documents techniques courts, 300 à 500 mots avec 50 à 100 mots de chevauchement. Pour des contrats ou de la documentation longue, 600 à 800 mots avec 150 mots de chevauchement. Tester sur un échantillon : si une question naturelle ne retrouve pas le passage évident, augmenter la taille.

Embedding local ou API cloud ?
Pour la confidentialité et le coût récurrent, local. Pour la qualité absolue sur des langues moins représentées, les modèles voyage-3 (Voyage AI) ou text-embedding-3-large (OpenAI) sont en avance, mais coûtent quelques dollars par million de tokens et envoient le texte chez le fournisseur.

Faut-il toujours un GPU pour les embeddings ?
Non. nomic-embed-text tourne très bien sur CPU avec un débit de 50 à 200 chunks par seconde sur un VPS récent. Le GPU devient utile uniquement pour les modèles d’embedding > 1 milliard de paramètres ou quand on combine embedding et inférence générative sur la même machine.

Comment évaluer la qualité globale du RAG ?
Construire un jeu d’évaluation : 30 à 100 paires (question, réponse attendue, document source). Lancer le pipeline complet et mesurer trois métriques : taux de retrieval (le bon chunk est-il dans le top 3 ?), taux d’exactitude (la réponse de l’agent contient-elle l’information correcte ?), taux d’hallucination (la réponse contient-elle des éléments absents du corpus ?). Refaire le test après chaque changement majeur.

Tutoriels associés

Ressources

Sponsoriser ce contenu

Cet emplacement est à vous

Position premium en fin d'article — c'est l'instant où les lecteurs sont le plus engagés. Réservez cet espace pour votre marque, votre formation ou votre offre.

Recevoir nos tarifs
Publicité