Al concluir esta guía, el lector dispondrá de un sistema RAG completamente funcional, robusto y adaptable a necesidades específicas, tales como un asistente de soporte interno, un sistema de tutoría personalizado o una herramienta de análisis de investigación.
Capítulo 1: Fundamentos Conceptuales – Arquitectura del Sistema
Previo a la implementación técnica, es fundamental comprender los componentes conceptuales del sistema y su interrelación. Una arquitectura robusta se fundamenta en principios sólidos.
Definición de una Base de Datos Vectorial
Una analogía pertinente para una base de datos vectorial es una biblioteca donde los volúmenes se organizan por similitud conceptual en lugar de por orden alfabético. En dicha biblioteca, un texto sobre «nutrición deportiva» se encontraría adyacente a uno sobre «biología celular del músculo», ya que ambos residen en un «espacio de significado» común.
Una base de datos vectorial opera bajo este principio. No almacena el texto en su forma literal, sino su representación matemática: un vector (o embedding). Este vector puede concebirse como una coordenada dentro de un vasto espacio multidimensional. En este ‘espacio de significado’, la distancia entre dos puntos vectoriales es inversamente proporcional a su similitud semántica; los conceptos relacionados se agrupan, mientras que los dispares se alejan. La conversión de texto a un punto en este espacio se realiza mediante un modelo de inteligencia artificial.
- Búsqueda Semántica: Al formular una consulta, esta también se transforma en un vector. La base de datos no realiza una búsqueda por palabras clave, sino que identifica los vectores (documentos) que presentan la mayor proximidad matemática al vector de la consulta. Este proceso, conocido como búsqueda por significado, es considerablemente más potente que los métodos de búsqueda tradicionales. Para cuantificar esta «proximidad», se emplean métricas como la distancia del coseno, la cual es idónea para evaluar la orientación y, por ende, la similitud temática de los vectores textuales.
- Relevancia para la Inteligencia Artificial: Se estima que el 80% de los datos a nivel mundial no están estructurados (e.g., PDFs, correos electrónicos, sitios web, transcripciones). Las bases de datos convencionales presentan dificultades para gestionar este tipo de información. Las bases de datos vectoriales capacitan a los sistemas de IA para localizar información pertinente dentro de grandes volúmenes de datos de manera eficiente, transformando la información desestructurada en conocimiento accionable.
El Rol de n8n como Plataforma de Orquestación
n8n es una herramienta que facilita la interconexión de diversas aplicaciones y servicios para automatizar procesos complejos. Su funcionamiento se basa en un lienzo visual donde se disponen y conectan «nodos». Cada nodo representa una operación discreta, como la lectura de un correo electrónico, la consulta a una base de datos o la invocación de una API.
En el contexto de este proyecto, n8n funcionará como el sistema nervioso central que orquestará la totalidad del proceso. Su función es recibir la consulta del usuario, coordinar las llamadas a los servicios externos (OpenAI para la vectorización, Qdrant para la búsqueda documental), procesar los resultados, formatear la información y, finalmente, generar una respuesta coherente. Actúa como el elemento integrador de los microservicios, permitiendo la visualización y depuración de la lógica completa del sistema RAG.
El Paradigma RAG (Retrieval-Augmented Generation)
RAG es el acrónimo de Generación Aumentada por Recuperación. Se trata de una técnica que mejora sustancialmente la calidad, fiabilidad y relevancia de los modelos de lenguaje (como GPT). El proceso, elegante en su simplicidad y potente en su ejecución, se articula en tres fases:
- Recuperar (Retrieve): Frente a una consulta, el sistema no intenta generar una respuesta de forma inmediata. Su primera acción es realizar una búsqueda en una base de conocimiento externa y fiable (la base de datos Qdrant) para localizar fragmentos de información que sean altamente pertinentes para la pregunta. Este paso fundamenta las respuestas del modelo en el corpus de datos proporcionado.
- Aumentar (Augment): La información recuperada («los hechos») se empaqueta y se añade al prompt o instrucción que se suministra al modelo de IA, junto con la consulta original. Este mecanismo es análogo a proporcionar a un estudiante material de referencia durante un examen: se le facilita tanto la pregunta como los textos exactos donde reside la respuesta.
- Generar (Generate): Finalmente, se solicita al modelo que genere una respuesta en lenguaje natural, sujeta a una instrucción crucial: debe basarse exclusivamente en la información proporcionada en el contexto. Esto obliga al modelo a utilizar datos verificables, lo que le permite citar fuentes y responder sobre temas muy específicos o recientes que no formaban parte de sus datos de entrenamiento originales.
Capítulo 2: Configuración del Entorno de Desarrollo
Una vez comprendidos los conceptos, se procederá a la preparación del entorno de trabajo. Una configuración meticulosa del entorno es un prerrequisito para el éxito del proyecto.
Requisitos Previos:
- Docker: Herramienta para la ejecución de aplicaciones en contenedores aislados. Se utilizará para ejecutar Qdrant de manera sencilla y reproducible.
- Python 3: Lenguaje de programación empleado para la preparación, vectorización y carga de los documentos.
- Una instancia de n8n: Puede ser una instancia en la nube (n8n.cloud) o auto-alojada para un control total.
- Una clave de API de OpenAI: Requerida para acceder a los modelos de generación de embeddings y de lenguaje.
Paso 1: Instanciación de la Base de Datos Vectorial (Qdrant)
Abra una terminal (o PowerShell en Windows) y ejecute el siguiente comando de Docker:
docker run -p 6333:6333 -v $(pwd)/qdrant_storage:/qdrant/storage qdrant/qdrant
La ejecución de este comando implica las siguientes operaciones:
docker run: Inicia un nuevo contenedor a partir de una imagen especificada.-p 6333:6333: El indicador-p(«publicar») mapea el puerto 6333 del contenedor al puerto 6333 de la máquina anfitriona, permitiendo la comunicación con la API de Qdrant.-v $(pwd)/qdrant_storage:/qdrant/storage: El indicador -v (‘volumen’) enlaza un directorio en su máquina anfitriona con un directorio dentro del contenedor. Específicamente,$(pwd)/qdrant_storagecrea un directorio local llamadoqdrant_storageen su ubicación actual de la terminal y lo sincroniza con/qdrant/storagedel contenedor, garantizando así la persistencia de los datos más allá del ciclo de vida del contenedor.qdrant/qdrant: Es el nombre de la imagen oficial de Qdrant que Docker descargará desde su repositorio.
Para verificar la correcta ejecución, acceda a http://localhost:6333 en un navegador web para visualizar la interfaz de Qdrant. Adicionalmente, el comando docker ps en la terminal confirmará que el contenedor se encuentra en estado de ejecución.
Paso 2: Preparación del Entorno de Python
Se requiere Python para interactuar programáticamente con las APIs de OpenAI y Qdrant.
- Instalación de librerías: Instale los clientes de Python necesarios para la comunicación con los servicios.
pip install openai qdrant-client - Configuración de la clave de API: Por motivos de seguridad, se recomienda gestionar las claves secretas a través de variables de entorno. En su terminal, ejecute:
export OPENAI_API_KEY="su_clave_de_api"(En Windows, utilice
set OPENAI_API_KEY="su_clave_de_api"). El script de Python podrá acceder a esta variable de forma segura.
Capítulo 3: Implementación del Almacenamiento y Recuperación de Datos
Con el entorno configurado, se procederá a poblar la base de datos con el corpus de conocimiento.
Paso 3: Carga de Documentos en Qdrant
Se creará un script de Python para leer documentos de ejemplo, convertirlos en vectores y almacenarlos en Qdrant junto con sus metadatos asociados.
Cree un archivo denominado cargar_qdrant.py con el siguiente contenido:
import openai
from qdrant_client import QdrantClient, models
import os
# 1. Configuración de la clave de API de OpenAI (leída desde la variable de entorno)
# Se verifica la existencia de la clave como buena práctica.
openai.api_key = os.getenv("OPENAI_API_KEY")
if not openai.api_key:
raise ValueError("La variable de entorno OPENAI_API_KEY no está configurada.")
# 2. Conexión a la instancia local de Qdrant
# El cliente establece comunicación con el servicio Qdrant ejecutado en Docker.
qdrant = QdrantClient(host="localhost", port=6333)
# 3. Definición de la colección
# Una colección en Qdrant es análoga a una tabla en una base de datos relacional.
collection_name = "mis_documentos"
vector_size = 1536 # Dimensión del modelo text-embedding-3-small de OpenAI. Es imperativo que coincida.
# Se utiliza `recreate_collection` para este ejemplo, asegurando un estado inicial limpio en cada ejecución. En un entorno de producción, se optaría por una lógica que verifique si la colección ya existe (`qdrant.get_collection`) y la cree solo si es necesario, para evitar la eliminación accidental de datos.
# La distancia del Coseno es óptima para datos textuales, ya que mide la similitud de orientación vectorial.
qdrant.recreate_collection(
collection_name=collection_name,
vectors_config=models.VectorParams(size=vector_size, distance=models.Distance.COSINE)
)
print(f"Colección '{collection_name}' creada o reiniciada.")
# 4. Corpus de documentos de ejemplo
# En un escenario de producción, estos datos provendrían de fuentes como archivos o APIs.
documentos = [
{"id": 1, "texto": "La diabetes es una enfermedad crónica que afecta la forma en que el cuerpo convierte los alimentos en energía.", "fuente": "Guía de Salud General"},
{"id": 2, "texto": "La hipertensión arterial se caracteriza por una presión sanguínea persistentemente elevada.", "fuente": "Artículo de Cardiología"},
{"id": 3, "texto": "El ejercicio físico regular mejora la salud cardiovascular y ayuda a controlar el peso.", "fuente": "Publicación de Bienestar"},
{"id": 4, "texto": "La gripe es una infección vírica que afecta a las vías respiratorias.", "fuente": "Folleto de Medicina Preventiva"}
]
# 5. Generación de embeddings y preparación de los puntos para Qdrant
puntos = []
for doc in documentos:
# Invocación a la API de OpenAI para la generación del embedding
response = openai.embeddings.create(
model="text-embedding-3-small", # Un modelo eficiente en coste y rendimiento
input=doc["texto"]
)
vector = response.data[0].embedding
# Creación del punto para Qdrant. El 'payload' es un objeto JSON que contiene metadatos
# que se desean recuperar junto con el vector (e.g., el texto original, la fuente).
puntos.append(
models.PointStruct(
id=doc["id"],
vector=vector,
payload={"texto": doc["texto"], "fuente": doc["fuente"]}
)
)
print(f"Embedding generado para el documento ID: {doc['id']}")
# 6. Inserción de los puntos en Qdrant mediante una operación por lotes para mayor eficiencia
qdrant.upsert(collection_name=collection_name, points=puntos, wait=True)
print("\n✅ Documentos insertados correctamente en Qdrant.")
Ejecute el script desde su terminal: python cargar_qdrant.py. El resultado mostrará la creación de la colección y la generación de embeddings para cada documento.
Paso 4: Validación del Proceso de Búsqueda
Para verificar la correcta ingesta de datos y la eficacia de la búsqueda semántica, se realizará una consulta directa mediante Python. Este paso es fundamental para la depuración y el aseguramiento de la calidad del sistema de recuperación.
Cree un archivo buscar_qdrant.py:
import openai
from qdrant_client import QdrantClient
import os
# 1. Configuración y conexión
openai.api_key = os.getenv("OPENAI_API_KEY")
qdrant = QdrantClient(host="localhost", port=6333)
collection_name = "mis_documentos"
# 2. Consulta de ejemplo
consulta = "¿Cómo se controla la presión arterial?"
# 3. Generación del embedding de la consulta (utilizando el mismo modelo que en la carga)
response = openai.embeddings.create(
model="text-embedding-3-small",
input=consulta
)
vector_consulta = response.data[0].embedding
# 4. Búsqueda en Qdrant de los 3 resultados más similares
resultados = qdrant.search(
collection_name=collection_name,
query_vector=vector_consulta,
limit=3
)
# 5. Presentación de resultados
print(f"🔎 Consulta: {consulta}")
print("📌 Resultados más relevantes:")
for r in resultados:
print(f" - Texto: {r.payload['texto']}")
print(f" Fuente: {r.payload['fuente']}")
# La puntuación de similitud del coseno varía entre -1 y 1. Un valor cercano a 1 indica alta similitud.
print(f" Puntuación de similitud: {r.score:.4f}\n")
Ejecute el script: python buscar_qdrant.py. La salida deberá presentar los documentos sobre hipertensión y ejercicio como los más relevantes, validando así el correcto funcionamiento de la búsqueda semántica.
Capítulo 4: Orquestación del Flujo de Trabajo con n8n
Con la base de conocimiento preparada y validada, se procederá a construir el flujo de automatización en n8n. En esta fase, los componentes individuales se integran en un proceso automatizado y coherente.
Para cada flujo, se proporciona un código JSON que puede ser importado directamente en el lienzo de n8n (Ctrl/Cmd + V).
Flujo 1: RAG con Citación de Fuentes
Este flujo implementa el RAG básico, incorporando la capacidad de citar las fuentes de información, lo que confiere verificabilidad y fiabilidad a las respuestas del asistente.
- JSON para importar:
{
"name": "RAG con Qdrant y OpenAI (multi-documento con fuentes)",
"nodes": [
{
"parameters": {
"jsCode": "return [{ consulta: \"¿Cómo se controla la presión arterial?\" }];"
},
"id": "EntradaConsulta",
"name": "Texto de entrada",
"type": "n8n-nodes-base.function",
"typeVersion": 2,
"position": [200, 300]
},
{
"parameters": {
"operation": "createEmbedding",
"model": "text-embedding-3-small",
"text": "={{$json[\"consulta\"]}}"
},
"id": "GenerarEmbedding",
"name": "OpenAI Embedding",
"type": "n8n-nodes-base.openAi",
"typeVersion": 1,
"position": [500, 300],
"credentials": {
"openAiApi": "TU_CREDENCIAL_OPENAI"
}
},
{
"parameters": {
"url": "http://localhost:6333/collections/mis_documentos/points/search",
"method": "POST",
"sendBody": true,
"jsonBody": "={\n \"vector\": $json[\"data\"][0][\"embedding\"],\n \"limit\": 3,\n \"with_payload\": true\n}"
},
"id": "BuscarQdrant",
"name": "Consulta Qdrant",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 3,
"position": [800, 300]
},
{
"parameters": {
"functionCode": "let docs = \"\";\nfor (let i = 0; i < $json.result.length; i++) {\n const r = $json.result[i];\n docs += `Fuente ${i+1} (${r.payload.fuente}): ${r.payload.texto}\\n\\n`;\n}\n\nreturn [{\n contexto: docs,\n consulta: $items(\"Texto de entrada\", 0).json.consulta\n}];"
},
"id": "PrepararContexto",
"name": "Preparar contexto con fuentes",
"type": "n8n-nodes-base.function",
"typeVersion": 2,
"position": [1100, 300]
},
{
"parameters": {
"operation": "chat",
"model": "gpt-4o-mini",
"messages": "=[{\n \"role\": \"system\",\n \"content\": \"Eres un asistente experto en salud. Usa solo la información del contexto para responder. Al final de tu respuesta, cita las fuentes que has usado entre paréntesis, por ejemplo: (Fuente 1, Fuente 3).\"\n}, {\n \"role\": \"user\",\n \"content\": \"Responde a la siguiente consulta basándote en el contexto proporcionado.\\n\\nConsulta: \" + $json[\"consulta\"] + \"\\n\\nContexto:\\n\" + $json[\"contexto\"]\n}]"
},
"id": "GenerarRespuesta",
"name": "OpenAI Chat con fuentes",
"type": "n8n-nodes-base.openAi",
"typeVersion": 1,
"position": [1400, 300],
"credentials": {
"openAiApi": "TU_CREDENCIAL_OPENAI"
}
},
{
"parameters": {
"jsCode": "return $json.choices[0].message.content;"
},
"id": "SalidaFinal",
"name": "Mostrar respuesta final",
"type": "n8n-nodes-base.function",
"typeVersion": 2,
"position": [1700, 300]
}
],
"connections": {}
}Nota Importante: Tras la importación, es necesario configurar las credenciales de OpenAI en los nodos correspondientes.
Flujo 2: El Sistema Completo – RAG con Memoria Conversacional
Esta es la versión final del flujo, que incorpora la capacidad de persistir el historial de la conversación mediante el Data Store de n8n.
- JSON para importar:
{
"name": "RAG con Qdrant + OpenAI (con historial de chat)",
"nodes": [
{
"parameters": {
"jsCode": "return [{ consulta: \"¿Y qué pasa si no se controla?\", sessionId: \"chat123\" }];"
},
"id": "EntradaConsulta",
"name": "Texto de entrada",
"type": "n8n-nodes-base.function",
"typeVersion": 2,
"position": [200, 300]
},
{
"parameters": {
"operation": "get",
"key": "={{$json.sessionId}}",
"options": {}
},
"id": "LeerHistorial",
"name": "Leer historial (Data Store)",
"type": "n8n-nodes-base.dataStore",
"typeVersion": 1,
"position": [200, 500],
"credentials": {
"dataStore": {
"id": "1",
"name": "Data Store"
}
}
},
{
"parameters": {
"operation": "createEmbedding",
"model": "text-embedding-3-small",
"text": "={{$json[\"consulta\"]}}"
},
"id": "GenerarEmbedding",
"name": "OpenAI Embedding",
"type": "n8n-nodes-base.openAi",
"typeVersion": 1,
"position": [500, 300],
"credentials": {
"openAiApi": "TU_CREDENCIAL_OPENAI"
}
},
{
"parameters": {
"url": "http://localhost:6333/collections/mis_documentos/points/search",
"method": "POST",
"sendBody": true,
"jsonBody": "={\n \"vector\": $json[\"data\"][0][\"embedding\"],\n \"limit\": 3,\n \"with_payload\": true\n}"
},
"id": "BuscarQdrant",
"name": "Consulta Qdrant",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 3,
"position": [800, 300]
},
{
"parameters": {
"functionCode": "let docs = \"\";\nfor (let i = 0; i < $json.result.length; i++) {\n const r = $json.result[i];\n docs += `Fuente ${i+1} (${r.payload.fuente}): ${r.payload.texto}\\n\\n`;\n}\n\nconst historial = $items(\"Leer historial (Data Store)\")[0].json.value || [];\n\nreturn [{\n contexto: docs,\n consulta: $items(\"Texto de entrada\")[0].json.consulta,\n historial: historial\n}];"
},
"id": "PrepararContexto",
"name": "Preparar contexto + historial",
"type": "n8n-nodes-base.function",
"typeVersion": 2,
"position": [1100, 300]
},
{
"parameters": {
"operation": "chat",
"model": "gpt-4o-mini",
"messages": "={{ \n [\n {\n \"role\": \"system\",\n \"content\": \"Eres un asistente médico. Usa el historial y las fuentes para responder. Cita las fuentes como (Fuente 1, Fuente 2). Sé conciso.\"\n },\n ...$json.historial,\n {\n \"role\": \"user\",\n \"content\": \"Usando el historial previo y el nuevo contexto, responde a mi última consulta.\\n\\nÚltima consulta: \" + $json[\"consulta\"] + \"\\n\\nContexto nuevo:\\n\" + $json[\"contexto\"]\n }\n ]\n}}"
},
"id": "GenerarRespuesta",
"name": "OpenAI Chat con historial",
"type": "n8n-nodes-base.openAi",
"typeVersion": 1,
"position": [1400, 300],
"credentials": {
"openAiApi": "TU_CREDENCIAL_OPENAI"
}
},
{
"parameters": {
"functionCode": "const respuesta = $json.choices[0].message.content;\nconst historialActualizado = $items(\"Preparar contexto + historial\")[0].json.historial;\n\nhistorialActualizado.push({ role: 'user', content: $items(\"Texto de entrada\")[0].json.consulta });\nhistorialActualizado.push({ role: 'assistant', content: respuesta });\n\nreturn [\n {\n respuesta: respuesta,\n historialParaGuardar: historialActualizado\n }\n];"
},
"id": "FormatearSalida",
"name": "Formatear salida y nuevo historial",
"type": "n8n-nodes-base.function",
"typeVersion": 2,
"position": [1700, 300]
},
{
"parameters": {
"operation": "set",
"key": "={{$items(\"Texto de entrada\")[0].json.sessionId}}",
"value": "={{$json.historialParaGuardar}}",
"options": {}
},
"id": "GuardarHistorial",
"name": "Guardar historial (Data Store)",
"type": "n8n-nodes-base.dataStore",
"typeVersion": 1,
"position": [1700, 500],
"credentials": {
"dataStore": {
"id": "1",
"name": "Data Store"
}
}
}
],
"connections": {}
}
Funcionamiento de la Memoria Conversacional:
- sessionId: Se introduce un
sessionIden la entrada para actuar como identificador único de cada sesión conversacional. - Lectura del Historial: Al inicio del flujo, se consulta el Data Store para recuperar cualquier conversación previa asociada a ese
sessionId. - Preparación del Contexto y el Historial: Un nodo de función combina los documentos recuperados de Qdrant con el historial de chat.
- Invocación a OpenAI Chat: El historial completo se transmite al modelo de OpenAI, proporcionándole el contexto conversacional necesario.
- Persistencia del Historial: Al final del flujo, el nuevo turno de la conversación se añade al historial, y el historial actualizado se guarda de nuevo en el Data Store.
Conclusión y Futuras Líneas de Trabajo
La finalización de esta guía culmina con la construcción de un sistema RAG completo, desde la concepción de la base de datos vectorial hasta la implementación de un flujo de trabajo totalmente automatizado con memoria conversacional. Se han abordado los conceptos de manejo de embeddings, búsqueda semántica y orquestación de servicios de IA.
Posibles extensiones del proyecto:
- Integración con una Interfaz de Usuario (Frontend): Utilice el nodo «Webhook» de n8n para exponer el flujo como una API, permitiendo su integración con aplicaciones de chat como Telegram, Slack o una interfaz web personalizada.
- Ingesta de Corpus de Documentos Personalizados: Adapte el script
cargar_qdrant.pypara procesar sus propios archivos (PDF, TXT, DOCX). Se recomienda implementar técnicas de ‘chunking’ (segmentación estratégica de documentos largos en fragmentos más pequeños y con posible solapamiento). Esta práctica es fundamental no solo para optimizar la precisión de la recuperación, al presentar al modelo fragmentos más enfocados temáticamente, sino también para gestionar las limitaciones de la ventana de contexto de los modelos de embeddings. - Evaluación de Modelos Alternativos: Explore el ecosistema de IA, evaluando diferentes modelos de embeddings (incluyendo modelos de código abierto de plataformas como Hugging Face) o de generación de texto para analizar su impacto en el coste, la latencia y la calidad de las respuestas.
- Implementación de Filtros de Metadatos: Qdrant soporta el filtrado de búsquedas basado en los metadatos del
payload. Esta funcionalidad permite combinar la búsqueda semántica con filtros estructurados, por ejemplo, para buscar únicamente en documentos de una fuente específica o dentro de un rango de fechas.
Este proyecto constituye una base sólida para el desarrollo de aplicaciones avanzadas de IA generativa. Los principios de RAG aquí implementados son fundamentales y perdurarán a medida que el campo continúe su rápida evolución.
