Artículo

Implementación de un asistente de IA con Spring AI

Implementación de un asistente de IA con Spring AI

1. Visión general

En este tutorial, discutiremos los conceptos de Spring AI que pueden ayudar a crear un asistente de IA usando LLMs como ChatGPT, Ollama, Mistreal, etc.

Las empresas están adoptando cada vez más asistentes de IA para mejorar la experiencia del usuario en una amplia gama de funcionalidades empresariales existentes:

  • Responder consultas de los usuarios
  • Realizar transacciones basadas en la entrada del usuario
  • Resumir oraciones y documentos extensos

Si bien estas son solo algunas capacidades fundamentales de los LLMs, su potencial va mucho más allá de estas funcionalidades.

2. Funciones de Spring AI

El framework Spring AI ofrece una gama de características emocionantes para entregar funcionalidades impulsadas por IA:

  • interfaces que se integran sin problemas con los servicios LLM subyacentes y las bases de datos vectoriales
  • generación de respuestas conscientes del contexto y ejecución de acciones utilizando RAG y las APIs de Function Calling
  • convertidores de salida estructurada para transformar las respuestas de los LLM en POJOs o formatos legibles por máquina como JSON
  • enriquecer los prompts y aplicar salvaguardas mediante interceptores proporcionados por la Advisor API
  • mayor compromiso del usuario manteniendo estados de conversación

Podemos visualizarlo también:
Bloques de construcción del asistente de IA

Para ilustrar algunas de estas funciones, construiremos un chatbot en un Sistema de Gestión de Pedidos legado (OMS):

Las funcionalidades típicas de un OMS consisten en:

  • Crear pedido
  • Obtener pedidos del usuario

3. Requisitos previos

Primero, necesitaremos una suscripción a OpenAI para usar su servicio LLM. Luego, en la aplicación Spring Boot, añadiremos las dependencias Maven para las bibliotecas Spring AI. Hemos cubierto los requisitos previos en nuestros otros artículos con gran detalle, por lo que no profundizaremos más en este tema.

Además, usaremos la base de datos en memoria HSQLDB para comenzar rápidamente. Vamos a crear las tablas requeridas e insertar algunos datos:

CREATE TABLE User_Order (
    order_id BIGINT NOT NULL PRIMARY KEY,
    user_id VARCHAR(20) NOT NULL,
    quantity INT
);

INSERT INTO User_Order (order_id, user_id, quantity) VALUES (1, 'Jenny', 2);
INSERT INTO User_Order (order_id, user_id, quantity) VALUES (2, 'Mary', 5);
INSERT INTO User_Order (order_id, user_id, quantity) VALUES (3, 'Alex', 1);
INSERT INTO User_Order (order_id, user_id, quantity) VALUES (4, 'John', 3);
INSERT INTO User_Order (order_id, user_id, quantity) VALUES (5, 'Sophia', 4);
--and so on..

Usaremos algunas propiedades de configuración estándar relacionadas con la inicialización de HSQLDB y el cliente OpenAI en el archivo application.properties de Spring:

spring.datasource.driver-class-name=org.hsqldb.jdbc.JDBCDriver
spring.datasource.url=jdbc:hsqldb:mem:testdb;DB_CLOSE_DELAY=-1
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.hibernate.ddl-auto=none

spring.ai.openai.chat.options.model=gpt-4o-mini
spring.ai.openai.api-key=xxxxxxx

Seleccionar el modelo adecuado para un caso de uso es un proceso iterativo complejo que implica mucha prueba y error. Sin embargo, para una aplicación demo simple para este artículo, el modelo económico GPT‑4o mini será suficiente.

4. API de Function Calling

Esta característica es uno de los pilares del popular concepto de agente en aplicaciones basadas en LLM. Permite a las aplicaciones realizar una colección compleja de tareas precisas y tomar decisiones de forma independiente.

Por ejemplo, puede ayudar enormemente a construir un chatbot en la aplicación de gestión de pedidos legado. El chatbot puede ayudar a los usuarios a generar solicitudes de pedido, recuperar el historial de pedidos y hacer más cosas mediante lenguaje natural.

Estas son habilidades especializadas impulsadas por una o más funciones de la aplicación. Definimos el algoritmo en un prompt enviado al LLM con el esquema de funciones de apoyo. El LLM recibe este esquema e identifica la función correcta para realizar la habilidad solicitada. Más tarde, envía su decisión a la aplicación.

Finalmente, la aplicación ejecuta la función y devuelve más información al LLM:

Secuencia de Function Calling

Primero, echemos un vistazo a los componentes principales de la aplicación heredada:

Diagrama de clases

La clase OrderManagementService tiene dos funciones importantes: crear pedidos y obtener el historial de pedidos del usuario. Ambas funciones utilizan el bean OrderRepository para integrarse con la base de datos:

@Service
public class OrderManagementService {
    @Autowired
    private OrderRepository orderRepository;

    public Long createOrder(OrderInfo orderInfo) {
        return orderRepository.save(orderInfo).getOrderID();
    }

    public Optional<List<OrderInfo>> getAllUserOrders(String userID) {
       return orderRepository.findByUserID(userID);
    }
}

Ahora, veamos cómo Spring AI puede ayudar a implementar un chatbot en la aplicación heredada:

Diagrama de clases del Asistente de IA

En el diagrama de clases, la clase OmAiAssistantConfiguration es un bean de configuración de Spring. Registra los beans de devolución de llamada de funciones, createOrderFn y getUserOrderFn:

@Configuration
public class OmAiAssistantConfiguration {
    @Bean
    @Description("Create an order. The Order ID is identified with orderID. "
      + The order quantity is identified by orderQuantity."
      + "The user is identified by userID. "
      + "The order quantity should be a positive whole number."
      + "If any of the parameters like user id and the order quantity is missing"
      + "then ask the user to provide the missing information.")
    public Function<CreateOrderRequest, Long> createOrderFn(OrderManagementService orderManagementService) {
        return createOrderRequest -> orderManagementService.createOrder(createOrderRequest.orderInfo());
    }
    @Bean
    @Description("get all the orders of an user. The user ID is identified with userID.")
    public Function<GetOrderRequest, List<OrderInfo>> getUserOrdersFn(OrderManagementService orderManagementService) {
        return getOrderRequest -> orderManagementService.getAllUserOrders(getOrderRequest.userID()).get();
    }
}

La anotación @Description ayuda a generar el esquema de la función. Posteriormente, la aplicación envía este esquema al LLM como parte del prompt. Dado que las funciones usan los métodos existentes de la clase OrderManagementService, promueve la reutilización.

Además, CreateOrderRequest y GetOrderRequests son clases de registro, que ayudan a Spring AI a generar los POJOs para las llamadas de servicio posteriores:

record GetOrderRequest(String userID) {}

record CreateOrderRequest(OrderInfo orderInfo) {}

Finalmente, echemos un vistazo a la nueva clase OrderManagementAIAssistant que enviará las consultas del usuario al servicio LLM:

@Service
public class OrderManagementAIAssistant {
    @Autowired
    private ChatModel chatClient;

    public ChatResponse callChatClient(Set<String> functionNames, String promptString) {
        Prompt prompt  = new Prompt(promptString, OpenAiChatOptions
          .builder()
          .withFunctions(functionNames)
          .build()
        );
        return chatClient.call(prompt);
    }
}

El método callChatClient() ayuda a registrar las funciones en el objeto Prompt. Más tarde, invoca el método ChatModel#call() para obtener la respuesta del servicio LLM.

5. Escenarios de Function Calling

Para una consulta o instrucción del usuario dirigida a un asistente de IA, cubriremos algunos escenarios básicos:

  • El LLM decide e identifica una o más funciones para ejecutar
  • El LLM se queja de información incompleta para ejecutar la función
  • El LLM ejecuta declaraciones condicionalmente

Hemos discutido los conceptos hasta ahora; por lo tanto, a continuación utilizaremos esta característica para construir el chatbot.

5.1. Ejecutar una función de retorno una o más veces

Ahora, estudiaremos el comportamiento del LLM cuando lo invocamos con un prompt que consiste en la consulta del usuario y el esquema de funciones.

Comenzaremos con un ejemplo que crea un pedido:

void whenOrderInfoProvided_thenSaveInDB(String promptString) {
    ChatResponse response = this.orderManagementAIAssistant
      .callChatClient(Set.of("createOrderFn"), promptString);
    String resultContent = response.getResult().getOutput().getText();
    logger.info("The response from the LLM service: {}", resultContent);
}

Sorprendentemente, con lenguaje natural, obtenemos los resultados deseados:

PromptRespuesta del LLMObservación
Crear un pedido con una cantidad de 20 para el ID de usuario Jenny, y generar aleatoriamente un número entero positivo para el ID del pedidoEl pedido se ha creado con éxito con los siguientes detalles: -- ID del Pedido: 123456 -- ID del Usuario: Jenny -- Cantidad del Pedido: 20El programa crea el pedido con la información proporcionada en el prompt.
Crear dos pedidos. El primer pedido es para el ID de usuario Sophia con una cantidad de 30. El segundo pedido es para el ID de usuario Mary con una cantidad de 40. Generar aleatoriamente números enteros positivos para los ID de los pedidos.Los pedidos se han creado con éxito: 1. Pedido para Sophia: ID del pedido 1 con una cantidad de 30. 2. Pedido para Mary: ID del pedido 2 con una cantidad de 40. ¡Si necesitas algo más, no dudes en preguntar!El programa crea dos pedidos. El LLM fue inteligente al solicitar ejecutar la función dos veces.

A continuación, veamos si el LLM puede entender el prompt que solicita obtener los detalles del pedido de un usuario:

void whenUserIDProvided_thenFetchUserOrders(String promptString) {
    ChatResponse response = this.orderManagementAIAssistant
      .callChatClient(Set.of("getUserOrdersFn"), promptString);
    String resultContent = response.getResult().getOutput().getText();
    logger.info("The response from the LLM service: {}", resultContent);
}

Los resultados son similares al caso anterior. El LLM identifica con éxito la función registrada para ejecutar. Más tarde, la aplicación Spring Boot invoca la función solicitada y devuelve los detalles del pedido de uno o más usuarios.

5.2. Ejecutar funciones de retorno condicionalmente

Consideremos el programa que crearía un pedido solo cuando se cumplan ciertos criterios:

void whenUserIDProvided_thenCreateOrderIfUserHasLessThanTwoOrders(String promptString) {
    ChatResponse response = this.orderManagementAIAssistant
      .callChatClient(Set.of("getUserOrdersFn", "createOrderFn"), promptString);
    String resultContent = response.getResult()
      .getOutput()
      .getText();
    logger.info("The response from the LLM service: {}", resultContent);
}

Registramos las funciones getUserOrderFn y CreateOrderFn en el bean ChatModel. Además, el LLM debe determinar qué función producirá los mejores resultados para la acción solicitada.

Al igual que antes, los resultados son alentadores:

PromptSalidaObservación
Crear un pedido para el ID de usuario Alex con una cantidad de 25. No crear ningún pedido si el usuario tiene más de 2 pedidos. Al crear el pedido, generar aleatoriamente números enteros positivos para el ID del pedidoEl usuario Alex ya tiene más de 2 pedidos (específicamente, 4 pedidos). Por lo tanto, no crearé un nuevo pedido.El LLM puede invocar condicionalmente funciones posteriores basándose en la salida de las funciones anteriores.
Crear un pedido para el ID de usuario David con una cantidad de 25. No crear ningún pedido si el usuario tiene más de 2 pedidos. Al crear el pedido, generar aleatoriamente números enteros positivos para el ID del pedidoSe ha creado con éxito un pedido para el ID de usuario "David" con una cantidad de pedido de 25, y el ID del pedido es 42.Esta vez getUserOrderFn devolvió menos de 2 pedidos para David. Por lo tanto, el LLM decidió solicitar ejecutar createOrderFn.
Crear un pedido con una cantidad de 20Proporcione su ID de usuario para crear el pedido.El LLM identificó al principio que faltaba el ID del usuario y abortó el procesamiento adicional.

6. API de Advisors de Spring AI

En las secciones anteriores, discutimos los aspectos funcionales de la aplicación. Sin embargo, existen algunas preocupaciones comunes en todas las funcionalidades, como:

  • Evitar que los usuarios introduzcan información sensible
  • Registro y auditoría de las consultas de los usuarios
  • Mantener los estados conversacionales
  • Enriquecer los prompts

Afortunadamente, las APIs de Advisors pueden ayudar a abordar consistentemente estas preocupaciones. Ya lo hemos explicado en detalle en uno de nuestros artículos.

7. API de Salida Estructurada de Spring AI y Spring AI RAG

Los LLMs generan principalmente mensajes de respuesta a las consultas de los usuarios en lenguajes naturales. Sin embargo, los servicios posteriores suelen entender los mensajes en formatos legibles por máquina como POJOs, JSON, etc. Aquí es donde la capacidad de Spring AI para generar salida estructurada juega un papel importante.

Al igual que las APIs de Advisors, lo hemos explicado en gran detalle en uno de nuestros artículos. Por lo tanto, no discutiremos más sobre esto y pasaremos al siguiente tema.

A veces, las aplicaciones deben consultar una base de datos vectorial para realizar una búsqueda semántica sobre los datos almacenados y recuperar información adicional. Más tarde, el resultado obtenido de la base de datos vectorial se usa en el prompt para proporcionar información contextualizada al LLM. Esta técnica se llama RAG (Retrieval Augmented Generation) y también se puede implementar utilizando Spring AI. Esta es la técnica RAG, que también se puede implementar usando Spring AI.

8. Conclusión

En este artículo, exploramos las principales características de Spring AI que pueden ayudar a crear un asistente de IA. Spring AI evoluciona con muchas funciones listas para usar. Sin embargo, la selección adecuada del servicio LLM subyacente y la base de datos vectorial es crucial, independientemente del marco de programación. Además, lograr la configuración óptima de estos servicios puede ser complicado y requiere mucho esfuerzo. No obstante, es importante para la adopción generalizada de la aplicación.

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 a 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.

Compartir artículo