Construyendo una Aplicación RAG con Nuxt y Gemini File Search: Tutorial Técnico Completo
La Generación Aumentada por Recuperación, conocida por sus siglas en inglés como RAG (Retrieval-Augmented Generation), representa uno de los paradigmas más relevantes en el desarrollo de aplicaciones modernas de inteligencia artificial. A diferencia de los modelos de lenguaje que operan únicamente sobre su conocimiento preentrenado, una arquitectura RAG permite enriquecer las respuestas del modelo con información externa, actualizada y específica del dominio. En este tutorial técnico se abordará la construcción de una aplicación RAG funcional utilizando el framework Nuxt junto con la API Gemini File Search de Google, cubriendo desde la configuración inicial del entorno hasta la implementación de una interfaz de usuario interactiva.
El interés por las arquitecturas RAG ha crecido exponencialmente en los últimos años, impulsado por la necesidad empresarial de combinar el poder generativo de los grandes modelos de lenguaje con fuentes de datos propias y verificables. Plataformas como Google, con su API Gemini, han democratizado el acceso a estas capacidades al proporcionar herramientas nativas para la indexación, recuperación semántica y generación de respuestas fundamentadas. Nuxt, por su parte, ofrece un entorno full-stack basado en Vue.js que facilita la creación tanto de backends robustos como de interfaces modernas, convirtiéndolo en una elección estratégica para este tipo de proyectos.
¿Qué es RAG y por qué es relevante?
RAG es una técnica que combina dos fases bien diferenciadas: la recuperación de fragmentos de información relevante desde una base de conocimiento indexada, y la generación de una respuesta coherente y precisa por parte de un modelo de lenguaje que recibe dichos fragmentos como contexto adicional. Este enfoque resuelve uno de los problemas fundamentales de los LLM puros: las alucinaciones. Al anclar las respuestas en documentos reales, se incrementa significativamente la fiabilidad y la trazabilidad del sistema.
Desde una perspectiva técnica, el flujo básico de RAG puede descomponerse en los siguientes pasos conceptuales:
- Indexación: Los documentos de origen se fragmentan (chunking) y se representan como vectores en un espacio semántico mediante modelos de embeddings.
- Recuperación: Ante una consulta del usuario, el sistema calcula la similitud semántica entre la consulta y los fragmentos indexados, devolviendo los más relevantes.
- Generación: El modelo de lenguaje recibe la consulta original junto con los fragmentos recuperados como contexto, produciendo una respuesta precisa y fundamentada.
- Presentación: La respuesta se entrega al usuario, idealmente con referencias a las fuentes utilizadas.
Gemini File Search automatiza y optimiza las dos primeras etapas de este flujo, gestionando el almacenamiento vectorial y la recuperación semántica de manera transparente para el desarrollador. Esto reduce considerablemente la complejidad de infraestructura que históricamente requería una implementación RAG, como la gestión de bases de datos vectoriales (Pinecone, Weaviate, Chroma) o la orquestación manual de embeddings.
Configuración inicial del proyecto Nuxt
El primer paso consiste en inicializar un proyecto Nuxt en su versión más reciente, asegurándose de habilitar el modo full-stack que permite tanto la renderización en el servidor como la creación de endpoints de API mediante el directorio server/. La instalación se realiza con el siguiente comando:
npx nuxi@latest init nuxt-rag-app
cd nuxt-rag-app
npm install
A continuación, es necesario instalar las dependencias clave del proyecto. La librería oficial de Google para IA generativa es el núcleo de la integración con Gemini, mientras que otras utilidades complementan el procesamiento de documentos y la gestión del entorno:
npm install @google/generative-ai
npm install @google/genai
npm install dotenv
La gestión de variables de entorno es un aspecto crítico de seguridad en cualquier aplicación que consuma APIs externas. En Nuxt, las variables sensibles como la clave de API de Google deben configurarse en el archivo .env en la raíz del proyecto y referenciarse a través del runtime config del framework, garantizando que nunca queden expuestas en el lado del cliente:
# .env
GOOGLE_API_KEY=tu_clave_api_aqui
GEMINI_MODEL=gemini-1.5-pro
El archivo nuxt.config.ts debe actualizarse para exponer estas variables exclusivamente en el servidor, utilizando la propiedad runtimeConfig:
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
googleApiKey: process.env.GOOGLE_API_KEY,
geminiModel: process.env.GEMINI_MODEL,
public: {
// Variables accesibles desde el cliente (no secretas)
}
}
})
Arquitectura del backend: utilidades y endpoints
La capa de servidor en Nuxt se organiza dentro del directorio server/, que Nitro (el motor de servidor subyacente) procesa automáticamente. Para mantener una arquitectura limpia y reutilizable, se recomienda separar la lógica de negocio en archivos de utilidades ubicados en server/utils/, mientras que los endpoints concretos residen en server/api/. Esta separación favorece la cohesión y facilita las pruebas unitarias de cada componente de forma independiente.
La utilidad principal de conexión con Gemini inicializa el cliente de Google Generative AI y expone funciones atómicas para las operaciones fundamentales del sistema RAG. Esta capa de abstracción desacopla la implementación específica de la API del resto de la lógica de la aplicación:
// server/utils/gemini.ts
import { GoogleGenAI } from '@google/genai'
let client: GoogleGenAI | null = null
export function getGeminiClient(): GoogleGenAI {
const config = useRuntimeConfig()
if (!client) {
client = new GoogleGenAI({ apiKey: config.googleApiKey })
}
return client
}
export async function createFileIndex(content: string, displayName: string) {
const genai = getGeminiClient()
// Crear un corpus o datastore para indexar el documento
const file = await genai.files.upload({
file: new Blob([content], { type: 'text/plain' }),
config: { displayName }
})
return file
}
export async function queryWithRAG(
query: string,
fileUris: string[],
modelName: string
) {
const genai = getGeminiClient()
const model = genai.getGenerativeModel({ model: modelName })
const contents = [
{
role: 'user',
parts: [
...fileUris.map(uri => ({ fileData: { fileUri: uri, mimeType: 'text/plain' } })),
{ text: query }
]
}
]
const result = await model.generateContent({ contents })
return result.response.text()
}
Con las utilidades base definidas, el siguiente paso es construir los endpoints de la API. Se necesitan al menos tres rutas: una para la indexación de documentos, una para consultar el almacén de datos existente y otra para realizar preguntas al sistema RAG. A continuación se muestra la implementación del endpoint de indexación:
// server/api/index-document.post.ts
import { createFileIndex } from '~/server/utils/gemini'
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const { content, displayName } = body
if (!content || !displayName) {
throw createError({
statusCode: 400,
statusMessage: 'Se requieren los campos content y displayName'
})
}
try {
const indexedFile = await createFileIndex(content, displayName)
return {
success: true,
fileUri: indexedFile.uri,
name: indexedFile.name,
displayName: indexedFile.displayName,
state: indexedFile.state
}
} catch (error: any) {
throw createError({
statusCode: 500,
statusMessage: `Error al indexar documento: ${error.message}`
})
}
})
El endpoint de consulta RAG recibe la pregunta del usuario junto con los identificadores de los archivos previamente indexados, invoca la función de generación con contexto y devuelve la respuesta estructurada al cliente. Es importante implementar validaciones robustas en estos puntos de entrada para garantizar la integridad de las peticiones:
// server/api/query.post.ts
import { queryWithRAG } from '~/server/utils/gemini'
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
const body = await readBody(event)
const { query, fileUris } = body
if (!query || !fileUris?.length) {
throw createError({
statusCode: 400,
statusMessage: 'Se requieren query y al menos un fileUri'
})
}
try {
const answer = await queryWithRAG(
query,
fileUris,
config.geminiModel || 'gemini-1.5-pro'
)
return {
success: true,
query,
answer,
sourcesUsed: fileUris.length
}
} catch (error: any) {
throw createError({
statusCode: 500,
statusMessage: `Error en la consulta RAG: ${error.message}`
})
}
})
Gestión del almacén de datos y estado del sistema
Un aspecto frecuentemente subestimado en las implementaciones RAG es la gestión del ciclo de vida de los documentos indexados. Gemini File API proporciona endpoints para listar, inspeccionar y eliminar archivos, funcionalidades esenciales para mantener el almacén de datos limpio y actualizado. En el contexto de Nuxt, se puede implementar un endpoint adicional que exponga el estado actual de todos los archivos indexados:
// server/api/files.get.ts
import { getGeminiClient } from '~/server/utils/gemini'
export default defineEventHandler(async () => {
const genai = getGeminiClient()
try {
const filesResponse = await genai.files.list()
const files = []
for await (const file of filesResponse) {
files.push({
name: file.name,
displayName: file.displayName,
uri: file.uri,
state: file.state,
createTime: file.createTime,
expirationTime: file.expirationTime,
sizeBytes: file.sizeBytes
})
}
return { success: true, files, total: files.length }
} catch (error: any) {
throw createError({
statusCode: 500,
statusMessage: `Error al obtener archivos: ${error.message}`
})
}
})
Es importante considerar que los archivos subidos a través de la API de Gemini tienen una vida útil limitada (actualmente 48 horas en el nivel gratuito), lo que hace necesario implementar estrategias de persistencia en producción. Para entornos empresariales, se recomienda almacenar los URIs de los archivos en una base de datos propia y gestionar proactivamente la reindexación antes del vencimiento, o evaluar las opciones de almacenamiento persistente disponibles en Google AI Studio para workloads de producción.
Implementación de la interfaz de usuario con Nuxt y Vue
La capa de presentación de la aplicación se construye aprovechando el sistema de componentes reactivos de Vue 3 y las capacidades de composición de Nuxt. La interfaz se organiza en torno a tres áreas funcionales principales: un panel de carga de documentos para la indexación, un visualizador del almacén de datos que muestra los archivos activos y sus estados, y un panel de consulta donde el usuario puede realizar preguntas al sistema RAG y visualizar las respuestas generadas.
<!-- pages/index.vue -->
<template>
<div class="rag-app">
<!-- Panel de indexación -->
<section class="indexing-panel">
<h2>Indexar Documento</h2>
<input v-model="docName" placeholder="Nombre del documento" />
<textarea v-model="docContent" placeholder="Contenido a indexar..." />
<button @click="indexDocument" :disabled="isIndexing">
{{ isIndexing ? 'Indexando...' : 'Indexar Documento' }}
</button>
<p v-if="indexStatus" class="status">{{ indexStatus }}</p>
</section>
<!-- Visualizador de archivos -->
<section class="files-panel">
<h2>Almacén de Datos</h2>
<button @click="loadFiles">Actualizar</button>
<ul>
<li v-for="file in files" :key="file.name">
<label>
<input
type="checkbox"
:value="file.uri"
v-model="selectedUris"
/>
{{ file.displayName }} — Estado: {{ file.state }}
</label>
</li>
</ul>
</section>
<!-- Panel de consulta -->
<section class="query-panel">
<h2>Consultar Sistema RAG</h2>
<textarea v-model="userQuery" placeholder="Escribe tu pregunta..." />
<button @click="submitQuery" :disabled="isQuerying || !selectedUris.length">
{{ isQuerying ? 'Generando respuesta...' : 'Consultar' }}
</button>
<div v-if="ragAnswer" class="answer-box">
<h3>Respuesta:</h3>
<p>{{ ragAnswer }}</p>
</div>
</section>
</div>
</template>
<script setup lang="ts">
const docName = ref('')
const docContent = ref('')
const userQuery = ref('')
const files = ref([])
const selectedUris = ref([])
const ragAnswer = ref('')
const isIndexing = ref(false)
const isQuerying = ref(false)
const indexStatus = ref('')
async function indexDocument() {
isIndexing.value = true
indexStatus.value = ''
try {
const result = await $fetch('/api/index-document', {
method: 'POST',
body: { content: docContent.value, displayName: docName.value }
})
indexStatus.value = `✓ Documento indexado: ${result.displayName} (${result.state})`
await loadFiles()
} catch (e: any) {
indexStatus.value = `✗ Error: ${e.message}`
} finally {
isIndexing.value = false
}
}
async function loadFiles() {
const result = await $fetch('/api/files')
files.value = result.files
}
async function submitQuery() {
isQuerying.value = true
ragAnswer.value = ''
try {
const result = await $fetch('/api/query', {
method: 'POST',
body: { query: userQuery.value, fileUris: selectedUris.value }
})
ragAnswer.value = result.answer
} finally {
isQuerying.value = false
}
}
onMounted(loadFiles)
</script>
La reactividad de Vue 3 simplifica enormemente la gestión de estados asincrónicos en la interfaz. Los flags isIndexing e isQuerying controlan el estado de los botones y proporcionan retroalimentación visual inmediata al usuario durante las operaciones que pueden tardar varios segundos, como la indexación de documentos grandes o la generación de respuestas complejas. La selección múltiple de archivos mediante checkboxes permite al usuario acotar el contexto de recuperación a subconjuntos específicos de la base de conocimiento.
Consideraciones de producción y mejores prácticas
Una vez validado el flujo básico en entorno de desarrollo, existen varios aspectos técnicos que deben abordarse antes de desplegar la aplicación en producción. La gestión de errores debe ser exhaustiva y contemplar no solo los errores de red o de validación de datos, sino también los estados transitorios de la API de Gemini, como archivos en estado PROCESSING que aún no están listos para ser consultados. Se recomienda implementar un mecanismo de polling o webhooks para verificar el estado de los archivos tras la indexación.
"Las arquitecturas RAG no son una solución universal; su efectividad depende directamente de la calidad de los datos indexados, la granularidad del chunking y la relevancia semántica de los fragmentos recuperados como contexto."
Entre las mejores prácticas identificadas para implementaciones RAG robustas en producción, destacan las siguientes recomendaciones:
- Chunking estratégico: Dividir documentos extensos en fragmentos de entre 512 y 1024 tokens con solapamiento para preservar el contexto entre segmentos adyacentes.
- Metadatos enriquecidos: Almacenar junto a cada chunk metadatos como la fuente original, la fecha de indexación y la sección del documento para facilitar la trazabilidad de las respuestas.
- Límites de tasa: Implementar rate limiting en los endpoints de la API para evitar abusos y controlar el consumo de cuota de la API de Google.
- Caché de respuestas: Para consultas frecuentes y predecibles, implementar una capa de caché (Redis, Memcached) que evite llamadas redundantes a la API generativa.
- Monitorización de calidad: Registrar métricas de relevancia de las respuestas mediante técnicas de evaluación automática como RAGAS para detectar degradaciones en la calidad del sistema.
- Seguridad de datos: Para documentos sensibles, evaluar soluciones on-premise o servicios con acuerdos de procesamiento de datos (DPA) antes de enviarlos a APIs externas.
La arquitectura presentada en este tutorial constituye una base sólida y extensible sobre la que construir sistemas RAG más complejos. La combinación de Nuxt como framework full-stack y Gemini File Search como capa de inteligencia artificial ofrece un equilibrio óptimo entre productividad de desarrollo, flexibilidad arquitectónica y potencia generativa. A medida que la API de Gemini continúa evolucionando con nuevas capacidades como el soporte nativo para múltiples modalidades y contextos de mayor longitud, las posibilidades de este tipo de aplicaciones seguirán expandiéndose, abriendo la puerta a sistemas de recuperación de conocimiento cada vez más sofisticados y precisos.

