Intermedio 11 minStack

RAG con ChromaDB e Mistral

Risposta diretta

Per un RAG locale con Mistral, lo stack più semplice è Ollama (generazione ed embedding con bge-m3) più ChromaDB in modalità file, senza server né PyTorch. Quanto al modello, ministral-3:8b (6,0 GB, licenza Apache 2.0, contesto dichiarato di 256K) è una buona scelta predefinita per una scheda da 8 a 12 GB, ministral-3:14b per 16 GB e mistral-small3.2:24b (15 GB) per capacità superiori. L'impostazione da non dimenticare: la finestra di contesto di Ollama, da aumentare affinché i passaggi trovino spazio nel prompt.

Questa guida mostra come costruire un assistente documentale completo, con due script Python e un modello Mistral eseguito sulla tua macchina: i tuoi PDF e file di testo vengono suddivisi in passaggi e indicizzati in ChromaDB; i passaggi recuperati vengono poi forniti al modello, che risponde citando le proprie fonti. Spiega anche quale modello Mistral scegliere in base alla memoria video disponibile e quali insidie portano un RAG a dare risposte fuori tema.

Di Mohamed Meguedmi·Agg. 2026-09-30·Testato su Windows, macOS e Linux

#Ciò che costruiamo: un RAG Mistral completamente locale

Il RAG (generazione aumentata tramite recupero) consiste nel trovare i passaggi dei tuoi documenti che riguardano la domanda, poi inserirli nel prompt del modello affinché risponda basandosi su di essi. Il risultato atteso qui è un piccolo strumento da riga di comando: uno script indicizza una cartella di documenti; un secondo legge una domanda, trova i cinque passaggi più vicini in ChromaDB, li trasmette a un modello Mistral tramite Ollama insieme alla domanda e mostra la risposta seguita dai file consultati. Niente lascia la macchina: Ollama rende disponibili il modello di generazione e il modello di embedding, ChromaDB memorizza i vettori in una cartella locale.

Il termine «Mistral» viene usato in due sensi: i modelli a pesi aperti di Mistral AI, che scarichi ed esegui autonomamente (oggetto di questa guida), e le API ospitate dall'azienda, che inviano i tuoi passaggi ai suoi server. Per i documenti riservati, solo la prima opzione rispetta il requisito «100% locale».

#Quale modello Mistral scegliere per il RAG

Il kit RAG Local

I tuoi documenti, la tua IA: un RAG locale affidabile sui tuoi PDF, sulle tue note e sulle tue email — senza inviare nulla nel cloud.

  • Spazio online a vita
  • PDF + file
  • Aggiornamenti a vita

Un RAG ha esigenze specifiche: il modello deve seguire un’istruzione rigorosa (« rispondi soltanto basandoti sui passaggi »), leggere più passaggi senza perdere il filo e rispondere in francese. La dimensione conta meno rispetto alla conversazione libera; invece, la memoria disponibile per il contesto conta di più. Le dimensioni riportate di seguito sono quelle visualizzate dalla libreria Ollama, con la quantizzazione predefinita.

Modelli Mistral nella libreria Ollama (rilevazione di settembre 2026)
ModelloDimensione in OllamaContesto annunciatoPer chi
ministral-3:3b3,0 GB256KMacchina senza GPU dedicata; risposte semplici, scarsa tolleranza alle istruzioni complesse
ministral-3:8b6,0 GB256KScelta predefinita ragionevole per una scheda da 8 a 12 GB o un portatile con 16 GB di memoria
ministral-3:14b9,1 GB256KScheda da 16 GB, oppure da 12 GB con un contesto moderato
mistral-nemo (12B)vedi la pagina Ollama128KAlternativa meno recente, ancora diffusa
mistral-small3.2:24b15 GB128KScheda da 24 GB o memoria unificata da 32 GB e oltre; il più affidabile nel rispettare le istruzioni sul formato
mistral (7B, versione 0.3)4,4 GB32KModello vecchio: da riservare a macchine molto limitate

La famiglia Ministral 3 (3B, 8B e 14B) è pubblicata con licenza Apache 2.0, come indicato nell'annuncio di Mistral 3, e la pagina Ollama la descrive come progettata per il deployment edge, capace di funzionare su una vasta gamma di hardware. Mistral Small 4, pubblicato nel 2026 con 119 miliardi di parametri in totale secondo il nome della sua scheda Hugging Face, è destinato a hardware server: non è un candidato per un computer personale. Per un ordine di grandezza della memoria necessaria, il calcolatore di VRAM del sito fornisce il peso del modello più la cache del contesto.

i
Contesto annunciato e contesto utile
Un contesto di 128K o 256K rappresenta la capacità massima del modello, non un'impostazione: Ollama ne utilizza molto meno per impostazione predefinita e un contesto ampio consuma memoria aggiuntiva. Per un RAG con cinque brani, bastano 8.000 token.

#Lo stack tecnologico

Generazione
Un modello Mistral fornito da Ollama, attraverso l'API HTTP locale sulla porta 11434.
Embeddings
bge-m3 servito da Ollama: la pagina della libreria lo descrive come un modello di BAAI versatile, multilingue e a più livelli di granularità, con 567 milioni di parametri. Evita di dover installare PyTorch e sentence-transformers.
Database vettoriale
ChromaDB in modalità locale (PersistentClient): una cartella, nessun server. Chroma fornisce un wrapper, OllamaEmbeddingFunction, che chiama l'API di embedding di Ollama.
Lettura dei file
pypdf per i PDF contenenti testo, lettura diretta per Markdown e testo semplice. Un PDF scansionato è un'immagine: serve prima il riconoscimento dei caratteri.

#Preparare l'ambiente

  1. 01
    Installa Ollama e ottieni i modelli
    Installa Ollama, poi scarica il modello di generazione e il modello di embedding con i due comandi seguenti.
  2. 02
    Creare l'ambiente Python
    Python 3.10 o successivo. Un ambiente virtuale mantiene le dipendenze del progetto separate.
  3. 03
    Posizionare i documenti
    Copia i tuoi PDF e i file Markdown e di testo in una cartella docs/ accanto agli script.
Modelli e dipendenze
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. Indicizzare i documenti in ChromaDB

Lo script legge ogni file, suddivide il testo in passaggi di circa 1.800 caratteri rispettando i confini dei paragrafi, poi li affida a Chroma, che chiama bge-m3 tramite Ollama per calcolare i vettori. Due dettagli contano: ogni passaggio conserva il nome del file nei metadati (per citare la fonte) e le aggiunte avvengono in lotti anziché un passaggio alla volta.

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'uso di upsert con identificatori basati sul nome del file e sul numero del passaggio permette di rieseguire lo script: reindicizzare la stessa cartella aggiorna i passaggi invece di duplicarli. Attenzione però: se un documento viene accorciato, i vecchi passaggi in eccesso rimangono nel database; per una modifica importante, elimina la cartella chroma_db e reindicizza. La scelta della dimensione dei passaggi è spiegata in dettaglio nella guida sulle strategie di chunking.

#3. Interrogare: ricerca, poi generazione

Il secondo script incorpora la domanda, recupera i cinque passaggi più vicini e compone il prompt. L'istruzione è decisiva: chiede di rispondere esclusivamente sulla base dei passaggi, di ammettere quando mancano le informazioni e di citare il file. Il parametro num_ctx aumenta la finestra di contesto: la documentazione di Ollama indica che la finestra predefinita è di 4.096 token e che la variabile OLLAMA_CONTEXT_LENGTH o il parametro num_ctx la modificano. Con cinque passaggi da 400 a 500 token, l'istruzione e la risposta, una finestra di 4.096 token è al limite: un contesto troppo corto viene troncato senza alcun avviso e il modello risponde senza aver letto la fine dei tuoi passaggi.

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))
Avviare
python index.py
python ask.py "Quel est le délai de préavis prévu au contrat ?"

#Verificare cosa restituisce ChromaDB prima di dare la colpa al modello

Quando una risposta è sbagliata, la causa va cercata in uno di due punti: la ricerca non ha recuperato il passaggio giusto oppure il modello lo ha utilizzato male. Puoi distinguere i due casi visualizzando i passaggi recuperati con la rispettiva distanza, senza chiamare il modello. Se il passaggio giusto non compare tra i primi cinque, modifica la suddivisione del testo, aggiungi una ricerca per parole chiave o un reranker. Se è presente e la risposta resta errata, il problema dipende dal prompt, dal contesto troncato o dal modello: prova il modello di dimensioni superiori prima di trarre conclusioni.

debug.py : mostrare i passaggi e la loro distanza
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 di memoria: ciò che deve stare in memoria contemporaneamente

Il RAG fa coesistere due modelli, quello che genera e quello che calcola i vettori, più la cache del contesto del primo. Ollama carica ogni modello su richiesta e può rimuoverne uno dalla memoria per fare spazio all'altro, aggiungendo un ritardo a ogni passaggio se la memoria è appena sufficiente. La tabella fornisce un ordine di grandezza per tre configurazioni; il peso del modello proviene dalla libreria Ollama, mentre il resto è un calcolo da affinare con il calcolatore di VRAM del sito.

Memoria da prevedere (pesi Ollama, contesto di 8 192 token)
ConfigurazionePeso del modello di generazioneDa aggiungereScheda prevista
ministral-3:8b + bge-m36,0 GBCache di contesto, modello di embedding (567 milioni di parametri, poco più di un GB a mezza precisione), margine per il sistemaDa 8 a 12 GB
ministral-3:14b + bge-m39,1 GBLo stesso: il contesto lungo diventa il fattore limitante su 12 GBDa 12 a 16 GB
mistral-small3.2:24b + bge-m315 GBIdem; prevedere un ampio margine24 GB o più
→
Se la memoria non basta
Riduci prima num_ctx (8.192 è già generoso per cinque passaggi), poi passa a un modello più piccolo. Evita anche di aumentare il numero di passaggi: troppi passaggi diluiscono la risposta tanto quanto riempiono la memoria.

#Le insidie che portano a risposte fuori tema

Il contesto predefinito troppo breve
Vedere sopra: senza aumentare num_ctx, gli ultimi passaggi vengono troncati. Sintomo tipico: ChromaDB recupera correttamente la risposta giusta, ma il modello dice di non trovarla.
PDF scansionati
pypdf legge solo il testo già presente. Una scansione restituisce un risultato vuoto: lo script lo segnala. Sottoponi prima il documento a un OCR, descritto nella guida su Tesseract.
Passaggi senza contesto
Un passaggio estrapolato dal suo documento (« il termine è di 30 giorni ») non chiarisce di cosa si parla. Anteponi a ogni passaggio il titolo del documento o della sezione.
Domanda senza risposta nei documenti
Senza l'istruzione «dillo chiaramente», un modello colma il vuoto con ciò che sa. Testa sempre una domanda la cui risposta non è nei tuoi file.
Identificatori e termini esatti
Un numero di contratto o di fascicolo non viene recuperato bene tramite gli embedding: aggiungi una ricerca per parole chiave, come descritto nella guida sulla ricerca ibrida.
!
Verificare prima di fidarsi
Un RAG cita le sue fonti, ma ciò non dimostra che la risposta sia corretta: apri il file citato per le decisioni che contano (contratti, cifre, scadenze).

#Per approfondire

Miglioramenti classificati in base al rapporto tra impegno richiesto ed effetto
MiglioramentoSforzoDa fare quando
Aumentare il numero di brani (k) da 5 a 8Una rigaLa risposta è distribuita su più passaggi
Chunking per titoli invece che per paragrafiMedioDocumenti strutturati (documentazione, contratti suddivisi in articoli)
Ricerca ibrida BM25 + vettorialeMedioDomande per identificativo, sigla o nome proprio
Reranker (bge-reranker-v2-m3)MedioLa risposta corretta viene recuperata ma classificata oltre il 5° posto
Interfaccia di chat (Open WebUI, API FastAPI)VariabileAltri utenti devono utilizzare lo strumento
Backup e reindicizzazione programmatiBassoLa cartella dei documenti cambia ogni settimana

Ogni miglioramento ha la propria guida: misura il recall su un insieme di 30–50 domande reali prima e dopo, anziché accumulare tecniche. Se preferisci un'interfaccia pronta senza scrivere codice, la guida sul RAG senza programmazione presenta Open WebUI e AnythingLLM.

#Domande frequenti sul RAG con Mistral

FAQ
Quale modello Mistral per un RAG locale?+
Per una scheda da 8 a 12 GB, ministral-3:8b (6,0 GB in Ollama, licenza Apache 2.0) è un buon punto di partenza; ministral-3:14b (9,1 GB) per 16 GB; mistral-small3.2:24b (15 GB) per 24 GB e oltre. Scegli in base alla memoria che resta dopo aver caricato il modello: serve spazio per il contesto.
Ollama può calcolare gli embedding al posto di sentence-transformers?+
Sì: Ollama offre un'API di embedding e propone bge-m3, un modello multilingue da 567 milioni di parametri. ChromaDB fornisce il wrapper OllamaEmbeddingFunction per chiamarlo. Il vantaggio è dover installare un solo motore, senza PyTorch; lo svantaggio è che bisogna mantenere Ollama attivo durante l'indicizzazione.
Perché il modello dice che non trova la risposta, anche se è presente nei miei documenti?+
Due cause frequenti. O la finestra di contesto di Ollama è troppo corta e i passaggi vengono troncati: aumenta num_ctx a 8.192. O i passaggi recuperati non contengono la risposta: verifica cosa restituisce ChromaDB prima della generazione, poi regola la suddivisione in passaggi o la ricerca.
È possibile usare l'API Mistral al posto di Ollama?+
Tecnicamente sì, ma i tuoi brani di testo verrebbero allora inviati ai server dell'azienda, il che contraddice il requisito di riservatezza per i documenti sensibili. Per continuare a lavorare in locale, mantieni i modelli a pesi aperti eseguiti da Ollama. Per i documenti non sensibili, è possibile usare l'API; in quel caso, occorre leggere le condizioni di trattamento dei dati del fornitore.
Come aggiungere nuovi documenti senza reindicizzare tutto?+
Copiali in docs/ e riesegui index.py: grazie a upsert e agli identificatori stabili, i passaggi esistenti vengono aggiornati e quelli nuovi aggiunti. Se modifichi la dimensione dei passaggi o il modello di embedding, elimina la cartella chroma_db e reindicizza tutto: i vettori precedenti non sono più comparabili.
Serve un GPU per questo RAG?+
Non necessariamente: ministral-3:3b funziona su un processore recente con 8 GB di memoria, con risposte lente. L'indicizzazione, invece, avviene una sola volta. Una GPU migliora soprattutto la risposta: maggiore velocità e la possibilità di usare un modello più grande. Misura il tempo di risposta sul tuo dispositivo prima di decidere di investire.
Questa guida ti è stata utile?

Un feedback, un errore, una precisazione? Facci sapere, così la guida migliora per tutti.