>-
>-
Perché è importante
La maggior parte del debugging RAG che ho visto segue una traiettoria simile: la qualità del retrieval è scarsa, il team prova un embedding model diverso, la qualità migliora di poco e il lavoro prosegue. Dopo qualche mese, la qualità degrada su una nuova classe di query e il ciclo ricomincia.
Ho passato buona parte del 2024 a costruire e fare debugging di pipeline RAG per settori regolamentati: Q&A su documenti finanziari, review di contratti di compliance, analisi di legal brief. La lezione che ho imparato è che l'embedding model non è quasi mai il primo collo di bottiglia da verificare. Più spesso lo è il chunking. Dopo il chunking, il re-ranking è di solito il passo successivo da valutare. Nei sistemi che ho diagnosticato, questi due elementi hanno spiegato molti dei fallimenti di retrieval osservati.
L'ordine conta perché gli interventi hanno curve di costo diverse. Cambiare embedding model richiede di re-indexare l'intero corpus: da ore a giorni, a seconda della scala. Cambiare chunking richiede di rifare preprocessing ed embedding, quindi ha un costo simile. Il tuning di un re-ranker, invece, viene eseguito a query time senza rebuild dell'indice. Se parti dall'embedding model, rischi di pagare il costo più alto prima di aver isolato il problema.
Questo è l'ordine di debugging che seguo ora, e il motivo per cui lo uso.
1. La diagnostica prima di qualsiasi ottimizzazione
Prima di cambiare qualunque cosa, misura. "I retrieval sembrano sbagliati" non è una diagnosi: è un sintomo. Il framework RAGAS ti dà tre metriche che vale la pena capire prima di toccare una config:
- Context Precision: tra i chunk recuperati, quale frazione è effettivamente rilevante? Una precision bassa significa rumore nella context window.
- Context Recall: tra le informazioni rilevanti nel corpus, quale frazione hai effettivamente recuperato? Una recall bassa significa che stai perdendo le risposte.
- Faithfulness: la risposta generata è grounded nel contesto recuperato? Una faithfulness bassa significa che il tuo LLM sta allucinando nonostante un retrieval corretto. È un problema diverso.
Esegui RAGAS su un set rappresentativo di query prima di fare altre modifiche. La diagnostica ti dice dove intervenire:
- Precision bassa → probabile problema di re-ranking. Stai recuperando chunk rilevanti, ma li stai sommergendo nel rumore.
- Recall bassa → probabile problema di chunking. La risposta esiste nel corpus, ma non arriva nella context window.
- Entrambe basse → conviene sistemare prima il chunking, poi valutare il re-ranking.
- Precision e recall buone, faithfulness bassa → problema di prompting o di modello, non di retrieval.
Molti team saltano questo passaggio e ottimizzano a intuizione. È un rischio evitabile. Ogni ottimizzazione senza un baseline RAGAS score resta un'ipotesi non verificata.
2. Il chunking è spesso l'intervento con il maggiore impatto
I tutorial RAG ingenui spezzano i documenti a conteggi fissi di token: 512 token, overlap di 50 token, fine. Può funzionare per prosa uniforme. Fallisce spesso quando il documento ha struttura: clausole, tabelle, sezioni normative, API reference, trascrizioni.
Considera un chunk da 512 token in un documento finanziario. Potrebbe iniziare a metà frase in una clausola, includere l'header di una tabella ma nessuna riga, e finire prima della definizione che dà significato alla clausola. L'embedding di quel chunk sarà semanticamente incoerente. Anche un buon embedding model fatica a produrre retrieval utile da input incoerenti.
# Naive: fixed-size chunks regardless of document structure
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(chunk_size=512, chunk_overlap=50)
chunks = splitter.split_text(document_text) # splits anywhere — mid-sentence, mid-table
# Better: structure-aware splitting respects semantic boundaries
from langchain.text_splitter import MarkdownHeaderTextSplitter
splitter = MarkdownHeaderTextSplitter(
headers_to_split_on=[("##", "section"), ("###", "subsection")]
)
chunks = splitter.split_text(markdown_text) # splits at heading boundaries
La correzione non è una splitter library migliore: è capire la struttura dei documenti e scegliere una strategia che preservi le unità semantiche. Per i contratti finanziari: split a livello di clausola. Per la documentazione API: una funzione per chunk. Per le trascrizioni di meeting: split per turno di speaker. Per prosa densa: livello paragrafo con overlap di frasi.
Il secondo problema strutturale è il metadata. Un chunk senza provenance — document ID, section title, creation date, source type — è solo testo sospeso. Se il retrieval restituisce un chunk da una policy superata, non puoi filtrarlo a query time senza metadata. Aggiungi source, date, section e document type a ogni chunk. Filtra prima della similarity search quando puoi; filtra dopo quando devi.
In un sistema legal Q&A che avevo costruito, la correzione del chunking ha portato la context recall dal 58% al 79% sul mio benchmark RAGAS interno, costruito con query rappresentative e giudizi di rilevanza mantenuti costanti tra run. Non avevo toccato l'embedding model.
3. Re-ranking: lo step di retrieval che molte pipeline saltano
La vector similarity search recupera documenti i cui embedding sono vicini al tuo query embedding in uno spazio ad alta dimensionalità. È una buona approssimazione della rilevanza semantica. Non è, da sola, un buon ranker.
Il problema è che la similarity basata su embedding viene calcolata indipendentemente per ogni documento: il modello vede la query e un documento alla volta. Un cross-encoder re-ranker vede la query e un candidate document insieme. Questo gli permette di modellare la loro interazione. È più lento, ma spesso fornisce un segnale di rilevanza migliore.
La pipeline pratica: recupera i top 50 candidate con vector search, poi riordina quei 50 con un cross-encoder per ottenere i top 5. I 5 finali vanno nella context window dell'LLM.
from sentence_transformers import CrossEncoder
# Stage 1: fast retrieval — top 50 candidates via vector search
candidates = vectorstore.similarity_search(query, k=50)
# Stage 2: cross-encoder re-ranking — top 5 from those 50
reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")
scores = reranker.predict([(query, doc.page_content) for doc in candidates])
ranked = sorted(zip(scores, candidates), key=lambda x: x[0], reverse=True)
top_5 = [doc for _, doc in ranked[:5]]
Quanto costa: inferenza cross-encoder su 50 candidate pair a query time. Con ms-marco-MiniLM-L-6-v2, nelle mie misurazioni locali ho visto un overhead nell'ordine di decine o poche centinaia di millisecondi, con variazioni forti tra CPU, GPU, batch size e runtime. È un costo spesso accettabile per workload Q&A asincroni o interattivi. Va invece misurato con attenzione per pipeline real-time con budget di latency stretto.
Per casi d'uso real-time in cui la latency del re-ranking è troppo alta, puoi usare un bi-encoder più forte a retrieval time, per esempio bge-large-en-v1.5, e saltare il cross-encoder. Scambi una parte di precision per latency. Il punto è rendere il tradeoff esplicito e misurarlo, invece di accettare una perdita implicita di precision da un retrieval debole senza re-ranking.
Nello stesso sistema legal Q&A, l'aggiunta del re-ranking ha portato la context precision dal 61% all'84% sullo stesso benchmark RAGAS interno, con query e giudizi di rilevanza invariati. Combinata con la correzione del chunking, la pipeline ha raggiunto precision 84% e recall 79% su quel benchmark.
4. Hybrid search: quando BM25 aiuta e quando no
La pure vector search tende a sottoperformare sulle query exact-match: product SKU, case number, nomi di persone, stringhe di versione specifiche. Se il corpus è pieno di identificatori, una componente keyword può essere necessaria.
Hybrid search combina dense retrieval, cioè vector similarity, con sparse retrieval, cioè BM25 keyword scoring. Poi unisce le liste di risultati, spesso con Reciprocal Rank Fusion. Molti vector database moderni — Qdrant, Weaviate, Pinecone — supportano questa modalità nativamente. Quando l'opzione è disponibile, preferisco l'implementazione nativa a una custom.
Il segnale per aggiungere BM25 si vede nei retrieval falliti. Se gli errori si concentrano su query con identificatori specifici — nomi, numeri, abbreviazioni — vale la pena valutare hybrid search. Se invece gli errori sono semantici, BM25 probabilmente non aiuterà. Per esempio: l'utente chiede "termination clause" e il documento dice "cessation of obligations". In quel caso è più utile rivedere chunking o embedding coverage.
Il parametro alpha, cioè il peso tra dense e sparse scoring, va calibrato sul tuo dataset reale. Parti da 0.5. Nella mia esperienza, i corpora finanziari e legali tendono spesso verso alpha 0.3–0.4, quindi più peso keyword. Le knowledge base general-purpose tendono più spesso verso 0.6–0.7, quindi più peso semantico. Misura il cambiamento con il tuo benchmark RAGAS prima di andare in produzione.
Un errore che vedo spesso: i team aggiungono hybrid search prima di aver sistemato il chunking, perché sembra più sofisticata e richiede meno modifiche all'indice. Ma hybrid search non risolve un problema di chunking. Se i chunk sono semanticamente incoerenti, BM25 ti darà chunk incoerenti recuperati con due metodi invece che uno.
5. Metadata filtering prima di scalare
Una failure mode compare solo a scala: il vector index cresce fino a milioni di documenti, la query latency aumenta, e la risposta istintiva è aggiungere compute. A volte serve. Spesso, però, il problema è che stai facendo full-corpus similarity search quando ti serve solo un subset.
Metadata filtering significa restringere il retrieval a un subset di documenti, prima o dopo la similarity search. Spesso costa meno di aggiungere hardware. Risolve anche problemi che un embedding model migliore non può risolvere, per esempio escludere documenti superati o limitare la ricerca a una categoria specifica.
Pre-filtering: "recupera solo documenti da Q3 2024", "recupera solo policy document per customer tier Premium". Questi filtri riducono la dimensione effettiva dell'indice prima dell'operazione costosa di similarity. Post-filtering: "filtra qualunque chunk il cui documento ha superseded: true", "filtra i chunk con un confidence score sotto soglia". Questi filtri ripuliscono risultati rumorosi dopo il retrieval.
Entrambi richiedono un metadata schema progettato a index time per i filtri che userai a query time. Conviene progettarlo prima di averne bisogno. Se indicizzi documenti con una data created_at, ma il tipo di query più frequente è "mostrami le regulation correnti" e non hai un campo effective_through, finirai per filtrare per data quando dovresti filtrare per policy status. Per correggerlo, potrebbe servirti un reindex completo.
6. Selezione dell'embedding model: una leva da valutare dopo la struttura
Dopo chunking, re-ranking e, se serve, hybrid search, la qualità del retrieval dovrebbe essere più chiara rispetto al baseline. A questo punto ha senso valutare l'embedding model con un benchmark riproducibile sul tuo corpus.
I casi in cui la scelta dell'embedding model può fare una differenza materiale sono abbastanza riconoscibili: vocabolario domain-specific che i modelli generali potrebbero non rappresentare bene — clinical notes, terminologia legale di nicchia, strumenti finanziari specializzati — e multilingual retrieval, dove il fine-tuning specifico per lingua può contare.
Per testo general-purpose in inglese, nella mia esperienza il gap tra embedding model competitivi è spesso più piccolo del gap tra un corpus ben chunked e uno mal chunked. Passare da un modello all'altro può aiutare, ma il risultato sulle query di produzione dipende dal corpus, dal chunking e dalla distribuzione delle query. Per questo lo tratto come un esperimento misurato, non come una correzione predefinita.
Quando vale la pena fare fine-tuning: se hai un labeled retrieval dataset, cioè coppie query → documento rilevante, fare fine-tuning di un bi-encoder sul tuo dominio può essere utile. Il costo però è reale: raccolta del dataset, training infrastructure, eval riproducibili e re-indexing dell'intero corpus a ogni aggiornamento del modello.
Prima di investire nel fine-tuning, esegui RAGAS dopo chunking e re-ranking. Se la recall resta bassa, potrebbe esserci qualcosa di strutturale da correggere prima: chunk incoerenti, metadata insufficienti, corpus incompleto o query non rappresentate. Se invece la recall è già alta sul tuo benchmark, il fine-tuning potrebbe non giustificare il costo. Il caso più adatto è una pipeline già ben strutturata con un retrieval gap domain-specific documentato.
L'ordine che seguo
Retrieval-Augmented Generation viene spesso trattato come un monolite. In pratica è uno stack di scelte indipendenti: strategia di chunking, embedding model, meccanismo di retrieval — vector, keyword, hybrid — e re-ranker. Ognuno ha failure mode, struttura di costo e leve di tuning diverse.
L'ordine di debugging che seguo è: misura prima, sistema il chunking, aggiungi re-ranking, considera hybrid search se falliscono le keyword query, valuta l'embedding model per ultimo se serve. Questa sequenza aiuta a tenere gli interventi costosi per i problemi che li richiedono davvero.
Se stai eseguendo una pipeline RAG in produzione e i tuoi numeri RAGAS non corrispondono all'esperienza degli utenti — o ne stai costruendo una e vuoi evitare un ciclo di debugging lungo — scrivimi.