Construyendo una Aplicación RAG Completa con Nuxt y la API Gemini File Search de Google
La inteligencia artificial generativa ha transformado radicalmente la manera en que los desarrolladores conciben las aplicaciones modernas. Sin embargo, uno de los desafíos más persistentes en el despliegue de modelos de lenguaje de gran escala (LLMs) es su incapacidad nativa para acceder a información actualizada o específica de un dominio determinado. Es precisamente aquí donde la arquitectura RAG —Retrieval-Augmented Generation— emerge como una solución técnicamente sólida y pragmáticamente poderosa. En este tutorial, exploraremos cómo construir una aplicación RAG completa desde cero, combinando el framework Nuxt con la API Gemini File Search de Google para lograr respuestas precisas y contextualmente enriquecidas.
Antes de escribir una sola línea de código, es fundamental comprender el ecosistema conceptual en el que nos moveremos. RAG es un patrón arquitectónico que combina la recuperación de información desde una base de datos vectorial o un almacén de documentos indexados, con la capacidad generativa de un LLM. En lugar de depender únicamente del conocimiento preentrenado del modelo, el sistema recupera fragmentos de texto relevantes en tiempo real y los suministra como contexto adicional en el prompt. El resultado es una IA que responde con mayor precisión, reduce las alucinaciones y puede operar sobre documentación propietaria sin necesidad de reentrenamiento. La API Gemini File Search de Google encapsula gran parte de esta complejidad, ofreciendo un mecanismo de indexación y consulta directamente integrado en su ecosistema de modelos.
Definición de Términos Clave del Ecosistema RAG
Para abordar este proyecto con rigor técnico, es imprescindible dominar la terminología específica del paradigma RAG. A continuación se detallan los conceptos fundamentales que estructuran toda la arquitectura:
- Embedding: Representación vectorial densa de un fragmento de texto en un espacio matemático de alta dimensión, que permite calcular similitud semántica entre documentos y consultas.
- Vector Store (Almacén Vectorial): Base de datos especializada en almacenar y recuperar eficientemente vectores de alta dimensionalidad mediante búsquedas por similitud coseno o distancia euclidiana.
- Chunking: Proceso de segmentación de documentos extensos en fragmentos manejables, optimizando tanto la precisión del embedding como el rendimiento de la recuperación.
- Grounding: Técnica que ancla las respuestas del LLM a fuentes de información verificables, mitigando las alucinaciones del modelo.
- Retriever: Componente responsable de identificar y extraer los fragmentos más relevantes del almacén ante una consulta dada.
- Augmented Prompt: El prompt final enviado al LLM, que incluye tanto la consulta original del usuario como el contexto recuperado por el retriever.
- File Search API: Servicio de Google que abstrae las operaciones de indexación, almacenamiento y recuperación semántica de documentos dentro del ecosistema Gemini.
Configuración Inicial del Entorno de Desarrollo
El primer paso consiste en inicializar un proyecto Nuxt con la configuración adecuada para soportar operaciones de servidor. Nuxt es un framework full-stack basado en Vue.js que ofrece un sistema de rutas de servidor nativo mediante su directorio server/, lo cual resulta idóneo para gestionar llamadas a APIs externas sin exponer credenciales al cliente. La versión recomendada es Nuxt 3, que incorpora soporte nativo para TypeScript, composables reactivos y un motor de renderizado optimizado.
# Crear el proyecto Nuxt
npx nuxi@latest init rag-gemini-app
cd rag-gemini-app
# Instalar el SDK oficial de Google Generative AI
npm install @google/generative-ai
# Instalar dependencias adicionales de utilidad
npm install @nuxtjs/tailwindcss
Una vez creado el scaffold del proyecto, es imperativo configurar la clave API de Gemini de forma segura. Nunca debe exponerse esta credencial en el código fuente del cliente. En Nuxt 3, la gestión de variables de entorno se realiza mediante el fichero .env en la raíz del proyecto, y su acceso en el lado del servidor se realiza a través del módulo useRuntimeConfig. Esta separación garantiza que la clave permanezca exclusivamente en el contexto del servidor Node.js, inaccesible desde el navegador.
# .env
GEMINI_API_KEY=tu_clave_api_de_google_ai_studio
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
geminiApiKey: process.env.GEMINI_API_KEY, // Solo accesible en servidor
public: {
// Variables públicas aquí
}
}
})
Arquitectura del Servidor: Endpoints para la Gestión del Flujo RAG
El núcleo de la aplicación reside en el directorio server/api/, donde definiremos múltiples endpoints que orquestan las distintas fases del pipeline RAG. Esta separación en rutas discretas sigue el principio de responsabilidad única y facilita el mantenimiento, las pruebas unitarias y la escalabilidad futura. El flujo completo contempla tres operaciones fundamentales: inicializar el vector store, cargar e indexar documentos y ejecutar consultas semánticas contra el almacén.
El primer endpoint que debemos implementar es el de inicialización del almacén de datos. Este endpoint se encarga de instanciar el cliente de la API Gemini y preparar el espacio de almacenamiento donde se indexarán los documentos. Internamente, la File Search API de Google maneja la creación del índice vectorial de forma transparente, abstrayendo la complejidad del proceso de embedding. La respuesta incluye un identificador único del almacén que será referenciado en operaciones posteriores.
// server/api/store/initialize.post.ts
import { GoogleGenerativeAI } from '@google/generative-ai'
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
try {
const genAI = new GoogleGenerativeAI(config.geminiApiKey)
// Inicializar el File Manager para gestión de documentos
const fileManager = genAI.getFileManager()
return {
success: true,
message: 'Vector store inicializado correctamente',
timestamp: new Date().toISOString()
}
} catch (error) {
throw createError({
statusCode: 500,
statusMessage: 'Error al inicializar el almacén vectorial'
})
}
})
El segundo endpoint crítico gestiona la carga e indexación de documentos. Este proceso recibe archivos desde el cliente, los transmite a la API de Gemini File Search y aguarda la confirmación de que el proceso de indexación ha concluido satisfactoriamente. Es importante implementar un mecanismo de polling o espera activa, ya que la indexación es una operación asíncrona que puede tardar varios segundos dependiendo del tamaño del documento. Una vez completada la indexación, el documento queda disponible para consultas semánticas.
// server/api/documents/upload.post.ts
import { GoogleGenerativeAI } from '@google/generative-ai'
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
const formData = await readFormData(event)
const file = formData.get('document') as File
if (!file) {
throw createError({ statusCode: 400, statusMessage: 'No se proporcionó archivo' })
}
const genAI = new GoogleGenerativeAI(config.geminiApiKey)
const fileManager = genAI.getFileManager()
// Convertir File a Buffer para la transmisión
const arrayBuffer = await file.arrayBuffer()
const buffer = Buffer.from(arrayBuffer)
// Cargar archivo a Gemini File API
const uploadResponse = await fileManager.uploadFile(buffer, {
mimeType: file.type,
displayName: file.name
})
// Polling hasta que el estado sea ACTIVE
let fileInfo = await fileManager.getFile(uploadResponse.file.name)
while (fileInfo.state === 'PROCESSING') {
await new Promise(resolve => setTimeout(resolve, 2000))
fileInfo = await fileManager.getFile(uploadResponse.file.name)
}
if (fileInfo.state === 'FAILED') {
throw createError({ statusCode: 500, statusMessage: 'Error en el procesamiento del archivo' })
}
return {
success: true,
fileUri: fileInfo.uri,
displayName: fileInfo.displayName,
mimeType: fileInfo.mimeType
}
})
El Endpoint de Consulta: Generación Aumentada por Recuperación
El endpoint de consulta representa el corazón funcional de toda la arquitectura RAG. En él convergen la recuperación semántica y la generación del LLM en una única operación coordinada. La lógica consiste en recibir la pregunta del usuario junto con las referencias URI de los documentos previamente indexados, construir un prompt aumentado que incluya tanto la consulta como el contexto documental, y enviarlo al modelo Gemini para obtener una respuesta fundamentada en el contenido específico de los archivos cargados. Este enfoque garantiza que el modelo no fabrique información, sino que la extraiga directamente de las fuentes suministradas.
// server/api/query/index.post.ts
import { GoogleGenerativeAI } from '@google/generative-ai'
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
const body = await readBody(event)
const { question, fileUris } = body
if (!question || !fileUris?.length) {
throw createError({ statusCode: 400, statusMessage: 'Parámetros insuficientes' })
}
const genAI = new GoogleGenerativeAI(config.geminiApiKey)
const model = genAI.getGenerativeModel({ model: 'gemini-1.5-flash' })
// Construir las partes del mensaje con referencias a archivos indexados
const fileParts = fileUris.map((uri: string) => ({
fileData: { fileUri: uri, mimeType: 'application/pdf' }
}))
const result = await model.generateContent([
...fileParts,
{
text: `Basándote ÚNICAMENTE en los documentos proporcionados, responde la siguiente pregunta de manera precisa y detallada: ${question}`
}
])
return {
answer: result.response.text(),
sources: fileUris,
timestamp: new Date().toISOString()
}
})
Desarrollo de la Interfaz de Usuario con Nuxt y Vue
La capa de presentación debe reflejar fielmente el estado del pipeline RAG, proporcionando al usuario retroalimentación visual en cada etapa del proceso. La interfaz se estructura en tres paneles principales: un área de carga de documentos con indicadores de progreso de indexación, una sección de consulta con historial de conversación y un panel de gestión de los textos indexados que permite visualizar, referenciar y eliminar documentos del almacén. La reactividad nativa de Vue 3 junto con los composables de Nuxt facilita enormemente la sincronización entre el estado del servidor y la representación visual.
<!-- pages/index.vue -->
<template>
<div class="rag-app-container">
<section class="upload-section">
<h3>Cargar Documentos</h3>
<input type="file" @change="handleFileUpload" accept=".pdf,.txt,.docx" />
<div v-if="uploadStatus" class="status-indicator">
<span :class="statusClass">{{ uploadStatus }}</span>
</div>
</section>
<section class="documents-section">
<h3>Documentos Indexados</h3>
<ul>
<li v-for="doc in indexedDocuments" :key="doc.uri">
{{ doc.displayName }}
<button @click="removeDocument(doc.uri)">Eliminar</button>
</li>
</ul>
</section>
<section class="query-section">
<h3>Consultar Base de Conocimiento</h3>
<textarea v-model="userQuery" placeholder="Introduce tu pregunta..." />
<button @click="executeQuery" :disabled="!userQuery || isLoading">
{{ isLoading ? 'Procesando...' : 'Consultar' }}
</button>
<div v-if="queryResult" class="result-container">
<blockquote>
<p>{{ queryResult }}</p>
</blockquote>
</div>
</section>
</div>
</template>
<script setup lang="ts">
const uploadStatus = ref('')
const indexedDocuments = ref<Array<{uri: string, displayName: string}>>([])
const userQuery = ref('')
const queryResult = ref('')
const isLoading = ref(false)
const statusClass = computed(() => ({
'text-green-500': uploadStatus.value.includes('completado'),
'text-yellow-500': uploadStatus.value.includes('procesando'),
'text-red-500': uploadStatus.value.includes('error')
}))
const handleFileUpload = async (event: Event) => {
const input = event.target as HTMLInputElement
const file = input.files?.[0]
if (!file) return
uploadStatus.value = 'Cargando e indexando documento...'
const formData = new FormData()
formData.append('document', file)
try {
const response = await $fetch('/api/documents/upload', {
method: 'POST',
body: formData
})
indexedDocuments.value.push({
uri: response.fileUri,
displayName: response.displayName
})
uploadStatus.value = 'Indexación completada exitosamente'
} catch {
uploadStatus.value = 'Error durante la indexación'
}
}
const executeQuery = async () => {
if (!userQuery.value || !indexedDocuments.value.length) return
isLoading.value = true
try {
const response = await $fetch('/api/query', {
method: 'POST',
body: {
question: userQuery.value,
fileUris: indexedDocuments.value.map(d => d.uri)
}
})
queryResult.value = response.answer
} finally {
isLoading.value = false
}
}
const removeDocument = (uri: string) => {
indexedDocuments.value = indexedDocuments.value.filter(d => d.uri !== uri)
}
</script>
Consideraciones de Seguridad y Optimización en Producción
Antes de desplegar la aplicación en un entorno productivo, es fundamental revisar una serie de aspectos críticos que determinan tanto la seguridad como el rendimiento del sistema. La gestión de la clave API ya ha sido abordada mediante variables de entorno, pero existen otras superficies de ataque que merecen atención. En particular, la validación y sanitización de los archivos cargados es un vector de riesgo que no debe subestimarse: deben implementarse restricciones estrictas sobre los tipos MIME aceptados, el tamaño máximo de archivo y la frecuencia de carga por usuario mediante mecanismos de rate limiting.
Desde la perspectiva del rendimiento, el proceso de polling durante la indexación puede convertirse en un cuello de botella si múltiples usuarios cargan documentos simultáneamente. Una estrategia recomendada es implementar una cola de trabajos asíncronos con almacenamiento de estado en Redis, combinada con Server-Sent Events (SSE) para notificar al cliente cuando la indexación ha completado, eliminando así la necesidad de polling activo desde el servidor.
- Validación de archivos: Verificar el tipo MIME real del archivo, no solo la extensión declarada por el cliente.
- Rate limiting: Implementar limitación de peticiones por IP y por usuario autenticado para prevenir abusos de la API.
- Gestión de errores granular: Distinguir entre errores transitorios de red y errores permanentes de la API para implementar lógicas de reintento apropiadas.
- Caché de respuestas: Almacenar en caché las respuestas a consultas frecuentes para reducir latencia y costos de API.
- Monitorización: Integrar herramientas de observabilidad como OpenTelemetry para trazar el pipeline completo de principio a fin.
- Limpieza de archivos: Implementar políticas de retención para eliminar automáticamente archivos indexados que ya no son necesarios, controlando así el costo del almacenamiento en la API de Gemini.
Conclusiones y Próximos Pasos
La combinación de Nuxt y la API Gemini File Search demuestra ser una plataforma técnicamente coherente y productivamente eficiente para construir aplicaciones RAG en el mundo real. La arquitectura descrita en este tutorial cubre el ciclo completo desde la ingesta de documentos hasta la generación de respuestas fundamentadas, todo ello con una clara separación entre la lógica de servidor y la capa de presentación. El resultado es una aplicación mantenible, segura y extensible que puede adaptarse a una amplia variedad de casos de uso: desde sistemas de soporte documental hasta asistentes técnicos internos o plataformas de consulta legal y médica.
"RAG no es simplemente una técnica de optimización de prompts; es un cambio de paradigma que permite a los LLMs operar como sistemas de conocimiento dinámicos, actualizables y verificables sin necesidad de costosos procesos de fine-tuning."
Como extensiones naturales de este proyecto, resulta especialmente interesante explorar la implementación de un sistema de reranking que mejore la calidad de los fragmentos recuperados antes de su inclusión en el prompt aumentado. Asimismo, la integración de mecanismos de memoria conversacional permitiría mantener el contexto a lo largo de múltiples turnos de diálogo, transformando la aplicación en un sistema de consulta verdaderamente conversacional. Finalmente, la evaluación sistemática de la calidad de las respuestas mediante métricas como RAGAS —que mide la fidelidad, relevancia y completitud de las respuestas generadas— representa un paso imprescindible para cualquier despliegue en producción que aspire a estándares de calidad empresarial.


