LangChain4j: potenciando tus aplicaciones Java con Inteligencia Artificial (IA) y Modelos de Lenguaje de Gran Escala (LLM). RAG (Retrieval-Augmented Generation).

0
207

LangChain4j se presenta como un aliado indispensable para integrar en nuestras aplicaciones Java, de manera muy sencilla, capacidades de Inteligencia Artificial y Modelos de Lenguaje a Gran Escala (LLM). Ya dimos unos primeros pasos con LangChain4j y vimos después su integración con Ollama y en este tercer tutorial vamos a explorar un poco más lo que se denomina RAG (Retrieval-Augmented Generation).

Contenido.

1. Introducción.

La generación mejorada por recuperación o Retrieval-Augmented Generation (RAG) es un proceso de optimización de las respuestas de un modelo de lenguaje de gran escala (LLM), de modo tal que, recuperando información de nuestra propia base de conocimiento, sin tener una relación directa con los orígenes de datos del entrenamiento, el LLM es capaz de generar una respuesta basada en nuestros propios datos.

Los LLMs se entrenan con grandes volúmenes de datos y usan miles de millones de parámetros para generar resultados en tareas como responder preguntas, traducir idiomas y completar frases. El patrón RAG aplicado a los LLMs hace que extiendan esas capacidades a dominios específicos o a la base de conocimiento interna de una organización, todo ello sin la necesidad de volver a entrenar el modelo.

Esa base de conocimiento es la que se almacena en nuestros propios espacios vectoriales (embeddings) que ya presentamos en el primero de los tutoriales y que formarían parte de este proceso de RAG.

Básicamente, el proceso se divide en dos partes:

Ingesta de información: se basa en extraer el contenido textual de un documento, aplicar un splitter y dividirlo en segmentos, a esos segmentos se les aplica un modelo de embedding para generar la representación numérica del texto en forma de vectores y tanto los segmentos textuales, como su representación en vectores se almacena en un repositorio.

 

Recuperación mejorada de la información: dada una consulta textual, se le aplica el mismo proceso para convertir el texto de la consulta en una representación numérica, generando una nueva consulta en base a la cuál se obtienen los segmentos más relevantes previamente almacenados, tanto la consulta original, como el resultado de la misma sobre nuestro repositorio de embeddings se envía al LLM para que genere un resultado.

En este tutorial vamos a configurar ambos procesos, mejorando esa prueba inicial que hicimos tanto en las consultas como la recuperación de información textual.

2. Entorno.

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 16′ (Apple M1 Pro, 32GB DDR4).
  • Sistema Operativo: Mac OS Sonoma 14.2.1

3. Configuración.

Vamos a exponer los componentes de ambos pasos.

3.1. Ingesta.

El «webcrawler» es muy similar a que la presentamos, hemos mejorado el selector css («.td-post-content») para recuperar el contenido de los tutoriales y eliminar el ruido de la cabecera y pie de comentarios. Y hemos añadido un metadato adicional con la url del tutorial.

package com.izertis.ai.workaholics.embeddings;

import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.DocumentSplitter;
import dev.langchain4j.data.document.loader.UrlDocumentLoader;
import dev.langchain4j.data.document.parser.TextDocumentParser;
import dev.langchain4j.data.document.splitter.DocumentSplitters;
import dev.langchain4j.data.document.transformer.HtmlTextExtractor;
import dev.langchain4j.model.Tokenizer;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.EmbeddingStoreIngestor;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Map;
import java.util.Optional;

@Component
@Slf4j
public class WorkaholicsWebCrawler {

    @Autowired
    private EmbeddingStore embeddingStore;

    @Autowired
    private EmbeddingModel embeddingModel;

    @Autowired
    private Optional<Tokenizer> tokenizer;

    @PostConstruct
    void init(){
        ingestUrl("https://www.adictosaltrabajo.com/2023/08/21/devops-eso-es-cosa-del-pasado-conoce-mlops/");
        ingestUrl("https://www.adictosaltrabajo.com/2023/07/27/nltk-python/");
        ingestUrl("https://www.adictosaltrabajo.com/2023/05/10/como-ia-puede-mejorar-eficiencia-programador/");
        ingestUrl("https://www.adictosaltrabajo.com/2023/05/06/diagramas-de-arquitectura-con-c4-model/");
        ingestUrl("https://www.adictosaltrabajo.com/2023/05/12/structurizr-para-generar-diagramas-de-arquitectura-con-c4-model/");
    }

    private void ingestUrl(String url) {
        log.info("ingesting {} ", url);
        final Document document = UrlDocumentLoader.load(url, new TextDocumentParser());

        final HtmlTextExtractor transformer = new HtmlTextExtractor(".td-post-content", Map.of("title", "h1.entry-title", "author", ".td-post-author-name", "date", ".td-post-date", "views", ".td-post-views"), true);
        document.metadata().add("url", url);

        DocumentSplitter splitter = DocumentSplitters.recursive(600, 5);
        if (tokenizer.isPresent()){
            splitter = DocumentSplitters.recursive(600, 5, tokenizer.get());
        }
        final EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
                .documentSplitter(splitter)
                .embeddingModel(embeddingModel)
                .embeddingStore(embeddingStore)
                .documentTransformer(transformer)
                .build();

        ingestor.ingest(document);
    }

}

LangChain4j proporciona un conjunto básico de loaders cuya documentación, a día de hoy incompleta, podéis encontrar aquí https://docs.langchain4j.dev/category/document-loaders, ya avisamos del estado incipiente del proyecto.

Almacenamiento.

Para el almacenamiento podemos configurar múltiples opciones que podéis encontrar aquí https://docs.langchain4j.dev/category/embedding-stores quizás las más interesantes sea Chroma DB o incluso ElasticSearch. También la documentación está incompleta a día de hoy, pero proporcionan ejemplos de cómo realizar la integración con las mismas.

Para el objetivo de este tutorial vamos a seguir usando el embedding en memoria.

    @Bean
    EmbeddingStore embeddingStore() {

        return new ExtendedInMemoryEmbeddingStore();
    }
Modelo de embedding.

Para generar la representación numérica del texto debemos configurar un modelo de embedding para realizar la transformación. Tenemos una implementación por cada NPL soportado, podéis encontrar al menos el listado aquí https://docs.langchain4j.dev/category/embedding-models.

En nuestra prueba vamos a configurar un modelo que se basa en el procesamiento de los vectores en nuestra propia máquina.

    @Bean
    @ConditionalOnMissingBean
    EmbeddingModel embeddingModel() {
        return new AllMiniLmL6V2EmbeddingModel();
    }

Condicionando su configuración a la pre-existencia o no de una implementación; puesto que los starters de Spring Boot de open-ai y ollama proporcionan una implementación basada en propiedades. Bastaría con añadir las siguientes propiedades para configurar un modelo de embedding basado en el api del NPL.

 
langchain4j:
  ollama:
    embedding-model:
      base-url: http://localhost:11434
      model-name: mistral:instruct
      timeout: PT60S

Este tipo de implementación hará uso del api del NPL para que sea el propio proveedor del LLM el que genere la representación numérica del texto, en vez de generarlo localmente.

Tokenización.

El último componente para la ingesta es el tokenizador, lo comentamos en el segundo tutorial, podemos usar el tokenizador de open-ai, condicionándolo para usar el de por defecto si usamos Ollama.

    @Bean
    @ConditionalOnProperty("langchain4j.open-ai.chat-model.model-name")
    Tokenizer openAiTokenizer() {
        return new OpenAiTokenizer(OpenAiModelName.GPT_3_5_TURBO);
    }

 

3.2. Recuperación.

Debemos configurar un ContentRetriever basado en una implementación que nos permita configurar el almacenamiento y el modelo de embeddings para que al realizar una consulta se haga la conversión bajo el mismo modelo para recuperar segmentos con similitudes del almacenamiento.

Un par de parámetros de ajustes, el número máximo de segmentos a recuperar y el score mínimo de matching para recuperar un segmento, como indica en los comentarios, la configuración de ambos parámetros dependerá de la naturaleza de nuestros datos y del tipo de almacenamiento que usemos.

    @Bean
    ContentRetriever contentRetriever(EmbeddingStore embeddingStore, EmbeddingModel embeddingModel) {

        // You will need to adjust these parameters to find the optimal setting, which will depend on two main factors:
        // - The nature of your data
        // - The embedding model you are using
        int maxResults = 1;
        double minScore = 0.6;

        return EmbeddingStoreContentRetriever.builder()
                .embeddingStore(embeddingStore)
                .embeddingModel(embeddingModel)
                .maxResults(maxResults)
                .minScore(minScore)
                .build();
    }

Por último, al configurar nuestra IA le inyectamos el contentRetriever configurado previamente.

    @Bean
    public WorkaholicsAgent customerSupportAgent(ChatLanguageModel chatLanguageModel,
                                                 ContentRetriever contentRetriever) {
        return AiServices.builder(WorkaholicsAgent.class)
                .chatLanguageModel(chatLanguageModel)
                .chatMemory(MessageWindowChatMemory.withMaxMessages(20))
                .contentRetriever(contentRetriever)
                .build();
    }

En realidad, con esta implementación estaremos usando un RetrievalAugmentor por defecto.

4. Búsqueda basada en metadatos.

Con esa implementación podemos hacer búsquedas como las que mostrábamos en tutoriales anteriores, si bien, frente al siguiente tipo de preguntas no obtenemos un resultado muy óptimo.

User: cuántos tutoriales se han publicado últimamente?
Agent: Lo siento, pero no tengo acceso en tiempo real a la información más reciente de "adictos al trabajo" para proporcionar el número exacto de tutoriales publicados últimamente. Te recomendaría visitar el sitio web directamente en https://www.adictosaltrabajo.com/ para obtener la información más actualizada sobre la cantidad de tutoriales publicados recientemente.


User: qué tutoriales ha publicado Jose Manuel Sánchez Suárez?
Agent: Lo siento, pero no tengo acceso en tiempo real a la información más reciente de "adictos al trabajo" para proporcionar tutoriales específicos de Jose Manuel Sánchez Suárez. Te recomendaría visitar el sitio web directamente en https://www.adictosaltrabajo.com/ para buscar los tutoriales publicados por ese autor.

 

Ya vimos que estamos almacenando en el embedding ciertos metadatos, ahora los hemos afinado y hemos añadido la url del tutorial. Si en vez de usar el RetrievalAugmentor por defecto definimos nuestro propio retrieval podemos configurar qué metadatos de los documentos almacenados pueden usarse para recuperar similitudes y enriquecer la información de las respuestas.

    @Bean
    public RetrievalAugmentor retrievalAugmentor(ContentRetriever contentRetriever, ContentInjector contentInjector){
        return DefaultRetrievalAugmentor.builder()
                .contentRetriever(contentRetriever)
                .contentInjector(contentInjector)
                .build();
    }
    @Bean
    ContentInjector contentInjector(){
        return DefaultContentInjector.builder()
        .metadataKeysToInclude(Arrays.asList("title", "author", "views", "date", "url"))
        .build();
    }

 

Una vez declarados ambos beans, debemos modificar el modo de creación del servicio como sigue:

    @Bean
    public WorkaholicsAgent customerSupportAgent(ChatLanguageModel chatLanguageModel,
                                                 RetrievalAugmentor retrievalAugmentor) {
        return AiServices.builder(WorkaholicsAgent.class)
                .chatLanguageModel(chatLanguageModel)
                .chatMemory(MessageWindowChatMemory.withMaxMessages(20))
                .retrievalAugmentor(retrievalAugmentor)
                .build();
    }

 

Comparemos las respuestas a las mismas preguntas anteriores:

User: cuántos tutoriales se han publicado últimamente?
Agent: Recientemente se ha publicado un tutorial sobre "Diagramas de arquitectura con C4 model" el día 6 de mayo de 2023. Puedes encontrar más información sobre este tema en el siguiente enlace: [Diagramas de arquitectura con C4 model](https://www.adictosaltrabajo.com/2023/05/06/diagramas-de-arquitectura-con-c4-model/).

User: qué tutoriales ha publicado Jose Manuel Sánchez Suárez?
Agent: Jose Manuel Sánchez Suárez ha publicado un tutorial titulado "Diagramas de arquitectura con C4 model" el día 6 de mayo de 2023. Puedes encontrar más detalles sobre este tutorial en el siguiente enlace: [Diagramas de arquitectura con C4 model](https://www.adictosaltrabajo.com/2023/05/06/diagramas-de-arquitectura-con-c4-model/).

 

La información no sigue siendo del todo correcta puesto que el tutorial de C4M no es el último tutorial, por fecha de publicación que hemos indexado, pero lo importante es que está usando los metadatos para resolver las consultas.

Atentos al siguiente bloque de preguntas:

User: donde puedo obtener información sobre mlops?
Agent: Puedes obtener información sobre MLOps en el tutorial titulado "¿DevOps? Eso es cosa del pasado, conoce MLOps" escrito por Luis Merino Ulizarna. Este tutorial fue publicado el 21 de agosto de 2023. Para acceder a más detalles sobre MLOps y su aplicación en el desarrollo de sistemas de machine learning, puedes visitar el siguiente enlace: [¿DevOps? Eso es cosa del pasado, conoce MLOps](https://www.adictosaltrabajo.com/2023/08/21/devops-eso-es-cosa-del-pasado-conoce-mlops/).

User: quién publicó ese tutorial?
Agent: El tutorial titulado "¿DevOps? Eso es cosa del pasado, conoce MLOps" fue publicado por Luis Merino Ulizarna. Puedes encontrar más información sobre este tema en el siguiente enlace: [¿DevOps? Eso es cosa del pasado, conoce MLOps](https://www.adictosaltrabajo.com/2023/08/21/devops-eso-es-cosa-del-pasado-conoce-mlops/).

User: cuántas visitas tiene?
Agent: El tutorial "¿DevOps? Eso es cosa del pasado, conoce MLOps" publicado por Luis Merino Ulizarna tiene 1170 visitas. Puedes acceder al tutorial completo en el siguiente enlace: [¿DevOps? Eso es cosa del pasado, conoce MLOps](https://www.adictosaltrabajo.com/2023/08/21/devops-eso-es-cosa-del-pasado-conoce-mlops/)

User: se publicó hace más de un año?
Agent: El tutorial "¿DevOps? Eso es cosa del pasado, conoce MLOps" se publicó el 21 de agosto de 2023, por lo que no ha pasado más de un año desde su publicación.

 

5. Contestando con información estructurada.

Existen distintas maneras de recuperar información estructurada a partir de contenido textual, como por ejemplo:

        static class Person {

            private String firstName;
            private String lastName;
            private LocalDate birthDate;

            @Override
            public String toString() {
                return "Person {" +
                        " firstName = \"" + firstName + "\"" +
                        ", lastName = \"" + lastName + "\"" +
                        ", birthDate = " + birthDate +
                        " }";
            }
        }

        interface PersonExtractor {

            @UserMessage("Extract information about a person from {{it}}")
            Person extractPersonFrom(String text);
        }

        public static void main(String[] args) {

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

            String text = "In 1968, amidst the fading echoes of Independence Day, "
                    + "a child named John arrived under the calm evening sky. "
                    + "This newborn, bearing the surname Doe, marked the start of a new journey.";


            Person person = extractor.extractPersonFrom(text);

            System.out.println(person); // Person { firstName = "John", lastName = "Doe", birthDate = 1968-07-04 }
        }

@UserMessage es como @SystemMessage, nos sirve para delimitar el comportamiento, pero para los mensajes de usuario; la etiqueta {{it}} hace referencia al único parámetro que recibe el método. Si lo invocamos nos devolvería la información de una persona extraída de un contenido textual.

Para nuestro caso de uso creo que podríamos probar con algo más simple que es condicionar la respuesta en el propio mensaje de sistema, intentando que, cada vez que una respuesta haga referencia a un tutorial publicado, devuelva la información en un formato concreto.

public interface WorkaholicsAgent {

    @SystemMessage("""
            You are an expert in the tutorials published by the best technology site in Spanish called \"adictos al trabajo\",
            accessible through the url \"https://www.adictosaltrabajo.com/\".
            You will provide accurate information on their content and examples based solely on the tutorials provided on the site itself
            When your answer refers to a tutorial, you will provide the information in the following format:
             Title: ...
             Author: ...
             Views: ...
             Url: ...
             Summary: ...
            Today is {{current_date}}.
            """
    )
    String chat(String userMessage);

}

Vamos a probarlo:

User: dónde puedo leer sobre cm4 y structurizr?
Puedes leer sobre Structurizr y C4 model en el siguiente tutorial:

Title: Structurizr para generar diagramas de arquitectura con C4 model
Author: Por Jose Manuel Sánchez Suárez
Views: 1088
Url: https://www.adictosaltrabajo.com/2023/05/12/structurizr-para-generar-diagramas-de-arquitectura-con-c4-model/
Summary: Structurizr está diseñado para ayudar al modelado con C4 model, cuyo objetivo es la visualización de la arquitectura de nuestro software desde distintos puntos de vista usando un modelo único. Structurizr se basa en la idea de diagramas como código pero separando el modelo de sus formas de visualización, permitiendo crear múltiples diagramas de arquitectura de software a partir de un único modelo.

User: me interesa saber de mlops
Agent: Puedes aprender más sobre MLOps en el siguiente tutorial:

Title: ¿DevOps? Eso es cosa del pasado, conoce MLOps
Author: Por Luis Merino Ulizarna
Views: 1170
Url: https://www.adictosaltrabajo.com/2023/08/21/devops-eso-es-cosa-del-pasado-conoce-mlops/
Summary: MLOps es una práctica en el desarrollo de sistemas de machine learning que busca aplicar los principios de la metodología DevOps a este tipo de sistemas, unificando el desarrollo de los modelos y las operaciones. Se enfoca en la colaboración entre equipos, automatización, infraestructura como código, control de versiones, cultura de aprendizaje y mejora continua, y enfoque en el cliente.

 

6. Referencias.

7. Conclusiones.

En este tutorial hemos revisado, con un mayor nivel de detalle, todos los componentes relacionados con los procesos de ingesta y recuperación mejorada de información (RAG) dentro de una aplicación generada con el soporte de LangChain4j y Spring Boot contra un LLM.

Hemos visto cómo recuperar información basada en metadatos y alguna de las técnicas para formatear las respuestas.

Y aún nos queda por ver lo mejor 😉

Stay tuned, again!

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad