Se hai mai chiesto a ChatGPT qualcosa sulla tua azienda e ti sei sentito rispondere con sicurezza una cosa completamente sbagliata, hai già capito il problema che RAG risolve. I modelli linguistici sanno tantissimo del mondo, ma non sanno niente dei tuoi dati. E quando non sanno, spesso inventano.
Ho costruito sistemi RAG per clienti che andavano dal supporto tecnico interno alla ricerca su archivi legali. In questa guida ti mostro come funziona davvero, con codice Python che puoi copiare ed eseguire. Niente pseudocodice, niente "lascio l'implementazione come esercizio".
Il problema: perché gli LLM "inventano"
Un Large Language Model è addestrato una volta sola, su un dataset congelato a una certa data (la knowledge cutoff). Da quel momento in poi, il modello non impara più nulla. Tre conseguenze pratiche:
- Non conosce i tuoi dati. I tuoi contratti, le tue procedure, il catalogo prodotti: il modello non li ha mai visti.
- Non sa cosa è successo dopo il training. Eventi recenti, prezzi aggiornati, nuove normative: invisibili.
- Quando non sa, allucina. Invece di dire "non lo so", il modello genera la risposta statisticamente più plausibile. Che spesso è falsa ma scritta in modo convincente.
Le due soluzioni classiche sono il fine-tuning (riaddestrare il modello sui tuoi dati) e RAG (dare al modello i dati giusti al momento della domanda). Il fine-tuning è costoso, lento e va rifatto a ogni aggiornamento dei dati. RAG no: aggiungi un documento e il sistema lo usa subito.
Cos'è RAG
RAG sta per Retrieval-Augmented Generation: generazione potenziata dal recupero. L'idea è semplice quanto efficace: prima di rispondere, il sistema cerca nei tuoi documenti i passaggi rilevanti per la domanda, poi li passa all'LLM come contesto, e gli chiede di rispondere usando quelle informazioni.
L'analogia che uso sempre: un LLM normale è uno studente che risponde a memoria a un esame. Un sistema RAG è lo stesso studente, ma con il libro aperto davanti. Continua a ragionare e a scrivere bene, ma adesso le risposte sono ancorate a una fonte reale.
RAG vs Fine-tuning: quando usare cosa
| Aspetto | RAG | Fine-tuning |
|---|---|---|
| Obiettivo | Dare conoscenza aggiornata | Cambiare comportamento/stile |
| Aggiornamento dati | Istantaneo (aggiungi un file) | Richiede nuovo training |
| Costo iniziale | Basso | Medio-alto (GPU + dataset) |
| Allucinazioni | Riduce drasticamente | Non risolve |
| Citazione fonti | Sì, nativa | No |
| Caso tipico | Q&A su documenti, knowledge base | Tono di voce, formato output, task specializzato |
Nella stragrande maggioranza dei progetti aziendali la risposta giusta è RAG. Il fine-tuning serve quando vuoi che il modello si comporti diversamente, non quando vuoi che sappia cose nuove. Se vuoi approfondire l'altra strada, ne ho parlato nella guida al fine-tuning degli LLM.
L'architettura di un sistema RAG
Un sistema RAG ha due fasi. La prima, l'indicizzazione, avviene una volta (o quando aggiorni i dati). La seconda, il retrieval + generation, avviene a ogni domanda.
INDICIZZAZIONE (offline)
documenti → splitter → embeddings → vector database
QUERY (a ogni domanda)
domanda → embedding → ricerca nel vector DB → top-k chunk
→ prompt (domanda + chunk) → LLM → risposta
I componenti chiave:
- Document loader: legge i file (PDF, Word, HTML, ...) e li trasforma in testo.
- Text splitter: spezza i documenti in chunk di dimensione gestibile.
- Embedding model: converte ogni chunk in un vettore numerico che ne cattura il significato.
- Vector database: memorizza i vettori e permette la ricerca per similarità semantica.
- Retriever: data una domanda, recupera i chunk più rilevanti.
- LLM: genera la risposta finale usando i chunk come contesto.
Setup dell'ambiente
Lavoriamo con LangChain (il framework più diffuso per orchestrare questi componenti), ChromaDB come vector database locale e Ollama per girare un LLM in locale, gratis. Se preferisci usare le API di OpenAI, ti mostro la variante più avanti.
# Ambiente virtuale (consigliato)
python -m venv venv
source venv/bin/activate # su Windows: venv\Scripts\activate
# Dipendenze
pip install langchain langchain-community langchain-chroma \
langchain-huggingface langchain-ollama \
chromadb sentence-transformers pypdf
Per l'LLM locale, installa Ollama e scarica un modello che gestisce bene l'italiano:
ollama pull llama3.1:8b
Implementazione step-by-step
Costruiamo un sistema che risponde a domande sulla documentazione interna di un'azienda. Mettiamo qualche file .pdf o .txt in una cartella documenti/ e partiamo.
1. Caricare i documenti
from langchain_community.document_loaders import DirectoryLoader, PyPDFLoader, TextLoader
# Carica tutti i PDF e i TXT dalla cartella
pdf_loader = DirectoryLoader("documenti/", glob="**/*.pdf", loader_cls=PyPDFLoader)
txt_loader = DirectoryLoader("documenti/", glob="**/*.txt", loader_cls=TextLoader)
documents = pdf_loader.load() + txt_loader.load()
print(f"Caricati {len(documents)} documenti")
2. Spezzare in chunk
Questo è il passaggio che la gente sottovaluta di più, ed è quello che fa la differenza tra un RAG che funziona e uno inutile. Se i chunk sono troppo grandi, il modello riceve rumore; se troppo piccoli, perde il contesto.
from langchain_text_splitters import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000, # caratteri per chunk
chunk_overlap=150, # sovrapposizione tra chunk consecutivi
separators=["\n\n", "\n", ". ", " ", ""],
)
chunks = splitter.split_documents(documents)
print(f"Generati {len(chunks)} chunk")
L'overlap serve a non spezzare a metà un concetto: un po' di testo viene ripetuto tra un chunk e il successivo, così l'informazione a cavallo del taglio non si perde.
3. Generare gli embeddings e indicizzare
Usiamo un modello di embedding multilingua che capisce bene l'italiano, eseguito in locale (nessuna API, nessun costo).
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_chroma import Chroma
embeddings = HuggingFaceEmbeddings(
model_name="intfloat/multilingual-e5-large",
encode_kwargs={"normalize_embeddings": True},
)
# Crea il vector database e lo salva su disco
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_db",
)
print("Indicizzazione completata")
La prima esecuzione scarica il modello di embedding (circa 2 GB) e indicizza tutto. Le volte successive puoi ricaricare il database già pronto senza reindicizzare:
vectorstore = Chroma(
persist_directory="./chroma_db",
embedding_function=embeddings,
)
4. Costruire la catena RAG
Adesso colleghiamo retriever e LLM. Uso la sintassi moderna di LangChain (LCEL), che è dichiarativa e leggibile.
from langchain_ollama import ChatOllama
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
# Recupera i 4 chunk più rilevanti per ogni domanda
retriever = vectorstore.as_retriever(search_kwargs={"k": 4})
llm = ChatOllama(model="llama3.1:8b", temperature=0)
prompt = ChatPromptTemplate.from_template(
"""Sei l'assistente interno dell'azienda. Rispondi alla domanda
usando ESCLUSIVAMENTE il contesto fornito. Se la risposta non è nel
contesto, dì "Non ho informazioni sufficienti per rispondere" invece
di inventare.
Contesto:
{context}
Domanda: {question}
Risposta:"""
)
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
5. Fare domande
domanda = "Qual è la procedura per richiedere le ferie?"
risposta = rag_chain.invoke(domanda)
print(risposta)
Ed ecco fatto: un sistema RAG completo e funzionante, tutto in locale, in meno di 50 righe di codice. La frase chiave del prompt è quella che impone di rispondere solo dal contesto: è la tua prima linea di difesa contro le allucinazioni.
Mostrare le fonti
In un contesto reale vuoi sapere da dove arriva la risposta. Recuperiamo i documenti a parte e mostriamo i riferimenti:
docs = retriever.invoke(domanda)
risposta = rag_chain.invoke(domanda)
print(risposta)
print("\n--- Fonti ---")
for doc in docs:
fonte = doc.metadata.get("source", "sconosciuta")
pagina = doc.metadata.get("page", "-")
print(f"• {fonte} (pag. {pagina})")
La citabilità delle fonti è uno dei motivi principali per cui le aziende scelgono RAG: ogni risposta è verificabile.
Usare le API di OpenAI invece dell'LLM locale
Se la qualità del modello locale non basta, o non hai una GPU, puoi sostituire embedding e LLM con le API di OpenAI cambiando due righe:
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
embeddings = OpenAIEmbeddings(model="text-embedding-3-large")
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
Il resto della pipeline resta identico. Il vantaggio del locale resta la privacy: con Ollama i tuoi documenti non escono mai dalla tua macchina, il che per dati sensibili è spesso un requisito non negoziabile.
Ottimizzazioni che fanno la differenza
Il RAG "base" che hai costruito funziona. Ma tra un prototipo e un sistema di produzione ci sono alcune scelte che cambiano radicalmente la qualità.
Chunk size e overlap. Non esiste il valore giusto universale. Per documentazione tecnica strutturata, chunk piccoli (500-800 caratteri) funzionano bene. Per testo discorsivo (contratti, report), conviene salire (1000-1500). Sperimenta e misura.
Il modello di embedding. È il componente che determina cosa viene recuperato. Per l'italiano, multilingual-e5-large è un'ottima scelta. Se i risultati sono scadenti, il 90% delle volte il problema è qui, non nell'LLM.
Reranking. Il retriever recupera i chunk più simili, che non sempre sono i più utili. Un reranker (es. un cross-encoder) riordina i risultati con un secondo passaggio più accurato. Recuperi 20 chunk, ne riordini e tieni i migliori 4. Migliora sensibilmente la precisione.
Hybrid search. La ricerca semantica è potente ma fatica con sigle, codici prodotto e nomi propri. Combinarla con la ricerca per parole chiave (BM25) cattura sia il significato sia i termini esatti. È quello che faccio nei sistemi seri.
Quando NON usare RAG
RAG non è la risposta a tutto:
- Task di puro ragionamento (logica, matematica, scrittura creativa): non c'è niente da recuperare, RAG non aiuta.
- Quando i dati sono già nel training del modello (conoscenza generale): aggiungi solo latenza inutile.
- Domande aggregate ("quanti contratti abbiamo firmato nel 2025?"): RAG recupera passaggi di testo, non sa contare su tutto l'archivio. Lì serve un database e una query SQL, magari pilotata dall'LLM.
Conclusioni
RAG è probabilmente la tecnica con il miglior rapporto valore/sforzo in tutto l'ecosistema LLM applicato alle aziende. In poche righe di codice passi da un modello che inventa a un assistente che risponde citando i tuoi documenti reali.
I punti da ricordare:
- Il segreto non è l'LLM, è la qualità del retrieval: chunking ed embedding fanno l'80% del risultato.
- Imponi sempre nel prompt di rispondere solo dal contesto: è la difesa contro le allucinazioni.
- Parti semplice (questo tutorial), poi aggiungi reranking e hybrid search solo se i numeri lo giustificano.
Se vuoi costruire una piattaforma RAG aziendale seria — con multi-tenant, controllo accessi, integrazione con i tuoi sistemi e modelli self-hosted per la privacy — è esattamente il tipo di progetto che realizziamo. Parliamone.
Il modello migliore non è quello con più parametri: è quello che risponde con i tuoi dati invece di inventarli.