Hai centinaia di PDF - manuali, contratti, dispense, normative - e vorresti "parlarci": fare domande in linguaggio naturale e ottenere risposte basate solo su quei documenti, con le citazioni giuste. La tecnica che lo rende possibile si chiama RAG (Retrieval-Augmented Generation): invece di sperare che il modello "sappia" la risposta, gli si forniscono i pezzi di testo pertinenti recuperati dai tuoi file. In questa guida costruiamo un RAG funzionante in Python, scegliendo per ogni pezzo lo strumento giusto e spiegando il perche'.
A chi serve e cosa otterrai
Questa guida e' pensata per chi sa muovere i primi passi con Python (sa installare pacchetti e lanciare uno script) e vuole un sistema che giri sul proprio computer, senza per forza mandare i documenti a un servizio esterno. Al termine avrai uno script che: legge una cartella di PDF, li spezza in frammenti, li trasforma in vettori numerici (embedding), li salva in un database vettoriale locale e risponde alle tue domande citando i passaggi usati. Prerequisiti reali: Python 3.10 o superiore, circa 8-16 GB di RAM, sistema Windows, macOS o Linux. La GPU aiuta ma non e' obbligatoria.
Quali strumenti usare (e quale consiglio come prima scelta)
Le scelte chiave sono tre: come generare gli embedding, dove salvarli e quale LLM usa per rispondere.
- Embedding:
sentence-transformerscon un modello comeall-MiniLM-L6-v2(leggero, gira su CPU) oppurenomic-embed-textvia Ollama. PRO: gratuiti e locali. CONTRO: qualita' inferiore agli embedding a pagamento di OpenAI o Google su testi molto tecnici. - Database vettoriale: Chroma, prima scelta consigliata perche' si installa con un
pip, salva su disco e non richiede server. Alternative: FAISS (velocissimo ma piu' grezzo) o Qdrant (ottimo per la produzione). - LLM che risponde: Ollama con un modello locale (Llama, Qwen, o un modello "Air" leggero) se vuoi tutto offline e gratis; in alternativa un'API (GPT-5, Claude, Gemini, DeepSeek) se vuoi la massima qualita' e non ti pesa mandare il contesto in cloud.
Prima scelta per iniziare: sentence-transformers + Chroma + Ollama. E' tutto gratuito, locale e sufficiente per imparare; passerai alle API solo se la qualita' delle risposte non basta.
Passo 1 - Installa l'ambiente
Crea una cartella di progetto, un ambiente virtuale e installa i pacchetti:
python -m venv .venv
# Windows: .venv\Scripts\activate
# macOS/Linux: source .venv/bin/activate
pip install chromadb sentence-transformers pypdf ollama
Se userai Ollama come LLM, installalo dal sito ufficiale e scarica un modello, per esempio:
ollama pull llama3.1:8b
Passo 2 - Estrai e spezza il testo
Il "chunking" e' decisivo: frammenti troppo grandi confondono il modello, troppo piccoli perdono il contesto. Un buon punto di partenza sono blocchi di circa 800-1000 caratteri con una sovrapposizione (overlap) di 150, per non tagliare le frasi a meta'. Metti i tuoi PDF in una cartella documenti/ e usa questo codice:
from pathlib import Path
from pypdf import PdfReader
def estrai_chunk(cartella, dim=900, overlap=150):
chunks = []
for pdf in Path(cartella).glob("*.pdf"):
reader = PdfReader(str(pdf))
testo = "\n".join((p.extract_text() or "") for p in reader.pages)
i = 0
while i < len(testo):
pezzo = testo[i:i+dim]
if pezzo.strip():
chunks.append({"testo": pezzo, "fonte": pdf.name})
i += dim - overlap
return chunks
chunks = estrai_chunk("documenti")
print(f"Estratti {len(chunks)} frammenti")
Passo 3 - Genera gli embedding e salvali in Chroma
Ora trasformiamo ogni frammento in un vettore e lo salviamo in un database persistente su disco:
import chromadb
from sentence_transformers import SentenceTransformer
modello = SentenceTransformer("all-MiniLM-L6-v2")
client = chromadb.PersistentClient(path="./vectordb")
coll = client.get_or_create_collection("pdf")
testi = [c["testo"] for c in chunks]
vettori = modello.encode(testi, show_progress_bar=True).tolist()
coll.add(
ids=[f"id-{i}" for i in range(len(chunks))],
documents=testi,
embeddings=vettori,
metadatas=[{"fonte": c["fonte"]} for c in chunks],
)
print("Indicizzazione completata")
Questo passaggio va eseguito una sola volta (o quando aggiungi documenti): il database resta salvato nella cartella vectordb.
Passo 4 - Interroga: retrieval + risposta del modello
Adesso il cuore del RAG: data una domanda, recuperiamo i frammenti piu' simili e li passiamo all'LLM con un prompt che gli impone di rispondere solo in base al contesto.
import ollama
def chiedi(domanda, k=4):
q = modello.encode([domanda]).tolist()
res = coll.query(query_embeddings=q, n_results=k)
contesto = "\n\n".join(res["documents"][0])
fonti = {m["fonte"] for m in res["metadatas"][0]}
prompt = f"""Rispondi alla domanda usando SOLO il contesto qui sotto.
Se la risposta non e' nel contesto, scrivi: "Non e' nei documenti".
CONTESTO:
{contesto}
DOMANDA: {domanda}"""
out = ollama.chat(model="llama3.1:8b",
messages=[{"role": "user", "content": prompt}])
return out["message"]["content"], fonti
risposta, fonti = chiedi("Qual e' la durata del periodo di recesso?")
print(risposta)
print("Fonti:", ", ".join(fonti))
Risultato atteso: una risposta sintetica tratta dai tuoi PDF, seguita dall'elenco dei file da cui proviene. Se l'informazione non c'e', il modello dichiara di non averla trovata invece di inventarla - esattamente cio' che vogliamo.
Prompt utili da copiare
"Rispondi in italiano, in massimo 5 frasi, citando tra parentesi il nome del file da cui proviene ogni affermazione."
"Elenca in forma di tabella tutte le scadenze e gli importi presenti nel contesto. Se un dato manca, scrivi 'non indicato'."
Errori comuni e come risolverli
- "Non e' nei documenti" anche quando l'informazione c'e': i chunk sono troppo piccoli o pochi. Aumenta
k(per esempio a 6-8) o la dimensione dei frammenti. - Risposte generiche o inventate: il modello sta ignorando il contesto. Rafforza il prompt ("usa SOLO il contesto") e abbassa la temperatura, oppure passa a un modello piu' capace via API.
- PDF scansionati che restituiscono testo vuoto: sono immagini, non testo. Serve un passaggio di OCR (per esempio con strumenti come Tesseract o un servizio di OCR) prima del chunking.
- Risposte lente: un modello da 8B su CPU e' lento. Usa una GPU, scegli un modello piu' piccolo o sposta la generazione su un'API.
Varianti e come proseguire
Una volta che la base funziona, i miglioramenti piu' efficaci sono due. Il primo e' il reranking: dopo aver recuperato 20 frammenti, un modello "cross-encoder" li riordina per pertinenza e ne tieni solo i migliori 4, alzando la precisione. Il secondo e' la ricerca ibrida, che combina la similarita' vettoriale con la ricerca per parole chiave (BM25), utile quando contano termini esatti come codici e numeri d'articolo. Se preferisci non scrivere tutta l'impalcatura a mano, framework come LangChain e LlamaIndex offrono gli stessi mattoni gia' pronti. E quando vorrai portare il sistema in produzione - piu' utenti, piu' documenti - valuta un database vettoriale come Qdrant e un LLM via API per garantire qualita' e stabilita'. Ma il principio resta quello che hai appena costruito: recuperare i pezzi giusti e lasciare che il modello ragioni solo su quelli.




