Hai mai chiesto a ChatGPT qualcosa di specifico sulla tua azienda e ricevuto una risposta generica e inutile? Non è un bug — è un limite strutturale. I modelli linguistici conoscono solo ciò su cui sono stati addestrati, e i tuoi dati interni non ci sono. Il Retrieval-Augmented Generation (RAG) è la soluzione più pragmatica a questo problema.
Il problema che RAG risolve
Le alternative tradizionali per "insegnare" dati a un LLM sono il fine-tuning (costoso, lento, difficile da aggiornare) o l'inserimento diretto nel prompt (limitato dalla context window, costoso per ogni query). RAG introduce un terzo approccio: recuperare dinamicamente solo i frammenti di informazione rilevanti per la domanda corrente, inserirli nel prompt e lasciare che il modello generi una risposta contestualizzata.
Architettura in due fasi
Un sistema RAG si divide in due fasi distinte: indicizzazione (offline) e recupero+generazione (online, ad ogni query). Le due fasi sono indipendenti e possono essere ottimizzate separatamente.
Fase 1 — Indicizzazione dei documenti
I documenti vengono suddivisi in chunk, convertiti in vettori numerici (embedding) tramite un modello apposito, e salvati in un vector store. Ogni vettore cattura il significato semantico del testo, non le parole esatte.
from anthropic import Anthropic
from sentence_transformers import SentenceTransformer
import chromadb
# Inizializza embedding model e vector store
embedder = SentenceTransformer('all-MiniLM-L6-v2')
client = chromadb.Client()
collection = client.create_collection("documenti_aziendali")
# Indicizza i chunk
def indicizza(chunks: list[str]):
embeddings = embedder.encode(chunks).tolist()
collection.add(
documents=chunks,
embeddings=embeddings,
ids=[f"chunk_{i}" for i in range(len(chunks))]
)
Fase 2 — Recupero e generazione
A ogni query, si calcola l'embedding della domanda, si recuperano i chunk più simili dal vector store, si costruisce un prompt con quei chunk come contesto e si invia al LLM per la risposta finale.
def rispondi(domanda: str, k: int = 4) -> str:
# Recupera i chunk rilevanti
query_vec = embedder.encode([domanda]).tolist()
risultati = collection.query(query_embeddings=query_vec, n_results=k)
contesto = "\n\n".join(risultati['documents'][0])
# Genera la risposta
client_ai = Anthropic()
risposta = client_ai.messages.create(
model="claude-opus-4-6",
max_tokens=1024,
messages=[{
"role": "user",
"content": f"Contesto:\n{contesto}\n\nDomanda: {domanda}"
}]
)
return risposta.content[0].text
Strategie di chunking
Il chunking è spesso il fattore più critico per la qualità del RAG. Chunk troppo piccoli perdono il contesto; troppo grandi introducono rumore. Le strategie principali sono:
- Fixed-size con overlap — semplice e veloce, overlap del 10-20% per non perdere il contesto ai bordi
- Semantic chunking — divide sui cambi di argomento semantico, più costoso ma più preciso
- Strutturale — segue la struttura del documento (sezioni, paragrafi, headers markdown)
- Gerarchico — mantiene chunk a granularità diverse e li collega, ottimo per documenti lunghi
Consiglio pratico: inizia sempre con chunk da 512 token e overlap del 15%. Misura la qualità delle risposte, poi ottimizza. Non esiste una configurazione universale.
RAG vs Fine-tuning: quando scegliere cosa
- Usa RAG quando i dati cambiano frequentemente o sono specifici a ogni utente
- Usa RAG quando hai bisogno di citare le fonti e garantire la tracciabilità
- Usa il fine-tuning quando vuoi cambiare lo stile o il comportamento del modello
- Usa il fine-tuning quando hai migliaia di esempi di alta qualità per un task molto specifico
- Combina entrambi (RAG + fine-tuned model) per i casi d'uso enterprise più esigenti