Questo tutorial spiega come costruire un sistema RAG (Retrieval Augmented Generation) in locale: un chatbot che risponde alle tue domande leggendo i tuoi PDF, senza mandare nulla a OpenAI o ad altri cloud. La pipeline gira su un laptop con 16 GB di RAM, usa solo software gratuito e open source, e è pensata per il caso d'uso classico — un avvocato, un consulente, un ricercatore — che ha cartelle di documenti su cui vuole interrogare l'IA preservando privacy e segreto professionale.
A chi serve questa guida e cosa otterrai
Il tutorial è pensato per chi sa scrivere un po' di Python e vuole un sistema che funzioni davvero, non un giocattolo da demo. Alla fine avrai uno script che:
- Legge tutti i PDF da una cartella locale.
- Li trasforma in chunk di testo e li indicizza con embedding.
- Salva l'indice su disco con ChromaDB, persistente tra sessioni.
- Risponde a domande in italiano citando i passaggi rilevanti dei documenti.
Niente lascia il PC: nessuna API key, nessun upload, nessuna telemetria. Tempo stimato per la prima esecuzione su 100 PDF: 12-25 minuti per l'indicizzazione, 8-15 secondi per ogni risposta.
Strumenti e modelli: cosa scegliere e perché
Per questo compito tre componenti contano davvero: runtime locale, modello di embedding, LLM generativo.
Runtime locale — Ollama (prima scelta). Disponibile per Mac, Linux e Windows, gestisce automaticamente download, quantizzazione e GPU offloading. Alternative: LM Studio (con GUI ma meno automatizzabile), llama.cpp puro (più veloce ma richiede setup manuale). Per RAG conviene Ollama perché ha endpoint HTTP standardizzati che LangChain interroga nativamente.
Modello di embedding — nomic-embed-text (137M parametri, 274 MB). Funziona molto bene su domande dirette in italiano e in inglese, ha un context window di 8192 token e usa pochissima RAM. Alternative: mxbai-embed-large (335M, leggermente migliore su domande complesse implicite, ma il doppio della memoria), multilingual-e5-large (specializzato multilingua, ottimo se hai PDF in 5+ lingue). Per documenti italiani standard, nomic-embed-text resta il miglior rapporto qualità-velocità.
LLM generativo. Tre opzioni a seconda dell'hardware:
- llama3.1:8b-instruct-q4_K_M (5 GB): funziona su qualsiasi laptop con 16 GB RAM, qualità ottima in italiano. Prima scelta.
- qwen3:14b-instruct-q4_K_M (9 GB): migliore su ragionamento, richiede 32 GB RAM o GPU con 12+ GB VRAM.
- gemma3:4b-instruct-q4_K_M (2,7 GB): per chi ha 8 GB RAM, qualità più bassa ma comunque utilizzabile.
Prerequisiti reali
- Sistema operativo: macOS 12+, Windows 10+, o Linux x86_64.
- Almeno 16 GB di RAM (8 GB se usi gemma3:4b).
- Spazio disco: 15 GB liberi per modelli + index.
- Python 3.10 o superiore.
- Connessione internet solo per il primo download dei modelli.
Passo 1 — Installare Ollama e scaricare i modelli
Su macOS o Linux:
curl -fsSL https://ollama.com/install.sh | sh
ollama pull llama3.1:8b
ollama pull nomic-embed-text
ollama serve &Su Windows scarica l'installer da ollama.com/download e poi apri il prompt:
ollama pull llama3.1:8b
ollama pull nomic-embed-textVerifica che funzioni:
ollama list
# deve mostrare llama3.1:8b e nomic-embed-text
ollama run llama3.1:8b "Ciao, mi rispondi in italiano?"Passo 2 — Ambiente Python e dipendenze
python3 -m venv .venv
source .venv/bin/activate # su Windows: .venv\Scripts\activate
pip install langchain langchain-community langchain-ollama langchain-chroma chromadb pypdf unstructuredPasso 3 — Indicizzare i PDF
Crea un file index.py:
import os
from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_ollama import OllamaEmbeddings
from langchain_chroma import Chroma
PDF_DIR = "./documenti"
DB_DIR = "./chroma_db"
docs = []
for f in os.listdir(PDF_DIR):
if f.lower().endswith(".pdf"):
loader = PyPDFLoader(os.path.join(PDF_DIR, f))
docs.extend(loader.load())
print(f"Caricati {len(docs)} blocchi pagina.")
splitter = RecursiveCharacterTextSplitter(
chunk_size=900,
chunk_overlap=120,
separators=["\n\n", "\n", ". ", " "],
)
chunks = splitter.split_documents(docs)
print(f"Generati {len(chunks)} chunk.")
embed = OllamaEmbeddings(model="nomic-embed-text")
vectordb = Chroma.from_documents(
documents=chunks,
embedding=embed,
persist_directory=DB_DIR,
collection_name="documenti_personali",
)
print("Indice salvato in", DB_DIR)Metti i tuoi PDF nella cartella documenti e lancia:
python index.pyLa prima esecuzione richiede tempo perché calcola gli embedding di ogni chunk. Su 100 PDF di 10 pagine ciascuno ci vogliono 12-25 minuti. Le esecuzioni successive sono incrementali se aggiungi vectordb.add_documents() nel codice.
Passo 4 — Costruire la chain di risposta
Crea un file chat.py:
from langchain_ollama import OllamaEmbeddings, ChatOllama
from langchain_chroma import Chroma
from langchain.prompts import ChatPromptTemplate
from langchain.schema.runnable import RunnablePassthrough
from langchain.schema.output_parser import StrOutputParser
embed = OllamaEmbeddings(model="nomic-embed-text")
vectordb = Chroma(
persist_directory="./chroma_db",
embedding_function=embed,
collection_name="documenti_personali",
)
retriever = vectordb.as_retriever(search_kwargs={"k": 5})
llm = ChatOllama(model="llama3.1:8b", temperature=0.1)
prompt = ChatPromptTemplate.from_template(
"""Sei un assistente che risponde in italiano basandoti SOLO sui passaggi forniti.
Se la risposta non è nei passaggi, dillo apertamente. Cita la fonte tra parentesi.
Passaggi:
{context}
Domanda: {question}
Risposta:"""
)
def format_docs(docs):
return "\n\n".join(
[f"[{d.metadata.get('source','?')} p.{d.metadata.get('page','?')}] {d.page_content}" for d in docs]
)
chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt | llm | StrOutputParser()
)
while True:
q = input("\n>> ")
if not q.strip(): break
print(chain.invoke(q))Lancia:
python chat.pyPrompt di prova e risultati attesi
Esempi che funzionano bene:
«Quali sono i criteri di scadenza per la responsabilità contrattuale citati nei contratti del 2024?»
«Riassumi in 5 punti la sezione che parla del trattamento dei dati personali nei documenti firmati da X»
«Trova tutte le clausole con la parola ‘non concorrenza’ e dimmi in quali file appaiono»
Risposta tipica: testo di 100-300 parole con citazioni in formato [file.pdf p.12]. Tempo medio: 8-15 secondi su llama3.1:8b. Se serve più rapido, abbassa k da 5 a 3 nel retriever.
Errori comuni e soluzioni
- connection refused on 127.0.0.1:11434: Ollama non è in esecuzione. Lancia
ollama serve. Su Mac/Linux puoi rendere persistente conbrew services start ollama. - chromadb.errors.NoIndexException: hai cancellato la cartella
chroma_dbma chat.py continua a cercarla. Rigenera conpython index.py. - Risposta molto lenta o GPU non usata: verifica con
nvidia-smi(Linux/Windows) o Activity Monitor (Mac). Se Ollama non usa la GPU, riavvialo conOLLAMA_GPU_LAYERS=999 ollama serve. - PDF scannerizzati senza testo: PyPDFLoader non li legge. Sostituisci con
UnstructuredPDFLoadere installaunstructured[pdf]+tesseract-ocrdi sistema. - Out of memory: passa a
gemma3:4boqwen3:1.7b; riducichunk_sizea 500 eka 3.
Varianti e step successivi
Tre estensioni utili una volta che funziona:
- Interfaccia web: Streamlit in 30 righe; oppure Gradio integrato con LangChain.
- Hybrid search: combina embedding con BM25 usando
EnsembleRetriever, ottimo per nomi propri e codici. - Re-ranking: passa i top-10 documenti recuperati a un cross-encoder come
bge-reranker-base(anche locale via Ollama) per migliorare la qualità del top-5.
Quando NON usare un RAG locale
Tre casi in cui un RAG cloud (Anthropic, OpenAI, Vertex AI) è meglio:
- Hai più di 50.000 documenti: la pipeline locale rallenta sopra quella soglia.
- Ti servono modelli più potenti (Claude 4.7 Sonnet, GPT-5) per ragionamenti complessi: i modelli locali non eguagliano ancora i frontier.
- Le query sono distribuite su molti utenti: serve un'infrastruttura server, non un laptop.
Per tutti gli altri casi — uso personale, studio professionale, ufficio piccolo o medio — il RAG locale costruito con Ollama, LangChain e Chroma è ormai una soluzione produttiva. Non è più un esperimento: è un sistema affidabile, e non costa nulla in API.




