Chiedi a un LLM generico qual è la politica di reso della tua azienda o cosa dicono i ticket di supporto della settimana scorsa, e otterrai o un “non lo so” o una risposta inventata ma plausibile. La sua conoscenza si ferma a quello che c’era nei dati di addestramento. Non ha mai visto i tuoi documenti e non può andare a cercarli a metà conversazione. La retrieval-augmented generation (RAG) risolve proprio questo: prima che il modello risponda, un passaggio separato trova il testo rilevante nei tuoi dati e lo passa al modello come parte del prompt.

Come funziona davvero la retrieval-augmented generation

Una chiamata LLM normale è un solo passaggio: prompt in ingresso, risposta in uscita, usando solo ciò che il modello ha imparato durante l’addestramento. Il RAG divide questo passaggio in due: prima recupera, poi genera.

  1. I tuoi documenti vengono divisi in chunk, convertiti in vettori (embedding) e salvati in un vector database in anticipo.
  2. Al momento della query, la domanda dell’utente viene trasformata in embedding nello stesso modo e confrontata con i chunk salvati per trovare le corrispondenze più vicine.
  3. Le corrispondenze migliori vengono inserite nel prompt come contesto, e solo a quel punto l’LLM genera una risposta.

Pensalo come la differenza tra un esame a libro aperto e uno a libro chiuso. Un LLM a libro chiuso risponde solo a memoria, ed è esattamente per questo che se le inventa quando la domanda va oltre quello che ha memorizzato. Il RAG gli mette davanti la pagina giusta, così risponde partendo da qualcosa che ha sotto gli occhi invece che da qualcosa che sta ricostruendo.

Il modello in sé non cambia mai. I suoi pesi restano esattamente quelli di prima di aggiungere il RAG; il retrieval è l’unico pezzo nuovo. Questo è anche il motivo per cui il RAG è economico da tenere aggiornato: modifichi un documento e la query successiva vede la modifica.

Costruire una pipeline RAG: chunking ed embedding dei dati

L’ingestion è la metà della pipeline che gira prima che qualcuno faccia una domanda. Trasforma i tuoi documenti in qualcosa che una ricerca per similarità può interrogare.

I documenti sono troppo lunghi per essere trasformati in embedding come unità singole. Un PDF di 20 pagine convertito in un unico vettore finisce per corrispondere un po’ a ogni query e bene a nessuna, quindi lo dividi prima in chunk. Uno splitter a dimensione fissa con overlap è sufficiente per iniziare:

1
2
3
4
5
6
7
8
def chunk_text(text, chunk_size=500, overlap=50):
    chunks = []
    start = 0
    while start < len(text):
        end = start + chunk_size
        chunks.append(text[start:end])
        start = end - overlap
    return chunks

500 caratteri con 50 di overlap è un punto di partenza ragionevole. Le pipeline in produzione di solito dividono per numero di token invece che per caratteri, usando uno splitter che conosce il tokenizer così un confine di chunk non cade a metà parola. L’overlap serve a far sì che una frase divisa tra due chunk compaia comunque intera in almeno uno dei due.

Ogni chunk viene poi trasformato in embedding e salvato. Chroma è un buon primo vector database perché gira in-process senza bisogno di un server, e la sua funzione di embedding di default (all-MiniLM-L6-v2) non richiede un’API key:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import chromadb

client = chromadb.Client()
collection = client.create_collection(name="support-docs")

collection.add(
    ids=["doc1", "doc2", "doc3"],
    documents=[
        "The refund window is 30 days from the delivery date.",
        "Refunds go back to the original payment method within 5 business days.",
        "Store credit never expires and can be used on any future order.",
    ],
)

Questa è tutta la fase di ingestion: chunk, embedding, salvataggio. Gira una volta per documento, e di nuovo ogni volta che un documento cambia.

Se non vuoi installare un vector database sulla tua macchina, la maggior parte di essi (Chroma, Weaviate, Milvus) distribuisce un’immagine ufficiale e funziona bene in un container; guarda cos’è Docker e a cosa serve se non ne hai mai usato uno.

Come funziona il retrieval: embedding della query e ranking dei risultati

Al momento della query, lo stesso modello di embedding trasforma la domanda dell’utente in un vettore, e il database restituisce i chunk salvati più vicini, usando qualsiasi metrica di distanza sia configurata — cosine similarity e L2 al quadrato sono le due che vedrai più spesso:

1
2
3
4
5
6
7
8
results = collection.query(
    query_texts=["How long do I have to return something?"],
    n_results=2,
)

print(results["documents"])
# [['The refund window is 30 days from the delivery date.',
#   'Refunds go back to the original payment method within 5 business days.']]

La query non usa mai la parola “reso” (refund) eppure trova la corrispondenza giusta. Ed è proprio per questo che conviene fare embedding invece di grep: due testi finiscono vicini nello spazio vettoriale quando hanno un significato simile, non quando condividono le stesse parole.

L’ultimo passaggio unisce i chunk recuperati nel prompt:

1
2
3
4
5
6
7
8
context = "\n".join(results["documents"][0])

prompt = f"""Answer the question using only the context below. If the context doesn't contain the answer, say so.

Context:
{context}

Question: How long do I have to return something?"""

prompt va a qualsiasi API di chat completion che tu stia già usando. Il RAG non richiede un modello o un provider specifico: è uno schema per decidere cosa mettere nel prompt prima di inviarlo. Se quella chiamata API richiede una chiave, tienila fuori dalla tua immagine come faresti con qualsiasi altra credenziale: come variabile d’ambiente o secret montato, mai come build argument. Variabili d’Ambiente e Secrets in Docker spiega la differenza.

RAG contro fine-tuning: quale risolve il tuo problema

Entrambi puntano a rendere un LLM più efficace nel tuo caso d’uso specifico, ma cambiano cose diverse.

RAGFine-tuning
Cosa cambiaNiente nel modello; aggiungi un passaggio di retrievalI pesi stessi del modello, tramite ulteriore addestramento
Aggiornare la conoscenzaModifichi o aggiungi un documento, la query successiva lo vedeRiaddestrare (o rifare un passaggio di fine-tuning)
Adatto aFatti, dati aggiornati, tutto ciò che cambia spessoTono, formato di output, schemi di ragionamento specifici per il task
Costo di aggiornamentoBasso — nessun training runAlto — serve un training run e un dataset
Fallisce quandoIl retrieval sbaglia o classifica male il chunkIl comportamento richiesto non si riduce a esempi input-output

Un bot di supporto la cui politica di reso cambia ogni trimestre è un problema da RAG: fai fine-tuning sulla politica del trimestre scorso e il modello sarà sicuro di sé ma sbagliato su quella nuova. Un modello che deve emettere uno schema JSON specifico ogni volta, o mantenere uno stile di ragionamento particolare, è più vicino a un problema di fine-tuning: nessuna quantità di contesto recuperato cambia il formato del suo output. Molti sistemi in produzione usano entrambi: un modello con fine-tuning per tono e struttura, alimentato con fatti dal RAG.

Dove si rompe la retrieval-augmented generation

Le demo di RAG sembrano facili perché hanno tre documenti e una risposta ovvia. I sistemi in produzione hanno migliaia di documenti, e ognuno dei seguenti problemi può silenziosamente rovinare una risposta:

  • Chunking che ignora la struttura. Uno splitter a dimensione fissa che taglia una tabella a metà, o separa un titolo dal paragrafo che introduce, produce chunk che fuori contesto sembrano incomprensibili — e ciò che è incomprensibile viene trasformato in embedding e recuperato male.
  • Un indice non aggiornato. La ricerca vettoriale non ha alcuna nozione di tempo. Se un documento viene aggiornato ma mai ri-processato in embedding, è ancora la vecchia versione a essere recuperata, e niente nella pipeline segnala che è sbagliata.
  • Il chunk giusto esiste ma non si posiziona abbastanza in alto. Il retrieval top-k restituisce solo i k risultati più vicini; se la risposta reale è il quindicesimo chunk più vicino e ne recuperi 5, il modello non la vede mai.
  • Troppo contesto, messo male. Infilare dieci chunk recuperati nel prompt non significa che il modello li pesi tutti allo stesso modo. I modelli sono misurabilmente peggiori nell’usare informazioni che si trovano nel mezzo di un contesto lungo rispetto a quelle all’inizio o alla fine.
  • Nessuna valutazione oltre “la risposta della demo sembrava giusta”. Senza un set di domande e risposte attese, una regressione nel retrieval causata da una modifica al chunking o da un bug di re-indicizzazione passa inosservata.

La maggior parte di questi sono problemi di retrieval, non del modello, e ognuno è visibile se ispezioni i chunk recuperati invece della sola risposta finale.

Come costruire la tua prima pipeline RAG

Costruisci prima la versione a tre passaggi: fai chunking di una manciata di documenti reali, trasformali in embedding in Chroma o in un altro store locale, e collega i chunk recuperati a un prompt esattamente come mostrato sopra. Poi fai le 10 domande che ti aspetti dagli utenti reali. Controlla se i chunk recuperati contengono la risposta prima di giudicare la risposta generata — un retrieval sbagliato rende inevitabile una risposta sbagliata, non importa come sia scritto il prompt. Solo una volta che questo ciclo regge su domande reali vale la pena regolare la dimensione dei chunk, cambiare modello di embedding o aggiungere un reranker.

Articoli correlati