Chat com tecnologia de IA generativa com usuários e documentos em Java com PaLM e LangChain4J

1. Introdução

Última atualização:05/02/2024

O que é IA generativa?

IA generativa ou inteligência artificial generativa se refere ao uso de IA para criar novos conteúdos, como texto, imagens, música, áudio e vídeos.

Ela usa modelos de fundação (modelos de IA grandes) capazes de realizar várias tarefas ao mesmo tempo, além de resumos, perguntas e respostas, classificações e muito mais. Além disso, com o mínimo de treinamento necessário, os modelos de fundação podem ser adaptados para casos de uso específicos com poucos dados de exemplo.

Como funciona a IA generativa?

A IA generativa usa um modelo de ML (machine learning) para aprender os padrões e as relações em um conjunto de dados de conteúdo criado por humanos. Em seguida, ela usa os padrões aprendidos para gerar novo conteúdo.

A maneira mais comum de treinar um modelo de IA generativa é usar o aprendizado supervisionado, que recebe um conjunto de conteúdo criado por humanos e rótulos correspondentes. Em seguida, ele aprende a gerar conteúdo semelhante ao criado por humanos e rotulado com os mesmos rótulos.

Quais são as aplicações comuns da IA generativa?

A IA generativa processa um vasto conteúdo, criando insights e respostas por meio de texto, imagens e formatos amigáveis. A IA generativa pode ser usada para:

  • Melhore as interações com os clientes usando experiências aprimoradas de chat e pesquisa
  • Explore grandes quantidades de dados não estruturados usando interfaces de conversa e resumos
  • Ajude com tarefas repetitivas, como responder a pedidos de propostas (RFPs), localizar conteúdo de marketing para cinco idiomas, verificar compliance de contratos com clientes e muito mais

Quais ofertas de IA generativa o Google Cloud tem?

Com a Vertex AI, é possível interagir, personalizar e incorporar modelos de fundação nos seus aplicativos com pouca ou nenhuma experiência em ML. Acesse modelos de fundação no Model Garden, ajuste modelos com uma interface simples no Generative AI Studio ou use modelos em um notebook de ciência de dados.

O Vertex AI para Pesquisa e Conversação oferece aos desenvolvedores a maneira mais rápida de criar mecanismos de pesquisa e chatbots com tecnologia de IA generativa.

Além disso, a Duet AI é uma colaboradora com tecnologia de IA disponível no Google Cloud e em ambientes de desenvolvimento integrado para ajudar você a fazer mais em menos tempo.

Qual é o foco deste codelab?

Este codelab se concentra no modelo de linguagem grande (LLM) do PaLM 2, hospedado na Vertex AI do Google Cloud, que abrange todos os produtos e serviços de machine learning.

Você vai usar o Java para interagir com a API PaLM, junto com o orquestrador do framework de LLM LangChain4J. Você vai ver diferentes exemplos concretos para usar o LLM para responder perguntas, gerar ideias, extrair conteúdo estruturado e de entidades e fazer resumos.

Fale mais sobre o framework do LangChain4J.

O framework LangChain4J é uma biblioteca de código aberto para integrar modelos de linguagem grandes nos seus aplicativos Java, orquestrando vários componentes, como o próprio LLM, além de outras ferramentas, como bancos de dados vetoriais (para pesquisas semânticas), carregadores de documentos e divisores (para analisar documentos e aprender com eles), analisadores de saída e muito mais.

c6d7f7c3fd0d2951.png

O que você vai aprender

  • Como configurar um projeto Java para usar PaLM e LangChain4J.
  • Como extrair informações úteis de conteúdo não estruturado (extração de entidade ou palavra-chave, saída em JSON)
  • Como criar uma conversa com os usuários
  • Como usar o modelo de chat para fazer perguntas sobre sua própria documentação

O que é necessário

  • Conhecimento da linguagem de programação Java.
  • um projeto do Google Cloud;
  • Um navegador, como o Chrome ou o Firefox

2. Configuração e requisitos

Configuração de ambiente autoguiada

  1. Faça login no Console do Google Cloud e crie um novo projeto ou reutilize um existente. Crie uma conta do Gmail ou do Google Workspace, se ainda não tiver uma.

295004821bab6a87.png

37d264871000675d.png

96d86d3d5655cdbe.png

  • O Nome do projeto é o nome de exibição para os participantes do projeto. É uma string de caracteres não usada pelas APIs do Google e pode ser atualizada quando você quiser.
  • O ID do projeto precisa ser exclusivo em todos os projetos do Google Cloud e não pode ser mudado após a definição. O console do Cloud gera automaticamente uma string exclusiva. Em geral, não importa o que seja. Na maioria dos codelabs, é necessário fazer referência ao ID do projeto, normalmente identificado como PROJECT_ID. Se você não gostar do ID gerado, crie outro aleatório. Se preferir, teste o seu e confira se ele está disponível. Ele não pode ser mudado após essa etapa e permanece durante o projeto.
  • Para sua informação, há um terceiro valor, um Número do projeto, que algumas APIs usam. Saiba mais sobre esses três valores na documentação.
  1. Em seguida, ative o faturamento no console do Cloud para usar os recursos/APIs do Cloud. A execução deste codelab não vai ser muito cara, se tiver algum custo. Para encerrar os recursos e evitar cobranças além deste tutorial, exclua os recursos criados ou exclua o projeto. Novos usuários do Google Cloud estão qualificados para o programa de US$ 300 de avaliação sem custos.

Inicie o Cloud Shell

Embora o Google Cloud possa ser operado remotamente em um laptop, neste codelab você vai usar o Cloud Shell, um ambiente de linha de comando executado no Cloud.

Ativar o Cloud Shell

  1. No Console do Cloud, clique em Ativar o Cloud Shelld1264ca30785e435.png.

cb81e7c8e34bc8d.png

Se você estiver iniciando o Cloud Shell pela primeira vez, verá uma tela intermediária com a descrição dele. Se aparecer uma tela intermediária, clique em Continuar.

d95252b003979716.png

Leva apenas alguns instantes para provisionar e se conectar ao Cloud Shell.

7833d5e1c5d18f54.png

Essa máquina virtual tem todas as ferramentas de desenvolvimento necessárias. Ela oferece um diretório principal persistente de 5 GB, além de ser executada no Google Cloud. Isso aprimora o desempenho e a autenticação da rede. Grande parte do trabalho neste codelab, se não todo, pode ser feito em um navegador.

Depois de se conectar ao Cloud Shell, você verá sua autenticação e o projeto estará configurado com o ID do seu projeto.

  1. Execute o seguinte comando no Cloud Shell para confirmar se a conta está autenticada:
gcloud auth list

Resposta ao comando

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

To set the active account, run:
    $ gcloud config set account `ACCOUNT`
  1. Execute o seguinte comando no Cloud Shell para confirmar que o comando gcloud sabe sobre seu projeto:
gcloud config list project

Resposta ao comando

[core]
project = <PROJECT_ID>

Se o projeto não estiver configurado, configure-o usando este comando:

gcloud config set project <PROJECT_ID>

Resposta ao comando

Updated property [core/project].

3. Como preparar seu ambiente para desenvolvedores

Neste codelab, você vai usar o terminal do Cloud Shell e o editor de código para desenvolver programas em Java.

Ativar as APIs Vertex AI

  1. No console do Google Cloud, verifique se o nome do projeto é exibido na parte de cima do console do Google Cloud. Se não for, clique em Selecionar um projeto para abrir o Seletor de projetos e escolha o que você quer.
  2. Se você não estiver na parte da Vertex AI do console do Google Cloud, faça o seguinte:
  3. Em Pesquisa, digite Vertex AI e retorne
  4. Nos resultados da pesquisa, clique em Vertex AI. O painel da Vertex AI vai aparecer.
  5. Clique em Ativar todas as APIs recomendadas no painel da Vertex AI.

Isso vai ativar várias APIs, mas a mais importante para este codelab é a aiplatform.googleapis.com, que também pode ser ativada na linha de comando, no terminal do Cloud Shell, executando o seguinte comando:

$ gcloud services enable aiplatform.googleapis.com

Como criar a estrutura do projeto com o Gradle

Para criar seus exemplos de código Java, você vai usar a ferramenta de build Gradle e a versão 17 do Java. Para configurar seu projeto com o Gradle, no terminal do Cloud Shell, crie um diretório (aqui, palm-workshop) e execute o comando gradle init nesse diretório:

$ 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

Você vai criar um application (opção 2), usando a linguagem Java (opção 3), sem usar subprojetos (opção 1), usando a sintaxe Groovy para o arquivo de build (opção 1), não usar novos recursos de build (opção 1), gerar testes com JUnit Jupiter (opção 4) e, para o nome do projeto, usar palm-workshopm. Da mesma forma, use palm-workshop. Da mesma forma, use palm-workshop.

A estrutura do projeto será semelhante a esta:

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

Vamos atualizar o arquivo app/build.gradle para adicionar algumas dependências necessárias. Você pode remover a dependência guava, se ela estiver presente, e substituí-la pelas dependências do projeto LangChain4J e a biblioteca de geração de registros para evitar mensagens de logger ausentes:

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

Há duas dependências para o LangChain4J:

  • um no projeto principal,
  • e outra para o módulo dedicado da Vertex AI.

Se quiser usar o Java 17 para compilar e executar nossos programas, adicione o seguinte bloco abaixo do bloco plugins {}:

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

Mais uma mudança a ser feita: atualize o bloco application de app/build.gradle para permitir que os usuários substituam a classe principal a ser executada na linha de comando ao invocar a ferramenta de build:

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

Para verificar se o arquivo de build está pronto para executar seu aplicativo, execute a classe principal padrão, que mostra uma mensagem Hello World! simples:

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

> Task :app:run
Hello World!

BUILD SUCCESSFUL in 3s
2 actionable tasks: 2 executed

Agora você já pode programar com o modelo de texto de linguagem grande do PaLM usando o projeto LangChain4J.

Para referência, o arquivo de build app/build.gradle completo vai ficar assim:

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. Como fazer sua primeira chamada para o modelo de chat do PaLM

Agora que o projeto está configurado corretamente, é hora de chamar a API PaLM.

Crie uma nova classe com o nome ChatPrompts.java no diretório app/src/main/java/palm/workshop (ao lado da classe App.java padrão) e digite o seguinte conteúdo:

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);
    }
}

Neste primeiro exemplo, é necessário importar a classe VertexAiChatModel e o ConversationalChain do LangChain4J para facilitar o gerenciamento do aspecto de vários turnos das conversas.

Em seguida, no método main, você vai configurar o modelo de linguagem do chat usando o builder do VertexAiChatModel para especificar:

  • o endpoint,
  • o projeto,
  • da região,
  • o editor,
  • e o nome do modelo (chat-bison@001).

Agora que o modelo de linguagem está pronto, você pode preparar um ConversationalChain. Esta é uma abstração de nível superior oferecida pelo LangChain4J para configurar em conjunto diferentes componentes para lidar com uma conversa, como o próprio modelo de linguagem do bate-papo, mas possivelmente outros componentes para lidar com o histórico da conversa do bate-papo ou para conectar outras ferramentas, como os recuperadores, para buscar informações de bancos de dados de vetores. Mas não se preocupe, vamos voltar a isso mais tarde neste codelab.

Em seguida, você vai ter uma conversa de vários turnos com o modelo de chat para fazer várias perguntas inter-relacionadas. Primeiro você se pergunta sobre os LLMs, depois pergunta o que pode fazer com eles e quais são alguns exemplos deles. Observe que o LLM sabe que "eles" não precisa se repetir ou LLMs, no contexto dessa conversa.

Para processar essa conversa com vários turnos, basta chamar o método execute() na cadeia, que o adiciona ao contexto da conversa, o modelo de chat gera uma resposta e a adiciona ao histórico de chat.

Para executar essa classe, execute o seguinte comando no terminal do Cloud Shell:

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

O resultado deve ser parecido com este:

$ ./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

O PaLM respondeu às suas 3 perguntas relacionadas.

O builder VertexAIChatModel permite definir parâmetros opcionais que já têm alguns valores padrão que você pode substituir. Veja alguns exemplos:

  • .temperature(0.2): para definir o nível de criatividade que você quer que a resposta seja. Zero significa que ela é baixa e muitas vezes mais factual, enquanto 1 é para resultados mais criativos.
  • .maxOutputTokens(50): no exemplo, 400 tokens foram solicitados (três tokens são aproximadamente equivalentes a quatro palavras), dependendo de quanto tempo você quer que a resposta gerada seja.
  • .topK(20): para selecionar aleatoriamente uma palavra de um número máximo de palavras prováveis para o preenchimento do texto (de 1 a 40)
  • .topP(0.95): para selecionar as palavras possíveis cuja probabilidade total se soma ao número de ponto flutuante (entre 0 e 1)
  • .maxRetries(3): caso a cota esteja ultrapassada de solicitação por tempo, o modelo poderá repetir a chamada três vezes, por exemplo.

5. Um chatbot útil com uma personalidade!

Na seção anterior, você começou imediatamente a fazer perguntas ao chatbot do LLM sem dar nenhum contexto específico a ele. Mas você pode se especializar em um chatbot para se tornar especialista em uma tarefa ou tópico específico.

Como fazer isso? Ao definir o cenário: explique ao LLM a tarefa em questão, o contexto, talvez dê alguns exemplos do que ela precisa fazer, qual perfil deve ter, em que formato você quer receber respostas e, possivelmente, um tom, se quiser que o chatbot se comporte de uma determinada maneira.

Este artigo sobre a elaboração de comandos ilustra bem essa abordagem com este gráfico:

8a4c67679dcbd085.png

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

Para ilustrar isso, vamos nos inspirar nos sites do prompts.chat, que listam várias ideias incríveis e divertidas de chatbots personalizados que podem ser usados como:

Confira um exemplo de como transformar um chatbot de LLM em um jogador de xadrez. Vamos implementar isso.

Atualize a classe ChatPrompts desta forma:

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);
        }
    }
}

Vamos detalhar isso passo a passo:

  • Algumas novas importações são necessárias para processar a memória do chat.
  • Você instancia o modelo de chat, mas com um pequeno número máximo de tokens (aqui são sete), já que queremos gerar o próximo passo, e não uma dissertação inteira sobre xadrez.
  • Em seguida, você vai criar um armazenamento de memória de chat para salvar as conversas.
  • Você cria uma memória de chat em janela real para reter os últimos movimentos.
  • Na recordação do chat, adicione um "sistema" que instrui o modelo de chat sobre quem ele deve ser (por exemplo, um jogador de xadrez especialista). O "sistema" adiciona algum contexto, enquanto "usuário" e "IA" as mensagens são a discussão em si.
  • Você cria uma cadeia de conversação que combina a memória e o modelo de chat.
  • Depois, temos uma lista de movimentos para a cor branca em que você vai fazer iterações. A cadeia é executada com o próximo movimento branco toda vez, e o modelo de chat responde com o melhor movimento seguinte.

Ao executar essa classe com esses movimentos, você verá a seguinte saída:

$ ./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 

Nossa! O PaLM sabe jogar xadrez? Não exatamente, mas durante o treinamento o modelo deve ter visto alguns comentários de jogos de xadrez ou até mesmo arquivos PGN (Portable Game Notation) de jogos anteriores. No entanto, esse chatbot provavelmente não vencerá o AlphaZero (a IA que derrota os melhores jogadores de Go, Shogi e xadrez), e a conversa pode desmoronar mais adiante, com o modelo sem se lembrar muito do estado real do jogo.

Os modelos de chat são muito eficientes e podem criar interações avançadas com os usuários, além de processar várias tarefas contextuais. Na próxima seção, vamos conhecer uma tarefa útil: extrair dados estruturados do texto.

6. Extração de informações de texto não estruturado

Na seção anterior, você criou conversas entre um usuário e um modelo de linguagem de chat. Mas com o LangChain4J, você também pode usar um modelo de chat para extrair informações estruturadas de texto não estruturado.

Digamos que você queira extrair o nome e a idade de uma pessoa com base na biografia ou descrição dela. Você pode instruir o modelo de linguagem grande a gerar estruturas de dados JSON com um comando bem ajustado (geralmente chamado de "engenharia de comando").

Você atualizará a classe ChatPrompts da seguinte maneira:

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);
    }
}

Vamos dar uma olhada nas várias etapas nesse arquivo:

  • A classe Person é definida para representar os detalhes que descrevem uma pessoa (nome e idade).
  • A interface PersonExtractor é criada com um método que, considerando uma string de texto não estruturada, retorna uma instância Person instanciada.
  • O extractPerson() recebe uma anotação @UserMessage, que associa um comando a ele. Esse é o comando que o modelo vai usar para extrair as informações e retornar os detalhes em um documento JSON, que vai ser analisado e desmarcado em uma instância Person.

Agora, vamos conferir o conteúdo do método main():

  • O modelo de chat é instanciado.
  • Um objeto PersonExtractor é criado graças à classe AiServices do LangChain4J.
  • Em seguida, você pode simplesmente chamar Person person = extractor.extractPerson(...) para extrair os detalhes da pessoa do texto não estruturado e receber uma instância de Person com o nome e a idade.

Agora execute esta classe com o seguinte comando:

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

> Task :app:run
Anna
23

Sim. Esta é Anna, ela tem 23 anos!

O interessante da abordagem AiServices é que você opera com objetos fortemente tipados. Você não está interagindo diretamente com o LLM do chat. Em vez disso, você está trabalhando com classes concretas, como a classe Person para representar as informações pessoais extraídas, e tem uma classe PersonExtractor com um método extractPerson() que retorna uma instância Person. A noção de LLM é abstraída e, como desenvolvedor Java, você está apenas manipulando classes e objetos normais.

7. Geração Aumentada de Recuperação: conversar com seus documentos

Vamos voltar às conversas. Desta vez, você poderá fazer perguntas sobre seus documentos. Você vai criar um chatbot capaz de recuperar informações relevantes de um banco de dados de extrações dos seus documentos. Essas informações serão usadas pelo modelo para "fundamentar" as respostas, em vez de tentar gerar respostas com base no treinamento. Esse padrão é chamado de Geração Aumentada de Recuperação (RAG, na sigla em inglês).

Em resumo, a Geração Aumentada de Recuperação tem duas fases:

  1. Fase de ingestão: os documentos são carregados, divididos em blocos menores, e uma representação vetorial deles (um "embedding vetorial") é armazenada em um "banco de dados vetorial", capaz de fazer pesquisas semânticas.

6c5bb5cb2e3b8088.png

  1. Fase de consulta: os usuários agora podem fazer perguntas ao bot de bate-papo sobre a documentação. A pergunta também será transformada em um vetor e comparada com todos os outros vetores do banco de dados. Os vetores mais semelhantes geralmente estão semanticamente relacionados e são retornados pelo banco de dados de vetores. Em seguida, o LLM recebe o contexto da conversa, os snippets de texto que correspondem aos vetores retornados pelo banco de dados, e ele precisa fundamentar a resposta analisando esses snippets.

2c279c506d7606cd.png

Como preparar seus documentos

Nesta nova demonstração, você vai fazer perguntas sobre a arquitetura de rede neural "transformer", pioneira no Google, que é como todos os modelos modernos de linguagem grande são implementados atualmente.

Recupere o artigo de pesquisa que descreveu essa arquitetura ("Atenção é tudo que você precisa") usando o comando wget para fazer o download do PDF da Internet:

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

Como implementar uma cadeia de recuperação de conversação

Vamos analisar cada parte da criação da abordagem de duas fases: primeiro com a ingestão de documentos e depois o tempo de consulta em que os usuários fazem perguntas sobre o documento.

Ingestão de documentos

A primeira etapa da fase de ingestão de documentos é localizar o arquivo PDF para download e preparar um PdfParser para a leitura dele:

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

Antes disso, em vez de criar o modelo de linguagem de chat comum, você vai criar uma instância de um modelo de "embedding". Ele é um modelo e endpoint específico cujo papel é criar representações vetoriais de textos (palavras, frases ou mesmo parágrafos).

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();

Agora você vai precisar que algumas turmas colaborem juntas para:

  • Carregar e dividir o documento PDF em partes.
  • Criar embeddings de vetores para todos esses blocos.
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);

Uma instância do InMemoryEmbeddingStore, um banco de dados de vetores na memória, é criada para armazenar os embeddings do vetor.

O documento é dividido em partes graças à classe DocumentSplitters. Ele vai dividir o texto do arquivo PDF em trechos de 500 caracteres, com uma sobreposição de 100 caracteres (com o fragmento seguinte, para evitar o corte de palavras ou frases, em pedaços).

A loja "ingestor" vincula o divisor de documentos, o modelo de embedding para calcular os vetores e o banco de dados de vetores na memória. Em seguida, o método ingest() vai fazer a ingestão.

Agora, a primeira fase terminou, o documento foi transformado em blocos de texto com seus embeddings vetoriais associados e armazenado no banco de dados de vetores.

Como fazer perguntas

É hora de se preparar para fazer perguntas! O modelo de chat normal pode ser criado para iniciar a conversa:

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();

Você também precisará de uma classe "retriever" que vai vincular o banco de dados vetorial (na variável embeddingStore) e o modelo de embedding. A função dele é consultar o banco de dados de vetores para encontrar vetores semelhantes no banco de dados. Para isso, ele calcula um embedding vetorial para a consulta do usuário:

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

Neste ponto, você pode instanciar a classe ConversationalRetrievalChain. Esse é apenas um nome diferente para o padrão de geração aumentada de recuperação:

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();

Essa "cadeia" vinculam-se:

  • O modelo de linguagem do chat que você configurou anteriormente.
  • O retriever compara uma consulta de embedding vetorial com os vetores no banco de dados.
  • Um modelo de comando informa explicitamente que o modelo de chat precisa responder com base nas informações fornecidas, ou seja, nos trechos relevantes da documentação com uma incorporação de vetor semelhante ao vetor da pergunta do usuário.

Agora você já pode fazer perguntas!

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);

Execute o programa com:

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

Na saída, você verá a resposta para suas perguntas:

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.

A solução completa

Para facilitar o processo de copiar e colar, veja o conteúdo completo da 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. Parabéns

Parabéns, você criou seu primeiro aplicativo de chat de IA generativa em Java usando o LangChain4J e a API PaLM. Você descobriu ao longo do caminho que modelos de chat de linguagem grandes são muito poderosos e capazes de lidar com várias tarefas, como perguntas/respostas, até mesmo na sua própria documentação, extração de dados e, até certo ponto, conseguiu jogar xadrez.

Qual é a próxima etapa?

Confira alguns dos codelabs abaixo para saber mais sobre o PaLM em Java:

Leia mais

Documentos de referência