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.
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.
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.
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.
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:
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.
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.
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).
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:
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:
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.
Sebbene estremamente potenti, i sistemi RAG non sono perfetti.
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.
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
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
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:
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:
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.
Costruire un RAG artigianale è stato illuminante. Ogni passo ci ha insegnato qualcosa sui compromessi tra semplicità e potenza, tra velocità e precisione.
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.
Guardando al breve termine, ci sono diverse aree su cui concentreremo i nostri sforzi:
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.
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.