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 é a 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 a IA generativa funciona?

A IA generativa usa um modelo de ML (aprendizado de máquina) para aprender os padrões e as relações em um conjunto de dados de conteúdo criado por humanos. Os padrões aprendidos são então usados para gerar novo conteúdo.

A maneira mais comum de treinar um modelo de IA generativa é usar o aprendizado supervisionado: o modelo 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 com os mesmos rótulos.

Quais são os aplicativos comuns de 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:

  • Melhorar as interações com os clientes por meio de experiências de pesquisa e chat aprimoradas
  • Explorar grandes quantidades de dados não estruturados por meio de interfaces de conversação e resumos
  • Ajudar com tarefas repetitivas, como responder a solicitações de propostas (RFPs), localizar o conteúdo de marketing em cinco idiomas, verificar a conformidade dos contratos do cliente e muito mais

Quais são as opções de IA generativa com o Google Cloud?

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

A ferramenta de 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 ferramenta com tecnologia de IA disponível no Google Cloud e em ambientes de desenvolvimento integrado para ajudar você a produzir mais e com mais rapidez.

Qual é o foco deste codelab?

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

Você vai usar Java para interagir com a API PaLM, em conjunto com o orquestrador de framework de LLM LangChain4J. Você vai conferir diferentes exemplos concretos para aproveitar o LLM em respostas a perguntas, geração de ideias, extração de entidades e conteúdo estruturado e resumo.

Quero saber mais sobre o framework LangChain4J!

O framework LangChain4J é uma biblioteca de código aberto para integrar modelos de linguagem grandes aos seus aplicativos Java, orquestrando vários componentes, como o próprio LLM, mas também outras ferramentas, como bancos de dados vetoriais (para pesquisas semânticas), carregadores e divisores de documentos (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 o PaLM e o 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 seus 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 do seu laptop, neste codelab usaremos 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 esta for a primeira vez que você inicia o Cloud Shell, uma tela intermediária vai aparecer com a descrição dele. Se isso acontecer, clique em Continuar.

d95252b003979716.png

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

7833d5e1c5d18f54.png

Essa máquina virtual contém 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. Neste codelab, quase todo o trabalho pode ser feito com um navegador.

Depois de se conectar ao Cloud Shell, você vai ver que sua conta já está autenticada e que o projeto está 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 comando a seguir no Cloud Shell para confirmar se 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 e o editor de código do Cloud Shell para desenvolver seus programas em Java.

Ativar APIs da Vertex AI

  1. No console do Google Cloud, verifique se o nome do seu projeto é exibido na parte de cima do console do Google Cloud. Se não estiver, clique em Selecionar um projeto para abrir o Seletor de projetos e escolha o projeto pretendido.
  2. Se você não estiver na parte da Vertex AI do console do Google Cloud, faça o seguinte:
  3. Em Pesquisar, insira "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 o 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 nele:

$ 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 aplicativo (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), sem usar novos recursos de build (opção não), gerando testes com JUnit Jupiter (opção 4). Para o nome do projeto, use palm-workshop e, para o pacote de origem, use palm.workshop.

A estrutura do projeto vai ficar assim:

├── 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 da biblioteca de geração de registros para evitar mensagens irritantes de logger ausente:

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 um para o módulo dedicado da Vertex AI.

Para usar o Java 17 na compilação e execução dos nossos programas, adicione o bloco a seguir abaixo do bloco plugins {}:

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

Mais uma mudança: atualize o bloco application de app/build.gradle para permitir que os usuários substituam a classe principal para execução 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 o aplicativo, execute a classe principal padrão, que imprime 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 PaLM usando o projeto LangChain4J.

Para referência, confira como o arquivo de build app/build.gradle completo deve ficar agora:

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 classe chamada 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, você precisa importar a classe VertexAiChatModel e o ConversationalChain do LangChain4J para facilitar o processamento do aspecto de várias interações das conversas.

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

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

Agora que o modelo de linguagem está pronto, você pode preparar um ConversationalChain. Essa é uma abstração de nível mais alto oferecida pelo LangChain4J para configurar diferentes componentes juntos e lidar com uma conversa, como o próprio modelo de linguagem de chat, mas potencialmente outros componentes para processar o histórico da conversa ou conectar outras ferramentas, como mecanismos de recuperação, para buscar informações de bancos de dados vetoriais. Mas não se preocupe, vamos voltar a isso mais tarde neste codelab.

Em seguida, você vai ter uma conversa multiturno com o modelo de chat para fazer várias perguntas relacionadas. Primeiro, você se pergunta sobre os LLMs, depois pergunta o que pode fazer com eles e quais são alguns exemplos. Perceba que você não precisa se repetir. O LLM sabe que "eles" significa LLMs no contexto da conversa.

Para fazer essa conversa multiturno, basta chamar o método execute() na cadeia. Ele vai adicionar ao contexto da conversa, o modelo de chat vai gerar uma resposta e também adicionar ao histórico de chat.

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

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

A resposta será parecida com esta:

$ ./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 três perguntas relacionadas!

O builder VertexAIChatModel permite definir parâmetros opcionais que já têm alguns valores padrão que podem ser substituídos. Veja alguns exemplos:

  • .temperature(0.2): para definir o nível de criatividade da resposta. 0 é para respostas menos criativas e mais factuais, enquanto 1 é para respostas mais criativas.
  • .maxOutputTokens(50): no exemplo, foram solicitados 400 tokens (3 tokens equivalem a cerca de 4 palavras), dependendo do tamanho desejado da resposta gerada.
  • .topK(20): para selecionar aleatoriamente uma palavra entre um número máximo de palavras prováveis para a conclusão de texto (de 1 a 40)
  • .topP(0.95): para selecionar as palavras possíveis cuja probabilidade total seja igual a esse número de ponto flutuante (entre 0 e 1).
  • .maxRetries(3): se você estiver excedendo a cota de solicitações por período, o modelo poderá tentar fazer a chamada três vezes, por exemplo.

5. Um chatbot útil com personalidade!

Na seção anterior, você começou a fazer perguntas ao chatbot de LLM sem dar um contexto específico. Mas é possível especializar um chatbot para que ele se torne especialista em uma tarefa ou um assunto específico.

Como fazer isso? Ao explicar ao LLM a tarefa em questão, o contexto, talvez alguns exemplos do que ele precisa fazer, qual perfil ele deve ter, em qual formato você quer receber as respostas e, potencialmente, um tom, se quiser que o chatbot se comporte de uma determinada maneira.

Este artigo sobre como criar comandos ilustra bem essa abordagem com este gráfico:

8a4c67679dcbd085.png

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

Para ilustrar esse ponto, vamos nos inspirar nos sites prompts.chat, que listam muitas ideias ótimas e divertidas de chatbots personalizados para que eles atuem como:

Há um exemplo para 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 as etapas:

  • Algumas novas importações são necessárias para processar a memória da conversa.
  • Você vai instanciar o modelo de chat, mas com um pequeno número máximo de tokens (aqui, 7), já que queremos apenas gerar a próxima jogada, não uma dissertação completa sobre xadrez.
  • Em seguida, crie um repositório de memória de chat para salvar as conversas.
  • Você cria uma memória de chat em janela para reter os últimos movimentos.
  • Na memória do chat, você adiciona uma mensagem "do sistema" que instrui o modelo de chat sobre quem ele deve ser (por exemplo, um jogador de xadrez especialista). A mensagem do "sistema" adiciona contexto, enquanto as mensagens do "usuário" e da "IA" são a conversa em si.
  • Você cria uma cadeia de conversas que combina a memória e o modelo de chat.
  • Em seguida, temos uma lista de movimentos para as peças brancas, que você está iterando. A cadeia é executada com o próximo movimento das brancas a cada vez, e o modelo de chat responde com o próximo melhor movimento.

Ao executar essa classe com esses movimentos, você vai 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 

Uau! 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 os arquivos PGN (Portable Game Notation) de jogos anteriores. No entanto, é provável que esse chatbot não vença o AlphaZero (a IA que derrota os melhores jogadores de Go, Shogi e xadrez), e a conversa pode sair do controle mais adiante, já que o modelo não se lembra do estado real do jogo.

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

6. Extrair 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 textos não estruturados.

Vamos supor que você queira extrair o nome e a idade de uma pessoa com base em uma biografia ou descrição dela. Você pode instruir o modelo de linguagem grande a gerar estruturas de dados JSON com um comando inteligentemente ajustado (isso é comumente chamado de engenharia de comandos).

Você vai 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 analisar as várias etapas deste arquivo:

  • Uma classe Person é definida para representar os detalhes que descrevem uma pessoa (nome e idade).
  • A interface PersonExtractor é criada com um método que, dada uma string de texto não estruturada, retorna uma instância Person instanciada.
  • O extractPerson() é anotado com 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 na forma de um documento JSON, que será analisado e desserializado em uma instância Person.

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

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

Agora, execute essa classe com o seguinte comando:

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

> Task :app:run
Anna
23

Sim. Esta é a Anna, ela tem 23 anos!

O que é de interesse particular nessa abordagem AiServices é que você opera com objetos fortemente tipados. Você não está interagindo diretamente com o LLM de 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 de "Person". A noção de LLM é abstraída, e, como desenvolvedor Java, você está apenas manipulando classes e objetos normais.

7. Geração aumentada por recuperação: converse 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 trechos dos seus documentos. Essas informações serão usadas pelo modelo para embasar as respostas, em vez de tentar gerar respostas com base no treinamento. Esse padrão é chamado de RAG, ou geração aumentada de recuperação.

Na Geração Aumentada por Recuperação, em resumo, há duas fases:

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

6c5bb5cb2e3b8088.png

  1. Fase de consulta: agora os usuários podem fazer perguntas ao chatbot sobre a documentação. A pergunta também será transformada em um vetor e comparada com todos os outros vetores no banco de dados. Os vetores mais semelhantes geralmente estão relacionados semanticamente 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 é solicitado a fundamentar a resposta com base nesses snippets.

2c279c506d7606cd.png

Como preparar os documentos

Para esta nova demonstração, você vai fazer perguntas sobre a arquitetura de rede neural "transformer", criada pelo Google, que é como todos os modelos de linguagem grandes modernos são implementados atualmente.

Para acessar o artigo de pesquisa que descreve essa arquitetura ("Attention is all you need"), use 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

Implementar uma cadeia de recuperação de conversas

Vamos analisar, parte por parte, como criar a abordagem de duas fases, primeiro com a ingestão de documentos e depois com o tempo de consulta, quando 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 fazer o download e preparar um PdfParser para leitura:

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

Em vez de criar o modelo de linguagem de chat comum, você vai criar uma instância de um modelo de embedding. É um modelo e endpoint específico cuja função é criar representações vetoriais de partes de texto (palavras, frases ou até 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();

Em seguida, você vai precisar de algumas classes para colaborar e:

  • Carregue e divida o documento PDF em blocos.
  • Crie embeddings de vetor 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 de InMemoryEmbeddingStore, um banco de dados de vetores na memória, é criada para armazenar os embeddings de vetor.

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

O "ingestor" da loja 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 cuidar da ingestão.

Agora, a primeira fase terminou, o documento foi transformado em partes de texto com os embeddings de vetor associados e armazenado no banco de dados de vetores.

Fazer perguntas

Chegou a hora de se preparar para fazer perguntas! O modelo de chat comum 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 vai precisar de uma classe "retriever" que vincule o banco de dados de vetores (na variável embeddingStore) e o modelo de embedding. O trabalho dele é consultar o banco de dados de vetores computando um embedding de vetor para a consulta do usuário e encontrar vetores semelhantes no banco de dados:

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

Neste ponto, você pode instanciar a classe ConversationalRetrievalChain (este é 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" une:

  • O modelo de linguagem de chat que você configurou antes.
  • O recuperador compara uma consulta de embedding de vetor aos vetores no banco de dados.
  • Um modelo de comando diz explicitamente que o modelo de chat precisa responder com base nas informações fornecidas (ou seja, os trechos relevantes da documentação cujo embedding de vetor é semelhante ao vetor da pergunta do usuário).

Agora você já pode fazer suas 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ê vai encontrar 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 a cópia e a colagem, aqui está 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. Ao longo do caminho, você descobriu que os modelos de chat de linguagem grandes são bastante poderosos e capazes de lidar com várias tarefas, como perguntas/respostas, mesmo na sua própria documentação, extração de dados e, até certo ponto, até mesmo jogar xadrez!

Qual é a próxima etapa?

Confira os seguintes codelabs para saber mais sobre o PaLM em Java:

Leia mais

Documentos de referência