Immagina di poter fare domande in italiano a una cartella piena di PDF — contratti, dispense universitarie, manuali, bilanci — e ricevere risposte precise con la citazione del punto esatto da cui provengono, senza caricare nulla su ChatGPT e senza pagare un abbonamento. Si chiama RAG (Retrieval-Augmented Generation) ed è la stessa tecnica che sta dietro alla maggior parte degli assistenti aziendali basati sui documenti. In questa guida la costruiamo da zero sul tuo computer, gratis e completamente offline: i tuoi file non escono mai dal disco.
A chi serve, cosa otterrai e cosa ti serve prima di iniziare
Questa guida è pensata per chi ha un minimo di dimestichezza con il terminale e vuole un assistente privato sui propri documenti: professionisti che maneggiano contratti riservati, studenti con centinaia di pagine di appunti, ricercatori, sviluppatori. Alla fine avrai uno script Python a cui passi una cartella di PDF e che risponde alle tue domande citando le fonti.
Prerequisiti reali:
- Sistema operativo: Windows, macOS o Linux.
- Python 3.10 o superiore installato (verifica con
python --version). - Hardware: almeno 8 GB di RAM per i modelli piccoli; 16 GB rendono tutto più fluido. Non serve una scheda video dedicata, ma se ce l'hai (NVIDIA con 6+ GB di VRAM) le risposte arriveranno molto più in fretta.
- Circa 5-6 GB di spazio libero per i modelli.
Quali strumenti usiamo e perché questi
Il RAG ha bisogno di tre mattoni: un motore che esegue i modelli in locale, un modello che trasforma il testo in numeri (embedding) per la ricerca semantica, e un modello linguistico che scrive la risposta. Ecco le scelte e le alternative.
- Ollama (prima scelta come motore): gratuito e open source, installa e serve i modelli con un solo comando, funziona su tutti i sistemi. In alternativa LM Studio (più grafico) o llama.cpp (più a basso livello). Consiglio Ollama perché espone un'API locale che le librerie Python usano senza configurazioni.
- nomic-embed-text come modello di embedding: leggero (circa 270 MB), veloce, ottimo rapporto qualità/dimensioni e supporta bene l'italiano. Alternativa:
mxbai-embed-large, più preciso ma più pesante. - Llama 3.2 (3B) come modello che genera la risposta: gira anche su portatili modesti. Se hai 16 GB di RAM o una GPU, sali a Qwen 2.5 (7B) o Llama 3.1 (8B), che ragionano meglio sui testi complessi.
- LangChain + Chroma per orchestrare tutto: LangChain collega i pezzi, Chroma è il database vettoriale che conserva gli embedding sul disco. Sono gratuiti e girano in locale. Alternativa: LlamaIndex, equivalente e altrettanto valido.
Costi e limiti: zero. Nessun abbonamento, nessuna chiave API, nessun limite di richieste. Il prezzo lo paghi solo in tempo di calcolo: su un portatile senza GPU una risposta può richiedere da 10 a 40 secondi.
Passo 1: installare Ollama e scaricare i modelli
Su macOS e Windows scarica l'installer dal sito ufficiale ollama.com. Su Linux basta un comando:
curl -fsSL https://ollama.com/install.sh | sh
Verifica che funzioni e poi scarica i due modelli che ci servono (embedding e generazione):
ollama --version
ollama pull nomic-embed-text
ollama pull llama3.2
Il primo pull scarica il modello di embedding, il secondo il modello linguistico. Puoi provare subito che il modello risponda:
ollama run llama3.2 "Rispondi in italiano: cos'è un database vettoriale in una frase?"
Passo 2: preparare l'ambiente Python
Crea una cartella per il progetto, un ambiente virtuale e installa le librerie:
mkdir rag-pdf && cd rag-pdf
python -m venv .venv
# macOS/Linux:
source .venv/bin/activate
# Windows (PowerShell):
# .venv\Scripts\Activate.ps1
pip install langchain langchain-community langchain-ollama langchain-chroma chromadb pypdf
Crea ora una sottocartella documenti e metti dentro i PDF che vuoi interrogare.
Passo 3: indicizzare i PDF (lo script di ingest)
Questo script legge tutti i PDF nella cartella, li spezza in blocchi (chunk), calcola gli embedding e li salva in Chroma. Va eseguito una sola volta (o quando aggiungi documenti). Salvalo come ingest.py:
import os
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 "documenti"
loader = PyPDFDirectoryLoader("documenti")
docs = loader.load()
print(f"Pagine caricate: {len(docs)}")
# 2. Spezza il testo in blocchi con un po' di sovrapposizione
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=150,
)
chunks = splitter.split_documents(docs)
print(f"Blocchi creati: {len(chunks)}")
# 3. Calcola gli embedding con nomic-embed-text e salvali su disco
embeddings = OllamaEmbeddings(model="nomic-embed-text")
Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="chroma_db",
)
print("Indice creato nella cartella chroma_db/")
Esegui:
python ingest.py
Vedrai il numero di pagine e di blocchi. La prima volta impiega qualche secondo per documento; il risultato resta salvato in chroma_db/.
Passo 4: fare domande ai documenti (lo script di query)
Ora il cuore del sistema. Recupera i blocchi più pertinenti alla domanda, li passa al modello con istruzioni precise e stampa la risposta con le fonti. Salvalo come chiedi.py:
import sys
from langchain_ollama import OllamaEmbeddings, ChatOllama
from langchain_chroma import Chroma
from langchain_core.prompts import ChatPromptTemplate
# Ricarica l'indice salvato
embeddings = OllamaEmbeddings(model="nomic-embed-text")
db = Chroma(persist_directory="chroma_db", embedding_function=embeddings)
# La domanda arriva dalla riga di comando
domanda = " ".join(sys.argv[1:]) or "Di cosa parlano i documenti?"
# Recupera i 4 blocchi più simili
risultati = db.similarity_search(domanda, k=4)
contesto = "\n\n---\n\n".join(d.page_content for d in risultati)
template = ChatPromptTemplate.from_template(
"Sei un assistente che risponde SOLO usando il contesto fornito.\n"
"Se la risposta non è nel contesto, dillo chiaramente.\n"
"Rispondi in italiano, in modo chiaro e conciso.\n\n"
"CONTESTO:\n{contesto}\n\n"
"DOMANDA: {domanda}\n\nRISPOSTA:"
)
modello = ChatOllama(model="llama3.2", temperature=0)
catena = template | modello
risposta = catena.invoke({"contesto": contesto, "domanda": domanda})
print("\n=== RISPOSTA ===\n")
print(risposta.content)
print("\n=== FONTI ===")
for d in risultati:
fonte = d.metadata.get("source", "?")
pagina = d.metadata.get("page", "?")
print(f"- {fonte} (pag. {pagina})")
Interroga i tuoi documenti così:
python chiedi.py "Qual è la durata del contratto e come si rinnova?"
Un esempio reale e cosa aspettarsi
Mettiamo che nella cartella ci sia un contratto di locazione. Una domanda come:
python chiedi.py "Quanti giorni di preavviso servono per disdire?"
restituirà qualcosa del genere:
=== RISPOSTA ===
Secondo il contratto, il conduttore può recedere con un preavviso di almeno sei mesi comunicato con lettera raccomandata. Per gravi motivi il termine resta di sei mesi.
=== FONTI ===
- documenti/contratto.pdf (pag. 3)
La parte importante è l'elenco FONTI: puoi sempre andare a verificare la pagina citata. È ciò che distingue un assistente RAG serio da un modello che "inventa" (allucina): la risposta è ancorata al testo reale.
Varianti e casi avanzati
- Risposte più intelligenti: sostituisci
llama3.2conqwen2.5ollama3.1nello script di query, dopo aver fattoollama pull qwen2.5. Servono più RAM ma migliora molto sui documenti tecnici e legali. - Recupero più ampio: aumenta
k=4ak=6per domande che toccano più parti del documento. - Altri formati: per file Word o testo usa
UnstructuredFileLoaderal posto diPyPDFDirectoryLoader. - Interfaccia grafica: con poche righe di Streamlit trasformi
chiedi.pyin un'app con casella di testo e pulsante, invece del terminale.
Errori comuni e come risolverli
Connection refusedo "could not connect to ollama": Ollama non è in esecuzione. Aprilo (icona nella barra) oppure lanciaollama servein un altro terminale.model 'nomic-embed-text' not found: hai dimenticato ilpull. Eseguiollama pull nomic-embed-text.- Il PDF non viene letto (0 pagine): è probabilmente una scansione (immagini, non testo). Passa prima da un OCR, ad esempio con Tesseract, per estrarre il testo.
- Risposte lente: normale su CPU. Riduci
chunk_size, usa il modello 3B, oppure abilita la GPU. - Risposte vaghe o "non lo so": hai indicizzato pochi documenti o i chunk sono troppo piccoli. Aumenta
chunk_sizeok, e assicurati di aver rilanciatoingest.pydopo aver aggiunto file.
Alternative e quando NON conviene il fai-da-te
Se i documenti non sono riservati e vuoi zero installazioni, NotebookLM di Google o la funzione "carica un file" di ChatGPT e Claude fanno un lavoro simile in pochi clic — ma i file finiscono su un server. Se invece hai centinaia di migliaia di pagine o molti utenti, valuta un database vettoriale gestito (Qdrant, Weaviate) e modelli più grandi via API. Il RAG locale che hai appena costruito è il punto ideale per chi mette la privacy al primo posto e vuole capire davvero come funziona la tecnologia, senza costi ricorrenti.
Da qui puoi proseguire aggiungendo la cronologia della conversazione (memoria), un "reranker" per ordinare meglio i risultati, o esportando l'assistente come piccola API interna. Le basi che hai messo — ingest, indice vettoriale, recupero e generazione con citazioni — sono le stesse dei sistemi in produzione.




