1. Visión general
Las aplicaciones web modernas están integrando cada vez más con Modelos de Lenguaje Grande (LLMs) para crear soluciones como chatbots y asistentes virtuales.
Sin embargo, aunque los LLMs son poderosos, tienden a generar alucinaciones, y sus respuestas pueden no ser siempre relevantes, adecuadas o fácticamente precisas.
Una solución para evaluar las respuestas de un LLM es usar otro LLM, preferiblemente uno separado.
Para lograr esto, Spring AI define la interfaz Evaluator y ofrece dos implementaciones para comprobar la relevancia y la precisión factual de la respuesta del LLM, a saber, RelevanceEvaluator y FactCheckingEvaluator.
En este tutorial, exploraremos cómo usar los Evaluators de Spring AI para probar las respuestas de los LLM. Utilizaremos las dos implementaciones básicas proporcionadas por Spring AI para evaluar las respuestas de un chatbot de Generación Aumentada por Recuperación (RAG).
2. Construir un chatbot RAG
Antes de poder comenzar a probar las respuestas de los LLM, necesitaremos un chatbot para probar. Para nuestra demostración, construiremos un chatbot RAG sencillo que responda a las preguntas de los usuarios basándose en un conjunto de documentos.
Utilizaremos Ollama, una herramienta de código abierto, para descargar y ejecutar nuestros modelos de completado de chat y de incrustación localmente.
2.1. Dependencias
Comencemos añadiendo las dependencias necesarias al archivo pom.xml de nuestro proyecto:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
<version>1.0.0-M5</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-markdown-document-reader</artifactId>
<version>1.0.0-M5</version>
</dependency>
La dependencia de inicio de Ollama nos ayuda a establecer una conexión con el servicio Ollama.
Además, importamos la dependencia de lector de documentos Markdown de Spring AI, que utilizaremos para convertir archivos .md en documentos que podemos almacenar en la base de vectores.
Dado que la versión actual, 1.0.0-M5, es una versión de hito, también necesitaremos añadir el repositorio Spring Milestones a nuestro pom.xml:
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
Este repositorio es donde se publican las versiones de hito, en lugar del repositorio Maven Central estándar.
Dado que estamos utilizando varios starters de Spring AI en nuestro proyecto, también incluiremos el Bill of Materials (BOM) de Spring AI en nuestro pom.xml:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.0.0-M5</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Con esta adición, ahora podemos eliminar la etiqueta version de ambas dependencias de inicio.
El BOM elimina el riesgo de conflictos de versiones y garantiza que nuestras dependencias de Spring AI sean compatibles entre sí.
2.2. Configurar un modelo de completado de chat y un modelo de incrustación
A continuación, configuremos nuestros modelos de completado de chat y de incrustación en el archivo application.yaml:
spring:
ai:
ollama:
chat:
options:
model: llama3.3
embedding:
options:
model: nomic-embed-text
init:
pull-model-strategy: when_missing
Aquí, especificamos el modelo llama3.3 proporcionado por Meta como nuestro modelo de completado de chat y el modelo nomic-embed-text proporcionado por Nomic AI como nuestro modelo de incrustación. Siéntase libre de probar esta implementación con modelos diferentes.
Además, establecemos la pull-model-strategy a when_missing. Esto garantiza que Spring AI descargue los modelos especificados si no están disponibles localmente.
Al configurar modelos válidos, Spring AI crea automáticamente beans del tipo ChatModel y EmbeddingModel, lo que nos permite interactuar con los modelos de completado de chat y de incrustación, respectivamente.
Utilicemos estos beans para definir los beans adicionales necesarios para nuestro chatbot:
@Bean
public VectorStore vectorStore(EmbeddingModel embeddingModel) {
return SimpleVectorStore
.builder(embeddingModel)
.build();
}
@Bean
public ChatClient contentGenerator(ChatModel chatModel, VectorStore vectorStore) {
return ChatClient.builder(chatModel)
.defaultAdvisors(new QuestionAnswerAdvisor(vectorStore))
.build();
}
Primero, definimos un bean VectorStore y utilizamos la implementación SimpleVectorStore, que es una implementación en memoria que emula un almacén de vectores utilizando la clase java.util.Map.
En una aplicación de producción, podemos considerar el uso de un almacén de vectores real como ChromaDB.
A continuación, utilizando los beans ChatModel y VectorStore, creamos un bean de tipo ChatClient, que es nuestro punto de entrada principal para interactuar con nuestro modelo de completado de chat.
Lo configuramos con un QuestionAnswerAdvisor, que utiliza el almacén de vectores para recuperar las porciones relevantes de los documentos almacenados según la pregunta del usuario y las proporciona como contexto al modelo de chat.
2.3. Poblar nuestro almacén de vectores en memoria
Para nuestra demostración, hemos incluido un archivo leave-policy.md que contiene información de ejemplo sobre políticas de licencia en el directorio src/main/resources/documents.
Ahora, para poblar el almacén de vectores con nuestro documento durante el inicio de la aplicación, crearemos una clase VectorStoreInitializer que implemente la interfaz ApplicationRunner:
@Component
class VectorStoreInitializer implements ApplicationRunner {
private final VectorStore vectorStore;
private final ResourcePatternResolver resourcePatternResolver;
// standard constructor
@Override
public void run(ApplicationArguments args) {
List<Document> documents = new ArrayList<>();
Resource[] resources = resourcePatternResolver.getResources("classpath:documents/*.md");
Arrays.stream(resources).forEach(resource -> {
MarkdownDocumentReader markdownDocumentReader = new MarkdownDocumentReader(resource, MarkdownDocumentReaderConfig.defaultConfig());
documents.addAll(markdownDocumentReader.read());
});
vectorStore.add(new TokenTextSplitter().split(documents));
}
}
Dentro del método run(), primero utilizamos la clase ResourcePatternResolver inyectada para obtener todos los archivos Markdown del directorio src/main/resources/documents. Aunque solo estamos trabajando con un solo archivo Markdown, nuestro método es extensible.
Luego, convertimos los resources obtenidos en objetos Document utilizando la clase MarkdownDocumentReader.
Finalmente, agregamos los documents al almacén de vectores después de dividirlos en fragmentos más pequeños usando la clase TokenTextSplitter.
Cuando invocamos el método add(), Spring AI convierte automáticamente nuestro contenido de texto plano en una representación vectorial antes de almacenarlo en el almacén de vectores. No necesitamos convertirlo explícitamente utilizando el bean EmbeddingModel.
3. Configurar Ollama con Testcontainers
Para facilitar el desarrollo y las pruebas locales, utilizaremos Testcontainers para configurar el servicio Ollama, cuyo requisito previo es una instancia activa de Docker.
3.1. Dependencias de prueba
Primero, añadamos las dependencias de prueba necesarias a nuestro pom.xml:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>ollama</artifactId>
<scope>test</scope>
</dependency>
Importamos la dependencia Spring AI Testcontainers para Spring Boot y el módulo Ollama de Testcontainers.
Estas dependencias proporcionan las clases necesarias para iniciar una instancia Docker efímera para el servicio Ollama.
3.2. Definir beans de Testcontainers
A continuación, creemos una clase @TestConfiguration que defina nuestros beans de Testcontainers:
@TestConfiguration(proxyBeanMethods = false)
class TestcontainersConfiguration {
@Bean
public OllamaContainer ollamaContainer() {
return new OllamaContainer("ollama/ollama:0.5.7");
}
@Bean
public DynamicPropertyRegistrar dynamicPropertyRegistrar(OllamaContainer ollamaContainer) {
return registry -> {
registry.add("spring.ai.ollama.base-url", ollamaContainer::getEndpoint);
};
}
}
Especificamos la última versión estable de la imagen Ollama al crear el bean OllamaContainer.
Luego, definimos un bean DynamicPropertyRegistrar para configurar la base-url del servicio Ollama. Esto permite que nuestra aplicación se conecte al contenedor iniciado.
Ahora, podemos usar esta configuración en nuestras pruebas de integración anotando nuestras clases de prueba con la anotación @Import(TestcontainersConfiguration.class).
4. Usar los Evaluators de Spring AI
Ahora que hemos construido nuestro chatbot RAG y configurado un entorno de prueba local, veamos cómo podemos usar las dos implementaciones disponibles de la interfaz Evaluator de Spring AI para probar las respuestas que genera.
4.1. Configurar el modelo de evaluación
La calidad de nuestras pruebas depende en última instancia de la calidad del modelo de evaluación que utilicemos. Elijaremos el estándar de la industria actual, el modelo bespoke-minicheck, que es un modelo de código abierto entrenado específicamente para pruebas de evaluación por Bespoke Labs. Ocupa el primer lugar en la tabla de clasificación LLM-AggreFact y solo produce una respuesta sí/no.
Configurémoslo en nuestro archivo application.yaml:
com:
baeldung:
evaluation:
model: bespoke-minicheck
A continuación, crearemos un bean ChatClient separado para interactuar con nuestro modelo de evaluación:
@Bean
public ChatClient contentEvaluator(
OllamaApi olamaApi,
@Value("${com.baeldung.evaluation.model}") String evaluationModel
) {
ChatModel chatModel = OllamaChatModel.builder()
.ollamaApi(olamaApi)
.defaultOptions(OllamaOptions.builder()
.model(evaluationModel)
.build())
.modelManagementOptions(ModelManagementOptions.builder()
.pullModelStrategy(PullModelStrategy.WHEN_MISSING)
.build())
.build();
return ChatClient.builder(chatModel)
.build();
}
Aquí, definimos un nuevo bean ChatClient utilizando el bean OllamaApi que Spring AI crea para nosotros y nuestra propiedad de modelo de evaluación personalizada, que inyectamos usando la anotación @Value.
Es importante señalar que utilizamos una propiedad personalizada para nuestro modelo de evaluación y creamos manualmente su clase ChatModel correspondiente, ya que la clase OllamaAutoConfiguration solo permite configurar un modelo a través de la propiedad spring.ai.ollama.chat.options.model, que ya hemos usado para nuestro modelo de generación de contenido.
4.2. Evaluar la relevancia de la respuesta del LLM con RelevancyEvaluator
Spring AI ofrece la implementación RelevancyEvaluator para comprobar si una respuesta del LLM es relevante para la consulta del usuario y el contexto recuperado del almacén de vectores.
Primero, creemos un bean para ello:
@Bean
public RelevancyEvaluator relevancyEvaluator(
@Qualifier("contentEvaluator") ChatClient chatClient) {
return new RelevancyEvaluator(chatClient.mutate());
}
Utilizamos la anotación @Qualifier para inyectar el bean ChatClient relevancyEvaluator que definimos anteriormente y crear una instancia de la clase RelevancyEvaluator.
Como su constructor espera un constructor Builder en lugar de una instancia directa de ChatClient, llamamos al método mutate() que devuelve un objeto ChatClient.Builder inicializado con la configuración de nuestro cliente existente.
Ahora, probemos la respuesta de nuestro chatbot para ver si es relevante:
String question = "How many days sick leave can I take?";
ChatResponse chatResponse = contentGenerator.prompt()
.user(question)
.call()
.chatResponse();
String answer = chatResponse.getResult().getOutput().getContent();
List<Document> documents = chatResponse.getMetadata().get(QuestionAnswerAdvisor.RETRIEVED_DOCUMENTS);
EvaluationRequest evaluationRequest = new EvaluationRequest(question, documents, answer);
EvaluationResponse evaluationResponse = relevancyEvaluator.evaluate(evaluationRequest);
assertThat(evaluationResponse.isPass()).isTrue();
String nonRelevantAnswer = "A lion is the king of the jungle";
evaluationRequest = new EvaluationRequest(question, documents, nonRelevantAnswer);
evaluationResponse = relevancyEvaluator.evaluate(evaluationRequest);
assertThat(evaluationResponse.isPass()).isFalse();
Comenzamos invocando nuestro ChatClient contentGenerator con una pregunta y extraemos la respuesta generada y los documentos utilizados para generarla desde el ChatResponse devuelto.
Luego, creamos un EvaluationRequest que contenga la pregunta, los documentos recuperados y la respuesta del chatbot. Lo pasamos al bean relevancyEvaluator y afirmamos que la respuesta es relevante utilizando el método isPass().
Sin embargo, cuando pasamos una respuesta completamente no relacionada sobre leones, el evaluador la identifica correctamente como no relevante.
4.3. Evaluar la precisión factual de la respuesta del LLM con FactCheckingEvaluator
De manera similar, Spring AI proporciona una implementación FactCheckingEvaluator para validar la precisión factual de la respuesta del LLM frente al contexto recuperado.
También creemos un bean FactCheckingEvaluator utilizando nuestro ChatClient contentEvaluator:
@Bean
public FactCheckingEvaluator factCheckingEvaluator(
@Qualifier("contentEvaluator") ChatClient chatClient) {
return new FactCheckingEvaluator(chatClient.mutate());
}
Finalmente, probemos la precisión factual de la respuesta de nuestro chatbot:
String question = "How many days sick leave can I take?";
ChatResponse chatResponse = contentGenerator.prompt()
.user(question)
.call()
.chatResponse();
String answer = chatResponse.getResult().getOutput().getContent();
List<Document> documents = chatResponse.getMetadata().get(QuestionAnswerAdvisor.RETRIEVED_DOCUMENTS);
EvaluationRequest evaluationRequest = new EvaluationRequest(question, documents, answer);
EvaluationResponse evaluationResponse = factCheckingEvaluator.evaluate(evaluationRequest);
assertThat(evaluationResponse.isPass()).isTrue();
String wrongAnswer = "You can take no leaves. Get back to work!";
evaluationRequest = new EvaluationRequest(question, documents, wrongAnswer);
evaluationResponse = factCheckingEvaluator.evaluate(evaluationRequest);
assertThat(evaluationResponse.isPass()).isFalse();
De manera similar al enfoque anterior, creamos un EvaluationRequest con la pregunta, los documentos recuperados y la respuesta del chatbot, y lo pasamos a nuestro bean factCheckingEvaluator.
Afirmamos que la respuesta del chatbot es fácticamente precisa según el contexto recuperado. Además, volvemos a probar la evaluación con una respuesta fácticamente incorrecta codificada y afirmamos que el método isPass() devuelve false.
Vale la pena señalar que si pasáramos nuestra wrongAnswer codificada al RelevancyEvaluator, la evaluación pasaría, ya que aunque la respuesta sea fácticamente incorrecta, sigue siendo relevante para el tema de licencias enfermas que el usuario preguntó.
5. Conclusión
En este artículo, hemos explorado cómo probar las respuestas de los LLM utilizando la interfaz Evaluator de Spring AI.
Construimos un chatbot RAG sencillo que responde a las preguntas de los usuarios basándose en un conjunto de documentos y utilizamos Testcontainers para configurar el servicio Ollama, creando un entorno de prueba local.
Luego, utilizamos las implementaciones RelevancyEvaluator y FactCheckingEvaluator proporcionadas por Spring AI para evaluar la relevancia y la precisión factual de las respuestas de nuestro chatbot.
El código que respalda este artículo está disponible en GitHub. Una vez que hayas iniciado sesión como Miembro Pro de Baeldung, comienza a aprender y programar en el proyecto.
Newsletter Semanal de Java
Cada viernes recibe lo más nuevo del ecosistema Java: frameworks, herramientas y mejores prácticas.
Sin spam. Cancela cuando quieras.