Intermédiaire 11 minStack

RAG avec ChromaDB et Mistral

Réponse directe

Pour un RAG local avec Mistral, la pile la plus simple est Ollama (génération et embeddings avec bge-m3) plus ChromaDB en mode fichier, sans serveur ni PyTorch. Côté modèle, ministral-3:8b (6,0 Go, licence Apache 2.0, contexte de 256K annoncé) est un bon défaut pour une carte de 8 à 12 Go, ministral-3:14b pour 16 Go, mistral-small3.2:24b (15 Go) au-delà. Le réglage à ne pas oublier : la fenêtre de contexte d'Ollama, à relever pour que les passages tiennent dans le prompt.

Ce guide construit un assistant documentaire complet, en deux scripts Python, avec un modèle Mistral exécuté sur votre machine : vos PDF et fichiers texte sont découpés, indexés dans ChromaDB, puis les passages retrouvés sont donnés au modèle qui répond en citant ses sources. Il précise aussi quel modèle Mistral choisir selon votre mémoire graphique et les pièges qui font qu'un RAG répond à côté.

Par Mohamed Meguedmi·Màj 2026-09-30·Testé sur Windows, macOS, Linux

#Ce qu'on construit : un RAG Mistral entièrement local

Le RAG (génération augmentée par récupération) consiste à retrouver les passages de vos documents qui concernent la question, puis à les coller dans le prompt du modèle pour qu'il réponde à partir d'eux. Le résultat attendu ici est un petit outil en ligne de commande : un script indexe un dossier de documents ; un second lit une question, retrouve les cinq passages les plus proches dans ChromaDB, les transmet à un modèle Mistral via Ollama avec la question, et affiche la réponse suivie des fichiers consultés. Rien ne quitte la machine : Ollama sert le modèle de génération et le modèle d'embeddings, ChromaDB stocke les vecteurs dans un dossier local.

Deux sens du mot « Mistral » circulent : les modèles à poids ouverts de Mistral AI, que l'on télécharge et exécute soi-même (objet de ce guide), et les API hébergées de l'entreprise, qui envoient vos passages sur ses serveurs. Pour des documents confidentiels, seul le premier respecte l'exigence « 100 % local ».

#Quel modèle Mistral choisir pour le RAG

Le kit RAG Local

Tes documents, ton IA : un RAG local fiable sur tes PDF, notes et mails — sans rien envoyer dans le cloud.

  • Espace en ligne à vie
  • PDF + fichiers
  • Mises à jour à vie

Un RAG a des besoins particuliers : le modèle doit suivre une consigne stricte (« réponds uniquement à partir des passages »), lire plusieurs passages sans se perdre, et répondre en français. La taille compte moins que pour la conversation libre ; en revanche la mémoire disponible pour le contexte compte plus. Les tailles ci-dessous sont celles affichées par la bibliothèque Ollama, en quantification par défaut.

Modèles Mistral dans la bibliothèque Ollama (relevé de septembre 2026)
ModèleTaille OllamaContexte annoncéPour qui
ministral-3:3b3,0 Go256KMachine sans GPU dédié ; réponses simples, tolérance faible aux consignes complexes
ministral-3:8b6,0 Go256KDéfaut raisonnable pour une carte de 8 à 12 Go ou un portable avec 16 Go de mémoire
ministral-3:14b9,1 Go256KCarte de 16 Go, ou 12 Go avec un contexte modéré
mistral-nemo (12B)voir la page Ollama128KAlternative plus ancienne, encore répandue
mistral-small3.2:24b15 Go128KCarte de 24 Go ou mémoire unifiée de 32 Go et plus ; le plus solide sur les consignes de format
mistral (7B, version 0.3)4,4 Go32KModèle ancien : à réserver aux machines très limitées

La famille Ministral 3 (3B, 8B et 14B) est publiée sous licence Apache 2.0, comme l'indique l'annonce de Mistral 3, et la page Ollama la décrit comme conçue pour un déploiement en périphérie, capable de tourner sur une large gamme de matériel. Mistral Small 4, publié en 2026 avec 119 milliards de paramètres au total d'après le nom de sa fiche Hugging Face, vise du matériel de serveur : il n'est pas un candidat pour une machine personnelle. Pour un ordre de grandeur de la mémoire nécessaire, le calculateur de VRAM du site donne le poids du modèle plus le cache de contexte.

i
Contexte annoncé et contexte utile
Un contexte de 128K ou 256K est une capacité maximale du modèle, pas un réglage : Ollama en utilise beaucoup moins par défaut, et un grand contexte consomme de la mémoire supplémentaire. Pour un RAG à cinq passages, 8 000 tokens suffisent.

#La pile technique

Génération
Un modèle Mistral servi par Ollama, par l'API HTTP locale sur le port 11434.
Embeddings
bge-m3 servi par Ollama : la page de la bibliothèque le décrit comme un modèle de BAAI polyvalent, multilingue et multi-granularité, de 567 millions de paramètres. Il évite d'installer PyTorch et sentence-transformers.
Base vectorielle
ChromaDB en mode local (PersistentClient) : un dossier, aucun serveur. Chroma fournit une enveloppe, OllamaEmbeddingFunction, qui appelle l'API d'embeddings d'Ollama.
Lecture des fichiers
pypdf pour les PDF contenant du texte, lecture directe pour Markdown et texte brut. Un PDF scanné est une image : il faut d'abord une reconnaissance de caractères.

#Préparer l'environnement

  1. 01
    Installer Ollama et récupérer les modèles
    Installez Ollama, puis téléchargez le modèle de génération et le modèle d'embeddings avec les deux commandes ci-dessous.
  2. 02
    Créer l'environnement Python
    Python 3.10 ou plus. Un environnement virtuel garde les dépendances du projet à part.
  3. 03
    Placer les documents
    Copiez vos PDF, fichiers Markdown et texte dans un dossier docs/ à côté des scripts.
Modèles et dépendances
ollama pull ministral-3:8b
ollama pull bge-m3

mkdir mon-rag && cd mon-rag
python3 -m venv venv
source venv/bin/activate   # .\venv\Scripts\activate sous Windows
pip install chromadb pypdf requests

#2. Indexer les documents dans ChromaDB

Le script lit chaque fichier, découpe le texte en passages d'environ 1 800 caractères en coupant sur les paragraphes, puis les confie à Chroma, qui appelle bge-m3 par Ollama pour calculer les vecteurs. Deux détails comptent : chaque passage garde le nom du fichier en métadonnée (pour citer la source), et les ajouts se font par lots plutôt qu'un passage à la fois.

index.py
from pathlib import Path
import chromadb
from chromadb.utils.embedding_functions.ollama_embedding_function import OllamaEmbeddingFunction
from pypdf import PdfReader

ef = OllamaEmbeddingFunction(url="http://localhost:11434", model_name="bge-m3")
coll = chromadb.PersistentClient(path="./chroma_db").get_or_create_collection("mes_docs", embedding_function=ef)

def lire(path: Path) -> str:
    if path.suffix.lower() == ".pdf":
        return "\n\n".join(p.extract_text() or "" for p in PdfReader(str(path)).pages)
    return path.read_text(encoding="utf-8", errors="ignore")

def decouper(texte: str, max_chars=1800):
    """Regroupe des paragraphes entiers jusqu'à max_chars ; un paragraphe trop long est coupé."""
    chunks, courant = [], ""
    for para in (p.strip() for p in texte.split("\n\n")):
        if not para:
            continue
        while len(para) > max_chars:
            if courant:
                chunks.append(courant); courant = ""
            chunks.append(para[:max_chars]); para = para[max_chars:]
        if len(courant) + len(para) + 2 > max_chars and courant:
            chunks.append(courant); courant = ""
        courant = (courant + "\n\n" + para).strip()
    if courant:
        chunks.append(courant)
    return chunks

n = 0
for path in sorted(Path("docs").rglob("*")):
    if path.suffix.lower() not in {".pdf", ".md", ".txt"}:
        continue
    chunks = decouper(lire(path))
    if not chunks:
        print(f"  ! {path.name} : aucun texte extrait (PDF scanné ?)")
        continue
    for i in range(0, len(chunks), 32):  # par lots de 32
        lot = chunks[i:i + 32]
        coll.upsert(
            ids=[f"{path.name}-{i + j}" for j in range(len(lot))],
            documents=lot,
            metadatas=[{"source": path.name}] * len(lot),
        )
    n += len(chunks)
    print(f"  + {path.name} : {len(chunks)} passages")
print(f"Terminé : {n} passages indexés")

L'utilisation d'upsert avec des identifiants construits sur le nom de fichier et le numéro de passage rend le script relançable : réindexer le même dossier met à jour les passages au lieu de les dupliquer. Attention toutefois : si un document raccourcit, les anciens passages surnuméraires restent dans la base ; pour un changement important, supprimez le dossier chroma_db et réindexez. Le choix de la taille des passages est détaillé dans le guide sur les stratégies de chunking.

#3. Interroger : recherche puis génération

Le second script embarque la question, récupère les cinq passages les plus proches et compose le prompt. L'instruction de consigne est décisive : elle demande de répondre uniquement à partir des passages, d'avouer quand l'information manque, et de citer le fichier. Le paramètre num_ctx relève la fenêtre de contexte : la documentation d'Ollama indique que la fenêtre par défaut est de 4 096 tokens, et que la variable OLLAMA_CONTEXT_LENGTH ou le paramètre num_ctx la modifient. Avec cinq passages de 400 à 500 tokens, la consigne et la réponse, 4 096 tokens sont à la limite : un contexte trop court est coupé silencieusement, et le modèle répond sans avoir lu la fin de vos passages.

ask.py
import sys, requests
import chromadb
from chromadb.utils.embedding_functions.ollama_embedding_function import OllamaEmbeddingFunction

MODELE = "ministral-3:8b"
ef = OllamaEmbeddingFunction(url="http://localhost:11434", model_name="bge-m3")
coll = chromadb.PersistentClient(path="./chroma_db").get_collection("mes_docs", embedding_function=ef)

SYSTEME = (
    "Tu réponds en français, uniquement à partir des passages fournis. "
    "Si la réponse n'y figure pas, dis-le clairement au lieu de deviner. "
    "Termine chaque affirmation par le nom du fichier source entre crochets."
)

def repondre(question: str, k: int = 5):
    res = coll.query(query_texts=[question], n_results=k)
    passages = list(zip(res["documents"][0], res["metadatas"][0]))
    contexte = "\n\n---\n\n".join(f"[{m['source']}]\n{p}" for p, m in passages)
    r = requests.post("http://localhost:11434/api/chat", json={
        "model": MODELE,
        "stream": False,
        "options": {"temperature": 0.2, "num_ctx": 8192},
        "messages": [
            {"role": "system", "content": SYSTEME},
            {"role": "user", "content": f"PASSAGES :\n{contexte}\n\nQUESTION : {question}"},
        ],
    }, timeout=300)
    r.raise_for_status()
    return r.json()["message"]["content"], sorted({m["source"] for _, m in passages})

if __name__ == "__main__":
    q = " ".join(sys.argv[1:]) or input("Question : ")
    reponse, sources = repondre(q)
    print("\n" + reponse)
    print("\nSources consultées :", ", ".join(sources))
Lancer
python index.py
python ask.py "Quel est le délai de préavis prévu au contrat ?"

#Vérifier ce que ChromaDB renvoie avant d'accuser le modèle

Quand une réponse est mauvaise, la cause se trouve à l'un de deux endroits : la recherche n'a pas ramené le bon passage, ou le modèle l'a mal utilisé. Les deux se distinguent en affichant les passages retrouvés avec leur distance, sans appeler le modèle. Si le bon passage est absent des cinq premiers, changez le découpage, ajoutez une recherche par mots-clés ou un reranker. S'il est présent et que la réponse reste fausse, le problème vient du prompt, du contexte tronqué ou du modèle : essayez le modèle de taille supérieure avant de conclure.

debug.py : afficher les passages et leur distance
import sys
import chromadb
from chromadb.utils.embedding_functions.ollama_embedding_function import OllamaEmbeddingFunction

ef = OllamaEmbeddingFunction(url="http://localhost:11434", model_name="bge-m3")
coll = chromadb.PersistentClient(path="./chroma_db").get_collection("mes_docs", embedding_function=ef)
res = coll.query(query_texts=[" ".join(sys.argv[1:])], n_results=8)
for doc, meta, dist in zip(res["documents"][0], res["metadatas"][0], res["distances"][0]):
    print(f"{dist:.3f}  {meta['source']}  {doc[:120]!r}")

#Budget mémoire : ce qui doit tenir en même temps

Le RAG fait cohabiter deux modèles, celui qui génère et celui qui calcule les vecteurs, plus le cache de contexte du premier. Ollama charge chaque modèle à la demande et peut décharger l'un pour faire de la place à l'autre, ce qui ajoute un délai à chaque bascule si la mémoire est juste. Le tableau donne un ordre de grandeur pour trois configurations ; le poids du modèle vient de la bibliothèque Ollama, le reste est un calcul à affiner avec le calculateur de VRAM du site.

Mémoire à prévoir (poids Ollama, contexte de 8 192 tokens)
ConfigurationPoids du modèle de générationÀ ajouterCarte visée
ministral-3:8b + bge-m36,0 GoCache de contexte, modèle d'embeddings (567 millions de paramètres, à peine plus d'un Go en demi-précision), marge du système8 à 12 Go
ministral-3:14b + bge-m39,1 GoIdem ; le contexte long devient le facteur limitant sur 12 Go12 à 16 Go
mistral-small3.2:24b + bge-m315 GoIdem ; prévoir une marge confortable24 Go ou plus
→
Si la mémoire manque
Réduisez d'abord num_ctx (8 192 est déjà généreux pour cinq passages), puis passez au modèle inférieur. Évitez aussi d'augmenter le nombre de passages : trop de passages dilue la réponse autant qu'il remplit la mémoire.

#Les pièges qui font répondre à côté

Le contexte par défaut trop court
Voir plus haut : sans num_ctx relevé, les derniers passages sont tronqués. Symptôme typique : la bonne réponse est bien récupérée par ChromaDB, mais le modèle dit qu'il ne la trouve pas.
PDF scannés
pypdf ne lit que du texte déjà présent. Un scan renvoie du vide : le script l'affiche. Passez d'abord le document par un OCR, décrit dans le guide sur Tesseract.
Passages sans contexte
Un passage arraché à son document (« le délai est de 30 jours ») ne dit pas de quoi il parle. Préfixez chaque passage du titre du document ou de la section.
Question sans réponse dans les documents
Sans la consigne « dis-le clairement », un modèle comble le vide avec ce qu'il sait. Testez toujours une question dont la réponse n'est pas dans vos fichiers.
Identifiants et termes exacts
Un numéro de contrat ou de dossier n'est pas bien retrouvé par les embeddings : ajoutez une recherche par mots-clés, comme décrit dans le guide sur la recherche hybride.
!
Vérifier avant de faire confiance
Un RAG cite ses sources, mais cela ne prouve pas que la réponse est juste : ouvrez le fichier cité pour les décisions qui comptent (contrats, chiffres, échéances).

#Pour aller plus loin

Améliorations classées par rapport effort et effet
AméliorationEffortÀ faire quand
Relever le nombre de passages (k) de 5 à 8Une ligneLa réponse est répartie sur plusieurs passages
Chunking par titres au lieu de paragraphesMoyenDocuments structurés (documentation, contrats à articles)
Recherche hybride BM25 + vectorielMoyenQuestions par identifiant, sigle ou nom propre
Reranker (bge-reranker-v2-m3)MoyenLa bonne réponse est récupérée mais rangée au-delà du 5e rang
Interface de chat (Open WebUI, API FastAPI)VariableD'autres personnes doivent utiliser l'outil
Sauvegarde et réindexation planifiéesFaibleLe dossier de documents évolue chaque semaine

Chaque amélioration a son propre guide : mesurez le rappel sur 30 à 50 questions réelles avant et après, plutôt que d'empiler les techniques. Si vous préférez une interface toute faite sans écrire de code, le guide sur le RAG sans coder présente Open WebUI et AnythingLLM.

#Questions fréquentes sur le RAG avec Mistral

FAQ
Quel modèle Mistral pour un RAG en local ?+
Pour une carte de 8 à 12 Go, ministral-3:8b (6,0 Go dans Ollama, licence Apache 2.0) est un bon point de départ ; ministral-3:14b (9,1 Go) pour 16 Go ; mistral-small3.2:24b (15 Go) pour 24 Go et plus. Choisissez selon la mémoire qui reste après le poids du modèle : il faut de la place pour le contexte.
Ollama peut-il calculer les embeddings à la place de sentence-transformers ?+
Oui : Ollama expose une API d'embeddings et propose bge-m3, un modèle multilingue de 567 millions de paramètres. ChromaDB fournit l'enveloppe OllamaEmbeddingFunction pour l'appeler. L'avantage est de n'avoir qu'un seul moteur à installer, sans PyTorch ; l'inconvénient est qu'il faut garder Ollama actif pendant l'indexation.
Pourquoi le modèle dit-il ne pas trouver la réponse alors qu'elle est dans mes documents ?+
Deux causes fréquentes. Soit la fenêtre de contexte d'Ollama est trop courte et les passages sont tronqués : relevez num_ctx à 8 192. Soit les passages retrouvés ne contiennent pas la réponse : vérifiez ce que renvoie ChromaDB avant la génération, puis ajustez le découpage ou la recherche.
Peut-on utiliser l'API Mistral à la place d'Ollama ?+
Techniquement oui, mais vos passages seraient alors envoyés aux serveurs de l'entreprise, ce qui contredit l'exigence de confidentialité pour des documents sensibles. Pour rester local, gardez les modèles à poids ouverts exécutés par Ollama. Pour des documents non sensibles, l'API est possible ; les conditions de traitement des données sont alors à lire chez le fournisseur.
Comment ajouter de nouveaux documents sans tout réindexer ?+
Copiez-les dans docs/ et relancez index.py : grâce à upsert et aux identifiants stables, les passages existants sont mis à jour, les nouveaux ajoutés. Si vous modifiez la taille des passages ou le modèle d'embeddings, supprimez le dossier chroma_db et réindexez tout : les vecteurs anciens ne sont plus comparables.
Faut-il un GPU pour ce RAG ?+
Pas obligatoirement : ministral-3:3b tourne sur un processeur récent avec 8 Go de mémoire, avec des réponses lentes. L'indexation, elle, s'exécute une fois. Un GPU améliore surtout la réponse : plus de vitesse, et la possibilité d'utiliser un modèle plus grand. Mesurez le temps de réponse sur votre machine avant de décider d'investir.
Ce guide vous a aidé ?

Un retour, une erreur, une précision ? Faites-nous signe, ça améliore le guide pour tout le monde.