Immagina di poter fare domande in linguaggio naturale ai tuoi documenti — manuali, contratti, appunti, paper — e ricevere risposte precise, con tanto di riferimento al punto esatto in cui l'informazione si trova. E' esattamente cio' che permette il RAG (Retrieval-Augmented Generation), la tecnica che combina un motore di ricerca semantico con un modello linguistico. In questa guida costruiremo, passo dopo passo, un sistema RAG in Python che risponde a domande sui tuoi PDF, funzionante anche completamente in locale e gratis.
A chi serve e cosa otterrai
Questa guida e' pensata per chi ha gia' un minimo di dimestichezza con Python (sai installare pacchetti ed eseguire uno script) e vuole andare oltre il semplice "incollare un PDF in ChatGPT". 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 rilevanti. Nessun dato lascera' il tuo computer, se scegli la via locale.
Prerequisiti reali: un computer con Windows, macOS o Linux; Python 3.10 o superiore; circa 8 GB di RAM (16 consigliati se usi un modello locale); una connessione a internet solo per il primo download dei modelli. Per la variante locale useremo Ollama; per la variante cloud basta una chiave API di OpenAI.
Quali strumenti useremo e perche'
Le scelte tecnologiche contano. Ecco cosa impiegheremo e le alternative:
- pypdf per estrarre il testo dai PDF. Semplice e affidabile per documenti testuali; per PDF scansionati (immagini) servirebbe invece un OCR come Tesseract.
- ChromaDB come database vettoriale. E' leggero, gira in locale senza server e si installa con un comando. Alternative: FAISS (piu' veloce ma piu' spartano), o soluzioni gestite come Pinecone e Qdrant per grandi volumi.
- Ollama con il modello
nomic-embed-textper gli embedding e un modello comellama3.1oqwen2.5per generare le risposte. Vantaggio: gratis, privato, offline. Svantaggio: qualita' e velocita' dipendono dal tuo hardware. - In alternativa, le API di OpenAI (
text-embedding-3-smallper gli embedding egpt-4o-miniper le risposte): qualita' elevata e nessun requisito hardware, ma i dati transitano sul cloud e c'e' un costo per token, per quanto ridotto.
La nostra prima scelta per imparare e' la via locale con Ollama: azzera i costi, protegge i dati riservati ed e' perfetta per capire ogni pezzo del meccanismo. Passeremo a OpenAI solo se serve piu' qualita' su documenti complessi.
Passo 1 — Preparare l'ambiente
Crea una cartella per il progetto e un ambiente virtuale, poi installa le dipendenze:
mkdir rag-pdf && cd rag-pdf
python -m venv venv
# su macOS/Linux:
source venv/bin/activate
# su Windows:
venv\Scripts\activate
pip install pypdf chromadb ollama openai
Per la via locale, installa Ollama dal sito ufficiale ollama.com, poi scarica i due modelli che ci servono:
ollama pull nomic-embed-text
ollama pull llama3.1
Verifica che Ollama sia attivo con ollama list: dovresti vedere entrambi i modelli nell'elenco. Metti infine i tuoi file PDF in una sottocartella chiamata documenti.
Passo 2 — Leggere e spezzare i PDF (chunking)
Un modello non puo' "ingoiare" un intero manuale in una volta: dobbiamo dividere il testo in frammenti (chunk) di dimensione gestibile, con una piccola sovrapposizione per non perdere il filo tra un pezzo e l'altro. Crea un file rag.py:
import os
from pypdf import PdfReader
def carica_pdf(cartella="documenti"):
testi = []
for nome in os.listdir(cartella):
if not nome.lower().endswith(".pdf"):
continue
reader = PdfReader(os.path.join(cartella, nome))
for i, pagina in enumerate(reader.pages):
testo = pagina.extract_text() or ""
if testo.strip():
testi.append({"fonte": f"{nome} - pag.{i+1}", "testo": testo})
return testi
def spezza(testo, dim=800, overlap=150):
parti, inizio = [], 0
while inizio < len(testo):
fine = inizio + dim
parti.append(testo[inizio:fine])
inizio = fine - overlap
return parti
Qui ogni pagina viene divisa in blocchi da circa 800 caratteri con 150 di sovrapposizione. Sono valori di partenza ragionevoli: frammenti troppo grandi diluiscono la ricerca, troppo piccoli perdono il contesto.
Passo 3 — Creare gli embedding e l'indice
Ora trasformiamo ogni frammento in un vettore numerico e lo salviamo in ChromaDB. Aggiungi a rag.py:
import chromadb
import ollama
client = chromadb.PersistentClient(path="./indice")
collezione = client.get_or_create_collection("pdf")
def embedding(testo):
r = ollama.embeddings(model="nomic-embed-text", prompt=testo)
return r["embedding"]
def indicizza():
documenti = carica_pdf()
idx = 0
for doc in documenti:
for pezzo in spezza(doc["testo"]):
collezione.add(
ids=[str(idx)],
embeddings=[embedding(pezzo)],
documents=[pezzo],
metadatas=[{"fonte": doc["fonte"]}],
)
idx += 1
print(f"Indicizzati {idx} frammenti.")
Esegui l'indicizzazione una sola volta (o quando aggiungi documenti). Grazie a PersistentClient, l'indice resta salvato su disco nella cartella indice e non va ricostruito a ogni avvio.
Passo 4 — Recuperare e generare la risposta
Il cuore del RAG: data una domanda, cerchiamo i frammenti piu' simili e li passiamo al modello come contesto, chiedendogli di rispondere solo in base a quelli. Completa lo script:
def chiedi(domanda, k=4):
q_emb = embedding(domanda)
res = collezione.query(query_embeddings=[q_emb], n_results=k)
frammenti = res["documents"][0]
fonti = [m["fonte"] for m in res["metadatas"][0]]
contesto = "\n\n".join(frammenti)
prompt = f"""Rispondi alla domanda usando SOLO il contesto qui sotto.
Se la risposta non e' nel contesto, di' che non e' presente nei documenti.
CONTESTO:
{contesto}
DOMANDA: {domanda}"""
r = ollama.chat(model="llama3.1",
messages=[{"role": "user", "content": prompt}])
print(r["message"]["content"])
print("\nFonti:", ", ".join(set(fonti)))
if __name__ == "__main__":
# indicizza() # scommenta al primo avvio
chiedi("Qual e' la procedura descritta nel documento?")
Al primo avvio scommenta indicizza(), lancia python rag.py e attendi la costruzione dell'indice; dalle esecuzioni successive puoi ricommentarla. Il parametro k=4 indica quanti frammenti recuperare: alzalo per domande ampie, abbassalo per risposte piu' mirate.
Passo 5 — Provare con una domanda vera
Supponiamo di aver caricato il manuale di un elettrodomestico. Una richiesta tipo:
"Ogni quanto va pulito il filtro e come si smonta?"
Il sistema recuperera' i frammenti pertinenti dal manuale e il modello formulera' una risposta discorsiva, chiudendo con l'indicazione della fonte (es. "manuale.pdf - pag.12"). Se l'informazione non c'e', dira' esplicitamente che non e' presente nei documenti: e' il comportamento che vogliamo, per evitare che il modello inventi.
Passare a OpenAI per piu' qualita'
Se i tuoi documenti sono complessi e vuoi risposte piu' rifinite, puoi sostituire Ollama con le API di OpenAI. Cambiano solo due funzioni:
from openai import OpenAI
oai = OpenAI(api_key="LA_TUA_CHIAVE")
def embedding(testo):
r = oai.embeddings.create(model="text-embedding-3-small", input=testo)
return r.data[0].embedding
# e nella funzione chiedi(), al posto di ollama.chat:
# r = oai.chat.completions.create(model="gpt-4o-mini",
# messages=[{"role":"user","content":prompt}])
# print(r.choices[0].message.content)
Attenzione a non mescolare gli embedding: se reindicizzi con OpenAI, devi rigenerare tutto l'indice, perche' i vettori dei due modelli non sono confrontabili tra loro.
Errori comuni e come risolverli
- "Connection refused" da Ollama: il servizio non e' avviato. Apri l'app Ollama o lancia
ollama servein un terminale separato. - Il PDF restituisce testo vuoto: probabilmente e' una scansione (immagini). Serve un OCR: converti prima le pagine con Tesseract, oppure usa una libreria come
pdfplumber. - Risposte generiche o fuori tema: aumenta la sovrapposizione tra i frammenti, riduci la loro dimensione o alza
k. Spesso il problema e' nel chunking, non nel modello. - Indicizzazione lentissima: gli embedding locali sono pesanti su CPU. Riduci il numero di documenti di prova, oppure passa agli embedding di OpenAI, molto piu' rapidi.
Varianti e come proseguire
Da questa base puoi crescere in molte direzioni: aggiungere un'interfaccia web con Streamlit per interrogare i documenti dal browser; introdurre il re-ranking dei risultati per migliorare la precisione; gestire la cronologia della conversazione per domande di follow-up; o usare framework piu' completi come LangChain e LlamaIndex, che astraggono molti di questi passaggi. Ma aver costruito il meccanismo "a mano", come abbiamo fatto qui, ti da' un vantaggio enorme: quando qualcosa non funziona, sai esattamente dove guardare. Il RAG non e' magia, e' recupero piu' generazione — e ora sai come metterli insieme.




