Chat basata sull'IA generativa con utenti e documenti in Java con PaLM e LangChain4J

1. Introduzione

Ultimo aggiornamento: 05/02/2024

Che cos'è l'AI generativa

Per AI generativa o intelligenza artificiale generativa si intende l'utilizzo dell'AI per la creazione di nuovi contenuti, come testo, immagini, musica, audio e video.

L'IA generativa sfrutta modelli di base (modelli IA di grandi dimensioni) in grado di eseguire più operazioni contemporaneamente e di eseguire operazioni pronte all'uso, tra cui riepilogo, domande e risposte, classificazione e altro ancora. Inoltre, con un addestramento minimo richiesto, i modelli di base possono essere adattati per casi d'uso mirati con pochissimi dati di esempio.

Come funziona l'AI generativa?

L'AI generativa utilizza un modello di machine learning per apprendere i pattern e le relazioni in un set di dati di contenuti creati dall'uomo. Quindi utilizza i pattern appresi per generare nuovi contenuti.

Il modo più comune per addestrare un modello di AI generativa consiste nell'utilizzare l'apprendimento supervisionato. Al modello viene assegnato un set di contenuti creati dall'uomo ed etichette corrispondenti. Poi, impara a generare contenuti simili a quelli creati dall'uomo ed etichettati con le stesse etichette.

Quali sono le applicazioni comuni dell'AI generativa?

L'IA generativa elabora ampi contenuti, creando insight e risposte tramite testo, immagini e formati facili da usare. L'IA generativa può essere utilizzata per:

  • Migliorare le interazioni con i clienti grazie a esperienze avanzate di chat e ricerca
  • Esplorare enormi quantità di dati non strutturati attraverso interfacce di conversazione e riassunti
  • Assistere nelle attività ripetitive come rispondere alle richieste di proposta (RFP), localizzare i contenuti di marketing in cinque lingue, verificare la conformità dei contratti con i clienti e altro ancora

Quali sono le offerte di AI generativa di Google Cloud?

Con Vertex AI, interagisci con i modelli di base, personalizzali e incorporali nelle tue applicazioni, senza bisogno di competenze di machine learning. Accedi ai modelli di base su Model Garden, ottimizza i modelli tramite una semplice UI su Generative AI Studio oppure utilizza i modelli in un blocco note di data science.

Vertex AI Search and Conversation offre agli sviluppatori il modo più rapido per creare motori di ricerca e chatbot basati sull'AI generativa.

Inoltre, Duet AI è il tuo collaboratore basato sull'AI, disponibile su Google Cloud e negli IDE per aiutarti a fare di più in tempi più rapidi.

Su cosa si concentra questo codelab?

Questo codelab si concentra sul modello linguistico di grandi dimensioni (LLM) PaLM 2, ospitato su Google Cloud Vertex AI, che comprende tutti i prodotti e servizi di machine learning.

Utilizzerai Java per interagire con l'API PaLM, insieme all'orchestratore del framework LLM LangChain4J. Esaminerai diversi esempi concreti per sfruttare l'LLM per rispondere a domande, generare idee, estrarre entità e contenuti strutturati e riassumere.

Scopri di più sul framework LangChain4J.

Il framework LangChain4J è una libreria open source per l'integrazione di modelli linguistici di grandi dimensioni nelle tue applicazioni Java, orchestrando vari componenti, come l'LLM stesso, ma anche altri strumenti come database vettoriali (per ricerche semantiche), caricatori e splitter di documenti (per analizzare i documenti e imparare da loro), analizzatori di output e altro ancora.

c6d7f7c3fd0d2951.png

Cosa imparerai a fare

  • Come configurare un progetto Java per utilizzare PaLM e LangChain4J
  • Come estrarre informazioni utili da contenuti non strutturati (estrazione di entità o parole chiave, output in formato JSON)
  • Come creare una conversazione con gli utenti
  • Come utilizzare il modello di chat per fare domande sulla tua documentazione

Che cosa ti serve

  • Conoscenza del linguaggio di programmazione Java
  • Un progetto Google Cloud
  • Un browser, ad esempio Chrome o Firefox

2. Configurazione e requisiti

Configurazione dell'ambiente autonomo

  1. Accedi alla console Google Cloud e crea un nuovo progetto o riutilizzane uno esistente. Se non hai ancora un account Gmail o Google Workspace, devi crearne uno.

295004821bab6a87.png

37d264871000675d.png

96d86d3d5655cdbe.png

  • Il nome del progetto è il nome visualizzato per i partecipanti a questo progetto. È una stringa di caratteri non utilizzata dalle API di Google. Puoi sempre aggiornarlo.
  • L'ID progetto è univoco in tutti i progetti Google Cloud ed è immutabile (non può essere modificato dopo l'impostazione). La console Cloud genera automaticamente una stringa univoca, di solito non ti interessa di cosa si tratta. Nella maggior parte dei codelab, dovrai fare riferimento all'ID progetto (in genere identificato come PROJECT_ID). Se non ti piace l'ID generato, puoi generarne un altro casuale. In alternativa, puoi provare a crearne uno e vedere se è disponibile. Non può essere modificato dopo questo passaggio e rimane per tutta la durata del progetto.
  • Per tua informazione, esiste un terzo valore, un numero di progetto, utilizzato da alcune API. Scopri di più su tutti e tre questi valori nella documentazione.
  1. Successivamente, devi abilitare la fatturazione in Cloud Console per utilizzare le risorse/API Cloud. Completare questo codelab non costa molto, se non nulla. Per arrestare le risorse ed evitare addebiti oltre a quelli previsti in questo tutorial, puoi eliminare le risorse che hai creato o il progetto. I nuovi utenti di Google Cloud possono usufruire del programma prova senza costi di 300$.

Avvia Cloud Shell

Sebbene Google Cloud possa essere gestito da remoto dal tuo laptop, in questo codelab utilizzerai Cloud Shell, un ambiente a riga di comando in esecuzione nel cloud.

Attiva Cloud Shell

  1. Nella console Cloud, fai clic su Attiva Cloud Shell d1264ca30785e435.png.

cb81e7c8e34bc8d.png

Se è la prima volta che avvii Cloud Shell, viene visualizzata una schermata intermedia che ne descrive le funzionalità. Se è stata visualizzata una schermata intermedia, fai clic su Continua.

d95252b003979716.png

Bastano pochi istanti per eseguire il provisioning e connettersi a Cloud Shell.

7833d5e1c5d18f54.png

Questa macchina virtuale è caricata con tutti gli strumenti di sviluppo necessari. Offre una home directory permanente da 5 GB e viene eseguita in Google Cloud, migliorando notevolmente le prestazioni e l'autenticazione della rete. Gran parte del lavoro per questo codelab, se non tutto, può essere svolto con un browser.

Una volta eseguita la connessione a Cloud Shell, dovresti vedere che il tuo account è autenticato e il progetto è impostato sul tuo ID progetto.

  1. Esegui questo comando in Cloud Shell per verificare che l'account sia autenticato:
gcloud auth list

Output comando

 Credentialed Accounts
ACTIVE  ACCOUNT
*       <my_account>@<my_domain.com>

To set the active account, run:
    $ gcloud config set account `ACCOUNT`
  1. Esegui questo comando in Cloud Shell per verificare che il comando gcloud conosca il tuo progetto:
gcloud config list project

Output comando

[core]
project = <PROJECT_ID>

In caso contrario, puoi impostarlo con questo comando:

gcloud config set project <PROJECT_ID>

Output comando

Updated property [core/project].

3. Preparazione dell'ambiente di sviluppo

In questo codelab utilizzerai il terminale e l'editor di codice di Cloud Shell per sviluppare i tuoi programmi Java.

Abilita le API Vertex AI

  1. Nella console Google Cloud, assicurati che il nome del progetto sia visualizzato nella parte superiore della console Google Cloud. In caso contrario, fai clic su Seleziona un progetto per aprire il selettore di progetti e seleziona il progetto che ti interessa.
  2. Se non ti trovi nella sezione Vertex AI della console Google Cloud, segui questi passaggi:
  3. In Cerca, inserisci Vertex AI, quindi premi Invio
  4. Nei risultati di ricerca, fai clic su Vertex AI. Viene visualizzata la dashboard di Vertex AI.
  5. Fai clic su Abilita tutte le API consigliate nella dashboard di Vertex AI.

Verranno abilitate diverse API, ma la più importante per il codelab è aiplatform.googleapis.com, che puoi abilitare anche dalla riga di comando, nel terminale Cloud Shell, eseguendo questo comando:

$ gcloud services enable aiplatform.googleapis.com

Creazione della struttura del progetto con Gradle

Per creare gli esempi di codice Java, utilizzerai lo strumento di compilazione Gradle e la versione 17 di Java. Per configurare il progetto con Gradle, nel terminale Cloud Shell crea una directory (in questo caso, palm-workshop) ed esegui il comando gradle init in quella directory:

$ mkdir palm-workshop
$ cd palm-workshop

$ gradle init

Select type of project to generate:
  1: basic
  2: application
  3: library
  4: Gradle plugin
Enter selection (default: basic) [1..4] 2

Select implementation language:
  1: C++
  2: Groovy
  3: Java
  4: Kotlin
  5: Scala
  6: Swift
Enter selection (default: Java) [1..6] 3

Split functionality across multiple subprojects?:
  1: no - only one application project
  2: yes - application and library projects
Enter selection (default: no - only one application project) [1..2] 1

Select build script DSL:
  1: Groovy
  2: Kotlin
Enter selection (default: Groovy) [1..2] 1

Generate build using new APIs and behavior (some features may change in the next minor release)? (default: no) [yes, no] 

Select test framework:
  1: JUnit 4
  2: TestNG
  3: Spock
  4: JUnit Jupiter
Enter selection (default: JUnit Jupiter) [1..4] 4

Project name (default: palm-workshop): 
Source package (default: palm.workshop): 

> Task :init
Get more help with your project: https://docs.gradle.org/7.4/samples/sample_building_java_applications.html

BUILD SUCCESSFUL in 51s
2 actionable tasks: 2 executed

Creerai un'applicazione (opzione 2), utilizzando il linguaggio Java (opzione 3), senza utilizzare i sottoprogetti (opzione 1), utilizzando la sintassi Groovy per il file di build (opzione 1), senza utilizzare nuove funzionalità di build (opzione No), generando test con JUnit Jupiter (opzione 4) e per il nome del progetto puoi utilizzare palm-workshop e, analogamente, per il pacchetto di origine puoi utilizzare palm.workshop.

La struttura del progetto sarà la seguente:

├── gradle 
│   └── ...
├── gradlew 
├── gradlew.bat 
├── settings.gradle 
└── app
    ├── build.gradle 
    └── src
        ├── main
        │   └── java 
        │       └── palm
        │           └── workshop
        │               └── App.java
        └── test
            └── ...

Aggiorniamo il file app/build.gradle per aggiungere alcune dipendenze necessarie. Puoi rimuovere la dipendenza guava, se presente, e sostituirla con le dipendenze del progetto LangChain4J e della libreria di logging per evitare messaggi di avviso relativi alla mancanza del logger:

dependencies {
    // Use JUnit Jupiter for testing.
    testImplementation 'org.junit.jupiter:junit-jupiter:5.8.1'

    // Logging library
    implementation 'org.slf4j:slf4j-jdk14:2.0.9'

    // This dependency is used by the application.
    implementation 'dev.langchain4j:langchain4j-vertex-ai:0.24.0'
    implementation 'dev.langchain4j:langchain4j:0.24.0'
}

Esistono due dipendenze per LangChain4J:

  • uno sul progetto principale,
  • e uno per il modulo Vertex AI dedicato.

Per utilizzare Java 17 per compilare ed eseguire i nostri programmi, aggiungi il seguente blocco sotto il blocco plugins {}:

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

Un'altra modifica da apportare: aggiorna il blocco application di app/build.gradle per consentire agli utenti di eseguire l'override della classe principale da eseguire dalla riga di comando quando viene richiamato lo strumento di compilazione:

application {
    mainClass = providers.systemProperty('javaMainClass')
                         .orElse('palm.workshop.App')
}

Per verificare che il file di build sia pronto per eseguire l'applicazione, puoi eseguire la classe principale predefinita che stampa un semplice messaggio Hello World!:

$ ./gradlew run -DjavaMainClass=palm.workshop.App

> Task :app:run
Hello World!

BUILD SUCCESSFUL in 3s
2 actionable tasks: 2 executed

Ora puoi programmare con il modello linguistico di grandi dimensioni PaLM utilizzando il progetto LangChain4J.

Per riferimento, ecco come dovrebbe apparire ora il file di build app/build.gradle completo:

plugins {
    // Apply the application plugin to add support for building a CLI application in Java.
    id 'application'
}

java {
    toolchain {
        // Ensure we compile and run on Java 17
        languageVersion = JavaLanguageVersion.of(17)
    }
}

repositories {
    // Use Maven Central for resolving dependencies.
    mavenCentral()
}

dependencies {
    // Use JUnit Jupiter for testing.
    testImplementation 'org.junit.jupiter:junit-jupiter:5.8.1'

    // This dependency is used by the application.
    implementation 'dev.langchain4j:langchain4j-vertex-ai:0.24.0'
    implementation 'dev.langchain4j:langchain4j:0.24.0'
    implementation 'org.slf4j:slf4j-jdk14:2.0.9'
}

application {
    mainClass = providers.systemProperty('javaMainClass').orElse('palm.workshop.App')
}

tasks.named('test') {
    // Use JUnit Platform for unit tests.
    useJUnitPlatform()
}

4. Effettuare la prima chiamata al modello di chat di PaLM

Ora che il progetto è configurato correttamente, è il momento di chiamare l'API PaLM.

Crea una nuova classe denominata ChatPrompts.java nella directory app/src/main/java/palm/workshop (accanto alla classe App.java predefinita) e digita i seguenti contenuti:

package palm.workshop;

import dev.langchain4j.model.vertexai.VertexAiChatModel;
import dev.langchain4j.chain.ConversationalChain;

public class ChatPrompts {
    public static void main(String[] args) {
        VertexAiChatModel model = VertexAiChatModel.builder()
            .endpoint("us-central1-aiplatform.googleapis.com:443")
            .project("YOUR_PROJECT_ID")
            .location("us-central1")
            .publisher("google")
            .modelName("chat-bison@001")
            .maxOutputTokens(400)
            .maxRetries(3)
            .build();

        ConversationalChain chain = ConversationalChain.builder()
            .chatLanguageModel(model)
            .build();

        String message = "What are large language models?";
        String answer = chain.execute(message);
        System.out.println(answer);

        System.out.println("---------------------------");

        message = "What can you do with them?";
        answer = chain.execute(message);
        System.out.println(answer);

        System.out.println("---------------------------");

        message = "Can you name some of them?";
        answer = chain.execute(message);
        System.out.println(answer);
    }
}

In questo primo esempio, devi importare la classe VertexAiChatModel e ConversationalChain di LangChain4J per semplificare la gestione dell'aspetto multiturno delle conversazioni.

Successivamente, nel metodo main, configurerai il modello linguistico di chat utilizzando lo strumento di creazione per VertexAiChatModel per specificare:

  • l'endpoint
  • il progetto,
  • la regione,
  • l'editore,
  • e il nome del modello (chat-bison@001).

Ora che il modello linguistico è pronto, puoi preparare un ConversationalChain. Si tratta di un'astrazione di livello superiore offerta da LangChain4j per configurare insieme diversi componenti per gestire una conversazione, come il modello linguistico di chat stesso, ma potenzialmente altri componenti per gestire la cronologia della conversazione di chat o per collegare altri strumenti come i retriever per recuperare informazioni dai database vettoriali. Ma non preoccuparti, ci torneremo più avanti in questo codelab.

Poi, avrai una conversazione multi-turno con il modello di chat per porre diverse domande correlate. Prima ti chiedi cosa sono gli LLM, poi cosa puoi fare con loro e quali sono alcuni esempi. Nota come non devi ripeterti: l'LLM sa che "loro" si riferisce agli LLM nel contesto della conversazione.

Per riprendere la conversazione multi-turno, basta chiamare il metodo execute() nella catena, che lo aggiungerà al contesto della conversazione. Il modello di chat genererà una risposta e la aggiungerà anche alla cronologia della chat.

Per eseguire questa classe, esegui questo comando nel terminale Cloud Shell:

./gradlew run -DjavaMainClass=palm.workshop.ChatPrompts

Dovresti visualizzare un output simile a questo:

$ ./gradlew run -DjavaMainClass=palm.workshop.ChatPrompts
Starting a Gradle Daemon, 2 incompatible and 2 stopped Daemons could not be reused, use --status for details

> Task :app:run
Large language models (LLMs) are artificial neural networks that are trained on massive datasets of text and code. They are designed to understand and generate human language, and they can be used for a variety of tasks, such as machine translation, question answering, and text summarization.
---------------------------
LLMs can be used for a variety of tasks, such as:

* Machine translation: LLMs can be used to translate text from one language to another.
* Question answering: LLMs can be used to answer questions posed in natural language.
* Text summarization: LLMs can be used to summarize text into a shorter, more concise form.
* Code generation: LLMs can be used to generate code, such as Python or Java code.
* Creative writing: LLMs can be used to generate creative text, such as poems, stories, and scripts.

LLMs are still under development, but they have the potential to revolutionize a wide range of industries. For example, LLMs could be used to improve customer service, create more personalized marketing campaigns, and develop new products and services.
---------------------------
Some of the most well-known LLMs include:

* GPT-3: Developed by OpenAI, GPT-3 is a large language model that can generate text, translate languages, write different kinds of creative content, and answer your questions in an informative way.
* LaMDA: Developed by Google, LaMDA is a large language model that can chat with you in an open-ended way, answering your questions, telling stories, and providing different kinds of creative content.
* PaLM 2: Developed by Google, PaLM 2 is a large language model that can perform a wide range of tasks, including machine translation, question answering, and text summarization.
* T5: Developed by Google, T5 is a large language model that can be used for a variety of tasks, including text summarization, question answering, and code generation.

These are just a few examples of the many LLMs that are currently being developed. As LLMs continue to improve, they are likely to play an increasingly important role in our lives.

BUILD SUCCESSFUL in 25s
2 actionable tasks: 2 executed

PaLM ha risposto alle tue tre domande correlate.

Il generatore VertexAIChatModel ti consente di definire parametri facoltativi che hanno già alcuni valori predefiniti che puoi ignorare. Ecco alcuni esempi:

  • .temperature(0.2): per definire il livello di creatività della risposta (0 indica una creatività bassa e spesso più oggettiva, mentre 1 indica risultati più creativi)
  • .maxOutputTokens(50): nell'esempio, sono stati richiesti 400 token (3 token equivalgono a circa 4 parole), a seconda della lunghezza della risposta generata
  • .topK(20): per selezionare in modo casuale una parola tra un numero massimo di parole probabili per il completamento del testo (da 1 a 40)
  • .topP(0.95): per selezionare le parole possibili la cui probabilità totale è pari a quel numero in virgola mobile (compreso tra 0 e 1)
  • .maxRetries(3): se superi la quota di richieste per periodo di tempo, puoi fare in modo che il modello riprovi la chiamata 3 volte, ad esempio

5. Un chatbot utile con una personalità.

Nella sezione precedente, hai iniziato subito a fare domande al chatbot LLM senza fornire un contesto particolare. Tuttavia, puoi specializzare un chatbot in modo che diventi un esperto in una determinata attività o in un determinato argomento.

E come potete farlo? Preparando il terreno: spiegando all'LLM l'attività da svolgere, il contesto, magari fornendo alcuni esempi di ciò che deve fare, il ruolo che deve assumere, il formato in cui vuoi ricevere le risposte e, potenzialmente, un tono, se vuoi che la chatbot si comporti in un determinato modo.

Questo articolo sulla creazione di prompt illustra bene questo approccio con questa immagine:

8a4c67679dcbd085.png

https://medium.com/@eldatero/master-the-perfect-chatgpt-prompt-formula-c776adae8f19

Per illustrare questo punto, prendiamo spunto dai siti web prompts.chat, che elencano molte idee divertenti e interessanti per chatbot personalizzati che possono agire come:

C'è un esempio per trasformare un chatbot LLM in un giocatore di scacchi. Implementiamolo.

Aggiorna la classe ChatPrompts come segue:

package palm.workshop;

import dev.langchain4j.chain.ConversationalChain;
import dev.langchain4j.data.message.SystemMessage;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.vertexai.VertexAiChatModel;
import dev.langchain4j.store.memory.chat.InMemoryChatMemoryStore;

public class ChatPrompts {
    public static void main(String[] args) {
        VertexAiChatModel model = VertexAiChatModel.builder()
            .endpoint("us-central1-aiplatform.googleapis.com:443")
            .project("YOUR_PROJECT_ID")
            .location("us-central1")
            .publisher("google")
            .modelName("chat-bison@001")
            .maxOutputTokens(7)
            .maxRetries(3)
            .build();

        InMemoryChatMemoryStore chatMemoryStore = new InMemoryChatMemoryStore();

        MessageWindowChatMemory chatMemory = MessageWindowChatMemory.builder()
            .chatMemoryStore(chatMemoryStore)
            .maxMessages(200)
            .build();

        chatMemory.add(SystemMessage.from("""
            You're an expert chess player with a high ELO ranking.
            Use the PGN chess notation to reply with the best next possible move.
            """
        ));


        ConversationalChain chain = ConversationalChain.builder()
            .chatLanguageModel(model)
            .chatMemory(chatMemory)
            .build();

        String pgn = "";
        String[] whiteMoves = { "Nf3", "c4", "Nc3", "e3", "Dc2", "Cd5"};
        for (int i = 0; i < whiteMoves.length; i++) {
            pgn += " " + (i+1) + ". " + whiteMoves[i];
            System.out.println("Playing " + whiteMoves[i]);
            pgn = chain.execute(pgn);
            System.out.println(pgn);
        }
    }
}

Analizziamo la procedura passo per passo:

  • Sono necessari alcuni nuovi import per gestire la memoria della chat.
  • Istanzia il modello di chat, ma con un numero ridotto di token massimi (in questo caso 7), in quanto vogliamo solo generare la prossima mossa, non un intero trattato sugli scacchi.
  • Successivamente, crea un archivio di memoria della chat per salvare le conversazioni.
  • Crea una memoria della chat in finestra effettiva, per conservare le ultime mosse.
  • Nella memoria della chat, aggiungi un messaggio "di sistema" che indica al modello di chat chi deve essere (ad es. un esperto giocatore di scacchi). Il messaggio "sistema" aggiunge un po' di contesto, mentre i messaggi "utente" e "AI" sono la discussione vera e propria.
  • Crei una catena conversazionale che combina la memoria e il modello di chat.
  • Poi, abbiamo un elenco di mosse per il bianco, che stai iterando. La catena viene eseguita con la mossa successiva del bianco ogni volta e il modello di chat risponde con la mossa migliore successiva.

Quando esegui questa classe con queste mosse, dovresti visualizzare il seguente output:

$ ./gradlew run -DjavaMainClass=palm.workshop.ChatPrompts
Starting a Gradle Daemon (subsequent builds will be faster)

> Task :app:run
Playing Nf3
1... e5
Playing c4
2... Nc6
Playing Nc3
3... Nf6
Playing e3
4... Bb4
Playing Dc2
5... O-O
Playing Cd5
6... exd5 

Woh! PaLM sa giocare a scacchi? Beh, non esattamente, ma durante l'addestramento, il modello deve aver visto alcuni commenti di partite a scacchi o anche i file PGN (Portable Game Notation) di partite passate. È probabile che questo chatbot non vinca contro AlphaZero (l'AI che sconfigge i migliori giocatori di Go, Shogi e scacchi) e la conversazione potrebbe deragliare più avanti, con il modello che non ricorda lo stato effettivo della partita.

I modelli di chat sono molto potenti e possono creare interazioni avanzate con gli utenti, oltre a gestire varie attività contestuali. Nella prossima sezione, esamineremo un'attività utile: l'estrazione di dati strutturati dal testo.

6. Estrazione di informazioni da testo non strutturato

Nella sezione precedente, hai creato conversazioni tra un utente e un modello linguistico di chat. Tuttavia, con LangChain4J puoi anche utilizzare un modello di chat per estrarre informazioni strutturate da testo non strutturato.

Supponiamo che tu voglia estrarre il nome e l'età di una persona, data una biografia o una descrizione della persona. Puoi chiedere al modello linguistico di grandi dimensioni di generare strutture di dati JSON con un prompt modificato in modo intelligente (questa operazione è comunemente chiamata "prompt engineering").

Aggiorna il corso ChatPrompts come segue:

package palm.workshop;

import dev.langchain4j.model.vertexai.VertexAiChatModel;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.UserMessage;

public class ChatPrompts {

    static class Person {
        String name;
        int age;
    }

    interface PersonExtractor {
        @UserMessage("""
            Extract the name and age of the person described below.
            Return a JSON document with a "name" and an "age" property, \
            following this structure: {"name": "John Doe", "age": 34}
            Return only JSON, without any markdown markup surrounding it.
            Here is the document describing the person:
            ---
            {{it}}
            ---
            JSON: 
            """)
        Person extractPerson(String text);
    }

    public static void main(String[] args) {
        VertexAiChatModel model = VertexAiChatModel.builder()
            .endpoint("us-central1-aiplatform.googleapis.com:443")
            .project("YOUR_PROJECT_ID")
            .location("us-central1")
            .publisher("google")
            .modelName("chat-bison@001")
            .maxOutputTokens(300)
            .build();
        
        PersonExtractor extractor = AiServices.create(PersonExtractor.class, model);

        Person person = extractor.extractPerson("""
            Anna is a 23 year old artist based in Brooklyn, New York. She was born and 
            raised in the suburbs of Chicago, where she developed a love for art at a 
            young age. She attended the School of the Art Institute of Chicago, where 
            she studied painting and drawing. After graduating, she moved to New York 
            City to pursue her art career. Anna's work is inspired by her personal 
            experiences and observations of the world around her. She often uses bright 
            colors and bold lines to create vibrant and energetic paintings. Her work 
            has been exhibited in galleries and museums in New York City and Chicago.    
            """
        );

        System.out.println(person.name);
        System.out.println(person.age);
    }
}

Diamo un'occhiata ai vari passaggi di questo file:

  • Una classe Person è definita per rappresentare i dettagli che descrivono una persona (il suo nome e la sua età).
  • L'interfaccia PersonExtractor viene creata con un metodo che, data una stringa di testo non strutturata, restituisce un'istanza Person.
  • extractPerson() è annotato con un'annotazione @UserMessage che associa un prompt. Questo è il prompt che il modello utilizzerà per estrarre le informazioni e restituire i dettagli sotto forma di documento JSON, che verrà analizzato e convertito in un'istanza Person.

Ora esaminiamo i contenuti del metodo main():

  • Viene creata un'istanza del modello di chat.
  • Un oggetto PersonExtractor viene creato grazie alla classe AiServices di LangChain4J.
  • A questo punto, puoi semplicemente chiamare Person person = extractor.extractPerson(...) per estrarre i dettagli della persona dal testo non strutturato e ottenere un'istanza Person con il nome e l'età.

Ora esegui questa classe con il seguente comando:

$ ./gradlew run -DjavaMainClass=palm.workshop.ChatPrompts

> Task :app:run
Anna
23

Sì. Questa è Anna, ha 23 anni.

Ciò che è particolarmente interessante di questo approccio AiServices è che operi con oggetti fortemente tipizzati. Non interagisci direttamente con l'LLM della chat. Al contrario, utilizzi classi concrete, come la classe Person per rappresentare le informazioni personali estratte, e hai una classe PersonExtractor con un metodo extractPerson() che restituisce un'istanza Person. Il concetto di LLM è astratto e, in qualità di sviluppatore Java, manipoli solo classi e oggetti normali.

7. Retrieval-Augmented Generation: chatta con i tuoi documenti

Torniamo alle conversazioni. Questa volta potrai porre domande sui tuoi documenti. Creerai un chatbot in grado di recuperare informazioni pertinenti da un database di estratti dei tuoi documenti. Queste informazioni verranno utilizzate dal modello per "ancorare" le sue risposte, anziché cercare di generare risposte provenienti dal suo addestramento. Questo pattern è chiamato RAG, ovvero Retrieval Augmented Generation.

Nella Retrieval Augmented Generation, in breve, ci sono due fasi:

  1. Fase di importazione: i documenti vengono caricati, suddivisi in blocchi più piccoli e viene memorizzata una rappresentazione vettoriale (un "incorporamento vettoriale") in un "database vettoriale" in grado di eseguire ricerche semantiche.

6c5bb5cb2e3b8088.png

  1. Fase di query: ora gli utenti possono porre domande al chatbot sulla documentazione. Anche la domanda verrà trasformata in un vettore e confrontata con tutti gli altri vettori nel database. I vettori più simili sono in genere correlati semanticamente e vengono restituiti dal database vettoriale. Successivamente, all'LLM viene fornito il contesto della conversazione, gli snippet di testo che corrispondono ai vettori restituiti dal database e gli viene chiesto di basare la sua risposta su questi snippet.

2c279c506d7606cd.png

Preparazione dei documenti

Per questa nuova demo, porrai domande sull'architettura di rete neurale "transformer", introdotta da Google, che è il modo in cui vengono implementati oggi tutti i moderni modelli linguistici di grandi dimensioni.

Puoi recuperare il documento di ricerca che descrive questa architettura ("Attention is all you need") utilizzando il comando wget per scaricare il PDF da internet:

wget -O attention-is-all-you-need.pdf \
    https://proceedings.neurips.cc/paper_files/paper/2017/file/3f5ee243547dee91fbd053c1c4a845aa-Paper.pdf

Implementare una catena di recupero conversazionale

Esaminiamo passo dopo passo come creare l'approccio in due fasi, prima con l'importazione del documento e poi con il momento della query, quando gli utenti pongono domande sul documento.

Importazione di documenti

Il primo passaggio della fase di importazione del documento consiste nell'individuare il file PDF, scaricarlo e preparare un PdfParser per leggerlo:

PdfDocumentParser pdfParser = new PdfDocumentParser();
Document document = pdfParser.parse(
    new FileInputStream(new File("/home/YOUR_USER_NAME/palm-workshop/attention-is-all-you-need.pdf")));

Anziché creare il solito modello linguistico di chat, prima creerai un'istanza di un modello di "embedding". Si tratta di un modello e di un endpoint particolari il cui ruolo è creare rappresentazioni vettoriali di parti di testo (parole, frasi o persino paragrafi).

VertexAiEmbeddingModel embeddingModel = VertexAiEmbeddingModel.builder()
    .endpoint("us-central1-aiplatform.googleapis.com:443")
    .project("YOUR_PROJECT_ID")
    .location("us-central1")
    .publisher("google")
    .modelName("textembedding-gecko@001")
    .maxRetries(3)
    .build();

Successivamente, avrai bisogno di alcune classi con cui collaborare per:

  • Carica e dividi il documento PDF in blocchi.
  • Crea i vector embedding per tutti questi blocchi.
InMemoryEmbeddingStore<TextSegment> embeddingStore = 
    new InMemoryEmbeddingStore<>();

EmbeddingStoreIngestor storeIngestor = EmbeddingStoreIngestor.builder()
    .documentSplitter(DocumentSplitters.recursive(500, 100))
    .embeddingModel(embeddingModel)
    .embeddingStore(embeddingStore)
    .build();
storeIngestor.ingest(document);

EmbeddingStoreRetriever retriever = EmbeddingStoreRetriever.from(embeddingStore, embeddingModel);

Viene creata un'istanza di InMemoryEmbeddingStore, un database vettoriale in memoria, per archiviare i vector embedding.

Il documento è suddiviso in blocchi grazie alla classe DocumentSplitters. Il testo del file PDF verrà suddiviso in snippet di 500 caratteri, con una sovrapposizione di 100 caratteri (con il blocco successivo, per evitare di tagliare parole o frasi, in frammenti).

L'"ingestore" del negozio collega lo splitter di documenti, il modello di embedding per calcolare i vettori e il database vettoriale in memoria. Dopodiché, il metodo ingest() si occuperà dell'importazione.

Ora la prima fase è terminata, il documento è stato trasformato in blocchi di testo con i relativi vector embedding e memorizzato nel database vettoriale.

Fare domande

È ora di prepararsi a fare domande. Per avviare la conversazione, puoi creare il modello di chat abituale:

VertexAiChatModel model = VertexAiChatModel.builder()
    .endpoint("us-central1-aiplatform.googleapis.com:443")
    .project("YOUR_PROJECT_ID")
    .location("us-central1")
    .publisher("google")
    .modelName("chat-bison@001")
    .maxOutputTokens(1000)
    .build();

Avrai anche bisogno di una classe "recuperatore" che colleghi il database vettoriale (nella variabile embeddingStore) e il modello di embedding. Il suo compito è eseguire query sul database vettoriale calcolando un vector embedding per la query dell'utente, in modo da trovare vettori simili nel database:

EmbeddingStoreRetriever retriever = 
    EmbeddingStoreRetriever.from(embeddingStore, embeddingModel);

A questo punto, puoi creare un'istanza della classe ConversationalRetrievalChain (si tratta solo di un nome diverso per il pattern Retrieval Augmented Generation):

ConversationalRetrievalChain rag = ConversationalRetrievalChain.builder()
    .chatLanguageModel(model)
    .retriever(retriever)
    .promptTemplate(PromptTemplate.from("""
        Answer to the following query the best as you can: {{question}}
        Base your answer on the information provided below:
        {{information}}
        """
    ))
    .build();

Questa "catena" lega insieme:

  • Il modello di lingua della chat che hai configurato in precedenza.
  • Il recuperatore confronta una query di vector embedding con i vettori nel database.
  • Un modello di prompt indica esplicitamente che il modello di chat deve rispondere basando la sua risposta sulle informazioni fornite (ovvero gli estratti pertinenti della documentazione il cui embedding vettoriale è simile al vettore della domanda dell'utente).

Ora puoi finalmente fare le tue domande.

String result = rag.execute("What neural network architecture can be used for language models?");
System.out.println(result);
System.out.println("------------");

result = rag.execute("What are the different components of a transformer neural network?");
System.out.println(result);
System.out.println("------------");

result = rag.execute("What is attention in large language models?");
System.out.println(result);
System.out.println("------------");

result = rag.execute("What is the name of the process that transforms text into vectors?");
System.out.println(result);

Esegui il programma con:

$ ./gradlew run -DjavaMainClass=palm.workshop.ChatPrompts

Nell'output dovresti vedere la risposta alle tue domande:

The Transformer is a neural network architecture that can be used for 
language models. It is based solely on attention mechanisms, dispensing 
with recurrence and convolutions. The Transformer has been shown to 
outperform recurrent neural networks and convolutional neural networks on 
a variety of language modeling tasks.
------------
The Transformer is a neural network architecture that can be used for 
language models. It is based solely on attention mechanisms, dispensing 
with recurrence and convolutions. The Transformer has been shown to 
outperform recurrent neural networks and convolutional neural networks on a 
variety of language modeling tasks. The Transformer consists of an encoder 
and a decoder. The encoder is responsible for encoding the input sequence 
into a fixed-length vector representation. The decoder is responsible for 
decoding the output sequence from the input sequence. The decoder uses the 
attention mechanism to attend to different parts of the input sequence when 
generating the output sequence.
------------
Attention is a mechanism that allows a neural network to focus on specific 
parts of an input sequence. In the context of large language models, 
attention is used to allow the model to focus on specific words or phrases 
in a sentence when generating output. This allows the model to generate 
more relevant and informative output.
------------
The process of transforming text into vectors is called word embedding. 
Word embedding is a technique that represents words as vectors in a 
high-dimensional space. The vectors are typically learned from a large 
corpus of text, and they capture the semantic and syntactic relationships 
between words. Word embedding has been shown to be effective for a variety 
of natural language processing tasks, such as machine translation, question 
answering, and sentiment analysis.

La soluzione completa

Per facilitare la copia e l'incollatura, ecco il contenuto completo della classe ChatPrompts:

package palm.workshop;

import dev.langchain4j.chain.ConversationalRetrievalChain;
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.parser.PdfDocumentParser;
import dev.langchain4j.data.document.splitter.DocumentSplitters;
import dev.langchain4j.data.segment.TextSegment; 
import dev.langchain4j.model.input.PromptTemplate;
import dev.langchain4j.model.vertexai.VertexAiChatModel;
import dev.langchain4j.model.vertexai.VertexAiEmbeddingModel;
import dev.langchain4j.retriever.EmbeddingStoreRetriever;
import dev.langchain4j.store.embedding.EmbeddingStoreIngestor;
import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class ChatPrompts {
    public static void main(String[] args) throws IOException {
        PdfDocumentParser pdfParser = new PdfDocumentParser();
        Document document = pdfParser.parse(new FileInputStream(new File("/ABSOLUTE_PATH/attention-is-all-you-need.pdf")));

        VertexAiEmbeddingModel embeddingModel = VertexAiEmbeddingModel.builder()
            .endpoint("us-central1-aiplatform.googleapis.com:443")
            .project("YOUR_PROJECT_ID")
            .location("us-central1")
            .publisher("google")
            .modelName("textembedding-gecko@001")
            .maxRetries(3)
            .build();

        InMemoryEmbeddingStore<TextSegment> embeddingStore = 
            new InMemoryEmbeddingStore<>();

        EmbeddingStoreIngestor storeIngestor = EmbeddingStoreIngestor.builder()
            .documentSplitter(DocumentSplitters.recursive(500, 100))
            .embeddingModel(embeddingModel)
            .embeddingStore(embeddingStore)
            .build();
        storeIngestor.ingest(document);

        EmbeddingStoreRetriever retriever = EmbeddingStoreRetriever.from(embeddingStore, embeddingModel);

        VertexAiChatModel model = VertexAiChatModel.builder()
            .endpoint("us-central1-aiplatform.googleapis.com:443")
            .project("genai-java-demos")
            .location("us-central1")
            .publisher("google")
            .modelName("chat-bison@001")
            .maxOutputTokens(1000)
            .build();

        ConversationalRetrievalChain rag = ConversationalRetrievalChain.builder()
            .chatLanguageModel(model)
            .retriever(retriever)
            .promptTemplate(PromptTemplate.from("""
                Answer to the following query the best as you can: {{question}}
                Base your answer on the information provided below:
                {{information}}
                """
            ))
            .build();

        String result = rag.execute("What neural network architecture can be used for language models?");
        System.out.println(result);
        System.out.println("------------");

        result = rag.execute("What are the different components of a transformer neural network?");
        System.out.println(result);
        System.out.println("------------");

        result = rag.execute("What is attention in large language models?");
        System.out.println(result);
        System.out.println("------------");

        result = rag.execute("What is the name of the process that transforms text into vectors?");
        System.out.println(result);
    }
}

8. Complimenti

Congratulazioni, hai creato la tua prima applicazione di chat con l'IA generativa in Java utilizzando LangChain4J e l'API PaLM. Nel corso del tempo, hai scoperto che i modelli di chat basati su LLM sono piuttosto potenti e in grado di gestire varie attività come domande/risposte, anche nella tua documentazione, estrazione di dati e, in una certa misura, sono persino in grado di giocare a scacchi.

Passaggi successivi

Dai un'occhiata ai seguenti codelab per approfondire l'utilizzo di PaLM in Java:

Further reading

Documenti di riferimento