Spring AI, un ejemplo con RAG, MCP y OpenAI
Construimos una tienda con venta por chat usando Spring AI, VectorDB, RAG, MCP y muchos otros conceptos.
Desde hace unos 10 días comencé a meterme mucho en esto de la IA, LLMs, RAG, MCP, Embbeding, bases de datos vectoriales y todo lo que rodea este mundo que está de moda actualmente, pero que luego de probar algunas cosas, estoy más que convencido que llegó para quedarse.
Un elemento clave que me motivó a adentrarme en este mundo fue la posibilidad de llevar a las aplicaciones Java los diferentes casos de uso que nos ofrece la IA, en ese sentido hoy te quiero presentar Spring AI 🌱🚀.
En este artículo haremos una presentación de algunos conceptos relacionados con la IA, como estos enfoques están soportados por Spring y haremos un ejemplo completo implementando una tienda de venta de dispositivos móviles cuyo canal de ventas es un simple chat, pero existirán microservicios que se comunicarán entre ellos usando MCP. Más adelante detallaremos el problema y su solución.
Conceptos principales de la IA para desarrolladores
Si ya estás en el tema de la IA puedes saltarte esta sección.
LLMs (Modelos de lenguaje de gran tamaño): Son modelos de aprendizaje profundo entrenados con enormes cantidades de datos para comprender y generar lenguaje humano de manera coherente. Utilizan arquitecturas como los transformadores para procesar y generar texto, siendo fundamentales en aplicaciones como chatbots y asistentes virtuales.
RAG (Generación aumentada por recuperación ): Es una técnica que combina modelos generativos con sistemas de recuperación de información. Antes de generar una respuesta, el modelo busca en bases de datos externas información relevante, mejorando la precisión y actualidad de sus respuestas. Esto es útil para proporcionar información actualizada y específica en tiempo real.
MCP (Protocolo de contexto del modelo): Es un estándar abierto que facilita la conexión de asistentes de IA con sistemas donde residen los datos, como repositorios de contenido y herramientas empresariales. Su objetivo es permitir que los modelos de lenguaje accedan a información relevante y actualizada, mejorando la calidad de sus respuestas. (Gracias Anthropic por esto 🩵)
Embeddings: Son representaciones numéricas de objetos del mundo real, como palabras, imágenes o videos, en forma de vectores en un espacio de alta dimensión. Estas representaciones permiten a los modelos de aprendizaje automático procesar y analizar datos complejos de manera eficiente, facilitando tareas como la búsqueda semántica y el análisis de similitud.
Bases de datos vectoriales: Son sistemas diseñados para almacenar y gestionar datos en forma de vectores de alta dimensión. Permiten realizar búsquedas rápidas y precisas basadas en similitud, siendo esenciales en aplicaciones que requieren recuperación eficiente de información, como sistemas de recomendación y búsqueda de imágenes.
User prompt: Es la entrada o instrucción proporcionada por el usuario a un modelo de IA para obtener una respuesta o realizar una tarea específica. Refleja las necesidades inmediatas del usuario y guía al modelo en la generación de una respuesta adecuada.
System prompt: Es una instrucción predefinida que establece el comportamiento general y el rol del modelo de IA durante una interacción. Define el contexto y las reglas bajo las cuales el modelo debe operar, influyendo en cómo interpreta y responde a las solicitudes del usuario. (Esto es clave cuando se desarrollan agentes para delimitar su ámbito de operación)
Alucinaciones: Se refiere a respuestas generadas por modelos de IA que son incorrectas o no tienen fundamento en los datos de entrenamiento. Estas "alucinaciones" pueden ser problemáticas en aplicaciones críticas, y se están desarrollando técnicas como RAG para mitigarlas.
Si entiendes estos conceptos ya estas list@ para entender el resto del artículo.
¿Qué es Spring AI?
Sin muchas vueltas, es un proyecto más del ecosistema Spring diseñado para facilitar la integración de capacidades de inteligencia artificial (IA) en aplicaciones Java, especialmente aquellas basadas en Spring/Boot. Lo mejor es que proporciona una fachada única que permite interactuar con diferentes modelos; usando Spring AI puedes sin cambiar el código de tu aplicación conectarte con Open AI, Amazon Bedrock, Vertex Gemini o un modelo local en Ollama, entre otros. Y no, NO es lo que piensas, NO ES solo un cliente para conecarse a los LLM, es mucho más que eso.
Si te está gustando el contenido considera apoyar mi trabajo con una contribución en Patreon.
Principales características
Compatibilidad con múltiples proveedores de modelos de IA y soporte para sus caraceterísticas principales:
Chat (lo usaremos en el ejemplo)
Embeddings (lo usaremos en el ejemplo)
Conversión de texto a imagen
Transcripción de audio
Conversión de texto a voz
Moderación de contenido
API portátil y unificada: Ofrece una interfaz coherente para interactuar con diferentes proveedores de IA, admitiendo tanto operaciones síncronas como en streaming, y brindando acceso a características específicas de cada modelo.
Soporte para bases de datos vectoriales: Se integra con proveedores como Apache Cassandra, Azure Vector Search, Chroma, Milvus, MongoDB Atlas, Neo4j, Oracle, PostgreSQL/PGVector, PineCone, Qdrant, Redis y Weaviate, facilitando el almacenamiento y recuperación eficiente de datos embebidos. (en el ejemplo usaremos PgVector)
Llamadas a herramientas y funciones: Permite que los modelos soliciten la ejecución de herramientas y funciones del lado del cliente, accediendo a información en tiempo real según sea necesario (puede ser con advisors llamando a herramientas locales a la app o remotas usando MCP).
Observabilidad: Proporciona información sobre las operaciones relacionadas con la IA, facilitando el monitoreo y la depuración.
Evaluación de modelos de IA: Incluye utilidades para evaluar el contenido generado y proteger contra respuestas erróneas o no deseadas. (lo usaremos en el ejemplo)
APIs especializadas: Ofrece una API fluida para comunicarse con modelos de chat de IA, similar a WebClient y RestClient, y una API de asesores que encapsula patrones recurrentes de IA generativa, transformando datos enviados y recibidos de modelos de lenguaje. (la usaremos en ejemplo)
Soporte para memoria de conversación y generación aumentada por recuperación (RAG): Facilita la gestión del contexto en interacciones de chat y mejora la generación de respuestas mediante la recuperación de información relevante. (la usaremos en el ejemplo)
Configuración automática con Spring Boot: Proporciona configuraciones predeterminadas y starters para todos los modelos de IA y almacenes vectoriales, simplificando la integración en aplicaciones Spring Boot.
Soporte para MCP: Tiene soporte completo para montar un cliente y servidor usando Model Context Protocol, permitiendo crear “servicios“ con lógica de negocio reusables (microservicios), pero cuya comunicación usa MCP. (la usaremos en el ejemplo)
Además de esto ofrece otro grupo de funcionalidades que puedes inspeccionar más a fondo en la página del proyecto.
Ejemplo con Spring AI: Tienda de aplicaciones móviles
Descripción del problema:
Desarrollar una tienda online cuyo canal de ventas sea por chat mediante un agente de ventas, el usuario deberá interactuar con el agente solicitando información sobre los dispositivos móviles que se venden, sus características, precio y cualquier otro detalle relevante. El agente deberá tener memoria para poder dar continuidad a la comunicación con el cliente. El cliente podrá consultar los diferentes métodos de pago disponibles para su compra y podrá realizar la compra del dispositivo de su ínteres. Para ejecutar los pagos debe crearse un servicio de pagos independiente y generico cuya comunicación con la tienda sea usando Model Context Protocol.
Puntos claves para resolver el problema:
El canal de ventas será por chat, por lo que se require la incorporación de un LLM que tenga esta capability. Casi todos los LLM hoy permiten esto, usaremos OpenAI (Este ejemplo igual lo hice con Vertex Gemini, en el código aún están comentadas las dependencias).
Los productos de la tienda son propios y para mejorar la calidad de las respuestas aplicaremos RAG, esto brindará información a los clientes sobre los productos a la venta. Los datos reales de los productos que venderemos serán nuestros.
Para implementar RAG precisamos una base de datos vectorial y de la capability de embedding para texto de OpenAI (o cualquier otro modelo que lo soporte).
Usando el modelo de OpenAI con Spring AI + PostgreSQL/PgVector vamos a convertir nuestra base de datos de productos y llevarla a un esquema vectorial.
Para darle acceso al LLM a nuestra base de datos vectorial con la información (que seria el RAG) debemos crear un Advisor en Spring IA, el tipo de advisor para implementar RAG es QuestionAnswerAdvisor.
Debemos restringir el contexto de nuestro chat, así no da respuestas fuera de su función que es vender móviles. Para esto usaremos un prompt de sistema al inicializar la aplicación.
Debemos implementar un mecanismo de memoria por usuario, así nuestro chat podrá mantener una comunicación fluida con el cliente. Para esto usaremos un mapa concurrente en Java, como key del mapa el username como valor un PromptChatMemoryAdvisor. Para este ejemplo la memoria de nuestro chat la montamos en la ram, pero pudieramos usar alguna otra implementación como implementar la memoria en la base de datos vectorial.
Como los pagos los tiene que procesar un servicio dedicado crearemos una api de payments que expondrá dos herramientas (en MCP / Spring AI las funcionalidades expuestas por la API se llaman Tools); una funcionalidad para listar los métodos de pagos disponibles y otra para ejecutar el pago. (En este ejemplo las funcionalidades están mockeadas, pero pueden ser perfectamente reales).
Tenemos que implementar el cliente MCP en la tienda para que se conecte a nuestro servidor MCP de payments.
Arquitectura de la aplicación
A alto nivel son dos servicios (la tienda y el servicio de pagos), el usuario se conecta a la tienda vía http/rest (en chat real seguro mejor usar algo como websockets), la tienda interactuará con el LLM, la base de datos vectorial y si el cliente necesita comprar con la herramienta de pagos vía MCP.
Cuando el usuario hace una pregunta (a partir de ahora un prompt de usuario) entra en juego la magia del RAG, el flujo es como se describe en el siguiente diagrama:
Pero para poder aumentar y dar respuestas de calidad primero tuvimos que llenar nuestra base de datos vectorial con la información de nuestros productos a la venta. Para ello seguimos el siguiente flujo:
El proceso de llenar la base de datos vectorial se realiza una sola vez, en un caso de uso real tendríamos que buscar la forma de sincronizarlas. También podemos exponer la información de la base de datos SQL como una tool y consultarla directamente, pero los resultados no serán tan buenos (en mi opinión).
Bueno vamos a ver algunos snippets de código ahora. Todo esta disponible para probar en GitHub.
Código del servicio principal: mobile store
Empecemos por las dependencias necesarias. Será ante todo una aplicación web por lo que el starter web estará presente, voy a colocar solo las principales:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pgvector-store-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-client-spring-boot-starter</artifactId>
</dependency>
Creo las dependencias son bastante claras de arriba a abajo: integración con OpenAI (por defecto con el modelo gpt-4o-mini, pero puede cambiarse), le sigue la integración con la base vectorial (extensión de postgresql), y el cliente MCP para poder conectarnos al servicio de payments.
Importante es crear nuestro cliente de chat (cliente para OpenAI), inicializarlo y disponibilizarlo como un bean de Spring.
@Bean
public ChatClient chatClient(ChatClient.Builder clientBuilder, McpSyncClient paymentMcpSyncClient) {
return clientBuilder
.defaultSystem(systemPrompt)
.defaultTools(new SyncMcpToolCallbackProvider(paymentMcpSyncClient))
.build();
}
Este cliente en realidad tiene cositas:
Con esta configuración le setteamos el prompt de sistema justo cuando lo creamos, así restringimos el scope.
.defaultSystem(systemPrompt)
En nuestro caso le pusimos:
private String systemPrompt = """
You are an AI assistant helping people find the phone of their dreams
in our store in Uruguay. If no information is available, simply inform
them that there are no devices available at the moment.
""";
Con esta otra parte le hacemos la conexión al servidor MCP de payments que veremos más adelante:
.defaultTools(new SyncMcpToolCallbackProvider(paymentMcpSyncClient))
Ese paymentMcpSyncClient es este Bean.
@Bean
public McpSyncClient paymentMcpSyncClient() {
var mcpClient = McpClient
.sync(new HttpClientSseClientTransport("http://localhost:8081"))
.build();
logger.info("McpSyncClient created");
mcpClient.initialize();
return mcpClient;
}
Una vez creado con el codigo anterior, le dimos memoria a nuestro chat con este código:
private Map<String, PromptChatMemoryAdvisor> memoryAdvisors;
....
this.memoryAdvisors = new ConcurrentHashMap<>();
.....
public PromptChatMemoryAdvisor getOrCreateChatMemoryAdvisor(String user) {
if (this.memoryAdvisors.containsKey(user)) {
logger.info("Retrieving chat memory advisor for user {}", user);
return this.memoryAdvisors.get(user);
}
this.memoryAdvisors.put(user, PromptChatMemoryAdvisor.builder(new InMemoryChatMemory()).build());
logger.info("Creating chat memory advisor for user {}", user);
return this.memoryAdvisors.get(user);
}
Cada vez que un usuario hace una pregunta o inicia una conversación le creamos o reusamos su PromptChatMemoryAdvisor, de esta forma nuestra aplicación siempre tendrá contexto.
La parte principal de la aplicación es esta sección de código:
public String chat(String user, String userQuery) {
var memory = this.getOrCreateChatMemoryAdvisor(user);
return this.chatClient
.prompt()
.user(userQuery)
.advisors(Arrays.asList(memory, this.questionAnswerAdvisor))
.call()
.content();
}
Este método será invocado desde el controlador y pasará el usuario que está en la conversación y el su consulta o prompt de usuario.
La parte que falta por explicar es: this.questionAnswerAdvisor
En Spring AI existe un Advisor llamado: QuestionAnswerAdvisor, este es el encargado de conectar con nuestra base vectorial para poder hacer el RAG correctamente. Para ello al incluir la dependencia de pgVector, y proveer configuraciones de conexión a postgress automaticamente se crea en el contexto un bean llamado VectorStore (una fachada para la VectorDB incluida en la app).
Entonces el código anterior se complementa con:
private QuestionAnswerAdvisor questionAnswerAdvisor;
private final VectorStore vectorStore;
...
this.questionAnswerAdvisor = new QuestionAnswerAdvisor(vectorStore);
El controlador de la aplicación para este ejemplo se sencilla y sin relevancia para este artículo:
@RestController
@RequestMapping("/store")
public class StoreAssistantController {
private final StoreAssistantService storeAssistantService;
public StoreAssistantController(StoreAssistantService storeAssistantService) {
this.storeAssistantService = storeAssistantService;
}
@GetMapping("/{user}/question")
public String chat(@PathVariable("user") String user, @RequestParam String question) {
return this.storeAssistantService.chat(user, question);
}
}
Algunas properties importantes:
# La conexión a postgre/pgvector
spring.application.name=mobile-store
spring.datasource.url=jdbc:postgresql://localhost/store
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.hikari.maximum-pool-size=10
# Cuando se vaya a popular la base vectorial
spring.ai.vectorstore.pgvector.initialize-schema=true
# La clave de la API de Open AI (esta no es gratis)
spring.ai.openai.api-key=${OPEN_AI_KEY}
Código del servicio de pagos: Payment Tool (mcp server)
Como vimos, tenemos que crear un servidor MCP, en la tienda ya vimos como crear el cliente, ahora veremos como creamos un servidor MCP.
Antes de ver algo de código Spring AI tiene 3 formas principales de comunicación para MCP a nivel de transporte:
stdio (sálida estándar, esta es útil para herramientas como claude desktop).
SSE basado en servlets
SSE con Webflux para variante reactiva.
En este ejemplo usamos SSE basado en servlets.
La implementación de MCP de Spring puedes consultarla acá, es interesante.
Bueno, veamos el código, empecemos por las dependencias:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-server-webmvc-spring-boot-starter</artifactId>
</dependency>
Como comentamos anteriormente, en un servidor MCP se exponen herramientas, las cuales son aprovechadas por el LLM para “hacer cosas“. Cuando un cliente MCP inicia se conecta al servidor y carga las herramientas disponibles para usar. En nuestro ejemplo creamos dos herramientas en un servicio de Spring.
@Service
public class PaymentService {
private final Logger log = LoggerFactory.getLogger(PaymentService.class);
@Tool(name = "execute_payment", description = "Execute a payment for buy a mobile")
private String executePayment(@ToolParam(description = "the id of the mobile") String productId,
@ToolParam(description = "the user") String user,
@ToolParam(description = "the price of the mobile") double amount) throws JsonProcessingException {
log.info("Executing payment id:{}, user: {}, amount:{}", productId, user, amount);
return "Payment executed successfully";
}
@Tool(name = "get_card_brands", description = "Get the card brands or payment methods available")
private String getCardBrandsAvailable() {
return "Visa, Mastercard";
}
}
Las tools se crean anotando métodos con @Tool y es importante proveer una descripción precisa de lo que hace la herramienta, de esta forma el LLM sabe cuando invocar la herramienta. Los parámetros deben ser de igual forma documentados.
La creación del servidor concluye creando un bean con las herramientas que se quieren exponer:
@Bean
public ToolCallbackProvider toolCallbackProvider(PaymentService paymentService) {
return MethodToolCallbackProvider.builder()
.toolObjects(paymentService)
.build();
}
Con esto ya nuestro servidor MCP está disponible.
Este proyecto está 100% disponible en Github, espero te sirva este artículo, aprendí muchas cosas nuevas programando esta solución, espero te sea útil.
Si te gusta el contenido considera apoyar mi trabajo con una contribución en Patreon.
Repositorio de la tienda: https://github.com/yoandypv/spring-ai-mobile-store
Repositorio del servidor MCP de pagos: https://github.com/yoandypv/spring-ai-mcp-payments-server
Disfruta 🚀🚀
Espectacular. Yoandy haz pensado alguna vez si con la comunidad que te sigue darle forma a un encuentro del tipo "sacavix-tech", creo que no pocos estariamos contentos de participar de alguna forma. Saludos
Buen artículo para motivar el uso de Spring AI y MCP, saludos Yoandy!