1. Donner une mémoire documentaire à son IA

Un modèle d'intelligence artificielle possède des connaissances acquises pendant son entraînement. Mais il ne connaît pas automatiquement les documents personnels, les procédures internes ou les informations propres à une entreprise. Pour lui permettre de consulter ces données, nous allons mettre en place un système RAG.

Le RAG permet à une IA de rechercher des informations dans une collection documentaire avant de formuler sa réponse. Au lieu de s'appuyer uniquement sur ses connaissances générales, le modèle reçoit des extraits de documents sélectionnés en fonction de la question posée.

Nous allons construire cette fonctionnalité en deux étapes de complexité croissante. La première utilise les outils déjà installés sur notre VPS. La seconde reprend le même principe, mais en développant nous-mêmes le mécanisme avec Python.

Deux niveaux de mise en œuvre :

  • Approche standard : utiliser AnythingLLM, Ollama et Qdrant pour construire un assistant documentaire sans développer de programme.
  • Approche indépendante : remplacer la gestion automatisée d'AnythingLLM par notre propre programme Python, directement connecté à Ollama et Qdrant.

Accéder directement à la construction d'un RAG indépendant avec Python.

2. L'architecture retenue

Notre infrastructure dispose déjà des composants nécessaires. Il n'est donc pas nécessaire d'installer une nouvelle application ou de développer immédiatement du code. Nous allons simplement faire travailler ensemble les services existants.

Dans cette première approche, chaque composant conserve un rôle précis :

  • AnythingLLM : interface de gestion des documents, de leur indexation et des conversations.
  • Ollama : moteur qui exécute le modèle de langage chargé de produire les réponses.
  • Modèle d'embedding : transforme les textes en représentations numériques pour permettre leur comparaison.
  • Qdrant : stocke les vecteurs et permet de retrouver les fragments documentaires les plus proches d'une question.

AnythingLLM assure la coordination de ces opérations. Il extrait le texte des documents, le découpe en fragments, demande au modèle d'embedding de les vectoriser, puis enregistre les résultats dans la base vectorielle. Lorsqu'une question arrive, il effectue la recherche et transmet les extraits retenus au modèle de langage.

Nous n'avons donc pas à programmer nous-mêmes chacune de ces opérations. C'est précisément l'intérêt de cette première approche.

3. Vérifier les composants disponibles

Avant de configurer le RAG, vérifions que les services nécessaires sont présents sur le VPS. Dans notre installation, Ollama, Qdrant et AnythingLLM sont déjà déployés sous Docker.

Depuis le terminal du VPS, nous pouvons contrôler leur état :

Cette commande affiche les conteneurs actifs. Vérifier que les services ollama, qdrant et anythingllm sont bien présents et en fonctionnement.

docker ps

Nous devons également vérifier les modèles disponibles dans Ollama :

La liste permet de retrouver le nom exact du modèle de génération installé. Ce nom devra être repris dans la configuration d'AnythingLLM.

ollama list

Un modèle de génération et un modèle d'embedding remplissent deux fonctions différentes. Notre modèle Qwen sert à produire les réponses. Pour la recherche documentaire, il faut également disposer d'un modèle capable de produire des embeddings.

Nous utiliserons ici nomic-embed-text comme modèle d'embedding de démonstration. Il devra être téléchargé s'il n'est pas déjà disponible :

Cette commande télécharge le modèle d'embedding dans Ollama. Il sera ensuite sélectionné dans les paramètres d'AnythingLLM.

ollama pull nomic-embed-text

Important : le modèle d'embedding utilisé pour indexer les documents doit être le même que celui utilisé pour vectoriser les questions. Changer de modèle après l'indexation impose généralement de recalculer les embeddings des documents.

4. Connecter AnythingLLM à Ollama

Ouvrons l'interface web d'AnythingLLM. Dans les paramètres, nous allons sélectionner Ollama comme fournisseur du modèle de langage.

Le principe est simple : AnythingLLM ne fait pas fonctionner lui-même le modèle Qwen. Il envoie les demandes à Ollama, qui exécute le modèle et renvoie les réponses.

Dans les paramètres du fournisseur LLM, sélectionner :

  • Provider : Ollama.
  • Base URL : l'adresse du service Ollama accessible depuis le conteneur AnythingLLM.
  • Model : le nom exact du modèle affiché par la commande ollama list.

Dans un réseau Docker partagé, l'adresse peut prendre la forme http://ollama:11434, si le nom du service est bien ollama et si les deux conteneurs partagent le même réseau. Il ne faut pas utiliser automatiquement localhost : depuis un conteneur, cette adresse désigne le conteneur lui-même.

Une fois les paramètres enregistrés, utiliser la fonction de test ou ouvrir une conversation pour vérifier que le modèle répond correctement.

5. Configurer le modèle d'embedding et Qdrant

Le modèle de langage est maintenant connecté. Il reste à configurer les deux éléments qui rendent possible la recherche documentaire : le modèle d'embedding et la base vectorielle.

Le modèle d'embedding

Dans les paramètres d'AnythingLLM, ouvrir la configuration du fournisseur d'embedding et sélectionner Ollama.

Renseigner l'adresse du service Ollama, puis sélectionner nomic-embed-text. Ce modèle transforme les fragments de texte en vecteurs numériques. Il sera utilisé aussi bien lors de l'indexation des documents que lors de la recherche.

La base vectorielle Qdrant

AnythingLLM possède une base vectorielle intégrée, LanceDB, utilisée par défaut. Mais notre infrastructure dispose déjà de Qdrant. Nous allons donc sélectionner Qdrant dans les paramètres de base vectorielle afin de conserver un stockage indépendant du conteneur AnythingLLM.

Dans la configuration de Qdrant, renseigner l'adresse du service et, si nécessaire, les informations d'authentification. Dans notre environnement Docker, l'adresse interne peut par exemple être http://qdrant:6333, à condition que le nom du service et le réseau soient bien ceux de notre configuration.

Les champs exacts de configuration dépendent de la version d'AnythingLLM installée. Il faudra donc vérifier l'écran de paramétrage lors de la mise en pratique sur le VPS, plutôt que recopier une adresse ou un identifiant supposé.

À retenir : AnythingLLM peut utiliser sa base intégrée ou une base vectorielle externe comme Qdrant. Dans notre architecture, nous choisissons Qdrant pour conserver un service indépendant, réutilisable par d'autres applications.

6. Créer un espace documentaire et indexer un premier fichier

AnythingLLM organise les documents au sein d'espaces de travail, appelés workspaces. Chaque espace possède ses propres documents et sert de périmètre de recherche à l'assistant.

Créons un espace de travail consacré à notre premier test. Nous pouvons par exemple l'appeler Documentation Web-Soul.

Préparons ensuite un petit document de référence. Pour ce premier essai, un fichier texte ou Markdown contenant quelques paragraphes explicatifs suffit. Il est préférable de commencer avec un document court, dont nous connaissons précisément le contenu.

Dans l'interface d'AnythingLLM :

    1. Ouvrir le nouvel espace de travail.
    2. Importer le fichier documentaire.
    3. Demander son ajout à l'espace de travail.
    4. Lancer l'indexation ou l'incorporation du document.
    5. Attendre la fin du traitement avant de poser une question.

L'importation et l'indexation sont deux opérations distinctes. Importer un fichier permet à AnythingLLM d'en extraire le contenu. L'ajout à l'espace de travail déclenche ensuite le découpage, la vectorisation et le stockage des fragments dans la base vectorielle.

7. Interroger le document et vérifier le résultat

Notre assistant documentaire est maintenant prêt à être testé. Posons une question dont la réponse figure explicitement dans le document que nous venons d'indexer.

Par exemple, si le document décrit l'architecture de notre VPS, nous pouvons demander : « Quels sont les services utilisés dans notre infrastructure Web-Soul ? »

AnythingLLM transforme la question en embedding, recherche les fragments les plus proches dans Qdrant, puis transmet les passages sélectionnés au modèle Qwen. Celui-ci formule sa réponse à partir de ce contexte.

Pour vérifier le fonctionnement du RAG, ne nous contentons pas de lire la réponse. Contrôlons également les sources ou les extraits documentaires utilisés par AnythingLLM.

  • La réponse reprend-elle correctement les informations du document ?
  • Les sources affichées correspondent-elles au fichier indexé ?
  • Une question sans rapport avec le document provoque-t-elle une réponse prudente plutôt qu'une invention ?

Un RAG ne garantit pas automatiquement l'exactitude des réponses. La qualité dépend notamment du document source, du découpage, du modèle d'embedding, de la recherche et de la capacité du modèle de langage à exploiter correctement les extraits fournis.

Résultat attendu : l'assistant répond à une question portant sur le document indexé et permet de retrouver les passages qui ont servi à construire sa réponse. Nous avons ainsi mis en place un premier RAG fonctionnel sans écrire de programme.

8. Les limites de l'approche standard

AnythingLLM simplifie considérablement la mise en place d'un RAG. Il prend en charge l'extraction des documents, leur découpage, la vectorisation, la recherche et la transmission du contexte au modèle de langage.

Cette simplicité a une contrepartie : une partie du fonctionnement est prise en charge par l'application. Nous utilisons ses choix de conception et ses mécanismes internes. Pour la majorité des usages documentaires, cette abstraction est un avantage.

Mais si nous souhaitons contrôler précisément le découpage, les métadonnées, les critères de recherche ou la manière dont les résultats sont transmis au modèle, nous pouvons aller plus loin et construire notre propre pipeline.

C'est l'objet de la seconde partie. Nous allons reprendre les mêmes composants, mais sans confier leur coordination à AnythingLLM.

9. Construire un RAG indépendant avec Python

Cette seconde approche reprend le même principe que précédemment, mais nous retirons la couche de gestion d'AnythingLLM. Un programme Python va assurer directement la communication entre les documents, Ollama et Qdrant.

L'objectif n'est pas de remplacer systématiquement AnythingLLM. Il s'agit de comprendre comment construire un RAG maîtrisé de bout en bout, avec des composants indépendants et réutilisables.

Le pipeline indépendant

Le programme devra réaliser deux opérations principales. La première consiste à préparer les documents et à enregistrer leurs vecteurs dans Qdrant. La seconde consiste à transformer une question en embedding, rechercher les fragments correspondants, puis demander à Ollama de formuler une réponse.

10. Préparer les modèles et les services

Nous réutilisons les services déjà présents sur le VPS. Aucun nouveau serveur permanent n'est nécessaire : le programme Python sera lancé ponctuellement, depuis le terminal, pour indexer les documents ou interroger la base.

Nous avons besoin de deux modèles distincts :

  • Un modèle d'embedding, ici nomic-embed-text, pour transformer les textes et les questions en vecteurs.
  • Un modèle de génération, ici notre modèle Qwen installé dans Ollama, pour produire les réponses.

Vérifions leur présence :

La commande permet de contrôler les noms exacts des modèles disponibles. Le nom du modèle de génération devra être repris dans le script Python.

ollama list

Si le modèle d'embedding n'est pas encore installé, nous pouvons le télécharger :

Le modèle d'embedding est indépendant du modèle Qwen. Il ne rédige pas de réponses : il produit les vecteurs nécessaires à la recherche.

ollama pull nomic-embed-text

11. Installer l'environnement Python

Nous allons utiliser Python pour communiquer avec l'API HTTP d'Ollama et celle de Qdrant. Le programme s'appuiera sur la bibliothèque requests pour les échanges HTTP et sur qdrant-client pour manipuler la base vectorielle.

Créons un répertoire dédié au projet :

Ce répertoire contiendra le programme et les documents de test. Nous pouvons choisir un autre emplacement si nécessaire.

mkdir -p ~/rag-web-soul/documents
cd ~/rag-web-soul

Créons ensuite un environnement virtuel Python pour isoler les dépendances du projet :

L'environnement virtuel évite de mélanger les bibliothèques de ce projet avec celles du système ou d'autres applications.

python3 -m venv .venv
source .venv/bin/activate

Installons les bibliothèques nécessaires :

requests permet d'appeler les API HTTP. qdrant-client fournit les fonctions nécessaires pour créer une collection, enregistrer les vecteurs et effectuer une recherche.

pip install requests qdrant-client

12. Préparer un document à indexer

Pour notre premier test, créons un document Markdown simple. Il doit contenir suffisamment d'informations pour permettre plusieurs questions, mais rester assez court pour faciliter la vérification des résultats.

Créons le fichier :

Le fichier sera enregistré dans le répertoire documents. Nous pouvons le modifier avec l'éditeur de texte de notre choix.

nano documents/architecture.md

Insérons un contenu de test, par exemple :

Ce document est volontairement court. Il permet de vérifier que le programme retrouve les informations précises avant de demander au modèle de répondre.

# Architecture Web-Soul

Le VPS utilise Debian et Docker pour héberger
plusieurs services indépendants.

Ollama exécute les modèles de langage.
Qdrant stocke les vecteurs documentaires.
AnythingLLM fournit une interface de gestion
des conversations et des documents.
n8n permet d'automatiser des workflows.

Le modèle Qwen est utilisé pour générer
les réponses de l'assistant.

13. Construire le programme RAG

Le programme va réunir les opérations nécessaires dans un seul fichier Python. Pour faciliter sa lecture, nous allons distinguer quatre parties : la configuration, le découpage des documents, l'indexation et la recherche suivie de la génération de réponse.

Configuration et connexions

Commençons par définir les adresses des services, les noms des modèles et la collection Qdrant. Les adresses ci-dessous supposent que le script peut accéder aux services depuis le VPS. Elles devront être adaptées si Ollama ou Qdrant ne sont pas accessibles sur les adresses indiquées.

Le modèle d'embedding doit rester identique pour l'indexation et les recherches. La dimension indiquée pour nomic-embed-text est de 768. Elle devra correspondre à la dimension réellement renvoyée par le modèle.

import requests
from pathlib import Path
from qdrant_client import QdrantClient, models

OLLAMA_URL = "http://localhost:11434"
QDRANT_URL = "http://localhost:6333"

EMBED_MODEL = "nomic-embed-text"

# Remplacer par le nom exact affiché par ollama list.

LLM_MODEL = "qwen3:4b-instruct"

COLLECTION = "web_soul_documents"
DOCUMENTS_DIR = Path("documents")

VECTOR_SIZE = 768

qdrant = QdrantClient(url=QDRANT_URL)

Découper les documents

Un document peut être trop long pour être transmis intégralement au modèle lors d'une recherche. Nous allons donc le diviser en fragments plus petits, appelés chunks. Chaque fragment sera indexé séparément.

Pour cette première démonstration, nous utilisons un découpage rudimentaire par nombre de caractères. Une approche plus évoluée pourrait tenir compte des paragraphes, des titres ou du nombre de tokens.

La fonction reçoit le texte d'un document et renvoie une liste de fragments. Le chevauchement permet de conserver une petite partie du fragment précédent pour limiter les coupures trop abruptes.

def split_text(text, chunk_size=900, overlap=120):
    text = " ".join(text.split())

chunks = []
start = 0

while start < len(text):
    end = start + chunk_size
    chunks.append(text[start:end])
    start += chunk_size - overlap

return chunks

Indexer les documents dans Qdrant

Nous allons maintenant créer la collection Qdrant si elle n'existe pas, lire les fichiers Markdown, produire les embeddings avec Ollama et enregistrer les vecteurs accompagnés de leur texte d'origine.

La collection est créée avec une dimension de 768 et une distance cosinus. Chaque point contient le vecteur, le texte du fragment et le nom du fichier source.

def create_collection():
    collections = [
        item.name for item in qdrant.get_collections().collections
    ]

if COLLECTION not in collections:
    qdrant.create_collection(
        collection_name=COLLECTION,
        vectors_config=models.VectorParams(
            size=VECTOR_SIZE,
            distance=models.Distance.COSINE
        )
    )

def get_embedding(text):
response = requests.post(
f"{OLLAMA_URL}/api/embed",
json={
"model": EMBED_MODEL,
"input": text,
"truncate": False
},
timeout=120
)
response.raise_for_status()

return response.json()["embeddings"][0]

def index_documents():
create_collection()

point_id = 1

for file_path in DOCUMENTS_DIR.glob("*.md"):
    text = file_path.read_text(encoding="utf-8")
    chunks = split_text(text)

    for chunk in chunks:
        vector = get_embedding(chunk)

        qdrant.upsert(
            collection_name=COLLECTION,
            points=[
                models.PointStruct(
                    id=point_id,
                    vector=vector,
                    payload={
                        "text": chunk,
                        "source": file_path.name
                    }
                )
            ]
        )

        point_id += 1

    print(f"Document indexé : {file_path.name}")

Rechercher et générer une réponse

Lorsqu'une question est posée, le programme commence par produire son embedding. Qdrant recherche ensuite les fragments les plus proches dans la collection. Nous transmettons ces extraits au modèle Qwen pour qu'il construise une réponse à partir du contexte retrouvé.

La recherche retourne les fragments les plus proches. Le programme rassemble leur contenu et leur nom de fichier, puis construit une demande destinée à Ollama.

def ask_question(question):
    query_vector = get_embedding(question)

results = qdrant.query_points(
    collection_name=COLLECTION,
    query=query_vector,
    limit=4,
    with_payload=True
).points

if not results:
    print("Aucun document pertinent trouvé.")
    return

context = "\n\n".join(
    f"Source : {item.payload['source']}\n"
    f"{item.payload['text']}"
    for item in results
)

prompt = f"""

Tu es un assistant documentaire.
Réponds à la question en utilisant uniquement
les informations présentes dans le contexte.

Si le contexte ne contient pas la réponse,
indique clairement que tu ne disposes pas
de cette information.

CONTEXTE :
{context}

QUESTION :
{question}
"""

response = requests.post(
    f"{OLLAMA_URL}/api/generate",
    json={
        "model": LLM_MODEL,
        "prompt": prompt,
        "stream": False
    },
    timeout=300
)

response.raise_for_status()

print("\nRÉPONSE :")
print(response.json()["response"])

print("\nSOURCES RETROUVÉES :")
for item in results:
    print(
        f"- {item.payload['source']} "
        f"(score : {item.score:.3f})"
    )

Point d'entrée du programme

La dernière partie permet de choisir l'opération à effectuer. Le programme peut indexer les documents ou lancer une recherche interactive.

La fonction index_documents() prépare la base documentaire. La fonction ask_question() permet ensuite de poser des questions successives.

if __name__ == "__main__":
    print("RAG Web-Soul")
    print("1 - Indexer les documents")
    print("2 - Poser une question")

choice = input("Choix : ").strip()

if choice == "1":
    index_documents()

elif choice == "2":
    while True:
        question = input("\nQuestion (q pour quitter) : ")

        if question.lower() == "q":
            break

        ask_question(question)

else:
    print("Choix invalide.")

14. Enregistrer et exécuter le programme

Nous allons enregistrer l'ensemble du code précédent dans un fichier nommé rag.py, dans le répertoire rag-web-soul.

Ouvrir le fichier dans l'éditeur de texte et y coller les différentes parties du programme dans l'ordre présenté.

nano rag.py

Pour lancer le programme, activer l'environnement virtuel si nécessaire, puis exécuter :

Le script propose deux opérations : indexer les documents ou poser une question sur les documents déjà indexés.

source .venv/bin/activate
python rag.py

Commençons par choisir l'indexation. Une fois les documents enregistrés dans Qdrant, relançons le programme et choisissons la recherche. Nous pouvons alors poser une question dont la réponse figure dans le fichier architecture.md.

Attention : ce script constitue un prototype pédagogique. Il illustre le fonctionnement général du RAG, mais ne constitue pas encore un outil de production. Il faudra notamment renforcer la gestion des erreurs, la mise à jour des documents, l'identification des fragments et la configuration des accès avant un usage régulier.

15. Vérifier le fonctionnement et maîtriser les ressources

Nous disposons maintenant d'un RAG indépendant d'AnythingLLM. Le programme lit les documents, produit leurs embeddings, les enregistre dans Qdrant et interroge cette base pour fournir un contexte au modèle Qwen.

Quelques vérifications permettent de contrôler le fonctionnement :

  • La collection web_soul_documents existe dans Qdrant.
  • Les documents ont bien été découpés et indexés.
  • Une question liée au document permet de retrouver des fragments pertinents.
  • Les sources affichées correspondent bien aux fichiers indexés.
  • Une question sans réponse dans le corpus est signalée comme telle.

Sur notre VPS de 4 cœurs et 8 Go de mémoire, il est préférable de conserver un corpus de test réduit et de procéder à l'indexation progressivement. Le modèle d'embedding et le modèle de génération peuvent solliciter la mémoire et le processeur, particulièrement lorsqu'ils sont chargés simultanément.

Le script peut être enrichi par la suite : traitement de PDF, prise en compte des métadonnées, découpage plus intelligent, suppression des anciens fragments, recherche hybride ou intégration dans un workflow n8n. Chaque évolution pourra être ajoutée sans transformer immédiatement le programme en service permanent.

À retenir

Le RAG permet à un modèle de langage de consulter une mémoire documentaire externe sans devoir réentraîner le modèle. Les documents sont transformés en fragments, vectorisés et stockés dans une base de recherche. Lorsqu'une question arrive, les extraits pertinents sont retrouvés et transmis au modèle pour construire sa réponse.

Avec AnythingLLM, cette chaîne est mise en place sans programmation. C'est la voie la plus rapide pour obtenir un assistant documentaire fonctionnel. Avec Python, Ollama et Qdrant, nous reprenons les mêmes opérations à notre compte et gagnons en contrôle sur le fonctionnement du système.

Ces deux approches ne s'opposent pas. Elles constituent deux niveaux de maîtrise d'une même architecture. La première facilite l'utilisation, la seconde ouvre la voie à des développements spécifiques et à une intégration plus poussée dans notre environnement LABO Web-Soul.

Table des matières

Laisser un commentaire

Les autres publications de cette section

0:00
0:00
Défiler vers le haut