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-text

Verifica 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 unstructured

Passo 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.py

La 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.py

Prompt 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 con brew services start ollama.
  • chromadb.errors.NoIndexException: hai cancellato la cartella chroma_db ma chat.py continua a cercarla. Rigenera con python 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 con OLLAMA_GPU_LAYERS=999 ollama serve.
  • PDF scannerizzati senza testo: PyPDFLoader non li legge. Sostituisci con UnstructuredPDFLoader e installa unstructured[pdf] + tesseract-ocr di sistema.
  • Out of memory: passa a gemma3:4b o qwen3:1.7b; riduci chunk_size a 500 e k a 3.

Varianti e step successivi

Tre estensioni utili una volta che funziona:

  1. Interfaccia web: Streamlit in 30 righe; oppure Gradio integrato con LangChain.
  2. Hybrid search: combina embedding con BM25 usando EnsembleRetriever, ottimo per nomi propri e codici.
  3. 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.