Construye RAG con LangChainJS, Milvus y Strapi
Proporcionar respuestas precisas y relevantes a las consultas de los usuarios es crucial al crear aplicaciones de IA. Aunque son capaces de mantener conversaciones naturales, los chatbots tradicionales y los modelos de IA a menudo carecen de acceso a información específica y actualizada. Sus respuestas provienen de datos de entrenamiento que podrían estar desactualizados o ser demasiado genéricos para necesidades especializadas.
Retrieval Augmented Generation (RAG) aborda esta limitación combinando las capacidades generativas de los modelos de IA con una base de conocimiento personalizada impulsada por una base de datos vectorial como Milvus. En lugar de depender únicamente del conocimiento preentrenado, RAG busca activamente en tu contenido para encontrar información relevante antes de generar respuestas. Este enfoque garantiza que las respuestas sean tanto precisas como contextualmente adecuadas.
Comprender RAG y la búsqueda vectorial
RAG cierra la brecha entre las respuestas genéricas de IA y el conocimiento especializado al integrar un mecanismo de recuperación con el proceso de generación. A diferencia de los modelos de lenguaje tradicionales que dependen únicamente del conocimiento preentrenado, RAG primero recupera información relevante de una base de conocimiento personalizada para complementar las respuestas de la IA.
Por ejemplo, cuando se le pregunta a un asistente de IA sobre una política o procedimiento específico, no depende solo de la memoria; en su lugar, busca en documentos relevantes antes de proporcionar una respuesta. Este proceso de recuperación está impulsado por modelos de embeddings y bases de datos vectoriales.
Ahora, veamos cómo funciona un RAG estándar.
Embeddings de texto: El texto se convierte en representaciones numéricas, o vectores, que capturan su significado semántico mediante un modelo de embeddings. Por ejemplo, las frases How do I return an item? y What's the refund process? pueden usar palabras diferentes pero compartir un significado similar, reflejado en su similitud vectorial.
Búsqueda vectorial: Cuando un usuario hace una pregunta, la consulta también se transforma en un vector. Luego, este vector se compara con otros almacenados en una base de datos vectorial, como Milvus, para encontrar las entradas semánticamente más similares.
Generación de respuestas: La información recuperada, junto con la consulta del usuario, se pasa a un modelo de lenguaje grande. El modelo genera una respuesta basada en el contenido recuperado, asegurando que la respuesta sea relevante y precisa.
Esta combinación de recuperación y generación garantiza que las respuestas de la IA sean contextualmente apropiadas y estén fundamentadas en los datos más relevantes de la base de conocimiento.
En las siguientes secciones, crearemos un sistema de preguntas frecuentes impulsado por RAG capaz de responder preguntas sobre Milvus utilizando una base de conocimiento gestionada en Strapi. Este sistema integrará Milvus para la búsqueda vectorial, LangChain.js para la coordinación del flujo de trabajo y los modelos de lenguaje de OpenAI para proporcionar respuestas precisas y contextuales. El contenido se organizará en Strapi, un CMS headless de Node.js de código abierto, y se consultará en tiempo real para fundamentar las respuestas de la IA en tus datos personalizados.
Si quieres echar un vistazo rápido, aquí está el código completo y el backend de Strapi.
Configurar tu entorno de desarrollo
Antes de escribir código, debemos configurar los tres componentes clave que analizamos anteriormente: Milvus para el almacenamiento vectorial, Strapi para la gestión de contenido y una clave de API de OpenAI para generar embeddings y respuestas. Configuremos cada uno.
Instalar Milvus
Comencemos configurando Milvus, nuestra base de datos vectorial. Puedes descargar el archivo Docker Compose preconfigurado manualmente o usando wget. En tu terminal, ejecuta:
wget https://github.com/milvus-io/milvus/releases/download/v2.0.2/milvus-standalone-docker-compose.yml -O docker-compose.yml
Con el archivo de configuración en su lugar, inicia Milvus usando Docker:
sudo docker-compose up -d
Si estás usando Docker Compose V2 en lugar de V1, usa docker compose en lugar de docker-compose. Puedes comprobar tu versión ejecutando docker compose version.
Instalación de Strapi
Ahora configuremos Strapi para gestionar el contenido de nuestra base de conocimientos. Crea un nuevo proyecto de Strapi usando el siguiente comando:
npx create-strapi-app@latest my-project
Elige SQLite como tu base de datos cuando se te solicite. Después de la instalación, Strapi se abrirá en tu navegador, donde crearás tu cuenta de administrador.
En el panel de administración de Strapi, crea un nuevo tipo de colección llamado MILVUS-KNOWLEDGEBASE. Añade dos campos esenciales: un campo Title usando el tipo Text y un campo Content usando el tipo Rich text.
Figura- Añadir campos a una colección de Strapi
Figura: Añadir campos a una colección de Strapi
Esta estructura nos ayudará a organizar eficazmente las entradas de nuestra base de conocimientos.
Una vez que tu colección esté configurada, ve a Content Manager para añadir contenido de muestra.
Figura- Colección de Strapi poblada con datos
Figura: Colección de Strapi poblada con datos
Asegúrate de publicar tus entradas después de crearlas; las entradas no publicadas no serán accesibles para nuestra aplicación.
Después de publicar las entradas, necesitamos hacer que el contenido esté disponible para el sistema RAG configurando permisos. Ve a Settings, luego a Roles y selecciona el rol Public. Busca tu colección MILVUS-KNOWLEDGEBASE y habilita los permisos find y findOne.
Obtener tu clave de API de OpenAI
La última pieza que necesitamos es una clave de API de OpenAI para generar embeddings y respuestas. Visita la plataforma de OpenAI y regístrate o inicia sesión. Navega hasta la sección de claves de API y crea una nueva clave secreta. Copia y guarda la clave en un lugar seguro, ya que la necesitaremos más adelante y solo podrás verla una vez.
Crear la aplicación React
Con nuestros servicios principales listos, configuremos la aplicación React que alojará nuestro sistema RAG. Usaremos la herramienta create-react-app para crear un nuevo proyecto React e instalar las dependencias necesarias.
Primero, crea una nueva aplicación React:
npx create-react-app rag-app
cd rag-app
Este comando configura un proyecto React completo con todas las herramientas de compilación y configuración necesarias. A continuación, instala los paquetes necesarios para nuestro sistema RAG:
npm install @langchain/community @langchain/openai @zilliz/milvus2-sdk-node axios cors express langchain react-markdown
Esto es lo que hará cada paquete en nuestra aplicación:
@langchain/community: Proporciona acceso a integraciones con diversas herramientas y servicios. Lo usaremos para la integración con el almacén vectorial Milvus.@langchain/openai: Gestiona las interacciones con los modelos de OpenAI. Lo usaremos para crear embeddings y generar respuestas usando GPT-3.5.@zilliz/milvus2-sdk-node: El SDK oficial de Milvus para Node.js. Esto nos permite comunicarnos directamente con nuestra instancia de Milvus para operaciones vectoriales.axios: Este es un cliente HTTP. Lo usaremos para obtener contenido de nuestra API de Strapi y hacer solicitudes entre nuestro frontend y backend.cors: Middleware para Express que habilita Cross-Origin Resource Sharing. Esto permite que nuestro frontend se comunique con nuestro servidor backend de forma segura.express: Un framework web para Node.js. Lo usaremos para crear nuestra API backend que coordina entre nuestro frontend, Milvus y OpenAI.langchain: La biblioteca principal de LangChain que ayuda a orquestar nuestro flujo de trabajo RAG, gestionando el flujo entre la recuperación de contenido y la generación de respuestas.react-markdown: Renderiza contenido Markdown en nuestros componentes de React. Lo usaremos para mostrar respuestas formateadas de nuestro asistente de IA.
Construcción del servidor backend
Ahora que hemos completado la configuración de nuestro entorno de desarrollo, comencemos a construir el servidor que impulsará nuestro sistema RAG. El servidor manejará varias tareas clave: obtener contenido de Strapi, convertir texto en incrustaciones vectoriales, almacenarlas en Milvus y coordinar el flujo de trabajo RAG para generar respuestas.
Crea un archivo llamado server.mjs y otro llamado .env en la raíz de tu proyecto. En el archivo .env, pega tu clave de OpenAI de la siguiente manera:
OPENAI_API_KEY= your-api-key
Este es el archivo que usaremos para almacenar nuestra información confidencial. Ahora abre el archivo del servidor y empecemos a programar.
1. Configuración del servidor y el entorno
Comienza importando las bibliotecas necesarias y configurando las variables de entorno para garantizar que el sistema se ejecute de forma segura.
import express from "express";
import axios from "axios";
import dotenv from "dotenv";
import cors from "cors";
import { ChatOpenAI } from "@langchain/openai";
import { createStuffDocumentsChain } from "langchain/chains/combine_documents";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
import { OpenAIEmbeddings } from "@langchain/openai";
import { Milvus } from "@langchain/community/vectorstores/milvus";
import { Document } from "langchain/document";
import { HumanMessage, AIMessage } from "@langchain/core/messages";
dotenv.config();
Importar las bibliotecas que instalamos en nuestro entorno a nuestro código garantiza que podamos llamar y usar sus funciones y métodos en nuestros códigos. La función dotenv.config() carga nuestra clave de API, asegurando que sea accesible en toda la aplicación sin exponerla directamente en el código.
2. Configuración de la aplicación
A continuación, centraliza la configuración de tu aplicación usando un objeto de configuración. Este paso ayuda a mantener todos los parámetros clave en un solo lugar, lo que facilita gestionarlos y ajustarlos según sea necesario.
const CONFIG = {
PORT: parseInt(process.env.PORT || "30080", 10),
STRAPI: {
URL: "http://localhost:1337/api/milvus-knowledgebases",
TIMEOUT: 5000
},
MILVUS: {
URL: "localhost:19530",
COLLECTION: "rag_collection",
PRIMARY_FIELD: "pk",
VECTOR_FIELD: "vector",
TEXT_FIELD: "text",
TEXT_MAX_LENGTH: 4096,
SEARCH_PARAMS: {
nprobe: 16,
offset: 0
},
BATCH_SIZE: 100
},
CHUNKING: {
SIZE: 2000,
OVERLAP: 200
},
TOP_K: 3
};
Este objeto de configuración define parámetros críticos para nuestra aplicación. La configuración PORT determina en qué puerto escucha el servidor. La sección STRAPI contiene la URL del sistema de gestión de contenido y un valor de tiempo de espera para evitar esperas prolongadas al obtener datos.
La sección MILVUS configura la base de datos vectorial, especificando detalles como el nombre de la colección, los límites de longitud del texto y los parámetros de búsqueda. nprobe controla la precisión de las búsquedas vectoriales ajustando cuántos clústeres examina Milvus. Finalmente, la sección CHUNKING describe cómo los documentos se dividen en partes más pequeñas, incluyendo el tamaño del fragmento y la superposición, asegurando que no se pierda contexto durante el procesamiento.
3. Inicialización del servidor y los servicios de IA
Con la configuración en su lugar, el siguiente paso es inicializar el servidor Express y configurar los servicios de IA. Estos incluyen el modelo de lenguaje grande para generar respuestas y el servicio de embeddings para convertir texto en vectores.
const app = express();
app.use(express.json({ limit: '1mb' }));
app.use(cors());
const model = new ChatOpenAI({
modelName: "gpt-3.5-turbo",
temperature: 0.7,
openAIApiKey: process.env.OPENAI_API_KEY,
maxRetries: 3,
timeout: 30000,
});
const embeddings = new OpenAIEmbeddings({
openAIApiKey: process.env.OPENAI_API_KEY,
maxRetries: 3,
});
const splitter = new RecursiveCharacterTextSplitter({
chunkSize: CONFIG.CHUNKING.SIZE,
chunkOverlap: CONFIG.CHUNKING.OVERLAP,
separators: ["\n\n", "\n", " ", ""]
});
El objeto app se crea usando Express, con middleware agregado para analizar las solicitudes JSON entrantes y gestionar solicitudes de origen cruzado usando CORS. Esto garantiza que el servidor pueda aceptar cargas útiles de diferentes dominios, lo cual es esencial para la comunicación con el frontend.
A continuación, creamos una instancia de ChatOpenAI que usaremos para conectar el sistema al modelo GPT-3.5 de OpenAI para generar respuestas. El parámetro temperature controla la creatividad de las respuestas, con valores más bajos que producen resultados más predecibles y valores más altos que generan resultados más variados y creativos.. La instancia de OpenAIEmbeddings convertirá texto en vectores, habilitando búsquedas semánticas. Finalmente, la instancia de RecursiveCharacterTextSplitter dividirá el texto en fragmentos según el tamaño y la superposición definidos en la configuración.
4. Preparación de documentos para el procesamiento
Antes de poder almacenar o buscar documentos usando Milvus, necesitamos limpiar y estructurar el contenido sin procesar en un formato consistente. Esto garantiza que los datos estén listos para embeddings y una recuperación eficiente más adelante en el flujo de trabajo.
let milvusStore = null;
let lastDataHash = null;
const processDocument = (content) => {
if (!content?.Content || !content.Title) return null;
const processedContent = content.Content
.map(section => {
if (section.type === 'paragraph' && section.children) {
return section.children
.map(child => child.text)
.join(' ')
.trim();
}
return null;
})
.filter(Boolean)
.join('\n\n');
return processedContent ? new Document({
pageContent: processedContent,
metadata: {
source: 'milvus_content',
id: content.id,
title: content.Title,
documentId: content.documentId
}
}) : null;
};
En el código anterior, las variables let milvusStore y let lastDataHash actúan como referencias globales. milvusStore contiene la conexión inicializada a la colección de Milvus, y lastDataHash rastrea si los datos subyacentes del documento han cambiado desde la última actualización.
La función processDocument nos ayudará a tomar datos sin procesar de Strapi y formatearlos en un objeto Document estructurado. Primero, comprueba si los campos de contenido y título están presentes. Si son válidos, recorre las secciones del contenido, extrayendo texto de los párrafos y limpiándolo al recortar espacios innecesarios. Estas secciones se unen en un bloque de texto cohesivo. Luego, la función devuelve el texto procesado envuelto en un objeto Document, incluyendo metadatos como el título, el ID y la fuente del documento. Este formato garantiza la compatibilidad con Milvus y LangChain para su procesamiento posterior.
5. Conexión o inicialización de Milvus
Ahora que los documentos están listos, el siguiente paso es integrarlos con Milvus. Estableceremos una conexión a una colección de Milvus existente o, si no existe, crearemos una y la llenaremos con los documentos preparados.
async function getMilvusStore() {
if (milvusStore) {
return milvusStore;
}
try {
// First try to connect to existing collection
try {
console.log('Attempting to connect to existing collection');
milvusStore = await Milvus.fromExistingCollection(
embeddings,
{
url: CONFIG.MILVUS.URL,
collectionName: CONFIG.MILVUS.COLLECTION,
primaryField: CONFIG.MILVUS.PRIMARY_FIELD,
vectorField: CONFIG.MILVUS.VECTOR_FIELD,
textField: CONFIG.MILVUS.TEXT_FIELD,
textFieldMaxLength: CONFIG.MILVUS.TEXT_MAX_LENGTH,
}
);
return milvusStore;
} catch (error) {
console.log('Collection does not exist, will create new one');
// If collection doesn't exist, fetch data and create it
const response = await axios.get(CONFIG.STRAPI.URL, {
timeout: CONFIG.STRAPI.TIMEOUT
});
const processPromises = response.data.data.map(processDocument);
const processedDocs = await Promise.all(processPromises);
const uniqueDocs = new Map();
processedDocs
.filter(Boolean)
.forEach(doc => uniqueDocs.set(doc.metadata.documentId, doc));
const docs = Array.from(uniqueDocs.values());
// Split documents
const splitPromises = docs.map(doc => splitter.splitDocuments([doc]));
const splitDocs = await Promise.all(splitPromises);
const allSplitDocs = splitDocs.flat();
console.log(`Creating new collection with ${allSplitDocs.length} documents`);
milvusStore = await Milvus.fromDocuments(
allSplitDocs,
embeddings,
{
url: CONFIG.MILVUS.URL,
collectionName: CONFIG.MILVUS.COLLECTION,
primaryField: CONFIG.MILVUS.PRIMARY_FIELD,
vectorField: CONFIG.MILVUS.VECTOR_FIELD,
textField: CONFIG.MILVUS.TEXT_FIELD,
textFieldMaxLength: CONFIG.MILVUS.TEXT_MAX_LENGTH,
}
);
lastDataHash = Buffer.from(JSON.stringify(response.data)).toString('base64');
return milvusStore;
}
} catch (error) {
console.error('Failed to initialize Milvus:', error);
throw error;
}
}`
Esta función gestiona la conexión a la colección de Milvus. Si la variable milvusStore ya contiene una conexión activa, la función la devuelve inmediatamente para evitar operaciones innecesarias. De lo contrario, intenta conectarse a una colección existente en Milvus mediante el método fromExistingCollection. Si la colección no existe, la función procede a crear una nueva. Primero, obtiene datos de Strapi, procesa los documentos mediante la función processDocument y garantiza que cada documento sea único usando el documentId como clave en un Map. Luego, los documentos se dividen en fragmentos más pequeños usando el splitter, lo que garantiza que los documentos no sean demasiado grandes para la incrustación o indexación en Milvus. A continuación, los fragmentos procesados se almacenan en la colección de Milvus mediante el método fromDocuments. El lastDataHash se actualiza para reflejar el estado actual de los datos de Strapi, garantizando que se haga seguimiento de los cambios.
6. Mantener Milvus actualizado
Milvus debe mantenerse sincronizado con Strapi para garantizar que la base de datos vectorial contenga los datos más recientes. Implementemos un proceso que detecte cambios en el contenido de Strapi, identifique documentos actualizados o nuevos y actualice la colección de Milvus en consecuencia.
// Actualizar datos de Milvus si es necesario
async function updateMilvusData() {
try {
const response = await axios.get(CONFIG.STRAPI.URL, {
timeout: CONFIG.STRAPI.TIMEOUT
});
const currentHash = Buffer.from(JSON.stringify(response.data)).toString('base64');
if (currentHash === lastDataHash) {
console.log('Datos sin cambios, omitiendo actualización');
return false;
}
console.log('Contenido cambiado, actualizando colección de Milvus');
const processPromises = response.data.data.map(processDocument);
const processedDocs = await Promise.all(processPromises);
const uniqueDocs = new Map();
processedDocs
.filter(Boolean)
.forEach(doc => uniqueDocs.set(doc.metadata.documentId, doc));
const docs = Array.from(uniqueDocs.values());
// Dividir documentos
const splitPromises = docs.map(doc => splitter.splitDocuments([doc]));
const splitDocs = await Promise.all(splitPromises);
const allSplitDocs = splitDocs.flat();
// Eliminar todos los documentos existentes antes de agregar los nuevos
await milvusStore.delete({});
// Agregar nuevos documentos en lotes
for (let i = 0; i < allSplitDocs.length; i += CONFIG.MILVUS.BATCH_SIZE) {
const batch = allSplitDocs.slice(i, i + CONFIG.MILVUS.BATCH_SIZE);
await milvusStore.addDocuments(batch);
console.log(`Agregado lote ${Math.floor(i / CONFIG.MILVUS.BATCH_SIZE) + 1} de ${Math.ceil(allSplitDocs.length / CONFIG.MILVUS.BATCH_SIZE)}`);
}
lastDataHash = currentHash;
console.log(`Milvus actualizado con ${allSplitDocs.length} documentos`);
return true;
} catch (error) {
console.error("Error al actualizar los datos de Milvus:", error);
throw error;
}
}
Esta función comienza obteniendo los datos más recientes de Strapi y calculando su hash. Si el hash coincide con lastDataHash, los datos no han cambiado y no se necesita ninguna actualización. De lo contrario, la función procesa los documentos actualizados y vacía la colección existente en Milvus usando el método delete. Los nuevos fragmentos de documentos se agregan luego en lotes, lo que garantiza un almacenamiento eficiente mientras se mantiene el registro para el seguimiento del progreso. Finalmente, actualiza el lastDataHash, para garantizar que las comprobaciones futuras detecten con precisión los cambios en los datos.
7. Manejo de consultas de usuarios
Después de poblar Milvus con nuestros datos, el siguiente paso es manejar las consultas de los usuarios. El sistema recuperará los documentos más relevantes de Milvus basándose en la similitud semántica con la consulta del usuario. Estos documentos se usarán luego para generar respuestas conscientes del contexto usando el modelo de lenguaje grande GPT-3.5 de OpenAI.
async function handleQuery(chatHistory, input) {
const store = await getMilvusStore();
try {
await updateMilvusData();
} catch (error) {
console.warn("Error al comprobar actualizaciones:", error);
}
const results = await store.similaritySearchWithScore(
input,
CONFIG.TOP_K
);
console.log('\n=== Documentos recuperados de Milvus ===');
results.forEach(([doc, score], index) => {
console.log(`\nDocumento ${index + 1} (puntuación: ${score}):`);
console.log('Título:', doc.metadata.title);
console.log('Contenido:', doc.pageContent.substring(0, 150) + '...');
});
const chain = await createStuffDocumentsChain({
llm: model,
prompt: ChatPromptTemplate.fromMessages([
[
"system",
`You are an AI assistant specializing in Milvus and Zilliz, vector database technologies. Your goal is to provide accurate and helpful answers based on the provided context.
**Guidelines:**
1. **Milvus/Zilliz-Related Queries:**
- If the query is about Milvus or Zilliz and the context includes relevant information, provide a detailed and structured response.
- Use proper Markdown formatting:
- \`\`\` for code blocks
- **Bold** for emphasis
- Bullet points for lists
- ### Headings for sections
- If no relevant information exists in the context, respond with: *"I don't have that information yet."*
2. **Unrelated Queries:**
- If the query is not about Milvus or Zilliz, respond with:
*"This topic is outside my expertise. I specialize in Milvus and Zilliz. Please ask questions related to these technologies."*
**Context:**
{context}
**User Query:**
{input}`
],
...chatHistory,
["user", "{input}"]
]),
documentPrompt: ChatPromptTemplate.fromTemplate("Content: {page_content}\n\n")
});
const response = await chain.invoke({
input,
context: results.map(([doc]) => doc)
});
return {
answer: response,
context: results.map(([doc]) => ({
content: doc.pageContent,
title: doc.metadata.title,
id: doc.metadata.id,
documentId: doc.metadata.documentId
}))
};
}
Esta función comienza asegurándose de que el almacén vectorial de Milvus esté actualizado. La función updateMilvusData se llama dentro de un bloque try-catch para manejar posibles errores sin interrumpir el proceso de consulta. Luego, la consulta se pasa al método similaritySearchWithScore de Milvus, que recupera los k documentos más similares de la base de datos. Los documentos recuperados se registran. Cada documento incluye metadatos como el título y el ID, así como una vista previa truncada de su contenido. Esto nos ayudará a saber si nuestro pipeline RAG está recuperando los documentos correctos.
A continuación, los documentos se introducen en un createStuffDocumentsChain de LangChain usando un prompt que instruye al modelo a responder basándose únicamente en el contexto proporcionado. Esto garantiza que GPT-3.5 de OpenAI genere una respuesta fundamentada en el contenido recuperado, sin depender de conocimiento externo, manteniendo la precisión y la relevancia. Finalmente, la función devuelve tanto la respuesta generada como el contexto de los documentos recuperados.
8. Configuración de endpoints de API
Ahora que el backend puede procesar consultas de usuarios e interactuar con Milvus, el siguiente paso es exponer esta funcionalidad mediante endpoints de API. Estos endpoints sirven como puente entre nuestro backend y frontend (u otras aplicaciones cliente).
app.post("/chat", async (req, res) => {
try {
const { chatHistory, input } = req.body;
if (!input?.trim()) {
return res.status(400).json({
error: "Invalid input",
message: "No input provided"
});
}
const formattedHistory = Array.isArray(chatHistory)
? chatHistory.map(msg =>
msg.role === "user"
? new HumanMessage(msg.content)
: new AIMessage(msg.content)
)
: [];
const response = await handleQuery(formattedHistory, input);
res.json(response);
} catch (error) {
console.error("Chat request error:", error);
res.status(500).json({
error: "Internal server error",
message: process.env.NODE_ENV === 'production'
? "An unexpected error occurred"
: error.message
});
}
});
app.get("/health", async (req, res) => {
try {
const store = await getMilvusStore();
res.json({
status: "ok",
timestamp: new Date().toISOString(),
milvusInitialized: !!store,
config: CONFIG
});
} catch (error) {
res.status(500).json({
status: "error",
error: error.message
});
}
});
El endpoint /chat procesa las solicitudes POST entrantes que contienen la consulta de un usuario y el historial de chat. El historial de chat se formatea en objetos HumanMessage y AIMessage para mantener el contexto. Si la consulta no es válida (por ejemplo, está vacía), el endpoint responde con un estado 400 Bad Request y un mensaje de error apropiado. Si la consulta es válida, se llama a la función handleQuery para procesar la entrada, recuperar documentos relevantes de Milvus y generar una respuesta. El resultado se devuelve como un objeto JSON. Si ocurre un error, se registra y el servidor responde con un 500 Internal Server Error.
Por otro lado, el endpoint /health proporciona una forma de supervisar el estado del servidor. Comprueba si el almacén de Milvus está inicializado y responde con un estado de ok o error, junto con la configuración del servidor y la marca de tiempo actual.
9. Iniciar el servidor
El paso final es iniciar el servidor y dejarlo disponible para gestionar solicitudes.
app.listen(CONFIG.PORT, () => {
console.log(`Server running on http://localhost:${CONFIG.PORT}`);
console.log('Configuration:', CONFIG);
});
El método app.listen inicia el servidor en el puerto especificado en la configuración, en este caso, el puerto 30080. Una vez que el servidor está en ejecución, registra un mensaje que indica la URL donde se puede acceder a él, así como la configuración actual. Esto proporciona una confirmación de que el backend está listo para gestionar solicitudes.
Figura- Servidor backend del sistema RAG en ejecución mientras registra las configuraciones actuales
Figura: Servidor backend del sistema RAG en ejecución mientras registra las configuraciones actuales
Para ejecutar el servidor, ve a la terminal y ejecuta este comando.
node .\server.mjs
Ahora hemos terminado de crear el backend de nuestro sistema RAG.
Crear el frontend de tu sistema RAG
Con el backend del sistema RAG completamente funcional, el siguiente paso es crear un frontend para permitir que los usuarios interactúen con el sistema. El frontend será una interfaz de chatbot que se conecta al backend, lo que permite a los usuarios introducir consultas y recibir respuestas generadas por IA.
Crear la interfaz de usuario
Comienza creando un archivo llamado ChatbotUI.js en el directorio src. Este archivo definirá el componente del chatbot y gestionará el flujo de consultas de usuario y respuestas del backend.
import React, { useState, useEffect, useRef } from 'react';
import ReactMarkdown from 'react-markdown';
import axios from 'axios';
import './ChatbotUI.css';
const ChatbotUI = () => {
const [chatHistory, setChatHistory] = useState([]);
const [userInput, setUserInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const chatContainerRef = useRef(null);
useEffect(() => {
// Scroll to the latest message when chat history updates
chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight;
}, [chatHistory]);
const handleUserInput = (e) => setUserInput(e.target.value);
const handleSendMessage = async () => {
if (userInput.trim() !== '') {
const newMessage = { role: 'user', content: userInput };
const updatedChatHistory = [...chatHistory, newMessage];
setChatHistory(updatedChatHistory);
setUserInput('');
setIsLoading(true);
try {
const response = await axios.post('http://localhost:30080/chat', {
chatHistory: updatedChatHistory,
input: userInput,
});
const botMessage = {
role: 'assistant',
content: response.data.answer || 'Lo siento, no entendí bien eso.',
};
setTimeout(() => {
setChatHistory((prevMessages) => [...prevMessages, botMessage]);
}, 1000); // Simular retraso de escritura
} catch (err) {
console.error('Error:', err);
setError('No se puede conectar al servidor.');
} finally {
setIsLoading(false);
}
}
};
return (
<div className="chatbot-ui">
{/* Encabezado */}
<div className="chat-header">
<img src="/milvus_logo.png" alt="Asistente" className="assistant-logo" />
<h2>Asistente de chat</h2>
</div>
{/* Mensajes del chat */}
<div className="chat-body" ref={chatContainerRef}>
{chatHistory.map((message, index) => (
<div
key={index}
className={`message ${message.role === 'user' ? 'user-message' : 'bot-message'}`}
>
<ReactMarkdown>{message.content}</ReactMarkdown>
</div>
))}
{isLoading && (
<div className="bot-message typing-indicator">
<span></span>
<span></span>
<span></span>
</div>
)}
{error && <div className="error-message">{error}</div>}
</div>
{/* Pie de página */}
<div className="chat-footer">
<input
type="text"
placeholder="Pregúntame lo que quieras..."
value={userInput}
onChange={handleUserInput}
onKeyPress={(e) => {
if (e.key === 'Enter') handleSendMessage();
}}
disabled={isLoading}
/>
<button onClick={handleSendMessage} disabled={isLoading}>
<span className="send-icon">✈</span>
</button>
</div>
</div>
);
};
export default ChatbotUI;
Este componente usa useState de React para gestionar estados como el historial del chat y la entrada del usuario. El hook useEffect garantiza que el contenedor del chat se desplace automáticamente hasta el mensaje más reciente. Cuando el usuario envía una consulta, esta se envía al endpoint de la API backend http://localhost:30080/chat mediante una solicitud POST de axios. La respuesta del backend se muestra luego en la interfaz de chat.
Dar estilo al chatbot
A continuación, crea un archivo llamado ChatbotUI.css en el directorio src. Este archivo dará estilo a la interfaz del chatbot.
/* Fondo degradado a pantalla completa */
.chatbot-ui {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100vh;
background: linear-gradient(135deg, #8358ff, #00a6ff); /* Paleta de colores de Milvus */
font-family: 'Roboto', sans-serif;
color: #fff;
}
/* Encabezado del chat */
.chat-header {
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(90deg, #1e1e2d, #28293e);
width: 100%;
padding: 15px;
border-radius: 15px 15px 0 0;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
}
.assistant-logo {
width: 50px;
height: 50px;
margin-right: 15px;
border-radius: 50%;
background: radial-gradient(circle, #8358ff, #00a6ff); /* Resplandor dinámico del logotipo */
animation: pulse 2s infinite;
}
.chat-header h2 {
font-size: 1.8rem;
font-weight: bold;
color: #ffffff;
}
.chat-header p {
font-size: 0.9rem;
color: #a0a0b1;
margin-top: 5px;
}
/* Cuerpo del chat */
.chat-body {
flex: 1;
width: 100%;
padding: 15px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 12px;
background: #1e1e2d;
color: #ffffff;
border-radius: 0 0 15px 15px;
}
.message {
max-width: 80%;
padding: 12px 15px;
border-radius: 10px;
font-size: 1rem;
line-height: 1.5;
animation: fadeIn 0.3s ease-in-out;
}
.user-message {
align-self: flex-end;
background: linear-gradient(90deg, #8358ff, #00a6ff);
color: #ffffff;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
}
.bot-message {
align-self: flex-start;
background: #29293f;
color: #d4d4e5;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
}
/* Typing Indicator */
.typing-indicator {
display: flex;
justify-content: flex-start;
gap: 5px;
}
.typing-indicator span {
width: 8px;
height: 8px;
background-color: #00a6ff;
border-radius: 50%;
animation: blink 1.2s infinite;
}
.typing-indicator span:nth-child(2) {
animation-delay: 0.2s;
}
.typing-indicator span:nth-child(3) {
animation-delay: 0.4s;
}
/* Footer */
.chat-footer {
display: flex;
align-items: center;
width: 100%;
padding: 10px 15px;
background: #1e1e2d;
border-top: 1px solid #28293e;
border-radius: 0 0 15px 15px;
box-shadow: 0 -4px 10px rgba(0, 0, 0, 0.2);
}
.chat-footer input {
flex: 1;
padding: 12px 15px;
border-radius: 30px;
border: 1px solid #8358ff;
font-size: 1rem;
outline: none;
background: #29293f;
color: #ffffff;
transition: border-color 0.3s ease;
}
.chat-footer input:focus {
border-color: #00a6ff;
}
.chat-footer button {
margin-left: 10px;
padding: 12px 20px;
border: none;
border-radius: 30px;
background: linear-gradient(90deg, #8358ff, #00a6ff);
color: white;
font-size: 1.2rem;
cursor: pointer;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.chat-footer button:hover {
transform: scale(1.1);
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.4);
}
.chat-footer button:disabled {
background: #29293f;
color: #a0a0b1;
cursor: not-allowed;
}
@keyframes blink {
0%, 80%, 100% {
opacity: 0;
}
40% {
opacity: 1;
}
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
box-shadow: 0 0 10px rgba(131, 88, 255, 0.5);
}
50% {
transform: scale(1.1);
box-shadow: 0 0 20px rgba(0, 166, 255, 0.7);
}
}
Los estilos CSS crean una interfaz de chatbot inspirada en la paleta de colores de Milvus. El fondo utiliza un degradado con tonos morado oscuro y cian, reflejando la marca de Milvus. El encabezado del chat sigue un estilo de degradado oscuro, mientras que los mensajes del usuario se resaltan con un degradado brillante que coincide con los colores principales de Milvus. Los mensajes del bot tienen un estilo con un tema más oscuro y discreto. Este diseño aprovecha los colores de Milvus para una interfaz de usuario unificada y funcional.
Figura- interfaz de usuario completada del sistema RAG
Figura: interfaz de usuario completada del sistema RAG
Así es como se ve la interfaz de usuario final.
Integración del Chatbot en la aplicación
Finalmente, actualiza el archivo App.js en el directorio src para renderizar el componente ChatbotUI.
import React from 'react';
import ChatbotUI from './ChatbotUI';
const App = () => {
return (
<div>
<ChatbotUI />
</div>
);
};
export default App;
Esto garantiza que el chatbot se renderice como el componente principal cuando se inicia la aplicación. El componente App sirve como punto de entrada para la aplicación React.
Prueba de tu sistema RAG
Después de completar la configuración del backend y del frontend, es hora de probar todo el pipeline RAG para asegurarte de que funcione como se espera. Aquí tienes cómo proceder:
Inicio del backend
Abre una terminal, navega hasta el directorio del proyecto e inicia el servidor backend:
node server.mjs
Asegúrate de que el backend se esté ejecutando en http://localhost:30080. Revisa los logs para verificar que Milvus se haya inicializado y esté listo para consultas.
Ejecución del frontend
Abre una terminal separada, navega hasta el directorio rag-app e inicia el frontend de React:
npm start
Esto iniciará la aplicación en http://localhost:3000 en tu navegador predeterminado.
Prueba de la interfaz del Chatbot
En la interfaz del chatbot, escribe una pregunta relacionada con Milvus o Zilliz. Además, intenta escribir preguntas fuera de estos temas para ver si el sistema responderá con el mensaje apropiado. Finalmente, intenta escribir preguntas que podrían estar fuera de la base de conocimiento de Strapi. Aquí hay resultados de ejemplo:
Figura- Resultado del sistema RAG de una consulta relacionada con Milvus
Figura: Resultado del sistema RAG de una consulta relacionada con Milvus
Figura- Resultados de prueba del sistema RAG
Figura: Resultados de prueba del sistema RAG
Las capturas de pantalla anteriores muestran que nuestro sistema RAG está funcionando según lo esperado y mostrando los resultados correctos. También está fundamentado en el uso de nuestra base de conocimiento, por lo que no conoce a los fundadores de Milvus, ya que esta información no existe en Strapi.
Continúa y añade más contenido a la base de conocimiento y prueba el sistema.
Conclusión
Integrar LangChain, Milvus y Strapi para crear un sistema de Generación Aumentada por Recuperación (RAG) demuestra cómo la IA puede ofrecer respuestas precisas y específicas del dominio, fundamentadas en conocimiento real y actualizado. Esta arquitectura es ideal para aplicaciones como atención al cliente, gestión del conocimiento y herramientas educativas. Con una comprensión clara de la arquitectura y esta guía paso a paso, ahora puedes crear sistemas RAG adaptados a tus necesidades específicas.
Recursos relacionados
Sigue leyendo

What Is a Vector Lakebase?
A Vector Lakebase is a unified, lake-native data architecture for AI that combines vector-database-grade serving with open lake storage, reusable lake-level indexes, and a shared semantic layer.

Introducing Zilliz CLI and Agent Skills for Zilliz Cloud
Manage your vector database from your terminal or AI coding agent. Zilliz CLI and Agent Skills work with Claude Code, Cursor, Codex, and Copilot.

DeepSeek-OCR Explained: Optical Compression for Scalable Long-Context and RAG Systems
Discover how DeepSeek-OCR uses visual tokens and Contexts Optical Compression to boost long-context LLM efficiency and reshape RAG performance.


