Gemini em Java com Vertex AI e LangChain4j

1. Introdução

Este codelab se concentra no modelo de linguagem grande (LLM) Gemini, hospedado na Vertex AI no Google Cloud. A Vertex AI é uma plataforma que abrange todos os produtos, serviços e modelos de machine learning no Google Cloud.

Você vai usar Java para interagir com a API Gemini usando o framework LangChain4j. Você vai conferir exemplos concretos para aproveitar o LLM na resposta a perguntas, geração de ideias, extração de entidades e conteúdo estruturado, geração aumentada de recuperação e chamada de função.

O que é a IA generativa?

A IA generativa é o uso da inteligência artificial para criar conteúdo, como texto, imagens, músicas, áudios e vídeos.

A IA generativa usa modelos de linguagem grandes (LLMs) capazes de realizar várias tarefas ao mesmo tempo, além de resumos, perguntas e respostas, classificações e muito mais. Com treinamento mínimo, 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 machine learning (ML) 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.

Quais são os aplicativos comuns de IA generativa?

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, localizar conteúdo de marketing em diferentes idiomas, verificar a conformidade dos contratos de clientes 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 base no Model Garden, ajuste modelos por meio de uma interface simples no Vertex 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.

Com tecnologia do Gemini, o Gemini para Google Cloud é um colaborador 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. O Gemini Code Assist oferece preenchimento, geração e explicações de código, além de permitir que você converse com ele para fazer perguntas técnicas.

O que é o Gemini?

Gemini é uma família de modelos de IA generativa desenvolvida pelo Google DeepMind, criada para casos de uso multimodais. Multimodal significa que ele pode processar e gerar diferentes tipos de conteúdo, como texto, código, imagens e áudio.

b9913d011999e7c7.png

O Gemini tem diferentes variações e tamanhos:

  • Gemini 2.0 Flash: nossos recursos de última geração mais recentes e recursos aprimorados.
  • Gemini 2.0 Flash-Lite: um modelo do Gemini 2.0 Flash otimizado para eficiência de custo e baixa latência.
  • Gemini 2.5 Pro: nosso modelo de raciocínio avançado mais avançado até o momento.
  • Gemini 2.5 Flash: um modelo de pensamento que oferece recursos abrangentes. Ela foi projetada para oferecer um equilíbrio entre preço e desempenho.

Principais recursos:

  • Multimodalidade: a capacidade do Gemini de entender e processar vários formatos de informação é um grande avanço em relação aos modelos de linguagem tradicionais que usam apenas texto.
  • Performance: o Gemini 2.5 Pro supera o estado da arte atual em muitos comparativos de mercado e foi o primeiro modelo a superar especialistas humanos no desafiador comparativo MMLU (compreensão massiva de linguagem multitarefa).
  • Flexibilidade: os diferentes tamanhos do Gemini o tornam adaptável a vários casos de uso, desde pesquisas em grande escala até implantação em dispositivos móveis.

Como interagir com o Gemini na Vertex AI usando Java?

Você tem duas opções:

  1. A biblioteca oficial da API Java da Vertex AI para Gemini.
  2. Framework LangChain4j.

Neste codelab, você vai usar o framework LangChain4j.

O que é o framework LangChain4j?

O framework LangChain4j é uma biblioteca de código aberto para integrar LLMs 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.

O projeto foi inspirado no projeto LangChain em Python, mas com o objetivo de atender a desenvolvedores Java.

bb908ea1e6c96ac2.png

O que você vai aprender

  • Como configurar um projeto Java para usar o Gemini e o LangChain4j
  • Como enviar seu primeiro comando ao Gemini por programação
  • Como transmitir respostas do Gemini
  • Como criar uma conversa entre um usuário e o Gemini
  • Como usar o Gemini em um contexto multimodal enviando texto e imagens
  • Como extrair informações estruturadas úteis de conteúdo não estruturado
  • Como manipular modelos de comando
  • Como fazer a classificação de texto, como a análise de sentimento
  • Como conversar usando seus próprios documentos (Geração Aumentada de Recuperação)
  • Como estender seus chatbots com chamadas de função
  • Como usar o Gemma localmente com o Ollama e o TestContainers

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.

fbef9caa1602edd0.png

a99b7ace416376c4.png

5e3ff691252acf41.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 Shell853e55310c205094.png.

3c1dabeca90e44e5.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.

9c92662c6a846a5c.png

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

9f0e51b578fecce5.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 do Cloud Shell para desenvolver seus programas Java.

Ativar APIs da Vertex AI

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.

É possível ativar as APIs da Vertex AI na seção "Vertex AI" do console do Google Cloud ou no terminal do Cloud Shell.

Para ativar no console do Google Cloud, primeiro acesse a seção "Vertex AI" do menu do console:

451976f1c8652341.png

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.

Como alternativa, você também pode ativar essa API no terminal do Cloud Shell com o seguinte comando:

gcloud services enable aiplatform.googleapis.com

Clone o repositório do GitHub

No terminal do Cloud Shell, clone o repositório deste codelab:

git clone https://github.com/glaforge/gemini-workshop-for-java-developers.git

Para verificar se o projeto está pronto para ser executado, tente executar o programa "Hello World".

Verifique se você está na pasta de nível superior:

cd gemini-workshop-for-java-developers/ 

Crie o wrapper do Gradle:

gradle wrapper

Execute com gradlew:

./gradlew run

Você verá esta resposta:

..
> Task :app:run
Hello World!

Abrir e configurar o Cloud Editor

Abra o código com o editor do Cloud Code no Cloud Shell:

42908e11b28f4383.png

No editor do Cloud Code, abra a pasta de origem do codelab selecionando File -> Open Folder e aponte para a pasta de origem do codelab (por exemplo, /home/username/gemini-workshop-for-java-developers/).

Configurar as variáveis de ambiente.

Abra um novo terminal no editor do Cloud Code selecionando Terminal -> New Terminal. Configure duas variáveis de ambiente necessárias para executar os exemplos de código:

  • PROJECT_ID: o ID do seu projeto do Google Cloud
  • LOCAL: a região em que o modelo do Gemini é implantado

Exporte as variáveis da seguinte maneira:

export PROJECT_ID=$(gcloud config get-value project)
export LOCATION=us-central1

4. Primeira chamada para o modelo do Gemini

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

Confira QA.java no diretório app/src/main/java/gemini/workshop:

package gemini.workshop;

import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.model.chat.ChatLanguageModel;

public class QA {
    public static void main(String[] args) {
        ChatLanguageModel model = VertexAiGeminiChatModel.builder()
            .project(System.getenv("PROJECT_ID"))
            .location(System.getenv("LOCATION"))
            .modelName("gemini-2.0-flash")
            .build();

        System.out.println(model.generate("Why is the sky blue?"));
    }
}

Neste primeiro exemplo, você precisa importar a classe VertexAiGeminiChatModel, que implementa a interface ChatModel.

No método main, configure o modelo de linguagem de chat usando o builder para VertexAiGeminiChatModel e especifique:

  • Projeto
  • Local
  • Nome do modelo (gemini-2.0-flash).

Agora que o modelo de linguagem está pronto, você pode chamar o método generate() e transmitir seu comando, pergunta ou instruções para enviar ao LLM. Aqui, você faz uma pergunta simples sobre o que faz o céu ser azul.

Mude o comando para testar outras perguntas ou tarefas.

Execute o exemplo na pasta raiz do código-fonte:

./gradlew run -q -DjavaMainClass=gemini.workshop.QA

A resposta será parecida com esta:

The sky appears blue because of a phenomenon called Rayleigh scattering.
When sunlight enters the atmosphere, it is made up of a mixture of
different wavelengths of light, each with a different color. The
different wavelengths of light interact with the molecules and particles
in the atmosphere in different ways.

The shorter wavelengths of light, such as those corresponding to blue
and violet light, are more likely to be scattered in all directions by
these particles than the longer wavelengths of light, such as those
corresponding to red and orange light. This is because the shorter
wavelengths of light have a smaller wavelength and are able to bend
around the particles more easily.

As a result of Rayleigh scattering, the blue light from the sun is
scattered in all directions, and it is this scattered blue light that we
see when we look up at the sky. The blue light from the sun is not
actually scattered in a single direction, so the color of the sky can
vary depending on the position of the sun in the sky and the amount of
dust and water droplets in the atmosphere.

Parabéns, você fez sua primeira chamada ao Gemini!

Resposta de streaming

Você notou que a resposta foi dada de uma só vez, depois de alguns segundos? Também é possível receber a resposta progressivamente, graças à variante de resposta de streaming. A resposta de streaming: o modelo retorna a resposta parte por parte, à medida que ela fica disponível.

Neste codelab, vamos usar a resposta não transmitida, mas vamos dar uma olhada na resposta transmitida para ver como ela pode ser feita.

Em StreamQA.java no diretório app/src/main/java/gemini/workshop, você pode ver a resposta de streaming em ação:

package gemini.workshop;

import dev.langchain4j.model.chat.StreamingChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiStreamingChatModel;

import static dev.langchain4j.model.LambdaStreamingResponseHandler.onNext;

public class StreamQA {
    public static void main(String[] args) {
        StreamingChatLanguageModel model = VertexAiGeminiStreamingChatModel.builder()
            .project(System.getenv("PROJECT_ID"))
            .location(System.getenv("LOCATION"))
            .modelName("gemini-2.0-flash")
            .maxOutputTokens(4000)
            .build();

        model.generate("Why is the sky blue?", onNext(System.out::println));
    }
}

Desta vez, importamos as variantes de classe de streaming VertexAiGeminiStreamingChatModel, que implementa a interface StreamingChatLanguageModel. Você também precisará importar estaticamente LambdaStreamingResponseHandler.onNext, que é um método conveniente que fornece StreamingResponseHandlers para criar um manipulador de streaming com expressões lambda Java.

Desta vez, a assinatura do método generate() é um pouco diferente. Em vez de retornar uma string, o tipo de retorno é void. Além do comando, você precisa transmitir um manipulador de respostas de streaming. Aqui, graças à importação estática mencionada acima, podemos definir uma expressão lambda que você transmite ao método onNext(). A expressão lambda é chamada sempre que uma nova parte da resposta fica disponível, enquanto a última é chamada apenas se ocorrer um erro.

Execute:

./gradlew run -q -DjavaMainClass=gemini.workshop.StreamQA

Você vai receber uma resposta semelhante à da aula anterior, mas desta vez, ela vai aparecer progressivamente no shell, em vez de esperar a exibição da resposta completa.

Configuração extra

Para a configuração, definimos apenas o projeto, o local e o nome do modelo, mas há outros parâmetros que podem ser especificados para o modelo:

  • temperature(Float temp): para definir o nível de criatividade da resposta. 0 é para respostas menos criativas e mais factuais, enquanto 2 é para respostas mais criativas.
  • topP(Float topP): para selecionar as palavras possíveis cuja probabilidade total seja igual a esse número de ponto flutuante (entre 0 e 1).
  • topK(Integer topK): 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)
  • maxOutputTokens(Integer max): para especificar o comprimento máximo da resposta dada pelo modelo. Em geral, quatro tokens representam aproximadamente três palavras.
  • maxRetries(Integer retries): caso você esteja excedendo a cota de solicitações por período ou a plataforma esteja enfrentando algum problema técnico, o modelo pode tentar fazer a chamada três vezes.

Até agora, você fez uma única pergunta ao Gemini, mas também pode ter uma conversa dividida em vários turnos. É isso que você vai conferir na próxima seção.

5. Converse com o Gemini

Na etapa anterior, você fez uma única pergunta. Agora é hora de ter uma conversa real entre um usuário e o LLM. Cada pergunta e resposta pode se basear nas anteriores para formar uma discussão real.

Confira Conversation.java na pasta app/src/main/java/gemini/workshop:

package gemini.workshop;

import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.service.AiServices;

import java.util.List;

public class Conversation {
    public static void main(String[] args) {
        ChatLanguageModel model = VertexAiGeminiChatModel.builder()
            .project(System.getenv("PROJECT_ID"))
            .location(System.getenv("LOCATION"))
            .modelName("gemini-2.0-flash")
            .build();

        MessageWindowChatMemory chatMemory = MessageWindowChatMemory.builder()
            .maxMessages(20)
            .build();

        interface ConversationService {
            String chat(String message);
        }

        ConversationService conversation =
            AiServices.builder(ConversationService.class)
                .chatLanguageModel(model)
                .chatMemory(chatMemory)
                .build();

        List.of(
            "Hello!",
            "What is the country where the Eiffel tower is situated?",
            "How many inhabitants are there in that country?"
        ).forEach( message -> {
            System.out.println("\nUser: " + message);
            System.out.println("Gemini: " + conversation.chat(message));
        });
    }
}

Algumas novas importações interessantes nesta classe:

  • MessageWindowChatMemory: uma classe que ajuda a lidar com o aspecto multiturno da conversa e mantém na memória local as perguntas e respostas anteriores.
  • AiServices: uma classe de abstração de nível superior que vai unir o modelo de chat e a memória de chat.

No método principal, você vai configurar o modelo, a memória de chat e o serviço de IA. O modelo é configurado como de costume com as informações de projeto, local e nome do modelo.

Para a memória de chat, usamos o builder do MessageWindowChatMemory para criar uma memória que mantém as últimas 20 mensagens trocadas. É uma janela deslizante sobre a conversa, cujo contexto é mantido localmente no nosso cliente de classe Java.

Em seguida, crie o AI service que vincula o modelo de chat à memória de chat.

Observe como o serviço de IA usa uma interface ConversationService personalizada que definimos, que o LangChain4j implementa e que recebe uma consulta String e retorna uma resposta String.

Agora é hora de conversar com o Gemini. Primeiro, uma saudação simples é enviada, depois uma primeira pergunta sobre a Torre Eiffel para saber em qual país ela pode ser encontrada. Perceba que a última frase está relacionada à resposta da primeira pergunta, já que você quer saber quantos habitantes há no país onde a Torre Eiffel está localizada, sem mencionar explicitamente o país que foi informado na resposta anterior. Isso mostra que as perguntas e respostas anteriores são enviadas com cada comando.

Execute a amostra:

./gradlew run -q -DjavaMainClass=gemini.workshop.Conversation

Você vai encontrar três respostas semelhantes a estas:

User: Hello!
Gemini: Hi there! How can I assist you today?

User: What is the country where the Eiffel tower is situated?
Gemini: France

User: How many inhabitants are there in that country?
Gemini: As of 2023, the population of France is estimated to be around 67.8 million.

Você pode fazer perguntas únicas ou ter conversas com vários turnos com o Gemini, mas até agora, a entrada era apenas de texto. E as imagens? Vamos analisar as imagens na próxima etapa.

6. Multimodalidade com o Gemini

O Gemini é um modelo multimodal. Ela aceita não apenas texto, mas também imagens e vídeos. Nesta seção, você vai conhecer um caso de uso para misturar texto e imagens.

Você acha que o Gemini vai reconhecer esse gato?

af00516493ec9ade.png

Imagem de um gato na neve tirada da Wikipedia

Confira Multimodal.java no diretório app/src/main/java/gemini/workshop:

package gemini.workshop;

import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.model.output.Response;
import dev.langchain4j.data.message.ImageContent;
import dev.langchain4j.data.message.TextContent;

public class Multimodal {

    static final String CAT_IMAGE_URL =
        "https://upload.wikimedia.org/wikipedia/" +
        "commons/b/b6/Felis_catus-cat_on_snow.jpg";


    public static void main(String[] args) {
        ChatLanguageModel model = VertexAiGeminiChatModel.builder()
            .project(System.getenv("PROJECT_ID"))
            .location(System.getenv("LOCATION"))
            .modelName("gemini-2.0-flash")
            .build();

        UserMessage userMessage = UserMessage.from(
            ImageContent.from(CAT_IMAGE_URL),
            TextContent.from("Describe the picture")
        );

        Response<AiMessage> response = model.generate(userMessage);

        System.out.println(response.content().text());
    }
}

Nas importações, observe que distinguimos diferentes tipos de mensagens e conteúdos. Um UserMessage pode conter um objeto TextContent e um ImageContent. Essa é a multimodalidade em ação: misturar texto e imagens. Não enviamos apenas um comando de string simples, mas um objeto mais estruturado que representa uma mensagem do usuário, composta por um conteúdo de imagem e um conteúdo de texto. O modelo envia de volta um Response que contém um AiMessage.

Em seguida, recupere o AiMessage da resposta usando content() e o texto da mensagem com text().

Execute a amostra:

./gradlew run -q -DjavaMainClass=gemini.workshop.Multimodal

O nome da imagem certamente deu uma dica do que ela continha, mas a saída do Gemini é semelhante a esta:

A cat with brown fur is walking in the snow. The cat has a white patch of fur on its chest and white paws. The cat is looking at the camera.

A combinação de comandos de texto e imagem abre casos de uso interessantes. É possível criar aplicativos que podem:

  • Reconhecer texto em imagens.
  • Verifique se uma imagem é segura para exibição.
  • Criar legendas para imagens.
  • Pesquise em um banco de dados de imagens com descrições em texto simples.

Além de extrair informações de imagens, também é possível extrair informações de texto não estruturado. É isso que você vai aprender na próxima seção.

7. Extrair informações estruturadas de texto não estruturado

Há muitas situações em que informações importantes são fornecidas em documentos de relatórios, e-mails ou outros textos longos de maneira não estruturada. O ideal é extrair os detalhes principais contidos no texto não estruturado, na forma de objetos estruturados. Vamos ver como fazer isso.

Vamos supor que você queira extrair o nome e a idade de uma pessoa com base em uma biografia, um currículo ou uma descrição dela. Você pode instruir o LLM a extrair JSON de texto não estruturado com um comando ajustado de forma inteligente (isso é comumente chamado de engenharia de comandos).

No exemplo abaixo, em vez de criar um comando que descreve a saída JSON, vamos usar um recurso avançado do Gemini chamado saída estruturada, ou às vezes geração restrita, forçando o modelo a gerar apenas conteúdo JSON válido, seguindo um esquema JSON especificado.

Confira ExtractData.java em app/src/main/java/gemini/workshop:

package gemini.workshop;

import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.SystemMessage;

import static dev.langchain4j.model.vertexai.SchemaHelper.fromClass;

public class ExtractData {

    record Person(String name, int age) { }

    interface PersonExtractor {
        @SystemMessage("""
            Your role is to extract the name and age 
            of the person described in the biography.
            """)
        Person extractPerson(String biography);
    }

    public static void main(String[] args) {
        ChatLanguageModel model = VertexAiGeminiChatModel.builder()
            .project(System.getenv("PROJECT_ID"))
            .location(System.getenv("LOCATION"))
            .modelName("gemini-2.0-flash")
            .responseMimeType("application/json")
            .responseSchema(fromClass(Person.class))
            .build();

        PersonExtractor extractor = AiServices.create(PersonExtractor.class, model);

        String bio = """
            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.    
            """;
        Person person = extractor.extractPerson(bio);

        System.out.println(person.name());  // Anna
        System.out.println(person.age());   // 23
    }
}

Vamos analisar as várias etapas deste arquivo:

  • Um registro Person é definido para representar os detalhes que descrevem uma pessoa (nome e idade).
  • A interface PersonExtractor é definida com um método que, dada uma string de texto não estruturada, retorna uma instância Person.
  • O extractPerson() é anotado com uma anotação @SystemMessage que associa um comando de instrução a ele. Esse é o comando que o modelo vai usar para orientar a extração das 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 é configurado e instanciado. Estamos usando dois novos métodos da classe de builder de modelos: responseMimeType() e responseSchema(). O primeiro diz ao Gemini para gerar JSON válido na saída. O segundo método define o esquema do objeto JSON que precisa ser retornado. Além disso, o último delega a um método de conveniência capaz de converter uma classe ou um registro Java em um esquema JSON adequado.
  • 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.

Execute a amostra:

./gradlew run -q -DjavaMainClass=gemini.workshop.ExtractData

Você verá esta resposta:

Anna
23

Sim, esta é a Anna, e ela tem 23 anos!

Com essa abordagem AiServices, você opera com objetos fortemente tipados. Você não está interagindo diretamente com o LLM. Em vez disso, você está trabalhando com classes concretas, como o registro Person para representar as informações pessoais extraídas, e tem um objeto 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 ao usar essa interface PersonExtractor.

8. Estruturar comandos com modelos de comandos

Quando você interage com um LLM usando um conjunto comum de instruções ou perguntas, há uma parte desse comando que nunca muda, enquanto outras partes contêm os dados. Por exemplo, se você quiser criar receitas, use um comando como "Você é um chef de cozinha talentoso. Crie uma receita com os seguintes ingredientes: ..." e adicione os ingredientes ao final desse texto. É para isso que servem os modelos de comandos, semelhantes a strings interpoladas em linguagens de programação. Um modelo de comando contém marcadores de posição que podem ser substituídos pelos dados certos para uma chamada específica ao LLM.

Mais concretamente, vamos estudar TemplatePrompt.java no diretório app/src/main/java/gemini/workshop:

package gemini.workshop;

import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.model.input.Prompt;
import dev.langchain4j.model.input.PromptTemplate;
import dev.langchain4j.model.output.Response;

import java.util.HashMap;
import java.util.Map;

public class TemplatePrompt {
    public static void main(String[] args) {
        ChatLanguageModel model = VertexAiGeminiChatModel.builder()
            .project(System.getenv("PROJECT_ID"))
            .location(System.getenv("LOCATION"))
            .modelName("gemini-2.0-flash")
            .maxOutputTokens(500)
            .temperature(1.0f)
            .topK(40)
            .topP(0.95f)
            .maxRetries(3)
            .build();

        PromptTemplate promptTemplate = PromptTemplate.from("""
            You're a friendly chef with a lot of cooking experience.
            Create a recipe for a {{dish}} with the following ingredients: \
            {{ingredients}}, and give it a name.
            """
        );

        Map<String, Object> variables = new HashMap<>();
        variables.put("dish", "dessert");
        variables.put("ingredients", "strawberries, chocolate, and whipped cream");

        Prompt prompt = promptTemplate.apply(variables);

        Response<AiMessage> response = model.generate(prompt.toUserMessage());

        System.out.println(response.content().text());
    }
}

Como de costume, você configura o modelo VertexAiGeminiChatModel com um alto nível de criatividade, uma temperatura alta e valores altos de topP e topK. Em seguida, crie um PromptTemplate com o método estático from(), transmitindo a string do nosso comando e usando as variáveis de marcador de posição de chaves duplas: {{dish}} e {{ingredients}}.

Para criar o comando final, chame apply(), que usa um mapa de pares de chave-valor que representam o nome do marcador de posição e o valor da string para substituir.

Por fim, chame o método generate() do modelo do Gemini criando uma mensagem do usuário com base nesse comando e a instrução prompt.toUserMessage().

Execute a amostra:

./gradlew run -q -DjavaMainClass=gemini.workshop.TemplatePrompt

Você vai ver uma saída gerada semelhante a esta:

**Strawberry Shortcake**

Ingredients:

* 1 pint strawberries, hulled and sliced
* 1/2 cup sugar
* 1/4 cup cornstarch
* 1/4 cup water
* 1 tablespoon lemon juice
* 1/2 cup heavy cream, whipped
* 1/4 cup confectioners' sugar
* 1/4 teaspoon vanilla extract
* 6 graham cracker squares, crushed

Instructions:

1. In a medium saucepan, combine the strawberries, sugar, cornstarch, 
water, and lemon juice. Bring to a boil over medium heat, stirring 
constantly. Reduce heat and simmer for 5 minutes, or until the sauce has 
thickened.
2. Remove from heat and let cool slightly.
3. In a large bowl, combine the whipped cream, confectioners' sugar, and 
vanilla extract. Beat until soft peaks form.
4. To assemble the shortcakes, place a graham cracker square on each of 
6 dessert plates. Top with a scoop of whipped cream, then a spoonful of 
strawberry sauce. Repeat layers, ending with a graham cracker square.
5. Serve immediately.

**Tips:**

* For a more elegant presentation, you can use fresh strawberries 
instead of sliced strawberries.
* If you don't have time to make your own whipped cream, you can use 
store-bought whipped cream.

Mude os valores de dish e ingredients no mapa, ajuste a temperatura, topK e tokP e execute o código novamente. Assim, você pode observar o efeito da mudança desses parâmetros no LLM.

Os modelos de comandos são uma boa maneira de ter instruções reutilizáveis e parametrizáveis para chamadas de LLM. Você pode transmitir dados e personalizar solicitações para diferentes valores fornecidos pelos usuários.

9. Classificação de texto com comandos de poucos disparos

Os LLMs são muito bons em classificar textos em diferentes categorias. Você pode ajudar um LLM nessa tarefa fornecendo alguns exemplos de textos e as categorias associadas. Essa abordagem é chamada de comando de poucos disparos (few-shot).

Vamos abrir TextClassification.java no diretório app/src/main/java/gemini/workshop para fazer um tipo específico de classificação de texto: análise de sentimento.

package gemini.workshop;

import com.google.cloud.vertexai.api.Schema;
import com.google.cloud.vertexai.api.Type;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.SystemMessage;

import java.util.List;

public class TextClassification {

    enum Sentiment { POSITIVE, NEUTRAL, NEGATIVE }

    public static void main(String[] args) {
        ChatLanguageModel model = VertexAiGeminiChatModel.builder()
            .project(System.getenv("PROJECT_ID"))
            .location(System.getenv("LOCATION"))
            .modelName("gemini-2.0-flash")
            .maxOutputTokens(10)
            .maxRetries(3)
            .responseSchema(Schema.newBuilder()
                .setType(Type.STRING)
                .addAllEnum(List.of("POSITIVE", "NEUTRAL", "NEGATIVE"))
                .build())
            .build();


        interface SentimentAnalysis {
            @SystemMessage("""
                Analyze the sentiment of the text below.
                Respond only with one word to describe the sentiment.
                """)
            Sentiment analyze(String text);
        }

        MessageWindowChatMemory memory = MessageWindowChatMemory.withMaxMessages(10);
        memory.add(UserMessage.from("This is fantastic news!"));
        memory.add(AiMessage.from(Sentiment.POSITIVE.name()));

        memory.add(UserMessage.from("Pi is roughly equal to 3.14"));
        memory.add(AiMessage.from(Sentiment.NEUTRAL.name()));

        memory.add(UserMessage.from("I really disliked the pizza. Who would use pineapples as a pizza topping?"));
        memory.add(AiMessage.from(Sentiment.NEGATIVE.name()));

        SentimentAnalysis sentimentAnalysis =
            AiServices.builder(SentimentAnalysis.class)
                .chatLanguageModel(model)
                .chatMemory(memory)
                .build();

        System.out.println(sentimentAnalysis.analyze("I love strawberries!"));
    }
}

Um tipo enumerado Sentiment lista os diferentes valores de um sentimento: negativo, neutro ou positivo.

No método main(), você cria o modelo de conversa no Gemini como de costume, mas com um pequeno número máximo de tokens de saída, já que você só quer uma resposta curta: o texto é POSITIVE, NEGATIVE ou NEUTRAL. Para restringir o modelo a retornar apenas esses valores, você pode aproveitar o suporte à saída estruturada que descobriu na seção de extração de dados. É por isso que o método responseSchema() é usado. Desta vez, você não vai usar o método conveniente de SchemaHelper para inferir a definição de esquema, mas vai usar o builder Schema para entender como é uma definição de esquema.

Depois que o modelo é configurado, você cria uma interface SentimentAnalysis que o AiServices do LangChain4j implementa para você usando o LLM. Essa interface contém um método: analyze(). Ela usa o texto a ser analisado como entrada e retorna um valor de enumeração Sentiment. Assim, você só manipula um objeto fortemente tipado que representa a classe de sentimento reconhecida.

Em seguida, para dar os "exemplos de poucas amostras" e incentivar o modelo a fazer o trabalho de classificação, crie uma memória de chat para transmitir pares de mensagens do usuário e respostas da IA que representam o texto e o sentimento associado a ele.

Vamos vincular tudo com o método AiServices.builder(), transmitindo nossa interface SentimentAnalysis, o modelo a ser usado e a memória de chat com os exemplos de poucos disparos. Por fim, chame o método analyze() com o texto a ser analisado.

Execute a amostra:

./gradlew run -q -DjavaMainClass=gemini.workshop.TextClassification

Você vai ver uma única palavra:

POSITIVE

Parece que gostar de morangos é um sentimento positivo!

10. Geração aumentada de recuperação

Os LLMs são treinados com uma grande quantidade de texto. No entanto, o conhecimento delas abrange apenas as informações que foram usadas no treinamento. Se houver novas informações divulgadas após a data limite do treinamento de modelo, esses detalhes não estarão disponíveis para ele. Assim, o modelo não poderá responder a perguntas sobre informações que não foram apresentadas a ele.

Por isso, abordagens como a geração aumentada por recuperação (RAG), que será abordada nesta seção, ajudam a fornecer as informações extras que um LLM pode precisar para atender às solicitações dos usuários, responder com informações mais atuais ou sobre informações particulares que não estão acessíveis no momento do treinamento.

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 com seus documentos divididos em partes menores ("pedaços"). Essas informações serão usadas pelo modelo para embasar as respostas, em vez de depender apenas do conhecimento contido no treinamento.

Na RAG, há duas fases:

  1. Fase de ingestão: os documentos são carregados na memória, divididos em partes menores e os embeddings de vetor (uma representação vetorial multidimensional alta das partes) são calculados e armazenados em um banco de dados de vetores capaz de fazer pesquisas semânticas. Essa fase de ingestão normalmente é feita uma vez, quando novos documentos precisam ser adicionados ao corpus de documentos.

cd07d33d20ffa1c8.png

  1. Fase de consulta: agora os usuários podem fazer perguntas sobre os documentos. 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 blocos de texto que correspondem aos vetores retornados pelo banco de dados e é solicitado a fundamentar a resposta analisando esses blocos.

a1d2e2deb83c6d27.png

Prepare seus documentos

Neste novo exemplo, você vai fazer perguntas sobre um modelo de carro fictício de uma fabricante também fictícia: o Cymbal Starlight! A ideia é que um documento sobre um carro fictício não faça parte do conhecimento do modelo. Se o Gemini conseguir responder corretamente a perguntas sobre esse carro, isso significa que a abordagem RAG funciona: ela consegue pesquisar no seu documento.

Implementar o chatbot

Vamos explorar como criar a abordagem de duas fases: primeiro com a ingestão de documentos e depois com o tempo de consulta (também chamado de "fase de recuperação") quando os usuários fazem perguntas sobre o documento.

Neste exemplo, as duas fases são implementadas na mesma classe. Normalmente, você teria um aplicativo que cuida da ingestão e outro que oferece a interface do chatbot aos usuários.

Além disso, neste exemplo, vamos usar um banco de dados de vetores na memória. Em um cenário de produção real, as fases de ingestão e consulta seriam separadas em dois aplicativos distintos, e os vetores seriam mantidos em um banco de dados independente.

Ingestão de documentos

A primeira etapa da fase de ingestão de documentos é localizar o arquivo PDF sobre nosso carro fictício e preparar um PdfParser para lê-lo:

URL url = new URI("https://raw.githubusercontent.com/meteatamel/genai-beyond-basics/main/samples/grounding/vertexai-search/cymbal-starlight-2024.pdf").toURL();
ApachePdfBoxDocumentParser pdfParser = new ApachePdfBoxDocumentParser();
Document document = pdfParser.parse(url.openStream());

Em vez de criar primeiro o modelo de linguagem de chat comum, você cria uma instância de um modelo de incorporação. É um modelo específico cuja função é criar representações vetoriais de partes de texto (palavras, frases ou até mesmo parágrafos). Ele retorna vetores de números de ponto flutuante, em vez de respostas de texto.

VertexAiEmbeddingModel embeddingModel = VertexAiEmbeddingModel.builder()
    .endpoint(System.getenv("LOCATION") + "-aiplatform.googleapis.com:443")
    .project(System.getenv("PROJECT_ID"))
    .location(System.getenv("LOCATION"))
    .publisher("google")
    .modelName("text-embedding-005")
    .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);

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 de repositório 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! Crie um modelo de chat para iniciar a conversa:

ChatLanguageModel model = VertexAiGeminiChatModel.builder()
        .project(System.getenv("PROJECT_ID"))
        .location(System.getenv("LOCATION"))
        .modelName("gemini-2.0-flash")
        .maxOutputTokens(1000)
        .build();

Você também precisa de uma classe de recuperação para vincular o banco de dados de vetores (na variável embeddingStore) ao 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:

EmbeddingStoreContentRetriever retriever =
    new EmbeddingStoreContentRetriever(embeddingStore, embeddingModel);

Crie uma interface que represente um assistente especialista em carros. Essa é uma interface que a classe AiServices vai implementar para você interagir com o modelo:

interface CarExpert {
    Result<String> ask(String question);
}

A interface CarExpert retorna uma resposta de string encapsulada na classe Result do LangChain4j. Por que usar esse wrapper? Porque ele não apenas dá a resposta, mas também permite examinar os trechos do banco de dados que foram retornados pelo extrator de conteúdo. Assim, é possível mostrar as fontes dos documentos usados para fundamentar a resposta final ao usuário.

Neste ponto, você pode configurar um novo serviço de IA:

CarExpert expert = AiServices.builder(CarExpert.class)
    .chatLanguageModel(model)
    .chatMemory(MessageWindowChatMemory.withMaxMessages(10))
    .contentRetriever(retriever)
    .build();

Esse serviço vincula:

  • O modelo de linguagem de chat que você configurou antes.
  • Uma memória de chat para acompanhar a conversa.
  • O recuperador compara uma consulta de embedding de vetor aos vetores no banco de dados.
.retrievalAugmentor(DefaultRetrievalAugmentor.builder()
    .contentInjector(DefaultContentInjector.builder()
        .promptTemplate(PromptTemplate.from("""
            You are an expert in car automotive, and you answer concisely.

            Here is the question: {{userMessage}}

            Answer using the following information:
            {{contents}}
            """))
        .build())
    .contentRetriever(retriever)
    .build())

Agora você pode fazer suas perguntas!

List.of(
    "What is the cargo capacity of Cymbal Starlight?",
    "What's the emergency roadside assistance phone number?",
    "Are there some special kits available on that car?"
).forEach(query -> {
    Result<String> response = expert.ask(query);
    System.out.printf("%n=== %s === %n%n %s %n%n", query, response.content());
    System.out.println("SOURCE: " + response.sources().getFirst().textSegment().text());
});

O código-fonte completo está em RAG.java no diretório app/src/main/java/gemini/workshop.

Execute a amostra:

./gradlew -q run -DjavaMainClass=gemini.workshop.RAG

Na saída, você vai encontrar respostas para suas perguntas:

=== What is the cargo capacity of Cymbal Starlight? === 

 The Cymbal Starlight 2024 has a cargo capacity of 13.5 cubic feet.
 

SOURCE: Cargo
The Cymbal Starlight 2024 has a cargo capacity of 13.5 cubic feet. The cargo area is located in the trunk of
the vehicle.
To access the cargo area, open the trunk lid using the trunk release lever located in the driver's footwell.
When loading cargo into the trunk, be sure to distribute the weight evenly. Do not overload the trunk, as this
could affect the vehicle's handling and stability.
Luggage

=== What's the emergency roadside assistance phone number? === 

The emergency roadside assistance phone number is 1-800-555-1212.
 

SOURCE: Chapter 18: Emergencies
Roadside Assistance
If you experience a roadside emergency, such as a flat tire or a dead battery, you can call roadside
assistance for help. Roadside assistance is available 24 hours a day, 7 days a week.
To call roadside assistance, dial the following number:
1-800-555-1212
When you call roadside assistance, be prepared to provide the following information:
Your name and contact information
Your vehicle's make, model, and year
Your vehicle's location

=== Are there some special kits available on that car? === 

 Yes, the Cymbal Starlight comes with a tire repair kit.
 

SOURCE: Lane keeping assist:  This feature helps to keep you in your lane by gently steering the vehicle back
into the lane if you start to drift.
Adaptive cruise control:  This feature automatically adjusts your speed to maintain a safe following
distance from the vehicle in front of you.
Forward collision warning:  This feature warns you if you are approaching another vehicle too
quickly.
Automatic emergency braking:  This feature can automatically apply the brakes to avoid a collision.

11. Chamadas de função

Há situações em que você quer que um LLM tenha acesso a sistemas externos, como uma API da Web remota que recupera informações ou realiza uma ação, ou serviços que executam algum tipo de computação. Exemplo:

APIs da Web remotas:

  • Acompanhar e atualizar pedidos de clientes.
  • Encontre ou crie um tíquete em um rastreador de problemas.
  • Extrair dados em tempo real, como cotações de ações ou medições de sensores de IoT.
  • Enviar um e-mail.

Ferramentas de computação:

  • Uma calculadora para problemas matemáticos mais avançados.
  • Interpretação de código para executar código quando os LLMs precisam de lógica de raciocínio.
  • Converter solicitações em linguagem natural em consultas SQL para que um LLM possa consultar um banco de dados.

A chamada de função (às vezes chamada de ferramentas ou uso de ferramentas) é a capacidade do modelo de solicitar que uma ou mais chamadas de função sejam feitas em nome dele, para que ele possa responder adequadamente a um comando do usuário com dados mais recentes.

Com um comando específico de um usuário e o conhecimento das funções relevantes para esse contexto, um LLM pode responder com uma solicitação de chamada de função. O aplicativo que integra o LLM pode chamar a função em nome dele e responder ao LLM com uma resposta. Em seguida, o LLM interpreta a resposta com uma resposta textual.

Quatro etapas das chamadas de função

Vamos conferir um exemplo de chamada de função: como receber informações sobre a previsão do tempo.

Se você perguntar ao Gemini ou a qualquer outro LLM sobre o clima em Paris, eles vão responder que não têm informações sobre a previsão do tempo atual. Se você quiser que o LLM tenha acesso em tempo real aos dados meteorológicos, defina algumas funções que ele possa solicitar para uso.

Confira o diagrama a seguir:

31e0c2aba5e6f21c.png

1️⃣ Primeiro, um usuário pergunta sobre o clima em Paris. O app de chatbot (usando LangChain4j) sabe que há uma ou mais funções à disposição para ajudar o LLM a atender à consulta. O chatbot envia o comando inicial e a lista de funções que podem ser chamadas. Aqui, uma função chamada getWeather() que usa um parâmetro de string para o local.

8863be53a73c4a70.png

Como o LLM não sabe sobre previsões do tempo, em vez de responder por texto, ele envia uma solicitação de execução de função. O chatbot precisa chamar a função getWeather() com "Paris" como parâmetro de local.

2️⃣ O chatbot invoca essa função em nome do LLM e recupera a resposta da função. Aqui, imaginamos que a resposta seja {"forecast": "sunny"}.

d1367cc69c07b14d.png

3️⃣ O app de chatbot envia a resposta JSON de volta para o LLM.

73a5f2ed19f47d8.png

4️⃣ O LLM analisa a resposta JSON, interpreta essas informações e responde com o texto de que o tempo está ensolarado em Paris.

20832cb1ee6fbfeb.png

Cada etapa como código

Primeiro, configure o modelo do Gemini como de costume:

ChatLanguageModel model = VertexAiGeminiChatModel.builder()
    .project(System.getenv("PROJECT_ID"))
    .location(System.getenv("LOCATION"))
    .modelName("gemini-2.0-flash")
    .maxOutputTokens(100)
    .build();

Você define uma especificação de ferramenta que descreve a função que pode ser chamada:

ToolSpecification weatherToolSpec = ToolSpecification.builder()
    .name("getWeather")
    .description("Get the weather forecast for a given location or city")
    .parameters(JsonObjectSchema.builder()
        .addStringProperty(
            "location", 
            "the location or city to get the weather forecast for")
        .build())
    .build();

O nome da função é definido, assim como o nome e o tipo do parâmetro, mas observe que tanto a função quanto os parâmetros recebem descrições. As descrições são muito importantes e ajudam o LLM a entender o que uma função pode fazer e, assim, julgar se ela precisa ser chamada no contexto da conversa.

Vamos começar a etapa 1 enviando a pergunta inicial sobre o clima em Paris:

List<ChatMessage> allMessages = new ArrayList<>();

// 1) Ask the question about the weather
UserMessage weatherQuestion = UserMessage.from("What is the weather in Paris?");
allMessages.add(weatherQuestion);

Na etapa 2, transmitimos a ferramenta que queremos que o modelo use, e ele responde com uma solicitação de execução da ferramenta:

// 2) The model replies with a function call request
Response<AiMessage> messageResponse = model.generate(allMessages, weatherToolSpec);
ToolExecutionRequest toolExecutionRequest = messageResponse.content().toolExecutionRequests().getFirst();
System.out.println("Tool execution request: " + toolExecutionRequest);
allMessages.add(messageResponse.content());

Etapa 3. Neste ponto, sabemos qual função o LLM quer que chamemos. No código, não estamos fazendo uma chamada real para uma API externa, apenas retornamos uma previsão do tempo hipotética diretamente:

// 3) We send back the result of the function call
ToolExecutionResultMessage toolExecResMsg = ToolExecutionResultMessage.from(toolExecutionRequest,
    "{\"location\":\"Paris\",\"forecast\":\"sunny\", \"temperature\": 20}");
allMessages.add(toolExecResMsg);

Na etapa 4, o LLM aprende sobre o resultado da execução da função e pode sintetizar uma resposta textual:

// 4) The model answers with a sentence describing the weather
Response<AiMessage> weatherResponse = model.generate(allMessages);
System.out.println("Answer: " + weatherResponse.content().text());

O código-fonte completo está em FunctionCalling.java no diretório app/src/main/java/gemini/workshop.

Execute a amostra:

./gradlew run -q -DjavaMainClass=gemini.workshop.FunctionCalling

O resultado será semelhante a este:

Tool execution request: ToolExecutionRequest { id = null, name = "getWeatherForecast", arguments = "{"location":"Paris"}" }
Answer:  The weather in Paris is sunny with a temperature of 20 degrees Celsius.

Na saída acima, você pode conferir a solicitação de execução da ferramenta e a resposta.

12. O LangChain4j processa a chamada de função

Na etapa anterior, você viu como as interações normais de pergunta/resposta de texto e solicitação/resposta de função são intercaladas. Entre elas, você forneceu a resposta da função solicitada diretamente, sem chamar uma função real.

No entanto, o LangChain4j também oferece uma abstração de nível mais alto que pode processar as chamadas de função de forma transparente para você, enquanto lida com a conversa normalmente.

Chamada de função única

Vamos analisar FunctionCallingAssistant.java parte por parte.

Primeiro, crie um registro que represente a estrutura de dados de resposta da função:

record WeatherForecast(String location, String forecast, int temperature) {}

A resposta contém informações sobre o local, a previsão e a temperatura.

Em seguida, crie uma classe que contenha a função real que você quer disponibilizar para o modelo:

static class WeatherForecastService {
    @Tool("Get the weather forecast for a location")
    WeatherForecast getForecast(@P("Location to get the forecast for") String location) {
        if (location.equals("Paris")) {
            return new WeatherForecast("Paris", "Sunny", 20);
        } else if (location.equals("London")) {
            return new WeatherForecast("London", "Rainy", 15);
        } else {
            return new WeatherForecast("Unknown", "Unknown", 0);
        }
    }
}

Essa classe contém uma única função, mas é anotada com a anotação @Tool, que corresponde à descrição da função que o modelo pode solicitar para chamar.

Os parâmetros da função (um único aqui) também são anotados, mas com a anotação curta @P, que também descreve o parâmetro. Você pode adicionar quantas funções quiser para disponibilizá-las ao modelo em cenários mais complexos.

Nesta classe, você retorna algumas respostas predefinidas, mas, se quiser chamar um serviço externo real de previsão do tempo, é no corpo desse método que você fará a chamada para esse serviço.

Como vimos ao criar um ToolSpecification na abordagem anterior, é importante documentar o que uma função faz e descrever a que os parâmetros correspondem. Isso ajuda o modelo a entender como e quando essa função pode ser usada.

Em seguida, o LangChain4j permite fornecer uma interface que corresponde ao contrato que você quer usar para interagir com o modelo. Aqui, é uma interface simples que recebe uma string representando a mensagem do usuário e retorna uma string correspondente à resposta do modelo:

interface WeatherAssistant {
    String chat(String userMessage);
}

Também é possível usar assinaturas mais complexas que envolvem UserMessage (para uma mensagem do usuário) ou AiMessage (para uma resposta do modelo) do LangChain4j, ou até mesmo um TokenStream, se você quiser lidar com situações mais avançadas. Esses objetos mais complicados também contêm informações extras, como o número de tokens consumidos etc. Mas, para simplificar, vamos usar apenas string na entrada e string na saída.

Vamos terminar com o método main(), que une todas as partes:

public static void main(String[] args) {
    ChatLanguageModel model = VertexAiGeminiChatModel.builder()
        .project(System.getenv("PROJECT_ID"))
        .location(System.getenv("LOCATION"))
        .modelName("gemini-2.0-flash")
        .build();

    WeatherForecastService weatherForecastService = new WeatherForecastService();

    WeatherAssistant assistant = AiServices.builder(WeatherAssistant.class)
        .chatLanguageModel(model)
        .chatMemory(MessageWindowChatMemory.withMaxMessages(10))
        .tools(weatherForecastService)
        .build();

    System.out.println(assistant.chat("What is the weather in Paris?"));
    System.out.println(assistant.chat("Is it warmer in London or in Paris?"));
}

Como de costume, você configura o modelo de conversa no Gemini. Em seguida, você instancia o serviço de previsão do tempo que contém a "função" que o modelo vai pedir para chamar.

Agora, use a classe AiServices novamente para vincular o modelo de chat, a memória de chat e a ferramenta (ou seja, o serviço de previsão do tempo com a função dele). AiServices retorna um objeto que implementa a interface WeatherAssistant definida. Só falta chamar o método chat() desse assistente. Ao invocar, você só verá as respostas de texto, mas as solicitações e respostas de chamada de função não ficarão visíveis para o desenvolvedor. Essas solicitações serão processadas de forma automática e transparente. Se o Gemini achar que uma função precisa ser chamada, ele vai responder com a solicitação de chamada de função, e o LangChain4j vai cuidar de chamar a função local em seu nome.

Execute a amostra:

./gradlew run -q -DjavaMainClass=gemini.workshop.FunctionCallingAssistant

O resultado será semelhante a este:

OK. The weather in Paris is sunny with a temperature of 20 degrees.

It is warmer in Paris (20 degrees) than in London (15 degrees).

Este foi um exemplo de uma única função.

Várias chamadas de função

Você também pode ter várias funções e deixar o LangChain4j lidar com várias chamadas de função em seu nome. Confira MultiFunctionCallingAssistant.java para um exemplo de várias funções.

Ela tem uma função para converter moedas:

@Tool("Convert amounts between two currencies")
double convertCurrency(
    @P("Currency to convert from") String fromCurrency,
    @P("Currency to convert to") String toCurrency,
    @P("Amount to convert") double amount) {

    double result = amount;

    if (fromCurrency.equals("USD") && toCurrency.equals("EUR")) {
        result = amount * 0.93;
    } else if (fromCurrency.equals("USD") && toCurrency.equals("GBP")) {
        result = amount * 0.79;
    }

    System.out.println(
        "convertCurrency(fromCurrency = " + fromCurrency +
            ", toCurrency = " + toCurrency +
            ", amount = " + amount + ") == " + result);

    return result;
}

Outra função para receber o valor de uma ação:

@Tool("Get the current value of a stock in US dollars")
double getStockPrice(@P("Stock symbol") String symbol) {
    double result = 170.0 + 10 * new Random().nextDouble();

    System.out.println("getStockPrice(symbol = " + symbol + ") == " + result);

    return result;
}

Outra função para aplicar uma porcentagem a um determinado valor:

@Tool("Apply a percentage to a given amount")
double applyPercentage(@P("Initial amount") double amount, @P("Percentage between 0-100 to apply") double percentage) {
    double result = amount * (percentage / 100);

    System.out.println("applyPercentage(amount = " + amount + ", percentage = " + percentage + ") == " + result);

    return result;
}

Depois, você pode combinar todas essas funções e uma classe MultiTools e fazer perguntas como "Qual é 10% do preço das ações da AAPL convertido de USD para EUR?"

public static void main(String[] args) {
    ChatLanguageModel model = VertexAiGeminiChatModel.builder()
        .project(System.getenv("PROJECT_ID"))
        .location(System.getenv("LOCATION"))
        .modelName("gemini-2.0-flash")
        .maxOutputTokens(100)
        .build();

    MultiTools multiTools = new MultiTools();

    MultiToolsAssistant assistant = AiServices.builder(MultiToolsAssistant.class)
        .chatLanguageModel(model)
        .chatMemory(withMaxMessages(10))
        .tools(multiTools)
        .build();

    System.out.println(assistant.chat(
        "What is 10% of the AAPL stock price converted from USD to EUR?"));
}

Execute da seguinte maneira:

./gradlew run -q -DjavaMainClass=gemini.workshop.MultiFunctionCallingAssistant

E você vai ver as várias funções chamadas:

getStockPrice(symbol = AAPL) == 172.8022224055534
convertCurrency(fromCurrency = USD, toCurrency = EUR, amount = 172.8022224055534) == 160.70606683716468
applyPercentage(amount = 160.70606683716468, percentage = 10.0) == 16.07060668371647
10% of the AAPL stock price converted from USD to EUR is 16.07060668371647 EUR.

Towards Agents (em inglês)

A chamada de função é um ótimo mecanismo de extensão para modelos de linguagem grandes, como o Gemini. Isso nos permite criar sistemas mais complexos, geralmente chamados de "agentes" ou "assistentes de IA". Esses agentes podem interagir com o mundo externo por APIs externas e com serviços que podem ter efeitos colaterais no ambiente externo (como envio de e-mails, criação de tíquetes etc.).

Ao criar agentes tão poderosos, faça isso com responsabilidade. Considere um human-in-the-loop antes de realizar ações automáticas. É importante ter em mente a segurança ao criar agentes com tecnologia de LLM que interagem com o mundo externo.

13. Executar o Gemma com o Ollama e o TestContainers

Até agora, usamos o Gemini, mas também existe o Gemma, o modelo irmão menor.

O Gemma é uma família de modelos abertos, leves e de última geração, criados com base na mesma pesquisa e tecnologia usadas para criar os modelos do Gemini. O modelo Gemma mais recente é o Gemma3, disponível em quatro tamanhos: 1B (somente texto), 4B, 12B e 27B. Os pesos estão disponíveis sem custo financeiro, e os tamanhos pequenos permitem que você execute o modelo por conta própria, mesmo no laptop ou no Cloud Shell.

Como executar a Gemma?

Há muitas maneiras de executar o Gemma: na nuvem, pela Vertex AI com um clique de um botão ou no GKE com algumas GPUs, mas também é possível executar localmente.

Uma boa opção para executar o Gemma localmente é com o Ollama, uma ferramenta que permite executar modelos pequenos, como Llama, Mistral e muitos outros na sua máquina local. É semelhante ao Docker, mas para LLMs.

Instale o Ollama seguindo as instruções para seu sistema operacional.

Se você estiver usando um ambiente Linux, primeiro instale e ative o Ollama.

ollama serve > /dev/null 2>&1 & 

Depois de instalado localmente, é possível executar comandos para extrair um modelo:

ollama pull gemma3:1b

Aguarde o modelo ser extraído. Isso pode levar alguns instantes.

Execute o modelo:

ollama run gemma3:1b

Agora, você pode interagir com o modelo:

>>> Hello!
Hello! It's nice to hear from you. What can I do for you today?

Para sair do prompt, pressione Ctrl+D.

Executar o Gemma no Ollama com TestContainers

Em vez de instalar e executar o Ollama localmente, é possível usar o Ollama em um contêiner, processado pelo TestContainers.

O TestContainers não é útil apenas para testes, mas também pode ser usado para executar contêineres. Há até um OllamaContainer específico que você pode aproveitar.

Confira o panorama completo:

2382c05a48708dfd.png

Implementação

Vamos analisar GemmaWithOllamaContainer.java parte por parte.

Primeiro, crie um contêiner Ollama derivado que extraia o modelo Gemma. Essa imagem já existe de uma execução anterior ou será criada. Se a imagem já existir, basta informar ao TestContainers que você quer substituir a imagem padrão do Ollama pela variante com tecnologia do Gemma:

    private static final String TC_OLLAMA_GEMMA3 = "tc-ollama-gemma3-1b";
    public static final String GEMMA_3 = "gemma3:1b";

    // Creating an Ollama container with Gemma 3 if it doesn't exist.
    private static OllamaContainer createGemmaOllamaContainer() throws IOException, InterruptedException {

        // Check if the custom Gemma Ollama image exists already
        List<Image> listImagesCmd = DockerClientFactory.lazyClient()
            .listImagesCmd()
            .withImageNameFilter(TC_OLLAMA_GEMMA3)
            .exec();

        if (listImagesCmd.isEmpty()) {
            System.out.println("Creating a new Ollama container with Gemma 3 image...");
            OllamaContainer ollama = new OllamaContainer("ollama/ollama:0.7.1");
            System.out.println("Starting Ollama...");
            ollama.start();
            System.out.println("Pulling model...");
            ollama.execInContainer("ollama", "pull", GEMMA_3);
            System.out.println("Committing to image...");
            ollama.commitToImage(TC_OLLAMA_GEMMA3);
            return ollama;
        }

        System.out.println("Ollama image substitution...");
        // Substitute the default Ollama image with our Gemma variant
        return new OllamaContainer(
            DockerImageName.parse(TC_OLLAMA_GEMMA3)
                .asCompatibleSubstituteFor("ollama/ollama"));
    }

Em seguida, crie e inicie um contêiner de teste do Ollama e um modelo de chat do Ollama, apontando para o endereço e a porta do contêiner com o modelo que você quer usar. Por fim, invoque model.generate(yourPrompt) como de costume:

public static void main(String[] args) throws IOException, InterruptedException {
    OllamaContainer ollama = createGemmaOllamaContainer();
    ollama.start();

    ChatLanguageModel model = OllamaChatModel.builder()
        .baseUrl(String.format("http://%s:%d", ollama.getHost(), ollama.getFirstMappedPort()))
        .modelName(GEMMA_3)
        .build();

    String response = model.generate("Why is the sky blue?");

    System.out.println(response);
}

Execute da seguinte maneira:

./gradlew run -q -DjavaMainClass=gemini.workshop.GemmaWithOllamaContainer

A primeira execução vai levar um tempo para criar e executar o contêiner, mas, depois disso, você vai ver a Gemma responder:

INFO: Container ollama/ollama:0.7.1 started in PT7.228339916S
The sky appears blue due to Rayleigh scattering. Rayleigh scattering is a phenomenon that occurs when sunlight interacts with molecules in the Earth's atmosphere.

* **Scattering particles:** The main scattering particles in the atmosphere are molecules of nitrogen (N2) and oxygen (O2).
* **Wavelength of light:** Blue light has a shorter wavelength than other colors of light, such as red and yellow.
* **Scattering process:** When blue light interacts with these molecules, it is scattered in all directions.
* **Human eyes:** Our eyes are more sensitive to blue light than other colors, so we perceive the sky as blue.

This scattering process results in a blue appearance for the sky, even though the sun is actually emitting light of all colors.

In addition to Rayleigh scattering, other atmospheric factors can also influence the color of the sky, such as dust particles, aerosols, and clouds.

O Gemma está sendo executado no Cloud Shell.

14. Parabéns

Parabéns! Você criou seu primeiro aplicativo de conversa com IA generativa em Java usando o LangChain4j e a API Gemini. Você descobriu ao longo do caminho que os modelos de linguagem grandes multimodais 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, interação com APIs externas e muito mais.

Qual é a próxima etapa?

Agora é sua vez de melhorar seus aplicativos com integrações avançadas de LLM.

Leia mais

Documentos de referência