L'impiego di Large Language Models (LLM) generativi nelle applicazioni Java sta emergendo come una soluzione potente per specifici casi d'uso, una risorsa che gli sviluppatori di applicazioni esperti dovrebbero assolutamente avere nel loro kit di strumenti. Tuttavia, un problema significativo che si presenta con questi modelli è la generazione di output di scarsa qualità, spesso "allucinati", ovvero privi di fondamento fattuale. Nel nostro percorso attraverso il panorama dell'IA generativa (GenAI), esamineremo il modello di soluzione Retrieval-augmented Generation (RAG), supportato da esempi di codice pratici. Come sempre, tutto sarà privo di componenti a pagamento o proprietarie e funzionerà al cento per cento in locale sul notebook dello sviluppatore.

L'evoluzione dell'IA generativa e i suoi limiti

L'intelligenza artificiale generativa (GenAI), i Large Language Models (LLM) e il machine learning si stanno affermando come approcci risolutivi in quasi tutti gli ambiti e gli strumenti lungo il processo di sviluppo software. Oltre a essere utilizzati come ausili o strumenti nello sviluppo e nella manutenzione di sistemi software, l'IA può essere impiegata come componente all'interno di tali sistemi. Dal punto di vista economico, l'utilizzo di potenti LLM open source per la generazione di soluzioni complesse potrebbe rivelarsi particolarmente interessante. Questa sembra essere una valutazione ampiamente condivisa dal mercato, come dimostrato dalla reazione delle borse mondiali all'apparizione di modelli OS come Deepseek R1 dalla Cina all'inizio dell'anno[1].

Nonostante la continua pubblicazione di modelli sempre più potenti, i modelli linguistici generativi all'avanguardia presentano ancora un problema significativo: incontrano difficoltà con compiti che richiedono conoscenze di dominio specialistiche. Tipicamente, gli LLM vengono addestrati su vasti dataset che includono contributi dai social media, libri, articoli scientifici e siti web "scrapti", permettendo loro di acquisire una potente conoscenza generale. Senza accesso a conoscenze esterne, tuttavia, un modello generativo è limitato a produrre risposte esclusivamente basate sulla conoscenza parametrica che ha appreso durante la sua fase di addestramento.

I dataset di addestramento per i modelli generativi sono inevitabilmente incompleti sotto vari aspetti, poiché non contengono, ad esempio, le seguenti informazioni:

  • Argomenti di nicchia: Conoscenze altamente specializzate o settoriali che non sono ampiamente diffuse nei dati di addestramento generali.
  • Sviluppi successivi alla data di taglio del dataset: Informazioni recenti o aggiornamenti che si sono verificati dopo la raccolta dei dati di addestramento.
  • Conoscenze da database o repository interni: Dati proprietari, specifici di un'azienda o di un'organizzazione, che non sono accessibili pubblicamente.

Questa carenza di conoscenze specializzate può portare a problemi in cui il modello genera informazioni imprecise o inventate. La tendenza a offrire informazioni false o inventate in modo convincente è nota come allucinazione e può causare danni finanziariamente significativi, e soprattutto danni alla reputazione, in casi d'uso commerciali dell'IA.

RAG: la soluzione alle carenze di conoscenza

Queste lacune nei dati di addestramento sono particolarmente pronunciate nell'addestramento di LLM OS generali. Tuttavia, nel panorama dell'elaborazione dati aziendale, le informazioni mancanti sono disponibili in misura più che sufficiente. La chiave per migliorare le prestazioni degli LLM generali in compiti specialistici e per ridurre le allucinazioni consiste, di conseguenza, nel fornire ai modelli informazioni aggiuntive che non erano incluse nei loro dati di addestramento.

Con la Generazione aumentata con recupero (RAG) è stato concepito un concetto di soluzione nel 2020 per tenere conto di questo aspetto[2]. RAG è uno dei diversi metodi per estendere le capacità degli LLM generativi con aspetti di conoscenza mancanti. Il vantaggio di questo concetto risiede nel fatto che i pesi del modello generativo sottostante non devono essere aggiornati. RAG consente ai modelli di accedere dinamicamente a dati esterni, migliorando così la precisione senza la necessità di un costoso e dispendioso retraining. Questo lo rende un'opzione di soluzione pratica per le applicazioni nell'EDP aziendale.

Alternative a RAG, come il fine-tuning, di solito modificano i pesi dell'LLM originale e richiedono quindi un (ri-)addestramento e, di conseguenza, l'infrastruttura corrispondente per il machine learning. RAG, invece, agisce come un ponte dinamico che arricchisce il prompt del modello con informazioni esterne rilevanti al momento dell'inferenza, preservando l'integrità e la stabilità del modello pre-addestrato e riducendo drasticamente i costi operativi e i tempi di implementazione per casi d'uso specifici.

Architettura e componenti principali di RAG

Fondamentalmente, il concetto RAG è un modello a pipeline composto da due fasi e tre componenti fondamentali (Figura 1, se fosse presente). Analizziamo in dettaglio ciascuno di questi elementi.

1. Database di conoscenza di dominio esterno

La prima componente è il Database di conoscenza di dominio esterno. La conoscenza di dominio esterna è la conoscenza non parametrica che aggiungiamo alla conoscenza parametrica interna degli LLM tramite RAG. Fonti popolari di conoscenza esterna includono dati e documenti da database aziendali interni e pagine intranet. Anche altre fonti di dati private, come i sistemi di gestione documentale, possono essere utilizzate in RAG. I dati possono variare notevolmente per tema e formato e sono spesso specifici del compito. Il compito principale del database di conoscenza di dominio è fornire blocchi di dati rilevanti dalla conoscenza di dominio per svolgere un compito specifico. Ciò significa che il database di conoscenza di dominio rappresenta o contiene tutta la conoscenza esterna, ma fornisce solo porzioni rilevanti di tale conoscenza sotto forma di blocchi di dati per risolvere una richiesta concreta. A tal fine, di solito viene eseguita una ricerca semantica basata sulla richiesta concreta sull'intero database di conoscenza. Per questa ricerca vengono spesso utilizzati i database vettoriali.

I database vettoriali sono cruciali in questo contesto perché consentono di archiviare rappresentazioni numeriche (vettori di embedding) di testi o altri dati. Quando una query viene posta, anch'essa viene convertita in un vettore, e il database può quindi trovare rapidamente i vettori più "vicini" (semanticamente simili) tra quelli archiviati. Questo processo è molto più efficiente e preciso rispetto a una ricerca per parole chiave tradizionale quando si tratta di trovare informazioni contestualmente rilevanti.

2. Prompt Template

La seconda componente è il Prompt Template. I prompt sono gli input testuali che utilizziamo per inviare le nostre richieste ai modelli generativi. I prompt possono contenere diversi elementi, ma tipicamente includono una query, istruzioni e un contesto che guida il modello nella generazione di una risposta rilevante. Un Prompt Template è un modello strutturato per creare prompt standardizzati, in cui possono essere inserite sia la richiesta originale dell'utente sia i suoi contesti di conoscenza esterni. In sostanza, il Prompt Template funge da ponte tra i dati esterni e il modello, fornendo al modello informazioni contestualmente rilevanti durante l'inferenza per generare una risposta più precisa. Un template semplice per un LLM in lingua inglese potrebbe apparire come mostrato nel Listing 1.

Listing 1: Esempio di un Prompt Template semplice

prompt_template = "Context information is below.\n"
                  "---------------------\n"
                  "{context_str}\n"
                  "---------------------\n"
                  "Given the context information and not prior knowledge, "
                  "answer the query.\n"
                  "Query: {query_str}\n"
                  "Answer: "

In questo esempio, {context_str} sarà riempito con i blocchi di conoscenza recuperati dal database di dominio esterno, e {query_str} con la domanda originale dell'utente. Le istruzioni nel template (ad esempio, "Given the context information and not prior knowledge, answer the query") sono fondamentali per guidare il comportamento dell'LLM, assicurando che si basi sulle informazioni fornite anziché allucinare.

3. LLM generativo

La componente finale in RAG è l'LLM generativo, che viene utilizzato per generare una risposta finale alla richiesta originale dell'utente. L'input esteso, arricchito con informazioni dal database di conoscenza esterno tramite il prompt template, viene inviato al modello, che genera una risposta combinando la conoscenza interna del modello con i dati esterni appena recuperati. Questo processo garantisce che la risposta sia sia linguisticamente coerente (grazie alle capacità innate dell'LLM) sia fattualmente accurata e aggiornata (grazie all'arricchimento RAG).

Le due fasi operative di RAG: Ingestion e Retrieval

In una pipeline RAG, sulla base dell'input originale (prompt), vengono recuperati blocchi di dati rilevanti dal database di conoscenza di dominio esterno e inseriti in un prompt template, estendendo così l'input originale con conoscenza di dominio non parametrica. L'input così esteso viene poi inviato al grande modello linguistico generativo per la generazione dell'output finale[2].

Dopo aver trattato le tre componenti principali del concetto RAG, vediamo ora le due fasi del suo funzionamento.

Fase 1: Ingestion

Nella prima fase, l'Ingestion, è necessario innanzitutto preparare il database di conoscenza di dominio esterno. Questo processo è cruciale per la successiva efficienza e accuratezza della fase di recupero. Le fasi tipiche dell'ingestion includono:

  • Caricamento dei dati: Recupero di documenti, testi o altre fonti di conoscenza da varie origini (database interni, file system, API, ecc.).
  • Segmentazione (Chunking): I documenti di grandi dimensioni vengono suddivisi in segmenti più piccoli e gestibili, o "chunk". Questo è fondamentale perché gli LLM hanno limiti di contesto e recuperare interi documenti potrebbe essere inefficiente o superare la capacità del modello. La dimensione e la sovrapposizione dei chunk devono essere attentamente calibrate per garantire che ogni chunk contenga abbastanza contesto per essere significativo, ma non sia così grande da diluire la rilevanza o superare i limiti di input dell'LLM.
  • Creazione di embedding: Ogni chunk di testo viene convertito in una rappresentazione vettoriale numerica (embedding) utilizzando un modello di embedding. Questi vettori catturano il significato semantico del testo, permettendo al sistema di comprendere le relazioni concettuali tra diverse porzioni di testo.
  • Indicizzazione nel database vettoriale: Gli embedding vettoriali generati vengono quindi archiviati e indicizzati in un database vettoriale. Questo permette ricerche di similarità rapide ed efficienti. Ogni vettore è associato all'ID del chunk di testo originale, in modo che il testo effettivo possa essere recuperato una volta trovati i vettori simili.

Questa fase assicura che la conoscenza esterna sia strutturata in un formato facilmente accessibile e ricercabile semanticamente, pronta per essere utilizzata quando un utente interroga il sistema.

Fase 2: Retrieval (Recupero)

La seconda fase è il Retrieval, che avviene in tempo reale quando l'utente invia una query al sistema RAG. Questa fase è responsabile di trovare le informazioni più rilevanti dalla base di conoscenza esterna preparata nella fase di Ingestion. I passaggi chiave del Retrieval includono:

  • Embedding della query: La query dell'utente viene anch'essa convertita in una rappresentazione vettoriale (embedding) utilizzando lo stesso modello di embedding impiegato nella fase di Ingestion. Questo garantisce che la query e i chunk di testo siano nello stesso spazio vettoriale, permettendo un confronto significativo.
  • Ricerca di similarità: L'embedding della query viene utilizzato per eseguire una ricerca di similarità nel database vettoriale. Il database restituisce i N chunk di testo (ad esempio, i 5 o 10 più rilevanti) i cui vettori sono più simili all'embedding della query. Questa "similarità" è determinata da metriche come la distanza coseno.
  • Recupero dei chunk di testo originali: Una volta identificati i vettori più simili, il sistema recupera i corrispondenti chunk di testo originali dalla memoria o dal database associato. Questi sono i "blocchi di dati rilevanti" che contengono le informazioni contestuali necessarie.
  • Arricchimento del prompt: I chunk di testo recuperati vengono quindi inseriti nel prompt template, come mostrato nell'esempio del Listing 1. Questo crea un prompt "aumentato" che include sia la domanda originale dell'utente sia le informazioni contestuali pertinenti recuperate.

Il prompt arricchito viene poi inviato al Large Language Model generativo, che utilizza queste informazioni aggiuntive per formulare una risposta più accurata, informata e priva di allucinazioni. Questo ciclo di recupero dinamico permette agli LLM di superare i limiti della loro conoscenza pre-addestrata, rendendoli strumenti estremamente versatili e affidabili per una vasta gamma di applicazioni aziendali e specialistiche.

In sintesi, il RAG offre un approccio pratico ed efficace per potenziare gli LLM, rendendoli più utili in contesti reali senza la necessità di complessi e costosi processi di riaddestramento. La sua architettura modulare e la capacità di operare con componenti open source e in locale lo rendono una soluzione attraente per gli sviluppatori Java che desiderano integrare l'IA generativa nelle loro applicazioni in modo responsabile ed efficiente.