RAG — Retrieval-Augmented Generation — è diventato il pattern dominante per integrare conoscenza dominio-specifica nei modelli linguistici. Ma tra un tutorial “hello world” e un sistema RAG che funziona davvero in produzione c’è un abisso. Questo articolo esplora le insidie reali che i tutorial non affrontano, partendo da un’implementazione in .NET con Semantic Kernel.
Il pipeline RAG in cinque fasi
Prima di entrare nei dettagli critici, è utile avere una mappa mentale del pipeline completo. Un sistema RAG ben strutturato si articola in cinque fasi sequenziali:
- Ingestion — caricamento dei documenti sorgente
- Chunking — suddivisione in segmenti adatti all’embedding
- Embedding — conversione dei chunk in vettori numerici
- Storage — persistenza dei vettori in un vector store
- Retrieval + Generation — ricerca dei chunk rilevanti e generazione della risposta
Ogni fase nasconde decisioni non banali. Vediamo quelle che fanno davvero la differenza.
Il chunking è la variabile più sottovalutata
La qualità del chunking influenza il retrieval più di qualsiasi altra scelta, incluso il modello di embedding. La maggior parte dei tutorial usa split naive basati su caratteri o token fissi, ignorando la struttura semantica del documento.
Con Semantic Kernel, la scelta corretta per documenti Markdown è TextChunker.SplitMarkdownParagraphs(), che rispetta i confini dei paragrafi:
var chunks = TextChunker.SplitMarkdownParagraphs(
lines: markdownContent.Split('\n').ToList(),
maxTokensPerParagraph: 512,
overlapTokens: 50
);
Due parametri critici spesso ignorati:
- overlapTokens: senza sovrapposizione, le frasi che cadono al confine tra due chunk vengono perse. Una sovrapposizione del 10-15% (qui 50 token su 512) risolve il problema.
- Pre-processing HTML→Markdown: se i sorgenti sono pagine web, convertire in Markdown prima del chunking (con librerie come HtmlAgilityPack) elimina tag irrilevanti che degradano la qualità dell’embedding.
Un’altra best practice avanzata: separare i blocchi di codice dalla prosa e taggare ogni chunk con metadati (tipo di contenuto, sezione, sorgente) per poter filtrare durante il retrieval.
Le soglie di rilevanza non sono universali
I tutorial usano tipicamente soglie di similarità coseno tra 0.75 e 0.80. Questo valore è quasi sempre sbagliato per il tuo corpus specifico. La soglia ottimale dipende da: qualità dell’embedding model, distribuzione semantica del corpus, tipologia delle query.
L’approccio corretto:
- Costruire manualmente un set di valutazione di 20-30 coppie query/risposta attesa
- Partire da 0.70 e iterare misurando Context Recall
- Non affidarsi mai ai default senza validazione corpus-specifica
Scegliere il vector store giusto
Semantic Kernel supporta diversi backend. La scelta sbagliata genera complessità inutile o performance inadeguate:
- VolatileMemoryStore — solo per demo, dati persi al restart
- SqliteMemoryStore — sviluppo locale e prime versioni production: zero infrastruttura, persistenza garantita
- Elasticsearch — stack esistenti con ricerca ibrida (full-text + vettoriale)
- Azure AI Search — produzione su Azure, gestione scalabilità automatica
- Qdrant / Pinecone — carichi vettoriali dedicati ad alta scala
Per molte applicazioni aziendali, SQLite è la scelta razionale fino a migliaia di documenti. Aggiungere infrastruttura vettoriale dedicata ha senso solo con volumi e requisiti di latenza che lo giustificano effettivamente.
Evitare il re-embedding a ogni avvio
Uno degli errori più costosi (in termini economici e di latenza) è re-embeddare l’intero corpus a ogni riavvio dell’applicazione. La soluzione è semplice: verificare l’esistenza della collection prima di procedere all’ingestion:
var collections = await sqliteStore.GetCollectionsAsync().ToListAsync();
if (!collections.Contains(CollectionName))
{
await ragService.IngestDocumentsAsync(documents, CollectionName);
}
Con Azure AI Search o Qdrant, la logica è analoga ma si basa sulle API specifiche del provider.
Il prompt di grounding non è opzionale
La costruzione del prompt è la difesa principale contro le allucinazioni. C’è una differenza sostanziale tra queste due istruzioni:
- “Usa il contesto seguente per rispondere” — il modello può integrare con la sua conoscenza generale
- “Rispondi SOLO usando il contesto seguente. Se la risposta non è nel contesto, dillo esplicitamente.” — vincolo semantico forte
La parola “SOLO” cambia radicalmente il comportamento del modello. In produzione, il prompt di sistema deve essere esplicito e non ambiguo.
Semantic caching per ridurre latenza e costi
Un ottimizzazione ad alto impatto spesso ignorata: se una query è semanticamente simile a una già elaborata, si può restituire la risposta cached senza chiamare il vector store né il modello:
var cachedAnswer = await cacheService.FindSimilarAsync(query, threshold: 0.92f);
if (cachedAnswer != null)
{
return cachedAnswer.Answer;
}
Con una soglia alta (0.90-0.95), il cache serve solo query davvero simili, evitando risposte errate. Per sistemi con pattern di query ripetitivi (FAQ, assistenti documentali), questo ottimizzazione può ridurre i costi LLM del 40-60%.
Osservabilità: cosa monitorare
Un sistema RAG senza osservabilità è un sistema cieco. I KPI da tracciare per ogni richiesta:
- TopChunkScore: se costantemente sotto 0.75, il retrieval fatica. Rivedere chunking o embedding model.
- ChunksRetrieved: se raggiunge sempre il limite massimo, espandere la finestra di ricerca.
- CacheHit: se sempre false con alta latenza, la soglia del cache è troppo restrittiva.
- Latency: separare la latenza di retrieval da quella LLM per identificare il bottleneck.
Valutazione continua e CI
Il rischio silenzioso del RAG è la regressione: una modifica al chunking o alla soglia migliora alcune query e ne peggiora altre. La soluzione è integrare un set di valutazione nel pipeline CI con xUnit:
- Context Recall: i chunk corretti vengono recuperati?
- Faithfulness: la risposta rimane ancorata al contesto?
- Answer Correctness: corrisponde alla risposta attesa?
Il set di test deve includere query facili, domande che richiedono sintesi multi-documento, e domande a cui il sistema non dovrebbe rispondere (out-of-scope) — queste ultime sono fondamentali per rilevare allucinazioni.
Conclusione
Costruire un sistema RAG che funziona nei demo è relativamente semplice con Semantic Kernel. Costruirne uno che funziona in produzione — con costi controllati, latenza accettabile, assenza di allucinazioni e monitoraggio efficace — richiede decisioni architetturali precise. Il chunking con overlap, le soglie calibrate sul corpus reale, il caching semantico e l’osservabilità non sono optional: sono la differenza tra un prototipo e un sistema affidabile.
Fonte originale: RAG in .NET: What the Tutorials Don’t Tell You di Jamie Maguire.