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

1. Introduzione

Ultimo aggiornamento: 2024-02-05

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, i modelli di base possono essere adattati per casi d'uso mirati con pochissimi dati di esempio.

Come funziona l'IA generativa?

L'IA generativa funziona utilizzando un modello di ML (machine learning) per apprendere i pattern e le relazioni in un set di dati di contenuti creati da persone fisiche. Quindi utilizza i pattern appresi per generare nuovi contenuti.

Il modo più comune per addestrare un modello di IA generativa è utilizzare l'apprendimento supervisionato: al modello viene assegnato un insieme di contenuti creati dall'uomo e le relative etichette. Poi, impara a generare contenuti simili a quelli creati dall'uomo ed etichettati con le stesse etichette.

Cosa sono le applicazioni comuni di IA 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:

  • Migliora le interazioni con i clienti grazie a esperienze di chat e ricerca avanzate
  • Esplora grandi quantità di dati non strutturati attraverso interfacce conversazionali 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 offerte di IA generativa offre Google Cloud?

Con Vertex AI, puoi interagire con i modelli di base, personalizzarli e incorporarli nelle tue applicazioni, il che richiede poca o nessuna esperienza in termini di ML. Accedi ai modelli di base su Model Garden, ottimizza i modelli tramite una semplice UI su Generative AI Studio o 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'IA generativa.

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

Su cosa si concentra questo codelab?

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

Utilizzerai Java per interagire con l'API PaLM, insieme allo strumento di orchestrazione del framework LLM LangChain4J. Osserverai diversi esempi concreti per sfruttare l'LLM per la risposta alle domande, la generazione di idee, l'estrazione di entità e contenuti strutturati e il riassunto.

Vorrei maggiori informazioni sul framework LangChain4J.

Il framework LangChain4J è una libreria open source per integrare modelli linguistici di grandi dimensioni (LLM) nelle applicazioni Java, orchestrando vari componenti, come l'LLM stesso, ma anche altri strumenti come database vettoriali (per le ricerche semantiche), caricatori di documenti e splitter (per analizzare documenti e imparare da essi), parser 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 JSON)
  • Come creare una conversazione con gli utenti
  • Come utilizzare il modello di chat per porre 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 da seguire in modo 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 progetto è il nome visualizzato dei partecipanti del progetto. Si tratta di una stringa di caratteri non utilizzata dalle API di Google. Puoi sempre aggiornarla.
  • L'ID progetto è univoco in tutti i progetti Google Cloud ed è immutabile (non può essere modificato dopo essere stato impostato). La console Cloud genera automaticamente una stringa univoca. di solito non ti importa cosa sia. Nella maggior parte dei codelab, dovrai fare riferimento al tuo ID progetto (in genere identificato come PROJECT_ID). Se l'ID generato non ti soddisfa, potresti generarne un altro casuale. In alternativa, puoi provarne una personalizzata per verificare se è disponibile. Non può essere modificato dopo questo passaggio e rimane per tutta la durata del progetto.
  • Per informazione, c'è un terzo valore, un numero di progetto, utilizzato da alcune API. Scopri di più su tutti e tre questi valori nella documentazione.
  1. Successivamente, dovrai abilitare la fatturazione nella console Cloud per utilizzare risorse/API Cloud. L'esecuzione di questo codelab non ha alcun costo. Per arrestare le risorse ed evitare di incorrere in fatturazione dopo questo tutorial, puoi eliminare le risorse che hai creato o eliminare il progetto. I nuovi utenti di Google Cloud sono idonei al programma prova senza costi di 300$.

Avvia Cloud Shell

Mentre Google Cloud può essere utilizzato 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. Dalla console Cloud, fai clic su Attiva Cloud Shell d1264ca30785e435.png.

cb81e7c8e34bc8d.png

Se è la prima volta che avvii Cloud Shell, ti verrà mostrata una schermata intermedia che descrive di cosa si tratta. Se ti è stata presentata una schermata intermedia, fai clic su Continua.

d95252b003979716.png

Il provisioning e la connessione a Cloud Shell dovrebbero richiedere solo qualche istante.

7833d5e1c5d18f54.png

Questa macchina virtuale viene 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 di rete e l'autenticazione. Gran parte, se non tutto, del lavoro in questo codelab può essere svolto con un browser.

Una volta stabilita la connessione a Cloud Shell, dovresti vedere che hai eseguito l'autenticazione e che 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 confermare che il comando gcloud è a conoscenza del 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 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 parte Vertex AI della console Google Cloud, segui questi passaggi:
  3. In Ricerca, inserisci Vertex AI e poi torna
  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.

Questa operazione abiliterà diverse API, ma la più importante per il codelab è aiplatform.googleapis.com, che puoi abilitare anche sulla 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 esempi di codice Java, utilizzerai lo strumento di creazione Gradle e la versione 17 di Java. Per configurare il tuo progetto con Gradle, crea una directory (qui, palm-workshop) nel terminale Cloud Shell 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 sottoprogetti (opzione 1), utilizzando la sintassi Groovy per il file di build (opzione 1), non utilizzare nuove caratteristiche di build (opzione no), generando test con JUnit Jupiter (opzione 4) e, per il nome del progetto, puoi utilizzare palm-workshop allo stesso modo.

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 con la libreria di log per evitare fastidiosi messaggi del logger mancanti:

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'
}

Ci sono 2 dipendenze per LangChain4J:

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

Per usare 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 dalla riga di comando quando richiamano lo strumento di build:

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 è tutto pronto per programmare con il modello di testo linguistico di grandi dimensioni (LLM) PaLM, utilizzando il progetto LangChain4J.

Come 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. Fare 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 (oltre 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 LangChain4J ConversationalChain per semplificare la gestione dell'aspetto multiturno delle conversazioni.

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

  • l'endpoint,
  • del progetto.
  • 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 in chat o per collegare altri strumenti come i retriever per recuperare informazioni da database vettoriali. Ma non preoccuparti: torneremo su questo argomento più avanti in questo codelab.

Quindi, creerai una conversazione in più passaggi con il modello di chat per porre diverse domande correlate. Prima ti chiedi di che si tratta degli LLM, poi chiedi cosa puoi fare con questi ultimi e quali sono alcuni esempi. Nota come non è necessario ripetere te stesso, l'LLM sa che "loro" nel contesto di quella conversazione.

Per accettare la conversazione in più passaggi, basta chiamare il metodo execute() della catena: questa verrà aggiunta al contesto della conversazione, il modello di chat genererà una risposta e la aggiungerà anche alla cronologia chat.

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

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

Dovresti vedere 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 3 domande correlate.

Lo strumento per la creazione di VertexAIChatModel consente di definire parametri facoltativi che hanno già alcuni valori predefiniti che puoi sostituire. Ecco alcuni esempi:

  • .temperature(0.2): per definire la creatività desiderata per la risposta (0 corrisponde a una creatività scarsa e spesso più fattuale, mentre 1 indica risultati più creativi)
  • .maxOutputTokens(50): nell'esempio, sono stati richiesti 400 token (3 token equivalgono circa a 4 parole), a seconda di quanto tempo vuoi che venga utilizzata la 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 possibili parole la cui probabilità totale somma la somma di quel numero in virgola mobile (compreso tra 0 e 1)
  • .maxRetries(3): se hai superato la richiesta per quota di tempo, puoi chiedere al modello di riprovare la chiamata 3 volte, ad esempio

5. Un chatbot utile con una personalità.

Nella sezione precedente, hai iniziato subito a porre domande al chatbot LLM senza fornire alcun contesto particolare. Tuttavia, puoi specializzarlo in modo da diventare un esperto di una particolare attività o di un particolare argomento.

E come potete farlo? Predisporre lo scenario: spiegando all'LLM l'attività da svolgere, il contesto, magari fornendo alcuni esempi di ciò che deve fare, quale utente tipo dovrebbe avere, in quale formato vorresti ricevere le risposte e potenzialmente un tono se vuoi che il chatbot si comporti in un certo modo.

Questo articolo sulla creazione di prompt illustra chiaramente questo approccio con questa grafica:

8a4c67679dcbd085.png

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

Per spiegare questo punto, prendi spunto dai siti web prompts.chat, che contengono un sacco di idee divertenti e divertenti di chatbot personalizzati con cui possono agire:

Esiste un esempio per trasformare un chatbot LLM in un giocatore di scacchi. Mettiamoci al lavoro.

Aggiorna il corso 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);
        }
    }
}

Analizziamola passo dopo passo:

  • Alcune nuove importazioni sono necessarie per gestire la memoria della chat.
  • Crei un'istanza del modello di chat, ma con un piccolo numero di token massimo (qui 7), dato che vogliamo generare solo la prossima mossa, non un'intera dissertazione sugli scacchi!
  • Successivamente, creerai un archivio di ricordi della chat in cui salvare le conversazioni in chat.
  • Puoi creare un ricordo della chat con le finestre, per conservare le ultime mosse.
  • Nella memoria della chat, aggiungi un "sistema" che indica al modello di chat chi dovrebbe essere (ad es. un giocatore di scacchi esperto). Il "sistema" messaggio aggiunge contesto, mentre "utente" e "IA" messaggi sono la discussione effettiva.
  • Crei una catena conversazionale che combina la memoria e il modello di chat.
  • Ecco un elenco di passaggi per il bianco, di cui stai eseguendo l'iterazione. La catena viene eseguita ogni volta con la mossa bianca successiva e il modello di chat risponde con la mossa migliore successiva.

Quando esegui questa classe con questi spostamenti, dovresti vedere 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 

Wow! PaLM sa come giocare a scacchi? Non esattamente, ma durante l'addestramento il modello deve aver visto alcuni commenti sulle partite a scacchi o persino i file PGN (Portable Game Notation) delle partite precedenti. Questo chatbot probabilmente non vincerà contro AlphaZero (l'IA che sconfigge i migliori giocatori di Go, Shogi e scacchi) e la conversazione potrebbe peggiorare più avanti, dato che il modello non ricorda davvero lo stato reale del gioco.

I modelli di chat sono molto potenti e possono creare interazioni avanzate con gli utenti e gestire varie attività contestuali. Nella prossima sezione, esamineremo un'attività utile: estrarre 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 della chat. Ma 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 di quella persona. Puoi indicare al modello LLM di generare strutture di dati JSON con un prompt ottimizzato in modo intelligente (comunemente noto come "prompt engineering").

Aggiornerai 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 in 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 creata.
  • extractPerson() è annotato con un'annotazione @UserMessage che associa un prompt all'elemento. Questo è il prompt che il modello utilizzerà per estrarre le informazioni e restituire i dettagli sotto forma di documento JSON, che verrà analizzato per te e non eseguirà il marshalling in un'istanza Person.

Ora diamo un'occhiata ai contenuti del metodo main():

  • Viene creata un'istanza del modello di chat.
  • Un oggetto PersonExtractor viene creato grazie alla classe AiServices di LangChain4J.
  • Quindi, 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 comando seguente:

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

> Task :app:run
Anna
23

Sì. Lei è Anna, ha 23 anni!

L'aspetto di particolare interesse di questo approccio AiServices è che operi con oggetti molto digitati. Non stai interagendo direttamente con l'LLM di chat. Al contrario, stai lavorando con classi concrete, come la classe Persona per rappresentare le informazioni personali estratte, e hai una classe PersonExtractor con un metodo extractPerson() che restituisce un'istanza Persona. La nozione di LLM è estrapolata e, come sviluppatore Java, stai semplicemente manipolando classi e oggetti normali.

7. Retrieval Augmented Generation: chattare con i 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 "base" le sue risposte, anziché tentare di generare risposte provenienti dall'addestramento. Questo pattern è chiamato RAG o generazione aumentata del recupero.

In Retrieval Augmented Generation, in breve, esistono due fasi:

  1. Fase di importazione: i documenti vengono caricati, suddivisi in blocchi più piccoli e una rappresentazione vettoriale ("incorporamento vettoriale") viene archiviata 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 generalmente semanticamente correlati e vengono restituiti dal database vettoriale. All'LLM viene poi fornito il contesto della conversazione, gli snippet di testo che corrispondono ai vettori restituiti dal database e gli viene chiesto di stabilire la risposta esaminando questi snippet.

2c279c506d7606cd.png

Preparare i documenti

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

Puoi recuperare il documento di ricerca che ha descritto 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

Implementazione di una catena di recupero delle conversazioni

Vediamo, pezzo per pezzo, come creare l'approccio in due fasi, prima con l'importazione del documento, poi la quantità di tempo in cui gli utenti pongono domande sul documento.

Importazione di documenti

Il primo passaggio della fase di importazione dei documenti consiste nell'individuazione del file PDF che viene scaricato e nella preparazione di un PdfParser per la lettura:

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 "incorporamento". Si tratta di un particolare modello e endpoint, 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 alcuni corsi per collaborare insieme al fine di:

  • Carica e dividi il documento PDF in blocchi.
  • Crea incorporamenti vettoriali per tutti questi chunk.
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 gli incorporamenti vettoriali.

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

Il negozio "importatore" collega la funzionalità di suddivisione documenti, il modello di incorporamento per calcolare i vettori e il database vettoriale in memoria. Successivamente, il metodo ingest() si occuperà di eseguire l'importazione.

Ora, la prima fase è terminata, il documento è stato trasformato in blocchi di testo con i relativi incorporamenti vettoriali associati e archiviato nel database vettoriale.

Porre domande

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

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 "retriever" che collegherà il database vettoriale (nella variabile embeddingStore) e il modello di incorporamento. Il suo compito è interrogare il database vettoriale calcolando un incorporamento vettoriale per la query dell'utente, al fine di trovare vettori simili nel database:

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

A questo punto, puoi creare un'istanza per la classe ConversationalRetrievalChain (è solo un nome diverso per il pattern di generazione aumentata del recupero):

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" unisce:

  • Il modello di linguaggio di chat che hai configurato in precedenza.
  • Il recuperatore confronta una query di incorporamento vettoriale 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 (ad esempio, gli estratti pertinenti della documentazione il cui incorporamento vettoriale è simile al vettore della domanda dell'utente).

E ora sei finalmente pronto a porre 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 semplificare il copia e incolla, ecco i contenuti completi del corso 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. Lungo il percorso hai scoperto che i modelli di chat linguistici di grandi dimensioni sono abbastanza potenti e in grado di gestire varie attività come domande e risposte, anche sulla tua documentazione, l'estrazione dei dati e, in una certa misura, sono stati persino in grado di giocare a scacchi.

Passaggi successivi

Dai un'occhiata ad alcuni dei seguenti codelab per andare oltre con PaLM in Java:

Per approfondire

Documenti di riferimento