Construyendo una aplicación RAG con Nuxt y la API Gemini File Search de Google
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 sólidos en el desarrollo moderno de aplicaciones de inteligencia artificial. A diferencia de los modelos de lenguaje convencionales que responden únicamente con base en sus datos de entrenamiento, una arquitectura RAG incorpora un mecanismo de recuperación dinámica que permite al modelo consultar una base de conocimiento propia antes de generar una respuesta. Esta distinción es fundamental cuando se trabaja con documentación técnica, bases de datos corporativas o cualquier tipo de contenido especializado que no forma parte del corpus público de entrenamiento.
En este artículo se detalla el proceso completo para construir una aplicación RAG funcional empleando Nuxt como framework full-stack y la API Gemini File Search de Google como motor de indexación y recuperación semántica. Nuxt, construido sobre Vue.js, ofrece una arquitectura híbrida que facilita la creación simultánea de endpoints de servidor y componentes de interfaz de usuario dentro de un mismo proyecto, lo cual resulta especialmente conveniente para prototipos y aplicaciones de producción de escala media. La combinación con el SDK de Gemini permite aprovechar capacidades avanzadas de embeddings y búsqueda vectorial sin necesidad de infraestructura adicional.
Conceptos fundamentales del patrón RAG
Antes de abordar la implementación, es necesario comprender el flujo de datos que gobierna cualquier sistema RAG. El proceso se divide en dos fases claramente diferenciadas: la fase de indexación y la fase de inferencia. Durante la indexación, los documentos fuente son procesados, divididos en fragmentos o chunks de tamaño controlado, convertidos en vectores de embeddings y almacenados en un índice recuperable. Durante la inferencia, la consulta del usuario se transforma también en un vector, se compara contra el índice para identificar los fragmentos más relevantes por similitud semántica, y esos fragmentos se inyectan como contexto en el prompt enviado al modelo generativo.
La API Gemini File Search abstrae gran parte de esta complejidad, gestionando internamente la vectorización y la búsqueda semántica. Sin embargo, comprender el flujo subyacente permite tomar decisiones arquitectónicas informadas, como el tamaño óptimo de los chunks, la estrategia de solapamiento entre fragmentos o la gestión del límite de tokens en el contexto enviado al modelo. En aplicaciones de producción, estos parámetros tienen un impacto directo en la calidad de las respuestas generadas.
Configuración del entorno y gestión de dependencias
El primer paso consiste en inicializar un proyecto Nuxt en su versión más reciente. Desde la terminal, el comando npx nuxi@latest init rag-nuxt-app genera la estructura base del proyecto. Una vez dentro del directorio, se procede a instalar las dependencias necesarias. El SDK oficial de Google Generative AI para JavaScript es la pieza central, complementado opcionalmente con utilidades para el manejo de archivos y la fragmentación de texto.
# Inicializar el proyecto
npx nuxi@latest init rag-nuxt-app
cd rag-nuxt-app
# Instalar dependencias principales
npm install @google/generative-ai
npm install @google/generative-ai/server
# Dependencias auxiliares para procesamiento de texto
npm install langchain @langchain/textsplitters
La gestión segura de credenciales es un aspecto crítico. La clave de API de Gemini debe almacenarse exclusivamente en variables de entorno y nunca exponerse en el código fuente ni en el cliente. Nuxt proporciona el archivo .env para este propósito, cuyas variables son accesibles en el servidor a través de useRuntimeConfig(). Es imperativo agregar el archivo .env al .gitignore del proyecto para evitar filtraciones accidentales en repositorios de control de versiones.
# Archivo .env en la raíz del proyecto
GEMINI_API_KEY=tu_clave_de_api_aqui
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
geminiApiKey: process.env.GEMINI_API_KEY,
public: {}
}
})
Funciones de utilidad en el servidor
Nuxt organiza la lógica de servidor dentro del directorio server/, que incluye subdirectorios para api/ (endpoints HTTP) y utils/ (funciones auxiliares reutilizables). Se recomienda encapsular la inicialización del cliente Gemini y las operaciones de indexación en funciones de utilidad independientes, promoviendo la separación de responsabilidades y facilitando las pruebas unitarias. La función principal de utilidad gestiona la instancia del cliente y expone métodos para subir archivos, listar archivos indexados y realizar consultas.
// server/utils/gemini.ts
import { GoogleGenerativeAI } from '@google/generative-ai'
import { GoogleAIFileManager } from '@google/generative-ai/server'
export const useGeminiClient = () => {
const config = useRuntimeConfig()
const genAI = new GoogleGenerativeAI(config.geminiApiKey)
const fileManager = new GoogleAIFileManager(config.geminiApiKey)
const model = genAI.getGenerativeModel({
model: 'gemini-1.5-flash',
})
return { genAI, fileManager, model }
}
La función de fragmentación de texto merece especial atención. Un tamaño de chunk demasiado pequeño puede perder contexto relevante, mientras que uno excesivamente grande puede saturar la ventana de contexto del modelo. Para la mayoría de los casos de uso con documentación técnica, un tamaño de entre 500 y 1000 tokens con un solapamiento del 10-20% ofrece un equilibrio adecuado. La siguiente función implementa una estrategia de fragmentación básica pero efectiva.
// server/utils/textSplitter.ts
export const splitTextIntoChunks = (
text: string,
chunkSize: number = 800,
overlap: number = 100
): string[] => {
const chunks: string[] = []
let start = 0
while (start < text.length) {
const end = Math.min(start + chunkSize, text.length)
chunks.push(text.slice(start, end))
start += chunkSize - overlap
}
return chunks
}
Endpoints de API para indexación y consulta
El sistema requiere al menos tres endpoints bien diferenciados: uno para cargar e indexar documentos, otro para listar los archivos disponibles en el almacenamiento y un tercero para realizar consultas RAG. Esta separación permite una interfaz de usuario modular y facilita la gestión del ciclo de vida de los documentos indexados. A continuación se muestra la implementación del endpoint de indexación, que recibe texto plano, lo fragmenta y lo sube a la API de Gemini File Search.
// server/api/index-document.post.ts
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const { text, fileName } = body
if (!text || !fileName) {
throw createError({
statusCode: 400,
message: 'Se requieren los campos text y fileName'
})
}
const { fileManager } = useGeminiClient()
const chunks = splitTextIntoChunks(text)
const uploadedFiles = []
for (const [index, chunk] of chunks.entries()) {
const blob = new Blob([chunk], { type: 'text/plain' })
const uploadResult = await fileManager.uploadFile(
// Convertir Blob a Buffer para la API
Buffer.from(await blob.arrayBuffer()),
{
mimeType: 'text/plain',
displayName: `${fileName}_chunk_${index}`,
}
)
uploadedFiles.push(uploadResult.file)
}
return {
success: true,
chunksIndexed: uploadedFiles.length,
files: uploadedFiles.map(f => ({ name: f.name, displayName: f.displayName }))
}
})
El endpoint de consulta es el núcleo del sistema RAG. Recibe la pregunta del usuario, recupera todos los archivos indexados disponibles y los inyecta como contexto en el prompt enviado a Gemini. La API File Search maneja internamente la selección de los fragmentos más relevantes mediante búsqueda semántica, lo que simplifica considerablemente la implementación en comparación con soluciones que requieren gestionar manualmente una base de datos vectorial como Pinecone o Qdrant.
// server/api/query.post.ts
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const { question } = body
if (!question) {
throw createError({ statusCode: 400, message: 'La pregunta es requerida' })
}
const { model, fileManager } = useGeminiClient()
// Recuperar todos los archivos indexados
const listResponse = await fileManager.listFiles()
const files = listResponse.files || []
if (files.length === 0) {
return { answer: 'No hay documentos indexados. Por favor, sube contenido primero.' }
}
// Construir el prompt con referencias a los archivos indexados
const fileParts = files.map(file => ({
fileData: {
mimeType: file.mimeType,
fileUri: file.uri
}
}))
const result = await model.generateContent([
...fileParts,
{
text: `Basándote exclusivamente en los documentos proporcionados, responde la siguiente pregunta de forma precisa y detallada. Si la información no está en los documentos, indícalo explícitamente.\n\nPregunta: ${question}`
}
])
return {
answer: result.response.text(),
sourcesCount: files.length
}
})
Interfaz de usuario con Nuxt y Vue
La capa de presentación se construye como un componente Vue estándar dentro del directorio pages/ de Nuxt. La interfaz debe proporcionar dos funcionalidades principales: un área para cargar o pegar texto que será indexado, y un campo de consulta con visualización de respuestas. El uso de useFetch y $fetch de Nuxt simplifica las llamadas a los endpoints de servidor, manejando automáticamente la serialización y los estados de carga.
<!-- pages/index.vue -->
<template>
<div class="container">
<h1>Aplicación RAG con Nuxt + Gemini</h1>
<!-- Sección de indexación -->
<section class="indexing-section">
<h2>Indexar Documento</h2>
<input v-model="fileName" placeholder="Nombre del documento" />
<textarea v-model="documentText" placeholder="Pega el texto a indexar..." rows="8" />
<button @click="indexDocument" :disabled="isIndexing">
{{ isIndexing ? 'Indexando...' : 'Indexar Documento' }}
</button>
<p v-if="indexResult">{{ indexResult }}</p>
</section>
<!-- Sección de consulta -->
<section class="query-section">
<h2>Realizar Consulta</h2>
<input v-model="userQuestion" placeholder="¿Qué deseas saber?" />
<button @click="submitQuery" :disabled="isQuerying">
{{ isQuerying ? 'Consultando...' : 'Enviar Pregunta' }}
</button>
<div v-if="answer" class="answer-box">
<h3>Respuesta:</h3>
<p>{{ answer }}</p>
</div>
</section>
</div>
</template>
<script setup lang="ts">
const fileName = ref('')
const documentText = ref('')
const userQuestion = ref('')
const answer = ref('')
const indexResult = ref('')
const isIndexing = ref(false)
const isQuerying = ref(false)
const indexDocument = async () => {
isIndexing.value = true
try {
const response = await $fetch('/api/index-document', {
method: 'POST',
body: { text: documentText.value, fileName: fileName.value }
})
indexResult.value = `✓ Documento indexado en ${response.chunksIndexed} fragmentos`
} catch (error) {
indexResult.value = 'Error al indexar el documento'
} finally {
isIndexing.value = false
}
}
const submitQuery = async () => {
isQuerying.value = true
try {
const response = await $fetch('/api/query', {
method: 'POST',
body: { question: userQuestion.value }
})
answer.value = response.answer
} catch (error) {
answer.value = 'Error al procesar la consulta'
} finally {
isQuerying.value = false
}
}
</script>
Consideraciones de arquitectura y escalabilidad
La implementación descrita representa un punto de partida sólido, pero existen aspectos críticos a considerar antes de llevarla a producción. La API Gemini File Search impone límites en el número de archivos almacenados y en la frecuencia de las solicitudes, por lo que en escenarios de alta concurrencia es necesario implementar estrategias de caché y cola de procesamiento. Asimismo, los archivos subidos a través de la File API tienen un período de retención limitado, lo que implica la necesidad de implementar lógica de reindexación periódica o utilizar almacenamiento persistente complementario.
Para aplicaciones con grandes volúmenes de documentos, considerar una arquitectura híbrida resulta ventajoso. En lugar de depender exclusivamente de la File API para la recuperación, se puede implementar una capa de pre-filtrado basada en metadatos (categoría, fecha, autor) antes de ejecutar la búsqueda semántica. Esto reduce significativamente el número de fragmentos evaluados y mejora tanto la velocidad como la precisión de las respuestas. A continuación se resumen las principales consideraciones arquitectónicas:
- Gestión del ciclo de vida de archivos: implementar endpoints para eliminar archivos obsoletos y mantener el índice actualizado.
- Límites de la API: monitorizar el uso de cuota y configurar reintentos con backoff exponencial.
- Seguridad: autenticar los endpoints de indexación para evitar la subida no autorizada de contenido.
- Observabilidad: registrar métricas de latencia, número de fragmentos recuperados y calidad percibida de las respuestas.
- Estrategia de chunking: evaluar splitters semánticos que respeten límites de párrafo o sección en lugar de cortes arbitrarios por caracteres.
"Una arquitectura RAG bien diseñada no solo mejora la precisión de las respuestas generadas, sino que además proporciona trazabilidad sobre las fuentes utilizadas, un requisito indispensable en aplicaciones empresariales donde la auditoría y la explicabilidad son prioritarias."
Pruebas y validación del flujo completo
La validación de un sistema RAG va más allá de las pruebas unitarias convencionales. Es necesario evaluar la calidad de las respuestas mediante métricas específicas como la fidelidad (¿la respuesta se basa en los documentos proporcionados?), la relevancia de la respuesta (¿responde realmente a la pregunta formulada?) y la relevancia del contexto recuperado (¿los fragmentos seleccionados son pertinentes?). Frameworks como RAGAS ofrecen herramientas automatizadas para medir estas dimensiones, aunque una evaluación humana inicial sigue siendo insustituible para calibrar el sistema.
Para las pruebas de integración del proyecto Nuxt, se recomienda utilizar Vitest junto con utilidades de testing de Nuxt para simular las llamadas a los endpoints de servidor sin necesidad de conectarse a la API real de Gemini. La creación de mocks del cliente Gemini permite verificar que la lógica de fragmentación, el formato de los prompts y el manejo de errores funcionan correctamente de forma aislada. Esta práctica acelera el ciclo de desarrollo y reduce los costes asociados al consumo de la API durante las fases de prueba.
En conclusión, la combinación de Nuxt como framework full-stack y la API Gemini File Search de Google ofrece una ruta de implementación eficiente para sistemas RAG que requieren mínima infraestructura adicional. La arquitectura resultante es extensible, permitiendo incorporar progresivamente características avanzadas como re-ranking de resultados, generación de respuestas con streaming o integración con fuentes de datos externas mediante conectores personalizados. El prototipo descrito en este artículo constituye una base técnica robusta sobre la cual construir soluciones de inteligencia artificial contextual orientadas a dominios de conocimiento específicos.


