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

1. Introducción

Última actualización: 5/2/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 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 de IA generativa es a través del aprendizaje supervisado; al modelo se le asigna 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 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:

  • Optimiza las interacciones con los clientes por medio de las experiencias de búsqueda y de chat mejoradas
  • Explora grandes cantidades de datos no estructurados a través de interfaces conversacionales y resúmenes
  • Brinda asistencia con tareas repetitivas, como responder solicitudes de propuestas (RFP), localizar contenido de marketing en cinco idiomas, revisar el cumplimiento de los contratos de los clientes y mucho más.

¿Qué ofertas de IA generativa tiene Google Cloud?

Con Vertex AI, puedes interactuar con los modelos de base, incorporarlos y personalizarlos en tus aplicaciones. Se necesita poca o ninguna experiencia en AA. Accede a los modelos de base en Model Garden, ajústalos con 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 potenciados por IA generativa.

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

¿En qué se enfoca este codelab?

Este codelab se enfoca en el modelo grande de lenguaje (LLM) PaLM 2, alojado en Vertex AI de Google Cloud, que abarca todos los productos y servicios del aprendizaje automático.

Usarás Java para interactuar con la API de PaLM y el organizador del framework de LLM LangChain4J. Verás diferentes ejemplos concretos para aprovechar el LLM para responder preguntas, generar ideas, extraer entidades y contenidos estructurados, y resumirlos.

¡Dame más información sobre el framework de LangChain4J!

El framework LangChain4J es una biblioteca de código abierto para integrar modelos grandes de lenguaje en tus aplicaciones de Java mediante la organización de varios componentes, como el LLM mismo, pero también otras herramientas, como bases de datos de vectores (para búsquedas semánticas), cargadores y divisores de documentos (para analizar documentos y aprender de ellos), analizadores de salida 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, resultado en JSON)
  • Cómo crear una conversación con tus usuarios
  • Cómo usar el modelo de chat para hacer preguntas en tu propia documentación

Requisitos

  • Conocimientos 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, verás una pantalla intermedia que describe en qué consiste. 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 mejora 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 estás autenticado y que el proyecto está configurado 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. Ejecuta el siguiente comando en Cloud Shell para confirmar que el comando de 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 de Java.

Habilita las APIs de Vertex AI

  1. En la consola de Google Cloud, asegúrate de que el nombre de tu proyecto aparezca en la parte superior de la consola de Google Cloud. Si no lo está, haz clic en Seleccionar un proyecto para abrir el Selector de proyectos y selecciona el proyecto deseado.
  2. Si no estás en la parte de Vertex AI de la consola de Google Cloud, haz lo siguiente:
  3. En Buscar, ingresa Vertex AI y, luego, regresa.
  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, ejecutando 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), usarás la sintaxis de Groovy para el archivo de compilación (opción 1), no usarás nuevas funciones de compilación (opción no), generando pruebas con JUnit Jupiter (opción 4) y, para el nombre del proyecto, puedes usar palm-workshop.De manera similar, puedes usar palm-workshop.

La estructura del proyecto será la siguiente:

├── 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 guava si está presente y reemplazarla con las dependencias para el proyecto LangChain4J y la biblioteca de registro para evitar la molestia de los mensajes de registrador que faltan:

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

Hay 2 dependencias para LangChain4J:

  • una en el proyecto principal,
  • y otro para el módulo dedicado de Vertex AI.

Si deseas 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 para hacer: actualiza el bloque application de app/build.gradle para permitir que los usuarios puedan anular la clase principal que se ejecutará en la línea de comandos cuando invoquen la herramienta de compilación:

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

Para comprobar 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

Ya tienes todo listo para programar con el modelo de texto de lenguaje grande de PaLM usando el proyecto LangChain4J.

A modo de referencia, así es como 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 control del aspecto de varios turnos de las conversaciones.

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

  • 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 manejar una conversación, como el modelo de lenguaje de chat en sí, pero posiblemente otros componentes para manejar el historial de la conversación de chat o conectar otras herramientas, como retrievers, para recuperar información de bases de datos vectoriales. Pero no te preocupes, volveremos a eso más adelante en este codelab.

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

Para tomar esa conversación de varios turnos, solo debes llamar al método execute() de la cadena, que lo 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 de VertexAIChatModel te permite definir parámetros opcionales que ya tienen algunos valores predeterminados que puedes anular. Estos son algunos ejemplos:

  • .temperature(0.2): Indica qué tan creativa quieres que sea la respuesta (0 es poco creativa y, a menudo, más fáctica, mientras que 1 es para más resultados de creatividades).
  • .maxOutputTokens(50): En el ejemplo, se solicitaron 400 tokens (3 tokens son equivalentes a 4 palabras), según el tiempo que desees que tenga la respuesta generada.
  • .topK(20): Permite seleccionar de forma aleatoria una palabra de la cantidad máxima de palabras probablemente para completar el texto (de 1 a 40).
  • .topP(0.95): Para seleccionar las palabras posibles cuya probabilidad total suma ese número de punto flotante (entre 0 y 1)
  • .maxRetries(3): En caso de que estés ejecutando más allá de la cuota de solicitudes por tiempo, 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 hacer preguntas al chatbot de LLM sin darle ningún contexto en particular. Pero puedes especializar un chatbot de este tipo para convertirte en un experto en una tarea en particular o en un tema en particular.

¿Cómo puede hacerlo? Preparando el escenario: explicando al LLM la tarea en cuestión, el contexto, tal vez dando algunos ejemplos de lo que tiene que hacer, qué persona debe tener, en qué formato te gustaría obtener respuestas y, posiblemente, un tono, si quieres que el chatbot se comporte de cierta manera.

En este artículo sobre la creación de instrucciones, se ilustra 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, inspirémonos en los sitios web prompts.chat, que enumera muchas ideas geniales y divertidas de chatbots personalizados para que actúen de la siguiente manera:

Hay un ejemplo para convertir un chatbot de LLM en un jugador de ajedrez. Vamos a implementarlo.

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.
  • Creas una instancia del modelo de chat, pero con una pequeña cantidad de tokens máximos (aquí 7), ya que solo queremos generar el siguiente movimiento, no toda una disertación sobre ajedrez.
  • A continuación, crearás un almacén de memoria de chat para guardar las conversaciones de chat.
  • Crearás una memoria de chat con ventanas real para retener los últimos movimientos.
  • En la memoria del chat, agregas un “sistema” , que instruye al modelo del chat quién debe ser (es decir, un experto en ajedrez). El “sistema” mensaje agrega algún contexto, mientras que "usuario" e “IA” los mensajes son el debate real.
  • Creas una cadena de conversación que combina la memoria y el modelo de chat.
  • Luego, tenemos una lista de movimientos para el blanco, que estás iterando. La cadena se ejecuta con el siguiente movimiento blanco cada vez, y el modelo del 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 

¡Vaya! ¿ PaLM sabe cómo jugar al ajedrez? Bueno, no exactamente, pero durante su entrenamiento, el modelo debe haber visto algunos comentarios sobre juegos de ajedrez, o incluso los archivos PGN (Notación de juego portátil) de juegos 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 desviarse más adelante, ya que el modelo no recuerda realmente el estado real del juego.

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

6. Extrae información de texto no estructurado

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

Digamos que quieres extraer el nombre y la edad de una persona, según una biografía o descripción de esa persona. Puedes indicarle al modelo grande de lenguaje que genere estructuras de datos JSON con una instrucción ingeniosamente ajustada (esto se suele denominar "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:

  • Una clase Person se define para representar los detalles que describen a una persona (su nombre y edad).
  • La interfaz PersonExtractor se crea con un método que, según una cadena de texto no estructurado, muestra una instancia de Person en la que se creó una instancia.
  • El extractPerson() está anotado con una anotación @UserMessage que asocia un mensaje con él. Esa es la indicación que el modelo usará para extraer la información y mostrar los detalles en forma de un documento JSON que se analizará por ti y se desordenará 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í. Ella es Anna, ¡tiene 23 años!

Lo más interesante de este enfoque de AiServices es que operas con objetos con tipado fuerte. No estás interactuando 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 muestra 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 aumentada de recuperación: Chatear con tus documentos

Volvamos a las conversaciones. Esta vez, podrás hacer preguntas sobre tus documentos. Compilarás un chatbot que pueda recuperar información relevante de una base de datos de extractos de tus documentos, y el modelo usará esa información para "bimar" sus respuestas, en lugar de intentar generar respuestas que provengan del entrenamiento. Este patrón se denomina RAG o generación aumentada de recuperación.

En pocas palabras, en la generación de aumento de recuperación, hay dos fases:

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

6c5bb5cb2e3b8088.png

  1. Fase de consulta: Ahora los usuarios pueden hacer 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. Por lo general, los vectores más similares están relacionados semánticamente y los muestra la base de datos de vectores. Luego, al LLM se le da el contexto de la conversación, los fragmentos de texto que corresponden a los vectores devueltos por la base de datos, y se le solicita fundamentar su respuesta observando esos fragmentos.

8c279c506d7606cd.png

Prepara tus documentos

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

Puedes recuperar el artículo de investigación que describió esta arquitectura ("Attention is all you need") usando 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

Implementa una cadena de recuperación conversacional

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

Transferencia de documentos

El primer paso de la fase de transferencia de documentos es encontrar 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 del chat habitual, antes de eso, crearás una instancia de un modelo de “incorporación”. Se trata de un modelo y extremo particular cuya función es crear representaciones vectoriales de los 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 entre sí para lograr lo siguiente:

  • Carga y divide el documento PDF en partes.
  • Crea incorporaciones vectoriales 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 las incorporaciones vectoriales.

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 que se corten palabras o oraciones, por partes).

La tienda “transferencia” vincula el divisor de documentos, el modelo de incorporación para calcular los vectores y la base de datos de vectores en la memoria. Luego, el método ingest() se encargará de realizar la transferencia.

Ahora que terminó la primera fase, el documento se transformó en fragmentos de texto con sus incorporaciones vectoriales asociadas y se almacenó en la base de datos de vectores.

Cómo hacer preguntas

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

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 vinculará la base de datos de vectores (en la variable embeddingStore) y el modelo de incorporación. Su trabajo es consultar la base de datos de vectores mediante el cálculo de una incorporación vectorial para la consulta 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 un nombre diferente para el patrón de generación de aumento de 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" se vincula:

  • El modelo de idioma de chat que configuraste antes
  • El retriever compara una consulta de incorporación vectorial 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 su respuesta en la información proporcionada (es decir, los extractos relevantes de la documentación cuya incorporación de vector sea similar al vector de la pregunta del usuario).

Ya estás listo para hacer 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 lo siguiente:

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

Deberías ver la respuesta a tus preguntas en el resultado:

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 el copiado y pegado, este es 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 de IA generativa en Java con LangChain4J y la API de PaLM. En el proceso, descubriste que los modelos grandes de chat en idiomas son bastante poderosos y capaces de manejar diversas tareas, como preguntas y respuestas, incluso con tu propia documentación, extracción de datos y, hasta cierto punto, ¡incluso pudo jugar algo de ajedrez!

¿Qué sigue?

Consulta algunos de los siguientes codelabs para avanzar con PaLM en Java:

Lecturas adicionales

Documentos de referencia