Immagina di avere una cartella piena di PDF — manuali, contratti, dispense, verbali — e di poter fare domande in linguaggio naturale ottenendo risposte basate solo su quei documenti, con tanto di citazione della fonte. Questa tecnica si chiama RAG (Retrieval-Augmented Generation) ed e' oggi il modo piu' affidabile per usare un modello linguistico sui propri dati senza che si inventi le risposte. In questa guida costruiamo, passo per passo, un piccolo assistente che risponde sui tuoi PDF, funzionante in locale, gratuito e rispettoso della privacy perche' i documenti non lasciano il tuo computer.
A chi serve, cosa otterrai e prerequisiti
Questa guida e' pensata per chi ha una minima dimestichezza con il terminale e con Python e vuole un sistema pratico: professionisti che lavorano su molti documenti, studenti, ricercatori, chiunque debba interrogare grandi quantita' di testo. Al termine avrai uno script che indicizza una cartella di PDF e risponde alle tue domande citando i passaggi rilevanti. Ti servono: un computer con almeno 8 GB di RAM (16 GB consigliati), Python 3.10 o superiore installato, circa 5 GB di spazio libero per i modelli e una connessione a internet solo per il download iniziale.
Quali strumenti usare e perche'
Un sistema RAG ha bisogno di tre componenti: un modello che genera le risposte (LLM), un modello che trasforma il testo in vettori numerici (embedding) e un database vettoriale che li conserva e li cerca. Ecco le scelte e le alternative:
- Ollama (prima scelta consigliata): esegue LLM ed embedding in locale, e' gratuito e mantiene i dati sul tuo computer. Pro: privacy totale, nessun costo per token. Contro: le risposte sono meno brillanti di un modello di frontiera e servono buone risorse hardware.
- API di OpenAI, Anthropic o Google (alternativa): qualita' superiore e nessun peso sull'hardware, ma i documenti vengono inviati a un server esterno e si paga a consumo. Adatta se i dati non sono riservati e serve la massima qualita'.
- Chroma come database vettoriale: leggero, si installa con pip e salva l'indice su disco. In alternativa FAISS (di Meta) e' velocissimo ma un po' piu' spartano.
Qui usiamo Ollama con il modello llama3.2 per generare e nomic-embed-text per gli embedding, orchestrati dalla libreria LangChain. Se preferisci la massima qualita' e non hai vincoli di privacy, piu' avanti trovi la variante con le API.
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 llama3.2
ollama pull nomic-embed-text
Il primo comando scarica un LLM compatto (circa 2 GB), il secondo il modello di embedding (circa 270 MB). Verifica che Ollama sia attivo con ollama list: dovresti vedere entrambi i modelli nell'elenco.
Passo 2: creare l'ambiente Python e installare le librerie
Crea una cartella di progetto, un ambiente virtuale e installa i pacchetti:
python3 -m venv venv
source venv/bin/activate # su Windows: venv\Scripts\activate
pip install langchain langchain-community langchain-ollama langchain-chroma langchain-text-splitters pypdf chromadb
Crea poi una sottocartella chiamata documenti e metti li' dentro i PDF che vuoi interrogare.
Passo 3: indicizzare i PDF
Il primo script legge i PDF, li spezza in frammenti (chunk) di dimensione gestibile, li trasforma in vettori e li salva in Chroma. Salvalo come indicizza.py:
from langchain_community.document_loaders import PyPDFDirectoryLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_ollama import OllamaEmbeddings
from langchain_chroma import Chroma
# 1. Carica tutti i PDF dalla cartella
loader = PyPDFDirectoryLoader("documenti")
pagine = loader.load()
print("Pagine caricate:", len(pagine))
# 2. Spezza il testo in frammenti sovrapposti
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=150,
)
frammenti = splitter.split_documents(pagine)
print("Frammenti creati:", len(frammenti))
# 3. Crea gli embedding e salvali in Chroma (cartella indice/)
embeddings = OllamaEmbeddings(model="nomic-embed-text")
db = Chroma.from_documents(
frammenti,
embeddings,
persist_directory="indice",
)
print("Indice creato in ./indice")
Esegui con python indicizza.py. Il risultato atteso e' un messaggio con il numero di pagine e frammenti e la creazione della cartella indice. La sovrapposizione (overlap) di 150 caratteri serve a non spezzare le frasi a meta' tra un frammento e l'altro.
Passo 4: fare domande ai documenti
Il secondo script carica l'indice, recupera i frammenti piu' pertinenti alla domanda e li passa all'LLM insieme a istruzioni precise. Salvalo come chiedi.py:
from langchain_ollama import OllamaEmbeddings, ChatOllama
from langchain_chroma import Chroma
embeddings = OllamaEmbeddings(model="nomic-embed-text")
db = Chroma(persist_directory="indice", embedding_function=embeddings)
retriever = db.as_retriever(search_kwargs={"k": 4})
llm = ChatOllama(model="llama3.2", temperature=0)
def rispondi(domanda):
frammenti = retriever.invoke(domanda)
contesto = "\n\n".join(f.page_content for f in frammenti)
prompt = (
"Rispondi alla domanda usando SOLO il contesto qui sotto. "
"Se la risposta non e' nel contesto, di' che non lo sai.\n\n"
"CONTESTO:\n" + contesto + "\n\n"
"DOMANDA: " + domanda
)
risposta = llm.invoke(prompt)
print(risposta.content)
print("\nFonti:")
for f in frammenti:
print("-", f.metadata.get("source"), "pag.", f.metadata.get("page"))
if __name__ == "__main__":
rispondi("Qual e' la durata prevista dal contratto e le penali di recesso?")
Esegui con python chiedi.py. Il risultato atteso e' una risposta basata sui tuoi documenti seguita dall'elenco delle fonti (nome file e pagina). L'istruzione "usa SOLO il contesto" e l'invito a dichiarare quando non sa sono la parte piu' importante: e' cio' che riduce drasticamente le risposte inventate.
Variante: usare le API per la massima qualita'
Se i documenti non sono riservati e vuoi risposte piu' brillanti, puoi sostituire i modelli locali con quelli via API. Per esempio, con OpenAI installa pip install langchain-openai, imposta la tua chiave nella variabile d'ambiente OPENAI_API_KEY e cambia due righe:
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
llm = ChatOpenAI(model="gpt-5.6", temperature=0)
Il resto dello script resta identico. Ricorda che in questo caso il testo dei frammenti viene inviato ai server del fornitore.
Errori comuni e come risolverli
- "Connection refused" o errori di rete da Ollama: Ollama non e' in esecuzione. Avvialo (l'app o il comando
ollama serve) e riprova. - "model not found": non hai scaricato il modello. Esegui di nuovo
ollama pull llama3.2eollama pull nomic-embed-text. - Risposte lente: normale con modelli in locale su CPU. Usa un modello piu' piccolo, riduci il parametro
kdei frammenti recuperati oppure passa a una macchina con GPU. - Risposte imprecise o "inventate": aumenta l'overlap, riduci
chunk_sizea 700-800 caratteri per frammenti piu' mirati, oppure alzaka 6 per dare piu' contesto. Verifica sempre che i PDF siano testo e non scansioni: in quel caso serve prima un passaggio di OCR.
Come proseguire
Da qui puoi migliorare il sistema in molti modi: aggiungere un'interfaccia web con Streamlit per interrogare i documenti dal browser, tenere la cronologia della conversazione per fare domande di follow-up, oppure introdurre un passaggio di "reranking" per ordinare meglio i frammenti recuperati. Se i tuoi documenti sono scansioni, integra uno strumento di OCR come Tesseract per estrarne il testo prima dell'indicizzazione. Il principio, comunque, resta sempre lo stesso: recuperare i pezzi giusti e istruire il modello a rispondere solo su quelli. E' questa combinazione — ricerca mirata piu' generazione vincolata — a rendere il RAG lo strumento oggi piu' solido per far parlare l'IA con i tuoi dati.




