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.

python
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.

python
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:

💡

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