Immagina di poter fare domande in linguaggio naturale a una cartella di PDF — manuali, contratti, dispense, verbali — e ricevere risposte con i riferimenti giusti, senza inviare nulla su Internet e senza pagare abbonamenti. È esattamente ciò che permette una pipeline RAG (Retrieval-Augmented Generation) eseguita in locale. In questa guida la costruiamo da zero con strumenti gratuiti e open source: Ollama per il modello e gli embedding, Python per la logica, ChromaDB come database vettoriale.

A chi serve, cosa otterrai, prerequisiti

Questa guida è pensata per chi ha una minima dimestichezza con il terminale e con Python e vuole un assistente privato sui propri documenti: professionisti che lavorano con molti file, studenti, ricercatori, piccole aziende che non possono caricare dati riservati su servizi esterni. Al termine avrai uno script che indicizza i tuoi PDF e risponde alle domande citando i passaggi rilevanti.

Prerequisiti reali:

  • Sistema operativo: Windows, macOS o Linux.
  • Hardware: almeno 8 GB di RAM (16 GB consigliati). Una GPU aiuta ma non è obbligatoria: i modelli piccoli girano anche su CPU.
  • Software: Python 3.10 o superiore e Ollama installato.
  • Nessun account e nessuna chiave API: tutto gira sul tuo computer.

Quali strumenti e modelli usare (e perché)

Servono due modelli: uno per generare gli embedding (la rappresentazione numerica del testo, usata per cercare i passaggi pertinenti) e uno per generare le risposte. La scelta consigliata come prima opzione:

  • Embedding: nomic-embed-text tramite Ollama. È leggero, gratuito e di ottima qualità per l'italiano e l'inglese.
  • Generazione: llama3.1:8b oppure qwen2.5:7b. Entrambi girano su un PC comune; se hai poca memoria puoi scendere a un modello da 3-4 miliardi di parametri.

Perché Ollama e non un servizio cloud? Perché è gratuito, non ha limiti di richieste, tiene i dati sul tuo computer ed è semplicissimo da avviare. In alternativa esistono LM Studio (interfaccia grafica) e, per chi accetta il cloud, le API di OpenAI o Google per gli embedding: più potenti su documenti enormi, ma a pagamento e con invio dei dati all'esterno. Per la privacy e il costo zero, la soluzione locale resta la migliore.

Con il RAG puoi interrogare intere raccolte di documenti senza inviarli online.

Passo 1 — Installare Ollama e scaricare i modelli

Scarica Ollama dal sito ufficiale ollama.com e installalo. Poi, da terminale, scarica i due modelli:

ollama pull nomic-embed-text
ollama pull llama3.1:8b

Verifica che Ollama sia attivo: di norma resta in ascolto su http://localhost:11434. Un test rapido:

ollama run llama3.1:8b "Rispondi in italiano: dimmi ciao in una riga."

Se ottieni una risposta, sei pronto.

Passo 2 — Preparare l'ambiente Python

Crea una cartella di progetto, un ambiente virtuale e installa le librerie:

mkdir rag-pdf && cd rag-pdf
python -m venv .venv
# su macOS/Linux:
source .venv/bin/activate
# su Windows:
.venv\Scripts\activate

pip install chromadb pypdf ollama

Crea nella cartella una sottocartella documenti e mettici dentro i PDF che vuoi interrogare.

Passo 3 — Indicizzare i PDF

Il primo script legge i PDF, li spezza in blocchi (chunk) di dimensione gestibile, ne calcola gli embedding e li salva in ChromaDB. Salvalo come indicizza.py:

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

client = chromadb.PersistentClient(path="./db")
collection = client.get_or_create_collection("documenti")

def leggi_pdf(percorso):
    testo = ""
    for pagina in PdfReader(percorso).pages:
        testo += (pagina.extract_text() or "") + "\n"
    return testo

def spezza(testo, dim=800, overlap=100):
    parole = testo.split()
    blocchi, i = [], 0
    while i < len(parole):
        blocchi.append(" ".join(parole[i:i+dim]))
        i += dim - overlap
    return blocchi

n = 0
for pdf in glob.glob("documenti/*.pdf"):
    for j, blocco in enumerate(spezza(leggi_pdf(pdf))):
        if not blocco.strip():
            continue
        emb = ollama.embeddings(model="nomic-embed-text", prompt=blocco)["embedding"]
        collection.add(
            ids=[f"{os.path.basename(pdf)}-{j}"],
            embeddings=[emb],
            documents=[blocco],
            metadatas=[{"fonte": os.path.basename(pdf)}]
        )
        n += 1
print(f"Indicizzati {n} blocchi.")

Eseguilo con python indicizza.py. Al termine avrai una cartella db con l'indice. Va rifatto solo quando aggiungi o modifichi documenti.

Passo 4 — Fare domande ai documenti

Il secondo script prende la domanda, recupera i blocchi più pertinenti e li passa al modello con un'istruzione precisa. Salvalo come chiedi.py:

import sys, chromadb, ollama

client = chromadb.PersistentClient(path="./db")
collection = client.get_collection("documenti")

domanda = " ".join(sys.argv[1:]) or "Di cosa parlano i documenti?"
emb = ollama.embeddings(model="nomic-embed-text", prompt=domanda)["embedding"]
res = collection.query(query_embeddings=[emb], n_results=4)

contesto = "\n\n".join(res["documents"][0])
fonti = ", ".join({m["fonte"] for m in res["metadatas"][0]})

prompt = f"""Rispondi alla domanda usando SOLO il contesto qui sotto.
Se la risposta non c'e', dillo chiaramente. Rispondi in italiano.

CONTESTO:
{contesto}

DOMANDA: {domanda}"""

out = ollama.chat(model="llama3.1:8b",
                  messages=[{"role": "user", "content": prompt}])
print(out["message"]["content"])
print(f"\nFonti consultate: {fonti}")

Ora puoi interrogare i tuoi PDF dal terminale:

python chiedi.py "Quali sono le scadenze previste dal contratto?"

Il risultato atteso è una risposta sintetica basata sui documenti, seguita dall'elenco dei file da cui è stata tratta. Se la domanda non trova riscontro nei testi, il modello lo dichiara invece di inventare: è l'istruzione «usa SOLO il contesto» a ridurre le allucinazioni.

Errori comuni e come risolverli

  • «Connection refused» su localhost:11434 → Ollama non è in esecuzione. Avvialo (l'app o il comando ollama serve).
  • Il PDF restituisce testo vuoto → è un documento scansionato (immagini). Serve prima un passaggio di OCR, ad esempio con lo strumento ocrmypdf.
  • Risposte scadenti o fuori tema → aumenta n_results a 6-8, oppure riduci la dimensione dei blocchi (da 800 a 500 parole) per un recupero più preciso.
  • Memoria insufficiente → usa un modello di generazione più piccolo (ad esempio llama3.2:3b).

Varianti e passi successivi

Per un'esperienza più comoda puoi avvolgere lo script in una piccola interfaccia web con Streamlit (poche righe in più), oppure sostituire il codice «a mano» con framework come LangChain o LlamaIndex, che offrono già pronti caricatori di documenti, strategie di suddivisione avanzate e memoria della conversazione. Se i documenti diventano migliaia, valuta strategie di chunking semantico e un re-ranking dei risultati per migliorare la precisione.

Quando NON usare questo approccio: se i tuoi file sono pochissimi e brevi, spesso conviene semplicemente incollarne il contenuto nel prompt di un modello a contesto lungo. Il RAG dà il meglio quando i documenti sono tanti, lunghi e cambiano nel tempo. Da qui puoi crescere: aggiungere il supporto ad altri formati (Word, testo, pagine web), gestire più collezioni tematiche e collegare il tutto a un'interfaccia grafica. Hai gettato le basi di un assistente documentale privato, gratuito e completamente sotto il tuo controllo.

Il codice è stato scritto per essere didattico e funzionante; verifica i nomi dei modelli disponibili con «ollama list» e consulta la documentazione ufficiale di Ollama e Chroma per le opzioni avanzate.