RAG 100% naturale

Già da tempo, all’interno della nuova sezione Devmy AI Lab, ci siamo immersi nel mondo dell’intelligenza artificiale con curiosità e spirito sperimentale. L’obiettivo? Approcciare le tecnologie AI in modo pratico e consapevole, partendo da piccoli test e prototipi, senza la pretesa di reinventare la ruota, ma con la volontà di capirla fino in fondo.

Uno degli esperimenti più interessanti che abbiamo condotto è l’implementazione di un sistema RAG, ovvero Retrieval-Augmented Generation, ma con qualche caratteristica che esce un po’ fuori dal comune.

 

Cosa sono i sistemi RAG (e come funzionano davvero)

Immagina di porre una domanda a un assistente virtuale: invece di rispondere basandosi solo su ciò che "ricorda", va a consultare una libreria di documenti per trovare le informazioni più pertinenti e poi ti risponde. Questo è, in estrema sintesi, il cuore di un sistema Retrieval-Augmented Generation.

A livello tecnico, un RAG è diviso in due fasi fondamentali. Nella prima, i documenti vengono trasformati in rappresentazioni numeriche che catturano il loro significato: questi vettori (embedding) vengono salvati in un database specializzato (e noi scegliamo Redis Vector Set). Nella seconda fase, quando l’utente pone una domanda, questa viene convertita a sua volta in un embedding e confrontata con quelle dei documenti: quelli più simili vengono recuperati e utilizzati per costruire una risposta.

 

Come si preparano i documenti

Prima di essere trasformati in embedding, i documenti devono essere elaborati con cura. Questo step di preprocessing è spesso sottovalutato, ma è cruciale per la qualità del sistema. Si parte dalla pulizia del testo: rimuovere elementi superflui come tag HTML, caratteri strani, formattazioni inutili. Poi si passa alla segmentazione, o "chunking", ovvero la suddivisione dei testi in porzioni più piccole. Il chunking serve a migliorare la granularità del retrieval, rendendo il sistema più preciso nel recuperare la parte giusta del contenuto.

 

Chunking semantico: oltre la semplice divisione

Mentre alcuni sistemi spezzano i testi in blocchi fissi (ad esempio ogni 500 parole), un approccio più avanzato è quello del chunking semantico. Qui l’obiettivo non è semplicemente dividere il testo per dimensione, ma identificare blocchi di significato coerente. Questo tipo di chunking analizza la struttura del testo, paragrafi, titoli, sottotitoli, oppure la coerenza semantica delle frasi, per evitare che concetti correlati vengano separati. Il vantaggio è duplice: da un lato si riduce il rischio di perdita di contesto, dall’altro si aumentano le probabilità che l’embedding catturi un significato completo e utile.

L’implementazione del chunking semantico può avvenire in diversi modi: sfruttando modelli linguistici per l’analisi della coerenza, utilizzando regole basate sulla punteggiatura o sulla struttura grammaticale, o anche combinando più tecniche.

 

La magia degli embedding

Un embedding è una rappresentazione numerica di un testo. Può sembrare astratto, ma si tratta semplicemente di un vettore che racchiude il significato del testo in uno spazio multidimensionale. Se due testi parlano dello stesso argomento, i loro vettori saranno vicini tra loro. È proprio su questa vicinanza che si basa il meccanismo di retrieval. Per generare gli embedding si usano modelli pre-addestrati, come quelli di OpenAI, Cohere, o la famiglia Sentence Transformers open-source. La scelta del modello è cruciale e dipende da vari fattori:

  • Performance: l'accuratezza del modello nel catturare le sfumature semantiche per il tipo di testo specifico.
  • Dominio: se il contenuto dei documenti è molto tecnico, potrebbe essere utile un modello specializzato o fine-tunato per quel dominio.
  • Lingua: la copertura linguistica del modello.
  • Dimensioni e Velocità: modelli più grandi possono essere più accurati ma richiedono più risorse computazionali e sono più lenti.
  • Costo: l'utilizzo di API a pagamento o l'hosting di modelli open-source hanno implicazioni economiche diverse.

 

I Database Vettoriali

Gli embedding non vengono salvati in un database tradizionale, ma in uno vettoriale: un sistema progettato per eseguire ricerche basate sulla somiglianza semantica (spesso usando algoritmi come HNSW - Hierarchical Navigable Small World - per l'efficienza). Invece di cercare una parola chiave, questi database trovano i vettori più simili a quello della tua domanda. Esistono diverse soluzioni, da quelle open-source come FAISS (libreria), Chroma, Qdrant, Weaviate a servizi gestiti come Pinecone o database tradizionali che hanno esteso le loro funzionalità, come appunto Redis, PostgreSQL (con pgvector) o Elasticsearch. Abbiamo esplorato più a fondo le caratteristiche e le tipologie di questi database in un nostro precedente articolo dedicato all'argomento, a cui rimandiamo per maggiori dettagli.

 

Oltre la semantica pura: la ricerca ibrida

Un approccio sempre più comune è la ricerca ibrida (Hybrid Search). Questa tecnica combina la ricerca vettoriale (semantica) con la ricerca lessicale tradizionale (basata su parole chiave, come BM25). Il vantaggio è poter sfruttare il meglio di entrambi i mondi: la capacità della ricerca semantica di trovare documenti concettualmente simili anche se usano parole diverse, e la precisione della ricerca lessicale nel trovare corrispondenze esatte per termini specifici, nomi propri o acronimi, che la sola ricerca semantica potrebbe mancare. Molti database vettoriali moderni offrono funzionalità per implementare facilmente la ricerca ibrida.

 

Retrieval, Ranking e Generazione: una coreografia delicata

Quando arriva una query, il sistema la trasforma in un embedding (e/o ne estrae parole chiave per la ricerca ibrida) e interroga il database per trovare i documenti (o chunk) più simili/rilevanti. Una volta ottenuta una lista iniziale di candidati, questi documenti spesso passano attraverso una fase cruciale: il Ranking (o Re-ranking).

 

L'importanza del Ranking

Il retrieval iniziale basato sulla similarità vettoriale (o ibrida) è veloce ma potrebbe non essere perfetto. Potrebbe restituire documenti che sono semanticamente vicini ma non esattamente ciò che serve per rispondere alla domanda specifica, oppure l'ordine di rilevanza potrebbe non essere ottimale. Il re-ranking serve a riordinare questa lista iniziale in modo più accurato, utilizzando tecniche più sofisticate (ma computazionalmente più costose) applicate solo ai top-k documenti recuperati. Tecniche comuni includono:

  1. Cross-Encoders: a differenza dei modelli di embedding (bi-encoders) che processano query e documento separatamente, i cross-encoders li processano insieme, permettendo una valutazione più profonda della loro rilevanza reciproca. Generano un punteggio di rilevanza più accurato ma sono molto più lenti, rendendoli ideali per riordinare un piccolo numero di candidati.
  2. LLM come “giudice” (LLM-as-a-Judge/Ranker): si può usare un modello linguistico grande per valutare la rilevanza di ciascun documento recuperato rispetto alla query originale, generando un punteggio o semplicemente riordinando. Questa strategia sfrutta la capacità di comprensione del linguaggio dell'LLM ma può essere costosa e lenta.
  3. Modelli learning-to-rank (LTR): approcci più classici che addestrano modelli specifici per il ranking basati su un insieme di feature estratte dalla coppia query-documento.

 

La generazione della risposta e il prompt engineering

Infine, i documenti (o chunk) selezionati e riordinati vengono passati al modello generativo (LLM), insieme alla domanda originale, per costruire una risposta personalizzata. È fondamentale come questa informazione viene presentata all'LLM tramite il prompt. Un buon prompt deve:

  • Contenere chiaramente la domanda originale dell'utente.
  • Includere i contesti recuperati in modo strutturato.
  • Istruire l'LLM a basare la risposta esclusivamente sulle informazioni fornite (per ridurre le "allucinazioni").
  • Specificare il formato o lo stile desiderato per la risposta.

 

Esempio concettuale di struttura di prompt:

1Sei un assistente AI. Rispondi alla domanda dell'utente basandoti SOLO sui seguenti contesti forniti. 
2Se i contesti non contengono la risposta, dichiara di non avere informazioni sufficienti.
3
4Contesto 1: "[Contenuto del documento 1]"
5Contesto 2: "[Contenuto del documento 2]"
6...
7Contesto N: "[Contenuto del documento N]"
8
9Domanda Utente: "[Domanda originale dell'utente]"

È importante sottolineare che il modello generativo non impara permanentemente da questi documenti: li usa solo per quella specifica risposta, in un contesto effimero.

 

I limiti nascosti (e meno nascosti) dei sistemi RAG

Sebbene estremamente potenti, i sistemi RAG non sono perfetti.

  • Chunking: dividere i testi può portare a perdita di significato, specialmente se un concetto viene spezzato. Anche il chunking semantico è un compromesso tra granularità e completezza.
  • Qualità del retrieval: se la domanda è vaga o i documenti non sono pertinenti, il sistema potrebbe recuperare informazioni irrilevanti o nessuna informazione utile.
  • Rilevanza vs Similarità: la similarità semantica (vicinanza vettoriale) non sempre coincide perfettamente con la rilevanza per una specifica domanda. Da qui l'importanza del re-ranking.
  • Allucinazioni e Fedeltà: c'è il rischio che il modello generativo ignori i documenti recuperati ("lost in the middle" problem) o inventi dettagli non presenti nel contesto (allucinazioni). Un buon prompt engineering e tecniche specifiche cercano di mitigare questo problema.
  • Latenza: ogni passaggio (embedding, ricerca, ranking, generazione) aggiunge latenza. Ottimizzare le performance è cruciale per l'usabilità.
  • Costi operativi: l'uso di API per embedding e generazione, l'hosting di database vettoriali e modelli, e l'eventuale necessità di valutazione umana comportano costi che devono essere considerati.

 

La nostra esperienza: un RAG minimalista e la scelta dei riassunti

Nel Devmy AI Lab abbiamo deciso di metterci alla prova con un approccio radicale: niente framework, niente chunking tradizionale, niente black-box. Volevamo capire davvero cosa succede dietro le quinte.

 

Processo di embedding

Tutto inizia quando viene inserito un nuovo documento nella knowledge base. Questo viene analizzato da un modulo che chiameremo Document Process. Questo modulo interroga un modello linguistico locale (nel nostro caso, Llama3.2 gestito tramite Ollama) per generare uno o più riassunti significativi. I riassunti non sono semplici abstract: servono a catturare i concetti chiave in modo sintetico, così da rendere più efficiente l’indicizzazione e il confronto semantico, agendo di fatto come una forma di "chunking concettuale". Questo processo, specialmente la generazione dei riassunti, può essere oneroso in termini di tempo durante la prima indicizzazione di un vasto corpus di documenti.

Una volta ottenuti i riassunti, questi, insieme al riferimento al documento originale, vengono salvati in un database, nel nostro caso un DB documentale.

Poi inizia la vera e propria fase di embedding: i riassunti vengono trasformati in vettori numerici tramite un modello dedicato. Per questa operazione abbiamo utilizzato il modello bge-m3, anch'esso eseguito localmente tramite Ollama. I vettori così ottenuti vengono salvati in Redis Vector Sets, scelto per la sua semplicità, velocità e integrazione con il nostro stack Node.js. Questo processo di embedding dei riassunti avviene ogni volta che un nuovo documento viene aggiunto alla knowledge base o quando uno esistente viene modificato.

 

Processo di embedding

embedding.png

 

Processo di query

Quando arriva una domanda dall'utente, la query viene sottoposta allo stesso modello di embedding utilizzato per i riassunti (bge-m3 via Ollama) per convertirla in un vettore. Successivamente, viene effettuata la ricerca semantica nel nostro Vector Database Redis per trovare i riferimenti dei riassunti i cui vettori sono più simili a quello della query. Tramite questi identificativi, recuperiamo i testi dei riassunti corrispondenti dal DB documentale.

A questo punto, i riassunti recuperati e la query originale passano al ranking process. In questa fase, un LLM (nuovamente Llama3.2 via Ollama) valuta la rilevanza di ciascun riassunto rispetto alla domanda specifica e applica una logica di selezione per scartare i contenuti meno pertinenti e ordinare i più rilevanti.

Infine, dai riassunti selezionati e ordinati, vengono recuperati i documenti originali completi. Questi documenti vengono quindi inseriti in un prompt strutturato, insieme alla richiesta originale dell'utente, e inviati al modello generativo (Llama3.2) che produce la risposta finale.

Abbiamo consapevolmente evitato il chunking classico dei documenti interi, preferendo lavorare con riassunti generati da LLM. In questo modo miriamo a mantenere il contesto essenziale senza frammentare i concetti, e allo stesso tempo evitiamo la complessità di gestire e confrontare embedding di chunk potenzialmente meno significativi o incompleti. I riassunti, più compatti e focalizzati sui concetti chiave, permettono un retrieval potenzialmente più mirato, pur mantenendo il legame con il contenuto originale completo.

 

Processo di querying

processo di querying.png

 

Come valutare il successo?

Per mettere alla prova il nostro RAG "artigianale", abbiamo condotto una fase di valutazione strutturata. Abbiamo preparato un dataset di circa 100 richieste, progettato per coprire un ampio spettro di scenari:

  • Richieste specifiche: domande che puntavano a informazioni contenute in un singolo documento o sezione ben definita della knowledge base.
  • Richieste trasversali: quesiti che richiedevano di collegare informazioni presenti in punti diversi di uno o più documenti.
  • Richieste multi-documento complesse: domande la cui risposta completa necessitava della sintesi di informazioni provenienti da più documenti distinti.
  • Richieste fuori contesto (Out-of-Domain): domande su argomenti non trattati nella nostra knowledge base.

Le interazioni (domanda-risposta) sono state loggate e successivamente analizzate sia tramite valutazione umana (basata su criteri di correttezza, completezza, coerenza e rilevanza) sia utilizzando un LLM come valutatore (impiegando prompt specifici per stimare faithfulness e relevance).

I risultati sono stati incoraggianti, ma con sfumature interessanti:

  • Performance eccellente su richieste specifiche e trasversali: in questi casi, la qualità delle risposte è stata giudicata "molto buona". Il sistema è riuscito a recuperare i riassunti pertinenti e a generare risposte accurate e coerenti, dimostrando l'efficacia dell'approccio basato sui riassunti per query focalizzate.
  • Performance discreta su richieste multi-documento complesse: qui la valutazione è scesa a "discreta". Le risposte generate erano generalmente coerenti e fedeli ai contenuti recuperati, ma tendevano a non essere pienamente esaustive. L'ipotesi è che, pur recuperando i documenti corretti tramite i riassunti, il processo di ranking o la finestra di contesto del modello generativo abbiano limitato la capacità di sintetizzare tutte le informazioni rilevanti sparse nei diversi testi originali forniti nel prompt finale.
  • Gestione corretta delle richieste fuori contesto: il sistema ha identificato correttamente quando le informazioni richieste non erano presenti nella knowledge base, rispondendo in modo appropriato (es. "Non ho informazioni su questo argomento nei documenti a mia disposizione") senza tentare di inventare risposte (allucinare).

Questa valutazione, seppur preliminare, ci ha fornito indicazioni preziose sui punti di forza e sulle aree di miglioramento del nostro approccio basato sui riassunti, in particolare sulla sfida di garantire l'esaustività per query complesse che richiedono l'aggregazione di informazioni da molteplici fonti.

 

Cosa abbiamo imparato

Costruire un RAG artigianale è stato illuminante. Ogni passo ci ha insegnato qualcosa sui compromessi tra semplicità e potenza, tra velocità e precisione.

  • Lavorare con i riassunti invece del chunking offre un buon mantenimento del contesto generale, ma è da valutare il costo nel generare i riassunti e nel processo di re-ranking.
  • Redis si è rivelato efficace per la ricerca vettoriale di base, ma richiede attenzione per la gestione della memoria e l'ottimizzazione di query complesse, specialmente su larga scala.
  • Scrivere tutto da zero offre trasparenza ma richiede tempo, cura e una profonda comprensione di ogni componente, inclusi i costi associati a ciascun passaggio.
  • La valutazione è tanto importante quanto la costruzione: senza metriche chiare, è difficile migliorare iterativamente.

 

Prossimi passi e visioni future

L'esperimento condotto finora è solo il punto di partenza. Il mondo dei sistemi RAG è in continua evoluzione e le direzioni per migliorare ed espandere il nostro prototipo sono molteplici.

 

Ottimizzazioni e miglioramenti immediati

Guardando al breve termine, ci sono diverse aree su cui concentreremo i nostri sforzi:

  1. Implementazione di una Cache Semantica: una delle ottimizzazioni più promettenti è l'introduzione di un livello di caching intelligente. Per le domande più frequenti o per quelle semanticamente molto simili a query già processate, potremmo memorizzare la risposta generata (o i documenti recuperati e riordinati) direttamente in Redis, magari utilizzando i vector set per identificare rapidamente le query "vicine". Questo eviterebbe di "scomodare" l'LLM per la generazione della risposta ogni volta, riducendo significativamente la latenza e i costi computazionali per le richieste comuni, pur mantenendo la capacità di generare risposte fresche per query nuove o significativamente diverse.
  2. Affinamento del Processo di Ranking: esploreremo tecniche di re-ranking più sofisticate, magari utilizzando modelli cross-encoder ottimizzati per reranking (es.: cross-encoder/ms-marco-MiniLM-L-6-v2).
  3. Feedback Loop e miglioramento continuo: introdurre meccanismi per raccogliere feedback esplicito (es. "questa risposta è stata utile?") o implicito (es. click-through rate su documenti suggeriti) per affinare continuamente sia il retrieval che la generazione.

 

L'evoluzione dei sistemi RAG: l'influenza dei protocolli MCP e A2A

Guardando più avanti, l'integrazione dei sistemi RAG con protocolli emergenti come il Model Context Protocol (MCP) e la comunicazione Agent-to-Agent (A2A) è destinata a trasformarne profondamente l'architettura, l'efficienza e le applicazioni. Questi protocolli promettono di catalizzare una nuova generazione di sistemi RAG più collaborativi, intelligenti e consapevoli del contesto. I protocolli MCP (che affronteremo in dettaglio in un prossimo articolo) e A2A agiranno da catalizzatori, abilitando una collaborazione fluida e sicura tra modelli specializzati, agenti autonomi, fonti di dati eterogenee e interfacce utente.

Hai un progetto in mente?

Contattaci

Autore

Andrea Ortis

Co-Founder e CTO di Devmy, appassionato di tecnologia e innovazione e calciatore mancato.

Oltre a guidare lo sviluppo tecnologico dell’azienda, è sempre alla ricerca di sfide complesse, nuove architetture e soluzioni all’avanguardia. Quando non è concentrato sui progetti dell'azienda, lo troverete impegnato a sperimentare tecnologie emergenti, esplorare nuove tendenze o fare brainstorming su come rendere Devmy ancora più innovativa.

Devmy su linkedin

Ultimi Articoli