RAG sta per Retrieval-Augmented Generation: invece di sperare che un modello «sappia» la risposta, gli si forniscono i pezzi giusti dei tuoi documenti al momento della domanda. In questa guida costruiamo un sistema RAG che gira interamente sul tuo computer: nessun dato esce, nessun abbonamento, nessuna API a pagamento. Potrai interrogare contratti, manuali, dispense o report in PDF e ottenere risposte con tanto di citazione della fonte.

A chi serve e cosa otterrai

È pensata per chi ha una base di documenti riservati (avvocati, consulenti, studenti, PMI, PA) e vuole un assistente che risponda solo in base a quei testi, in locale. Al termine avrai uno script Python che: legge una cartella di PDF, li spezza in frammenti, li trasforma in vettori (embedding), li salva in un database vettoriale e risponde alle domande citando i passaggi usati.

Prerequisiti reali: un computer con almeno 8-16 GB di RAM (Windows, macOS o Linux), Python 3.10+ installato, e circa 5 GB liberi per i modelli. Non serve una GPU: funziona anche su CPU, solo più lentamente.

Quali strumenti usiamo e perché

  • Ollama — per far girare in locale sia il modello di linguaggio sia quello di embedding. È gratuito, semplice e multipiattaforma. È la nostra prima scelta perché elimina ogni complessità di configurazione.
  • nomic-embed-text — modello di embedding leggero ed efficace, scaricabile via Ollama.
  • llama3.2 (o qwen2.5) — il modello che genera la risposta. Sceglieremo una taglia piccola per restare leggeri; chi ha più RAM può salire.
  • ChromaDB — database vettoriale locale, persistente, zero configurazione. Alternative valide: FAISS (più veloce ma senza persistenza integrata) o LanceDB.
  • pypdf — per estrarre il testo dai PDF.

Perché non LangChain? Si può usare, ma per capire davvero cosa succede preferiamo codice esplicito e poche dipendenze. Aggiungere LangChain o LlamaIndex è una variante che vedremo alla fine.

Il RAG locale permette di interrogare documenti riservati senza inviarli ad alcun cloud.

Passo 1 — Installare Ollama e i modelli

Scarica Ollama dal sito ufficiale (ollama.com) e installalo. Poi, da terminale, scarica i due modelli che ci servono:

ollama pull nomic-embed-text
ollama pull llama3.2

Verifica che Ollama risponda:

ollama run llama3.2 "Dimmi ciao in italiano"

Se ottieni un saluto, sei pronto. Ollama espone in automatico un server locale su http://localhost:11434.

Passo 2 — Creare l'ambiente Python

python -m venv .venv
# Windows: .venv\Scripts\activate
source .venv/bin/activate
pip install ollama chromadb pypdf

Crea una cartella documenti/ e mettici dentro i PDF da interrogare.

Passo 3 — Indicizzare i PDF

Questo script legge i PDF, li divide in frammenti con sovrapposizione (per non spezzare le frasi a metà), calcola gli embedding con Ollama e li salva in ChromaDB.

import os, glob
import chromadb
from pypdf import PdfReader
import ollama

client = chromadb.PersistentClient(path="./chroma_db")
coll = client.get_or_create_collection("documenti")

def chunk(text, size=900, overlap=150):
    out, i = [], 0
    while i < len(text):
        out.append(text[i:i+size])
        i += size - overlap
    return out

doc_id = 0
for pdf in glob.glob("documenti/*.pdf"):
    reader = PdfReader(pdf)
    for page_n, page in enumerate(reader.pages):
        testo = page.extract_text() or ""
        for frammento in chunk(testo):
            if not frammento.strip():
                continue
            emb = ollama.embeddings(model="nomic-embed-text", prompt=frammento)["embedding"]
            coll.add(
                ids=[f"d{doc_id}"],
                embeddings=[emb],
                documents=[frammento],
                metadatas=[{"fonte": os.path.basename(pdf), "pagina": page_n + 1}],
            )
            doc_id += 1

print(f"Indicizzati {doc_id} frammenti.")

Eseguilo una sola volta (o quando aggiungi documenti). Alla fine avrai una cartella chroma_db/ con l'indice persistente.

Passo 4 — Fare domande con citazione della fonte

Ora la parte interessante: recuperiamo i frammenti più simili alla domanda e li passiamo al modello, chiedendogli di rispondere solo in base a quelli.

import chromadb, ollama

client = chromadb.PersistentClient(path="./chroma_db")
coll = client.get_collection("documenti")

def chiedi(domanda, k=4):
    q_emb = ollama.embeddings(model="nomic-embed-text", prompt=domanda)["embedding"]
    res = coll.query(query_embeddings=[q_emb], n_results=k)
    contesto = ""
    for doc, meta in zip(res["documents"][0], res["metadatas"][0]):
        contesto += f"[{meta['fonte']} p.{meta['pagina']}] {doc}\n\n"

    prompt = (
        "Rispondi alla domanda usando SOLO il contesto qui sotto. "
        "Se la risposta non c'è, dillo chiaramente. Cita la fonte tra parentesi.\n\n"
        f"CONTESTO:\n{contesto}\n"
        f"DOMANDA: {domanda}"
    )
    r = ollama.chat(model="llama3.2", messages=[{"role": "user", "content": prompt}])
    return r["message"]["content"]

print(chiedi("Qual è la durata del contratto e come si rinnova?"))

Risultato atteso: una risposta in italiano che riassume la clausola richiesta e indica, tra parentesi, il file e la pagina da cui proviene (ad esempio «La durata è di 24 mesi con rinnovo tacito... (contratto.pdf p.3)»). Se l'informazione non è nei documenti, il modello deve dirlo invece di inventare.

Il modello risponde solo in base ai frammenti recuperati, citando file e pagina.

Prompt utili da riusare

«Elenca in punti tutti gli obblighi a carico del fornitore citati nei documenti, con la fonte di ciascuno.»
«C'è una clausola di recesso? Riportala testualmente e indica pagina e file. Se non esiste, scrivi: non presente.»

Errori comuni e come risolverli

  • «Connection refused» su localhost:11434 → Ollama non è in esecuzione. Avvialo (apri l'app o esegui ollama serve).
  • Il PDF restituisce testo vuoto → è un PDF scansionato (immagini). Serve l'OCR: integra pytesseract + pdf2image per estrarre il testo prima di indicizzare.
  • Risposte vaghe o inventate → aumenta k (più frammenti), riduci la dimensione dei chunk, o usa un modello più grande (ollama pull qwen2.5:7b). Rafforza l'istruzione «usa solo il contesto».
  • Lentezza → su CPU è normale; scegli modelli piccoli, oppure usa una GPU. L'indicizzazione si fa una volta sola.

Varianti e passi successivi

Per un'interfaccia grafica, avvolgi le funzioni in Streamlit (pip install streamlit) con una casella di testo e un pulsante. Per qualità superiore, sostituisci il recupero «per similarità» con un re-ranking (un secondo modello che riordina i frammenti) o aggiungi la ricerca per parole chiave (BM25) accanto a quella vettoriale: è il cosiddetto hybrid search. Se preferisci un framework, LlamaIndex e LangChain offrono gli stessi passaggi con poche righe, utili quando il progetto cresce. Infine, se i documenti sono enormi, valuta chunk più intelligenti (per paragrafo o per titolo) invece del taglio fisso a caratteri.

Quando NON usare il RAG locale: se ti serve la massima qualità di ragionamento o devi elaborare migliaia di richieste al secondo, conviene un'API cloud. Ma per privacy, costo zero e controllo totale sui propri documenti, il RAG in locale con Ollama è oggi la soluzione più sensata — e in mezz'ora è in piedi.