Construyendo una aplicación RAG end-to-end con Nuxt y Gemini File Search
La inteligencia artificial generativa ha evolucionado de forma vertiginosa en los últimos años, pero uno de sus talones de Aquiles históricos ha sido la alucinación contextual: la tendencia de los modelos a generar respuestas plausibles pero incorrectas cuando carecen de información específica sobre un dominio. La técnica conocida como Retrieval-Augmented Generation (RAG) surge precisamente como respuesta a esta limitación, permitiendo anclar las respuestas del modelo en documentos reales, indexados y verificables. En este tutorial exploraremos cómo implementar una solución RAG completa utilizando el framework Nuxt en su vertiente fullstack y la API Gemini File Search de Google, combinando backend, frontend y orquestación de IA en una sola arquitectura coherente.
¿Qué es RAG y por qué importa en aplicaciones modernas?
RAG, o Generación Aumentada por Recuperación, es un patrón arquitectónico que divide el proceso de respuesta de un modelo de lenguaje grande (LLM) en dos fases diferenciadas: primero, la recuperación de fragmentos relevantes desde una base de conocimiento previamente indexada; segundo, la generación de una respuesta que sintetiza tanto la consulta del usuario como la evidencia recuperada. Esto contrasta radicalmente con el enfoque de fine-tuning, que requiere reentrenar el modelo con datos propios, un proceso costoso en tiempo y recursos computacionales.
Para comprender la arquitectura RAG es indispensable manejar tres conceptos nucleares que estructuran todo el pipeline:
- Chunk: Fragmento o trozo de texto extraído de un documento más extenso. El proceso de chunking divide los documentos en unidades manejables que pueden ser vectorizadas e indexadas de forma independiente, permitiendo recuperaciones más granulares y precisas.
- Store: Depósito o almacén donde se persisten los embeddings vectoriales y los metadatos asociados a cada chunk. En el contexto de Gemini File Search, este componente es gestionado directamente por la API de Google, abstrayendo la complejidad del almacenamiento vectorial.
- Tool: Herramienta que el modelo LLM puede invocar durante la generación para recuperar información del Store. Gemini expone el File Search como una tool nativa, lo que permite al modelo decidir autónomamente cuándo consultar la base de conocimiento.
La ventaja competitiva de RAG frente a otras aproximaciones radica en su dinamismo: es posible actualizar la base de conocimiento sin necesidad de modificar ni reentrenar el modelo base. Para aplicaciones empresariales que manejan documentación técnica, políticas internas o bases de datos de productos que cambian con frecuencia, esto representa un diferencial tecnológico de primer orden.
Configuración del entorno y estructura del proyecto Nuxt
Nuxt se posiciona como un framework fullstack por excelencia gracias a su sistema de server routes, que permite definir endpoints de API directamente dentro del mismo repositorio que gestiona la interfaz de usuario. Esta característica lo convierte en una opción idónea para proyectos RAG donde la lógica de orquestación con la API de Gemini debe residir en el servidor, evitando exponer claves de API al cliente.
El primer paso consiste en inicializar un proyecto Nuxt e instalar el SDK oficial de Google Generative AI:
# Crear un nuevo proyecto Nuxt
npx nuxi@latest init rag-nuxt-app
cd rag-nuxt-app
# Instalar el SDK de Google Generative AI
npm install @google/generative-ai
# Instalar dependencias adicionales
npm install @nuxtjs/tailwindcss La gestión segura de la clave de API es un aspecto crítico que no debe subestimarse. Nuxt provee un mecanismo nativo a través del archivo nuxt.config.ts y las variables de entorno del servidor, garantizando que el token nunca sea serializado en el bundle del cliente:
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
// Variables privadas - solo accesibles en el servidor
geminiApiKey: process.env.GEMINI_API_KEY,
// Variables públicas - expuestas al cliente (evitar datos sensibles)
public: {
appName: 'RAG Assistant'
}
}
}) El archivo .env en la raíz del proyecto almacenará la clave real durante el desarrollo, mientras que en producción esta variable deberá configurarse directamente en el entorno de despliegue (Vercel, Railway, Netlify, entre otros). Es imperativo incluir .env en el archivo .gitignore para prevenir filtraciones accidentales en repositorios públicos.
Implementación del backend: endpoints para gestionar el Store
El núcleo de la aplicación RAG reside en tres endpoints de servidor que orquestan las operaciones fundamentales del ciclo de vida del conocimiento. Nuxt estructura estos endpoints como archivos dentro del directorio server/api/, donde el nombre y la convención de directorio determinan la ruta HTTP resultante.
El primer endpoint se encarga de crear un nuevo depósito de búsqueda (corpus) en Gemini File Search. Este corpus actúa como contenedor lógico donde se agruparán todos los documentos relacionados con un dominio específico:
// server/api/corpus/create.post.ts
import { GoogleGenerativeAI } from '@google/generative-ai'
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
const body = await readBody(event)
const genAI = new GoogleGenerativeAI(config.geminiApiKey)
// Inicializar el cliente de Semantic Retrieval
const response = await $fetch(
'https://generativelanguage.googleapis.com/v1beta/corpora',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-goog-api-key': config.geminiApiKey
},
body: {
display_name: body.displayName || 'Mi Base de Conocimiento'
}
}
)
return response
}) El segundo endpoint gestiona la indexación de documentos de texto plano. Recibe el contenido textual, lo fragmenta en chunks de tamaño apropiado y los persiste en el corpus previamente creado. La API de Gemini File Search automatiza el proceso de generación de embeddings, lo que simplifica considerablemente la implementación respecto a soluciones que requieren gestionar vectorstores de forma manual:
// server/api/corpus/[corpusId]/documents/index.post.ts
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
const corpusId = getRouterParam(event, 'corpusId')
const body = await readBody(event)
// Crear documento dentro del corpus
const document = await $fetch(
`https://generativelanguage.googleapis.com/v1beta/${corpusId}/documents`,
{
method: 'POST',
headers: { 'x-goog-api-key': config.geminiApiKey },
body: {
display_name: body.title,
custom_metadata: [
{ key: 'source', string_value: body.source || 'manual_upload' }
]
}
}
)
// Indexar chunks del documento
const chunks = splitIntoChunks(body.content, 500)
const chunkRequests = chunks.map((text, index) => ({
chunk: {
data: { string_value: text },
custom_metadata: [{ key: 'position', numeric_value: index }]
}
}))
await $fetch(
`https://generativelanguage.googleapis.com/v1beta/${document.name}/chunks:batchCreate`,
{
method: 'POST',
headers: { 'x-goog-api-key': config.geminiApiKey },
body: { requests: chunkRequests }
}
)
return { success: true, documentName: document.name, chunksIndexed: chunks.length }
})
function splitIntoChunks(text: string, maxWords: number): string[] {
const words = text.split(' ')
const chunks: string[] = []
for (let i = 0; i < words.length; i += maxWords) {
chunks.push(words.slice(i, i + maxWords).join(' '))
}
return chunks
} El tercer endpoint es el más sofisticado: el motor de consultas que combina la recuperación semántica con la generación de respuestas. Utiliza el modelo Gemini con la tool de retrieval habilitada, permitiendo al modelo consultar automáticamente el corpus antes de formular su respuesta:
// server/api/query.post.ts
import { GoogleGenerativeAI } from '@google/generative-ai'
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
const body = await readBody(event)
const genAI = new GoogleGenerativeAI(config.geminiApiKey)
const model = genAI.getGenerativeModel({
model: 'gemini-1.5-flash',
tools: [
{
retrieval: {
vertexAiSearch: undefined,
googleSearchRetrieval: undefined,
// Configurar corpus de Gemini Semantic Retrieval
}
}
]
})
// Configurar la tool de retrieval con el corpus específico
const response = await $fetch(
'https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent',
{
method: 'POST',
headers: { 'x-goog-api-key': config.geminiApiKey },
body: {
contents: [{ parts: [{ text: body.question }], role: 'user' }],
tools: [{
retrieval: {
source: {
semantic_retriever: {
source: body.corpusName,
query: { parts: [{ text: body.question }] },
max_chunks_count: 5,
minimum_relevance_score: 0.5
}
}
}
}]
}
}
)
return {
answer: response.candidates[0].content.parts[0].text,
groundingMetadata: response.candidates[0].groundingMetadata
}
}) Construcción de la interfaz de usuario con Nuxt y Vue
La capa frontend de la aplicación RAG debe cumplir dos funcionalidades primarias: permitir la carga e indexación de documentos de texto plano, y proporcionar una interfaz de conversación donde el usuario pueda realizar preguntas sobre el corpus indexado. Nuxt facilita esta implementación a través de sus composables reactivos y la integración nativa con Vue 3.
El componente de carga de documentos implementa un área de texto donde el usuario puede pegar contenido directamente, junto con metadatos opcionales como título y fuente. La interacción asíncrona se gestiona mediante useFetch o $fetch, manteniendo retroalimentación visual del estado de indexación:
<!-- components/DocumentUploader.vue -->
<template>
<div class="uploader-container">
<h3>Indexar nuevo documento</h3>
<input v-model="documentTitle" placeholder="Título del documento" />
<textarea
v-model="documentContent"
placeholder="Pega el contenido de texto plano aquí..."
rows="10"
/>
<button @click="indexDocument" :disabled="isIndexing">
{{ isIndexing ? 'Indexando...' : 'Indexar documento' }}
</button>
<p v-if="indexingResult">✓ Indexados {{ indexingResult.chunksIndexed }} chunks</p>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{ corpusId: string }>()
const documentTitle = ref('')
const documentContent = ref('')
const isIndexing = ref(false)
const indexingResult = ref(null)
async function indexDocument() {
if (!documentContent.value.trim()) return
isIndexing.value = true
try {
indexingResult.value = await $fetch(
`/api/corpus/${props.corpusId}/documents`,
{
method: 'POST',
body: {
title: documentTitle.value,
content: documentContent.value
}
}
)
} finally {
isIndexing.value = false
}
}
</script> La interfaz de chat presenta los mensajes del usuario y las respuestas del asistente en tiempo real, con soporte para mostrar las fuentes de grounding que Gemini retorna como metadatos. Esta transparencia sobre qué fragmentos del corpus fundamentaron cada respuesta es un elemento diferenciador que genera confianza en el usuario final y facilita la auditoría de la información:
<!-- pages/chat.vue -->
<template>
<div class="chat-interface">
<div class="messages-container" ref="messagesContainer">
<div
v-for="message in messages"
:key="message.id"
:class="['message', message.role]"
>
<p>{{ message.content }}</p>
<div v-if="message.sources?.length" class="sources">
<small>Fuentes consultadas: {{ message.sources.join(', ') }}</small>
</div>
</div>
</div>
<div class="input-area">
<input
v-model="currentQuestion"
@keyup.enter="sendQuestion"
placeholder="Haz una pregunta sobre tus documentos..."
/>
<button @click="sendQuestion" :disabled="isLoading">Enviar</button>
</div>
</div>
</template> Flujo completo y consideraciones de producción
El flujo de datos en la aplicación RAG construida sigue una secuencia bien definida que garantiza tanto la fidelidad de las respuestas como la trazabilidad del proceso. Al recibir una pregunta del usuario, el endpoint de consulta envía la interrogante al modelo Gemini junto con la configuración de la tool de retrieval. El modelo evalúa si necesita consultar el corpus —decisión autónoma basada en el contenido de la pregunta— y de ser así, emite una búsqueda semántica que recupera los chunks más relevantes. Finalmente, genera una respuesta fundamentada en esa evidencia documental.
Para entornos de producción, es importante considerar los siguientes aspectos técnicos que impactan directamente en el rendimiento y la experiencia de usuario:
- Estrategia de chunking: El tamaño óptimo de los chunks depende del dominio. Documentos técnicos densos pueden requerir chunks más pequeños (200-300 palabras) para maximizar la precisión semántica, mientras que narrativas o documentos con contexto amplio se benefician de chunks de 500-800 palabras.
- Gestión de límites de la API: La API de Gemini impone límites de requests por minuto (RPM) y tokens por minuto (TPM). Implementar un sistema de cola con reintentos exponenciales es recomendable para aplicaciones con múltiples usuarios concurrentes.
- Caché de respuestas: Preguntas frecuentes con alta similitud semántica pueden beneficiarse de un sistema de caché en memoria (Redis) para reducir latencia y costos de API.
- Validación de contenido: Antes de indexar, es recomendable sanitizar el texto para eliminar caracteres especiales que puedan interferir con el proceso de embedding.
- Monitoreo del corpus: Implementar endpoints de gestión que permitan listar, actualizar y eliminar documentos del corpus para mantener la base de conocimiento actualizada.
"La arquitectura RAG no es simplemente una mejora incremental sobre los LLMs estándar; representa un cambio paradigmático en cómo las organizaciones pueden democratizar el acceso a su conocimiento institucional, combinando la fluidez lingüística de los modelos generativos con la precisión y auditabilidad de sistemas de recuperación estructurados."
La combinación de Nuxt como framework fullstack y Gemini File Search como motor de retrieval ofrece un equilibrio notable entre productividad de desarrollo y capacidad técnica. El desarrollador puede construir una aplicación RAG funcional en pocas horas sin necesidad de gestionar infraestructura vectorial propia, bases de datos especializadas como Pinecone o Weaviate, ni pipelines complejos de generación de embeddings. Esto democratiza significativamente el acceso a la tecnología RAG para equipos de desarrollo web que no cuentan con especialización en machine learning, abriendo la puerta a una nueva generación de aplicaciones web inteligentes fundamentadas en datos verificables y trazables.


