Chat potenciado por IA generativa con usuarios y documentos en Java con PaLM y LangChain4J

1. Introducción

Última actualización: 05/02/2024

¿Qué es la IA generativa?

La IA generativa o inteligencia artificial generativa hace referencia al uso de la IA para crear contenido nuevo, como texto, imágenes, música, audio y videos.

La IA generativa usa modelos de base (modelos de IA grandes) que pueden realizar varias tareas a la vez y de manera predeterminada, como resúmenes, preguntas y respuestas, clasificación y mucho más. Además, con el entrenamiento mínimo necesario, los modelos de base se pueden adaptar a casos de uso segmentados con muy pocos ejemplos de datos.

¿Cómo funciona la IA generativa?

La IA generativa usa un modelo de AA (aprendizaje automático) para aprender los patrones y las relaciones en un conjunto de datos de contenido creado por humanos. Luego, usa los patrones aprendidos para generar contenido nuevo.

La forma más común de entrenar un modelo generativo de IA es usar el aprendizaje supervisado, en el que el modelo recibe un conjunto de contenido creado por humanos y las etiquetas correspondientes. Luego, aprende a generar contenido similar al creado por humanos y etiquetado con las mismas etiquetas.

¿Cuáles son las aplicaciones comunes de la IA generativa?

La IA generativa procesa mucho contenido, lo que crea estadísticas y respuestas a través de texto, imágenes y formatos fáciles de usar. Estos son algunos usos de la IA generativa:

  • Mejorar las interacciones con los clientes a través de experiencias de chat y búsqueda mejoradas
  • Explorar grandes cantidades de datos no estructurados a través de interfaces y resúmenes coloquiales
  • Ayudar con tareas repetitivas, como responder solicitudes de propuestas (RFP), localizar contenido de marketing en cinco idiomas y verificar los contratos de los clientes para garantizar el cumplimiento, entre otras

¿Qué ofertas de IA generativa tiene Google Cloud?

Con Vertex AI, puedes interactuar con modelos de base, incorporarlos y personalizarlos en tus aplicaciones sin necesidad de tener experiencia en AA. Accede a los modelos base en Model Garden, ajústalos mediante una IU simple en Generative AI Studio o usa modelos en un notebook de ciencia de datos.

Vertex AI Search and Conversation ofrece a los desarrolladores la forma más rápida de compilar motores de búsqueda y chatbots con tecnología de IA generativa.

Además, Duet AI es tu colaborador potenciado por IA disponible en IDE y Google Cloud para ayudarte a realizar más tareas con mayor rapidez.

¿En qué se enfoca este codelab?

En este codelab, nos enfocaremos en el modelo de lenguaje grande (LLM) PaLM 2, alojado en Vertex AI de Google Cloud, que abarca todos los productos y servicios de aprendizaje automático.

Usarás Java para interactuar con la API de PaLM, junto con el orquestador del framework de LLM LangChain4J. Verás diferentes ejemplos concretos para aprovechar el LLM en la respuesta de preguntas, la generación de ideas, la extracción de entidades y contenido estructurado, y la generación de resúmenes.

Más información sobre el framework de LangChain4j

El framework LangChain4J es una biblioteca de código abierto para integrar modelos de lenguaje grandes en tus aplicaciones de Java, ya que organiza varios componentes, como el LLM en sí, pero también otras herramientas, como bases de datos vectoriales (para búsquedas semánticas), cargadores y divisores de documentos (para analizar documentos y aprender de ellos), analizadores de resultados y mucho más.

c6d7f7c3fd0d2951.png

Qué aprenderás

  • Cómo configurar un proyecto de Java para usar PaLM y LangChain4j
  • Cómo extraer información útil de contenido no estructurado (extracción de entidades o palabras clave, salida en JSON)
  • Cómo crear una conversación con tus usuarios
  • Cómo usar el modelo de chat para hacer preguntas sobre tu propia documentación

Requisitos

  • Conocimiento del lenguaje de programación Java
  • Un proyecto de Google Cloud
  • Un navegador, como Chrome o Firefox

2. Configuración y requisitos

Configuración del entorno de autoaprendizaje

  1. Accede a Google Cloud Console y crea un proyecto nuevo o reutiliza uno existente. Si aún no tienes una cuenta de Gmail o de Google Workspace, debes crear una.

295004821bab6a87.png

37d264871000675d.png

96d86d3d5655cdbe.png

  • El Nombre del proyecto es el nombre visible de los participantes de este proyecto. Es una cadena de caracteres que no se utiliza en las APIs de Google. Puedes actualizarla cuando quieras.
  • El ID del proyecto es único en todos los proyectos de Google Cloud y es inmutable (no se puede cambiar después de configurarlo). La consola de Cloud genera automáticamente una cadena única. Por lo general, no importa cuál sea. En la mayoría de los codelabs, deberás hacer referencia al ID de tu proyecto (suele identificarse como PROJECT_ID). Si no te gusta el ID que se generó, podrías generar otro aleatorio. También puedes probar uno propio y ver si está disponible. No se puede cambiar después de este paso y se usa el mismo durante todo el proyecto.
  • Recuerda que hay un tercer valor, un número de proyecto, que usan algunas APIs. Obtén más información sobre estos tres valores en la documentación.
  1. A continuación, deberás habilitar la facturación en la consola de Cloud para usar las APIs o los recursos de Cloud. Ejecutar este codelab no costará mucho, tal vez nada. Para cerrar recursos y evitar que se generen cobros más allá de este instructivo, puedes borrar los recursos que creaste o borrar el proyecto. Los usuarios nuevos de Google Cloud son aptos para participar en el programa Prueba gratuita de $300.

Inicia Cloud Shell

Si bien Google Cloud se puede operar de manera remota desde tu laptop, en este codelab usarás Cloud Shell, un entorno de línea de comandos que se ejecuta en la nube.

Activar Cloud Shell

  1. En la consola de Cloud, haz clic en Activar Cloud Shelld1264ca30785e435.png.

cb81e7c8e34bc8d.png

Si es la primera vez que inicias Cloud Shell, aparecerá una pantalla intermedia en la que se describirá qué es. Si apareció una pantalla intermedia, haz clic en Continuar.

d95252b003979716.png

El aprovisionamiento y la conexión a Cloud Shell solo tomará unos minutos.

7833d5e1c5d18f54.png

Esta máquina virtual está cargada con todas las herramientas de desarrollo necesarias. Ofrece un directorio principal persistente de 5 GB y se ejecuta en Google Cloud, lo que permite mejorar considerablemente el rendimiento de la red y la autenticación. Gran parte de tu trabajo en este codelab, si no todo, se puede hacer con un navegador.

Una vez que te conectes a Cloud Shell, deberías ver que te autenticaste y que el proyecto se configuró con tu ID del proyecto.

  1. En Cloud Shell, ejecuta el siguiente comando para confirmar que tienes la autenticación:
gcloud auth list

Resultado del comando

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

To set the active account, run:
    $ gcloud config set account `ACCOUNT`
  1. En Cloud Shell, ejecuta el siguiente comando para confirmar que el comando gcloud conoce tu proyecto:
gcloud config list project

Resultado del comando

[core]
project = <PROJECT_ID>

De lo contrario, puedes configurarlo con el siguiente comando:

gcloud config set project <PROJECT_ID>

Resultado del comando

Updated property [core/project].

3. Cómo preparar tu entorno de desarrollo

En este codelab, usarás la terminal y el editor de código de Cloud Shell para desarrollar tus programas en Java.

Habilita las APIs de Vertex AI

  1. En la consola de Google Cloud, asegúrate de que el nombre del proyecto se muestre en la parte superior de la consola de Google Cloud. Si no es así, haz clic en Selecciona un proyecto para abrir el Selector de proyectos y selecciona el proyecto que desees.
  2. Si no estás en la parte de Vertex AI de la consola de Google Cloud, haz lo siguiente:
  3. En Búsqueda, ingresa Vertex AI y, luego, muestra lo siguiente:
  4. En los resultados de la búsqueda, haz clic en Vertex AI. Aparecerá el panel de Vertex AI.
  5. Haz clic en Habilitar todas las APIs recomendadas en el panel de Vertex AI.

Esto habilitará varias APIs, pero la más importante para el codelab es aiplatform.googleapis.com, que también puedes habilitar en la línea de comandos, en la terminal de Cloud Shell, con el siguiente comando:

$ gcloud services enable aiplatform.googleapis.com

Cómo crear la estructura del proyecto con Gradle

Para compilar tus ejemplos de código Java, usarás la herramienta de compilación Gradle y la versión 17 de Java. Para configurar tu proyecto con Gradle, en la terminal de Cloud Shell, crea un directorio (aquí, palm-workshop) y ejecuta el comando gradle init en ese directorio:

$ 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

Compilarás una aplicación (opción 2) con el lenguaje Java (opción 3), sin usar subproyectos (opción 1), con la sintaxis de Groovy para el archivo de compilación (opción 1), sin usar nuevas funciones de compilación (opción no), generar pruebas con JUnit Jupiter (opción 4) y, para el nombre del proyecto, puedes usar palm-workshop y, de manera similar, para el paquete fuente, puedes usar palm.workshop.

La estructura del proyecto se verá de la siguiente manera:

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

Actualicemos el archivo app/build.gradle para agregar algunas dependencias necesarias. Puedes quitar la dependencia de guava si está presente y reemplazarla por las dependencias del proyecto LangChain4J y la biblioteca de registro para evitar mensajes molestos sobre la falta de un registrador:

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

LangChain4j tiene 2 dependencias:

  • uno en el proyecto principal
  • y uno para el módulo dedicado de Vertex AI.

Para usar Java 17 para compilar y ejecutar nuestros programas, agrega el siguiente bloque debajo del bloque plugins {}:

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

Un cambio más que debes realizar: actualiza el bloque application de app/build.gradle para permitir que los usuarios anulen la clase principal que se ejecutará en la línea de comandos cuando se invoque la herramienta de compilación:

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

Para verificar que tu archivo de compilación esté listo para ejecutar tu aplicación, puedes ejecutar la clase principal predeterminada que imprime un mensaje Hello World! simple:

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

> Task :app:run
Hello World!

BUILD SUCCESSFUL in 3s
2 actionable tasks: 2 executed

Ahora puedes programar con el modelo de texto de lenguaje grande de PaLM usando el proyecto LangChain4j.

Como referencia, así debería verse el archivo de compilación app/build.gradle completo ahora:

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. Cómo hacer tu primera llamada al modelo de chat de PaLM

Ahora que el proyecto está configurado correctamente, es momento de llamar a la API de PaLM.

Crea una clase nueva llamada ChatPrompts.java en el directorio app/src/main/java/palm/workshop (junto con la clase App.java predeterminada) y escribe el siguiente contenido:

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

En este primer ejemplo, debes importar la clase VertexAiChatModel y el ConversationalChain de LangChain4J para facilitar el manejo del aspecto de varios turnos de las conversaciones.

A continuación, en el método main, configurarás el modelo de lenguaje de chat con el compilador de VertexAiChatModel para especificar lo siguiente:

  • el extremo
  • el proyecto,
  • la región,
  • el publicador,
  • y el nombre del modelo (chat-bison@001).

Ahora que el modelo de lenguaje está listo, puedes preparar un ConversationalChain. Esta es una abstracción de nivel superior que ofrece LangChain4j para configurar en conjunto diferentes componentes para controlar una conversación, como el propio modelo de lenguaje de chat, pero potencialmente otros componentes para controlar el historial de la conversación de chat o para conectar otras herramientas, como recuperadores, para recuperar información de bases de datos vectoriales. Pero no te preocupes, volveremos a hablar de eso más adelante en este codelab.

Luego, tendrás una conversación de varios turnos con el modelo de chat para hacer varias preguntas interrelacionadas. Primero te preguntas qué son los LLM, luego qué puedes hacer con ellos y cuáles son algunos ejemplos. Observa que no tienes que repetirte, el LLM sabe que "ellos" significa LLMs en el contexto de esa conversación.

Para continuar con esa conversación de varios turnos, solo tienes que llamar al método execute() en la cadena. Se agregará al contexto de la conversación, el modelo de chat generará una respuesta y también la agregará al historial de chat.

Para ejecutar esta clase, ejecuta el siguiente comando en la terminal de Cloud Shell:

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

Deberías ver un resultado similar al siguiente:

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

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

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

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

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

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

BUILD SUCCESSFUL in 25s
2 actionable tasks: 2 executed

PaLM respondió tus 3 preguntas relacionadas.

El compilador VertexAIChatModel te permite definir parámetros opcionales que ya tienen algunos valores predeterminados que puedes anular. Estos son algunos ejemplos:

  • .temperature(0.2): Para definir qué tan creativa quieres que sea la respuesta (0 es poco creativa y, a menudo, más fáctica, mientras que 1 es para resultados más creativos)
  • .maxOutputTokens(50): En el ejemplo, se solicitaron 400 tokens (3 tokens equivalen aproximadamente a 4 palabras), según la longitud que desees que tenga la respuesta generada.
  • .topK(20): Para seleccionar aleatoriamente una palabra entre una cantidad máxima de palabras probables para la función de completar texto (de 1 a 40)
  • .topP(0.95): Para seleccionar las palabras posibles cuya probabilidad total sume ese número de punto flotante (entre 0 y 1)
  • .maxRetries(3): En caso de que superes la cuota de solicitudes por período, puedes hacer que el modelo vuelva a intentar la llamada 3 veces, por ejemplo.

5. Un chatbot útil con personalidad

En la sección anterior, comenzaste de inmediato a hacerle preguntas al chatbot de LLM sin darle ningún contexto en particular. Sin embargo, puedes especializar un chatbot de este tipo para que se convierta en un experto en una tarea o un tema en particular.

¿Cómo puede hacerlo? Preparar el escenario: Explicar al LLM la tarea que debe realizar, el contexto, tal vez dar algunos ejemplos de lo que tiene que hacer, qué arquetipo debe tener, en qué formato te gustaría recibir respuestas y, potencialmente, un tono, si quieres que el chatbot se comporte de cierta manera.

En este artículo sobre la creación de instrucciones, se ilustra muy bien este enfoque con el siguiente gráfico:

8a4c67679dcbd085.png

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

Para ilustrar este punto, busquemos inspiración en los sitios web de prompts.chat, que enumeran muchas ideas geniales y divertidas de chatbots personalizados para que actúen como lo siguiente:

Hay un ejemplo para convertir un chatbot de LLM en un jugador de ajedrez. Implementemos eso.

Actualiza la clase ChatPrompts de la siguiente manera:

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

Analicemos esto paso a paso:

  • Se necesitan algunas importaciones nuevas para controlar la memoria del chat.
  • Instancias el modelo de chat, pero con una pequeña cantidad de tokens máximos (aquí 7), ya que solo queremos generar el siguiente movimiento, no una disertación completa sobre el ajedrez.
  • A continuación, crearás un almacén de memoria de chat para guardar las conversaciones.
  • Creas una memoria de chat con ventanas real para conservar los últimos movimientos.
  • En la memoria del chat, agregas un mensaje del "sistema" que le indica al modelo de chat quién se supone que es (es decir, un experto jugador de ajedrez). El mensaje del "sistema" agrega algo de contexto, mientras que los mensajes del "usuario" y la "IA" son la discusión real.
  • Creas una cadena de conversación que combina la memoria y el modelo de chat.
  • Luego, tenemos una lista de movimientos para las blancas, que estás iterando. La cadena se ejecuta con el siguiente movimiento blanco cada vez, y el modelo de chat responde con el siguiente mejor movimiento.

Cuando ejecutes esta clase con estos movimientos, deberías ver el siguiente resultado:

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

¡Guau! ¿PaLM sabe jugar al ajedrez? Bueno, no exactamente, pero durante su entrenamiento, el modelo debe haber visto algunos comentarios de partidas de ajedrez o incluso los archivos PGN (Portable Game Notation) de partidas anteriores. Sin embargo, es probable que este chatbot no le gane a AlphaZero (la IA que derrota a los mejores jugadores de Go, Shogi y ajedrez), y la conversación podría descarrilarse más adelante, ya que el modelo no recuerda el estado real del juego.

Los modelos de chat son muy potentes y pueden crear interacciones enriquecidas con tus usuarios, además de controlar varias tareas contextuales. En la siguiente sección, veremos una tarea útil: extraer datos estructurados del texto.

6. Extracción de información de texto no estructurado

En la sección anterior, creaste conversaciones entre un usuario y un modelo de lenguaje de chat. Sin embargo, con LangChain4J, también puedes usar un modelo de chat para extraer información estructurada de texto no estructurado.

Supongamos que deseas extraer el nombre y la edad de una persona a partir de su biografía o descripción. Puedes indicarle al modelo de lenguaje grande que genere estructuras de datos JSON con una instrucción ingeniosamente modificada (esto se conoce comúnmente como "ingeniería de instrucciones").

Actualizarás la clase ChatPrompts de la siguiente manera:

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

Veamos los distintos pasos de este archivo:

  • Se define una clase Person para representar los detalles que describen a una persona (su nombre y edad).
  • La interfaz PersonExtractor se crea con un método que, dada una cadena de texto no estructurada, devuelve una instancia de Person.
  • El extractPerson() se anota con una anotación @UserMessage que asocia una instrucción con él. Esa es la instrucción que usará el modelo para extraer la información y devolver los detalles en forma de un documento JSON, que se analizará y se deserializará en una instancia de Person.

Ahora veamos el contenido del método main():

  • Se crea una instancia del modelo de chat.
  • Se crea un objeto PersonExtractor gracias a la clase AiServices de LangChain4j.
  • Luego, puedes llamar a Person person = extractor.extractPerson(...) para extraer los detalles de la persona del texto no estructurado y obtener una instancia de Person con el nombre y la edad.

Ahora, ejecuta esta clase con el siguiente comando:

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

> Task :app:run
Anna
23

Sí. Esta es Anna, tiene 23 años.

Lo que resulta de especial interés con este enfoque de AiServices es que trabajas con objetos con escritura segura. No interactúas directamente con el LLM de chat. En cambio, trabajas con clases concretas, como la clase Person para representar la información personal extraída, y tienes una clase PersonExtractor con un método extractPerson() que devuelve una instancia de Person. La noción de LLM se abstrae, y, como desarrollador de Java, solo manipulas clases y objetos normales.

7. Generación mejorada por recuperación: Chatea con tus documentos

Volvamos a las conversaciones. Esta vez, podrás hacer preguntas sobre tus documentos. Crearás un chatbot que pueda recuperar información pertinente de una base de datos de extractos de tus documentos, y el modelo usará esa información para fundamentar sus respuestas, en lugar de intentar generar respuestas basadas en su entrenamiento. Este patrón se denomina RAG o Generación mejorada por recuperación.

En la generación mejorada por recuperación, en pocas palabras, hay dos fases:

  1. Fase de transferencia: Los documentos se cargan, se dividen en fragmentos más pequeños y se almacena una representación vectorial de ellos (un "embedding de vector") en una "base de datos de vectores" que puede realizar búsquedas semánticas.

6c5bb5cb2e3b8088.png

  1. Fase de preguntas: Ahora los usuarios pueden hacerle preguntas al chatbot sobre la documentación. La pregunta también se transformará en un vector y se comparará con todos los demás vectores de la base de datos. Los vectores más similares suelen estar relacionados semánticamente y los devuelve la base de datos de vectores. Luego, se le proporciona al LLM el contexto de la conversación y los fragmentos de texto que corresponden a los vectores que devolvió la base de datos, y se le pide que fundamente su respuesta consultando esos fragmentos.

2c279c506d7606cd.png

Cómo preparar tus documentos

En esta nueva demostración, harás preguntas sobre la arquitectura de red neuronal "Transformer", pionera de Google, que es la forma en que se implementan todos los modelos de lenguaje grandes modernos en la actualidad.

Puedes recuperar el documento de investigación que describió esta arquitectura ("Attention is all you need") con el comando wget para descargar el PDF de Internet:

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

Cómo implementar una cadena de recuperación conversacional

Exploremos, paso a paso, cómo crear el enfoque de 2 fases, primero con la transferencia de documentos y, luego, con el tiempo de consulta cuando los usuarios hacen preguntas sobre el documento.

Transferencia de documentos

El primer paso de la fase de incorporación de documentos es ubicar el archivo PDF que descargamos y preparar un PdfParser para leerlo:

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

En lugar de crear el modelo de lenguaje de chat habitual, primero crearás una instancia de un modelo de "incorporación". Este es un modelo y un endpoint particulares cuya función es crear representaciones vectoriales de fragmentos de texto (palabras, oraciones o incluso párrafos).

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

A continuación, necesitarás algunas clases para colaborar y realizar las siguientes acciones:

  • Carga y divide el documento PDF en fragmentos.
  • Crea embeddings de vectores para todos estos fragmentos.
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);

Se crea una instancia de InMemoryEmbeddingStore, una base de datos de vectores en la memoria, para almacenar los embeddings de vectores.

El documento se divide en fragmentos gracias a la clase DocumentSplitters. Dividirá el texto del archivo PDF en fragmentos de 500 caracteres, con una superposición de 100 caracteres (con el siguiente fragmento, para evitar cortar palabras o frases, en partes).

El "ingestor" de la tienda vincula el divisor de documentos, el modelo de embedding para calcular los vectores y la base de datos de vectores en memoria. Luego, el método ingest() se encargará de realizar la transferencia.

Ahora, la primera fase finalizó, el documento se transformó en fragmentos de texto con sus embeddings de vectores asociados y se almacenó en la base de datos de vectores.

Cómo hacer preguntas

Es hora de prepararse para hacer preguntas. Se puede crear el modelo de chat habitual para iniciar la conversación:

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

También necesitarás una clase "retriever" que vincule la base de datos de vectores (en la variable embeddingStore) y el modelo de embedding. Su trabajo es consultar la base de datos de vectores calculando un embedding de vector para la búsqueda del usuario, para encontrar vectores similares en la base de datos:

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

En este punto, puedes crear una instancia de la clase ConversationalRetrievalChain (este es solo otro nombre para el patrón de generación mejorada por recuperación):

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

Esta "cadena" une los siguientes elementos:

  • El modelo de idioma del chat que configuraste antes.
  • El retriever compara una consulta de embedding de vector con los vectores de la base de datos.
  • Una plantilla de instrucción indica explícitamente que el modelo de chat debe responder basándose en la información proporcionada (es decir, los fragmentos pertinentes de la documentación cuyo embedding de vector es similar al vector de la pregunta del usuario).

Y ahora sí, ya puedes hacer tus preguntas.

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

Ejecuta el programa con el siguiente comando:

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

En el resultado, deberías ver la respuesta a tus preguntas:

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

La solución completa

Para facilitar la tarea de copiar y pegar, aquí se incluye el contenido completo de la clase 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. Felicitaciones

¡Felicitaciones! Compilaste con éxito tu primera aplicación de chat con IA generativa en Java con LangChain4j y la API de PaLM. En el camino, descubriste que los modelos de chat de lenguaje grandes son bastante potentes y capaces de manejar varias tareas, como responder preguntas, incluso en tu propia documentación, extraer datos y, hasta cierto punto, incluso jugar al ajedrez.

¿Qué sigue?

Consulta los siguientes codelabs para profundizar en PaLM en Java:

Lecturas adicionales

Documentos de referencia