Hai una cartella piena di PDF — manuali, contratti, dispense, ricerche — e vorresti poterli "interrogare" come fai con ChatGPT, ma senza caricare nulla in cloud e senza pagare un abbonamento? Questa guida ti spiega come costruire un sistema RAG (Retrieval-Augmented Generation) che gira interamente sul tuo computer: l'intelligenza artificiale risponde alle tue domande citando i tuoi documenti, e i dati non escono mai dalla tua macchina. Useremo Ollama per i modelli, Python per la logica e ChromaDB come database vettoriale. Tutto gratis e open source.

A chi serve e cosa otterrai

Questo tutorial è pensato per chi ha un minimo di dimestichezza con il terminale e con Python di base, ma non serve essere esperti di machine learning. È perfetto per professionisti che gestiscono documentazione riservata (avvocati, commercialisti, studi tecnici, sanità), studenti e ricercatori che lavorano su molti paper, e sviluppatori che vogliono un punto di partenza per un assistente documentale aziendale. Alla fine avrai uno script funzionante a cui chiedere, in italiano, cose come "Qual è la durata del preavviso nel contratto?" e ottenere una risposta basata sul testo reale dei tuoi file, con l'indicazione della fonte.

Prerequisiti reali: un computer con Windows, macOS o Linux; almeno 8 GB di RAM (16 GB consigliati per modelli più capaci); circa 5-10 GB di spazio libero per i modelli; Python 3.10 o successivo installato. Una GPU non è obbligatoria, ma se ce l'hai velocizza molto le risposte.

Cos'è un RAG, spiegato semplice

Un modello linguistico da solo non conosce i tuoi documenti e, se interrogato, tende a "inventare". Il RAG risolve il problema in tre mosse: (1) spezza i tuoi PDF in piccoli pezzi di testo; (2) trasforma ogni pezzo in numeri (i cosiddetti embedding) e li archivia in un database vettoriale; (3) quando fai una domanda, cerca i pezzi più simili alla domanda e li passa al modello come "contesto", chiedendogli di rispondere usando solo quelle informazioni. Il risultato è una risposta ancorata ai tuoi dati, molto meno soggetta a errori inventati.

Un RAG locale ti permette di interrogare i tuoi PDF senza caricarli in cloud.

Quali strumenti usare e perché Ollama è la prima scelta

Per far girare un modello in locale ci sono diverse opzioni. Ecco le principali, con pro e contro per questo compito:

  • Ollama (consigliato): gratis, leggerissimo da installare, espone un'API locale e gestisce sia i modelli di chat sia quelli di embedding. È la scelta più semplice per uno script Python. Contro: meno "visuale" di altri.
  • LM Studio: interfaccia grafica comoda, ottimo per provare i modelli, ma per uno script da terminale è meno immediato di Ollama.
  • AnythingLLM: applicazione no-code che fa già RAG con interfaccia pronta; perfetta se non vuoi scrivere codice, ma meno personalizzabile.
  • API cloud (OpenAI, Google, Mistral): qualità di embedding superiore, ma i dati escono dalla tua macchina e c'è un costo. Da evitare se la privacy è il motivo per cui sei qui.

Useremo Ollama con due modelli: llama3.1:8b (o un equivalente) per generare le risposte e nomic-embed-text per gli embedding. Sono entrambi gratuiti e girano bene anche senza GPU di fascia alta.

Preparare l'ambiente: Ollama, modelli e librerie Python

Per prima cosa installa Ollama dal sito ufficiale (ollama.com); su macOS e Windows è un normale installer, su Linux basta un comando. Poi scarica i due modelli dal terminale:

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

Verifica che Ollama sia attivo (di solito parte da solo e resta in ascolto su http://localhost:11434). Crea ora una cartella per il progetto e installa le librerie Python necessarie, preferibilmente in un ambiente virtuale:

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

Infine crea una sottocartella documenti/ e mettici dentro i PDF che vuoi interrogare.

Lo script passo passo: dal PDF alle risposte

Lo script fa due cose: prima indicizza i PDF (li legge, li spezza, li trasforma in embedding e li salva in ChromaDB), poi entra in modalità domanda-risposta. Ecco il codice completo, commentato; salvalo come rag.py.

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

CARTELLA = "documenti"
MODELLO_CHAT = "llama3.1:8b"
MODELLO_EMBED = "nomic-embed-text"

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

def spezza(testo, dim=800, overlap=150):
    # divide il testo in pezzi con un po' di sovrapposizione
    pezzi, i = [], 0
    while i < len(testo):
        pezzi.append(testo[i:i + dim])
        i += dim - overlap
    return pezzi

def indicizza():
    n = 0
    for pdf in glob.glob(os.path.join(CARTELLA, "*.pdf")):
        reader = PdfReader(pdf)
        for npag, pagina in enumerate(reader.pages):
            testo = (pagina.extract_text() or "").strip()
            if not testo:
                continue
            for j, pezzo in enumerate(spezza(testo)):
                emb = ollama.embeddings(model=MODELLO_EMBED, prompt=pezzo)["embedding"]
                collection.add(
                    ids=[f"{os.path.basename(pdf)}-{npag}-{j}"],
                    embeddings=[emb],
                    documents=[pezzo],
                    metadatas=[{"fonte": os.path.basename(pdf), "pagina": npag + 1}],
                )
                n += 1
    print(f"Indicizzati {n} frammenti.")

def chiedi(domanda, k=4):
    emb = ollama.embeddings(model=MODELLO_EMBED, prompt=domanda)["embedding"]
    res = collection.query(query_embeddings=[emb], n_results=k)
    contesto = "\n\n".join(res["documents"][0])
    fonti = ", ".join(f'{m["fonte"]} p.{m["pagina"]}' for m in res["metadatas"][0])
    prompt = (
        "Rispondi alla domanda usando SOLO il contesto qui sotto. "
        "Se la risposta non c'e', scrivi che non e' presente nei documenti.\n\n"
        f"CONTESTO:\n{contesto}\n\nDOMANDA: {domanda}"
    )
    out = ollama.chat(model=MODELLO_CHAT, messages=[{"role": "user", "content": prompt}])
    print("\nRISPOSTA:", out["message"]["content"])
    print("FONTI:", fonti)

if __name__ == "__main__":
    if len(sys.argv) > 1 and sys.argv[1] == "index":
        indicizza()
    else:
        while True:
            d = input("\nDomanda (invio vuoto per uscire): ").strip()
            if not d:
                break
            chiedi(d)

La prima volta lancia l'indicizzazione (da ripetere solo quando aggiungi nuovi PDF):

python3 rag.py index

Poi avvia la modalità interattiva e inizia a fare domande:

python3 rag.py
Lo script indicizza i PDF e poi risponde alle domande citando la fonte.

Provarlo: tre domande di esempio e cosa restituisce

Supponiamo di aver indicizzato un contratto di lavoro e un manuale tecnico. Ecco tre prompt utili e il tipo di risposta atteso:

Qual è il periodo di preavviso per le dimissioni indicato nel contratto?

Risposta attesa: la durata esatta riportata nel testo (es. "30 giorni"), con l'indicazione del file e della pagina da cui è tratta.

Riassumi in cinque punti la procedura di manutenzione descritta nel manuale.

Risposta attesa: un elenco sintetico costruito sui frammenti recuperati, senza aggiungere passaggi non presenti.

Nei documenti si parla di garanzia sui ricambi? Se sì, per quanto tempo?

Risposta attesa: se l'informazione c'è, viene riportata; se non c'è, il modello dichiara che non è presente nei documenti — esattamente il comportamento che vogliamo.

Migliorare i risultati: chunk, modelli, prompt

Se le risposte non ti soddisfano, agisci su tre leve. Dimensione dei frammenti: pezzi troppo piccoli perdono il contesto, troppo grandi confondono il modello; 600-1000 caratteri con un po' di sovrapposizione è un buon punto di partenza. Numero di frammenti recuperati (il parametro k): aumentalo a 6-8 per domande complesse, ma attenzione a non saturare il contesto. Modello di chat: se hai abbastanza RAM, prova un modello più grande o più recente per risposte migliori; se la macchina è lenta, scegline uno più piccolo. Infine, cura il prompt: l'istruzione di usare "solo il contesto" e di ammettere quando non sa è ciò che riduce di più gli errori inventati.

Errori comuni e come risolverli

  • "Connection refused" su localhost:11434 → Ollama non è in esecuzione. Avvialo (apri l'app o lancia ollama serve).
  • Il PDF non produce testo → probabilmente è una scansione (immagine). Serve un passaggio OCR, ad esempio con ocrmypdf, prima di indicizzarlo.
  • Risposte lente → normale senza GPU; usa un modello più piccolo o riduci k. La fase di indicizzazione è la più pesante, ma si fa una volta sola.
  • "model not found" → hai dimenticato di scaricare il modello con ollama pull, oppure hai scritto male il nome.
  • Risposte generiche o fuori tema → controlla che l'indicizzazione sia andata a buon fine (il numero di frammenti dev'essere > 0) e aumenta la sovrapposizione tra i chunk.

Alternative e quando non usare un RAG locale

Se non vuoi scrivere codice, AnythingLLM o GPT4All offrono lo stesso risultato con un'interfaccia grafica e si appoggiano comunque a modelli locali. Se invece i tuoi documenti sono pochi e non riservati, caricarli direttamente in NotebookLM di Google o in ChatGPT è più rapido e dà ottimi risultati, al prezzo di mandare i dati in cloud. Il RAG locale conviene quando contano privacy, costo zero e controllo. Non è invece la scelta migliore per archivi enormi (centinaia di migliaia di documenti), dove servono database vettoriali gestiti e infrastruttura dedicata, né quando ti basta una risposta occasionale su un singolo file. Da qui puoi crescere: aggiungere un'interfaccia web con Streamlit, gestire più collezioni tematiche o sostituire il modello con uno aperto più recente come quelli della famiglia Qwen o Mistral. La base, però, è questa — ed è già pienamente tua.