Mar 05, 2026
Cómo montar un sistema RAG con LlamaParse, Voyage AI y Claude: del PDF a la respuesta inteligente
Los modelos de lenguaje como Claude o GPT son extraordinariamente capaces, pero tienen un límite fundamental: no saben lo que no estaba en su entrenamiento. No conocen el manual interno de tu empresa, no han leído la normativa específica de tu sector, y desde luego no tienen acceso a la documentación que subiste a tu servidor hace tres meses.
La solución clásica era el fine-tuning: reentrenar el modelo con tus datos. Caro, lento, y con una trampa: cada vez que el documento cambia, hay que reentrenar. Para contenido que se actualiza con frecuencia —legislación, manuales técnicos, bases de conocimiento corporativas— el fine-tuning es prácticamente inviable.
Existe un enfoque mucho más pragmático: RAG (Retrieval-Augmented Generation). En lugar de enseñarle al modelo todo tu contenido de antemano, le buscas en tiempo real los fragmentos relevantes y se los das como contexto justo antes de que responda.
En este artículo explico cómo construir ese sistema de principio a fin usando tres herramientas: LlamaParse para procesar los documentos fuente, Voyage AI para la búsqueda semántica, y Claude (Anthropic) para generar la respuesta final.
El problema: PDFs como fuente de verdad
La mayoría del conocimiento especializado vive en PDFs. Y los PDFs son un desastre para procesarlos con código.
Un extractor de texto básico (pdfplumber, PyPDF2) funciona bien en documentos simples, pero en cuanto aparece una tabla, una columna doble, un pie de página entrelazado con el cuerpo del texto o una imagen escaneada, el resultado es basura. Si luego intentas que un LLM responda basándose en ese texto mal extraído, la calidad de las respuestas se desploma.
LlamaParse resuelve este problema. En lugar de extraer texto plano, utiliza un modelo especializado para entender la estructura del documento y devuelve Markdown bien formateado: encabezados, tablas, listas, secciones. El resultado es texto que un LLM puede consumir de forma limpia.
// Ingesta básica con LlamaParse
const parser = new LlamaParseReader({ resultType: "markdown" });
const documents = await parser.loadData("manual-tecnico.pdf");
// → Markdown estructurado con secciones, tablas y jerarquía de títulos
El siguiente paso es dividir ese Markdown en chunks: fragmentos de ~800 caracteres con algo de solapamiento (150 caracteres) entre ellos. El solapamiento es importante para no cortar ideas a la mitad.
function splitIntoChunks(text: string, size = 800, overlap = 150): string[] {
const chunks: string[] = [];
let i = 0;
while (i < text.length) {
chunks.push(text.slice(i, i + size));
i += size - overlap;
}
return chunks;
}
Estos chunks son los “documentos” que más tarde buscaremos.
La búsqueda semántica: Voyage AI y los embeddings
Aquí está el núcleo del sistema. Necesitamos una forma de encontrar, entre miles de chunks, los cinco o diez más relevantes para una pregunta concreta. La búsqueda por palabras clave no es suficiente: si alguien pregunta “¿qué obligaciones tiene el empleador en materia de prevención?”, queremos encontrar chunks que hablen de “responsabilidades del empresario” o “normativa de seguridad laboral”, aunque no usen exactamente las mismas palabras.
Los embeddings resuelven esto. Un modelo de embeddings convierte cualquier texto en un vector (una lista de números) que representa su significado semántico. Textos con significados similares producen vectores similares. Podemos entonces medir la distancia entre vectores para encontrar los chunks más cercanos semánticamente a cualquier consulta.
Voyage AI ofrece modelos de embeddings de alta calidad, especialmente en inglés y español. Su modelo voyage-3-large produce vectores de 1024 dimensiones con muy buena capacidad de recuperación en textos técnicos y legales.
El proceso es en dos fases:
Fase 1 — Indexación (se ejecuta una sola vez, o cuando cambia el contenido):
// Para cada chunk del documento
for (const chunk of chunks) {
const response = await fetch("https://api.voyageai.com/v1/embeddings", {
method: "POST",
headers: { Authorization: `Bearer ${VOYAGE_API_KEY}` },
body: JSON.stringify({
model: "voyage-3-large",
input: [chunk.content],
input_type: "document", // ← importante: modo documento en la ingesta
output_dimension: 1024,
}),
});
const { data } = await response.json();
chunk.embedding = data[0].embedding; // array de 1024 números
// Guardar chunk + embedding en base de datos vectorial
}
Fase 2 — Recuperación (se ejecuta en cada consulta del usuario):
async function findRelevantChunks(question: string, topK = 5) {
// Embed la pregunta del usuario
const response = await fetch("https://api.voyageai.com/v1/embeddings", {
method: "POST",
headers: { Authorization: `Bearer ${VOYAGE_API_KEY}` },
body: JSON.stringify({
model: "voyage-3-large",
input: [question],
input_type: "query", // ← modo consulta en la búsqueda
output_dimension: 1024,
}),
});
const { data } = await response.json();
const queryEmbedding = data[0].embedding;
// Buscar los K chunks más similares por distancia coseno
return vectorDB.findNearest(queryEmbedding, topK);
}
La diferencia entre input_type: "document" y input_type: "query" no es trivial: Voyage AI optimiza los vectores según si son para indexar contenido o para buscar, lo que mejora notablemente la precisión de recuperación.
La respuesta: Claude con contexto inyectado
Con los chunks recuperados, el paso final es construir el prompt para Claude. La idea es simple: le damos los fragmentos relevantes como contexto y le pedimos que responda basándose exclusivamente en ellos.
async function answerWithRAG(question: string) {
// 1. Recuperar contexto relevante
const relevantChunks = await findRelevantChunks(question, 5);
const context = relevantChunks
.map((c, i) => `[Fragmento ${i + 1}]:\n${c.content}`)
.join("\n\n---\n\n");
// 2. Construir el prompt con contexto
const systemPrompt = `Eres un asistente experto. Responde únicamente basándote
en los fragmentos de documentación proporcionados. Si la información no está en
los fragmentos, indícalo explícitamente en lugar de inventar una respuesta.
DOCUMENTACIÓN RELEVANTE:
${context}`;
// 3. Llamar a Claude con streaming
const result = streamText({
model: anthropic("claude-sonnet-4-5"),
system: systemPrompt,
messages: [{ role: "user", content: question }],
});
return result.toUIMessageStreamResponse();
}
La instrucción “si la información no está en los fragmentos, indícalo” es crítica. Sin ella, Claude puede rellenar huecos con su conocimiento general, que puede no coincidir con tu documentación específica.
La arquitectura completa
Juntando todo, el sistema tiene dos pipelines claramente separados:
PIPELINE DE INGESTA (offline, se ejecuta al actualizar documentos)
─────────────────────────────────────────────────────────────────
PDF → LlamaParse → Markdown → Chunks → Voyage AI (embed) → Base de datos vectorial
PIPELINE DE CONSULTA (tiempo real, por cada mensaje del usuario)
──────────────────────────────────────────────────────────────────
Pregunta → Voyage AI (embed query) → findNearest() → Top-5 chunks
→ System prompt con contexto → Claude → Respuesta en streaming
Esta separación es deliberada. La ingesta puede tardar minutos (un PDF de 300 páginas genera miles de chunks y embeddings), pero la consulta debe ser rápida. Separándolos, la latencia en producción depende solo del embedding de la pregunta (~200ms) + la búsqueda vectorial (~50ms) + la generación de Claude (~2-10s según longitud).
Por qué este stack en concreto
¿Por qué Voyage AI en lugar de los embeddings de OpenAI?
Los modelos de Voyage, especialmente voyage-3-large, muestran mejor rendimiento en benchmarks de recuperación para texto técnico y legal en español. Además, los input_type explícitos (document/query) permiten optimizar cada fase por separado. En términos de coste, Voyage es competitivo y ofrece un tier gratuito generoso para empezar.
¿Por qué LlamaParse en lugar de un extractor básico?
Para documentos simples, un extractor básico funciona. Pero en cuanto el PDF tiene tablas con datos estructurados, columnas múltiples o numeración compleja, la extracción fallida contamina los chunks y degrada toda la cadena. LlamaParse añade un coste (el plan gratuito cubre 1000 páginas/día), pero la mejora en calidad de extracción se traduce directamente en mejores respuestas finales.
¿Por qué RAG en lugar de fine-tuning?
Tres razones prácticas: el contenido se puede actualizar sin reentrenar, el coste es órdenes de magnitud menor, y el modelo puede citar sus fuentes (puedes devolver qué chunk respaldó cada respuesta, mejorando la trazabilidad y auditabilidad).
Consideraciones para producción
-
Tamaño de los chunks: 800 caracteres con 150 de solapamiento es un punto de partida razonable para documentos técnicos densos. Para documentos más narrativos, chunks más grandes (1200-1500 chars) pueden funcionar mejor.
-
Número de chunks recuperados: Más contexto no siempre es mejor. Con 10+ chunks el prompt se vuelve muy largo y Claude puede perder el hilo. 3-7 chunks suele ser el punto óptimo.
-
Pre-filtrado antes de la búsqueda vectorial: Si tienes múltiples fuentes o categorías de documentos, filtra por metadatos antes del KNN. Buscar entre 500 chunks filtrados es más preciso que buscar entre 50.000 sin filtrar.
-
Cache de embeddings de consultas frecuentes: Las preguntas más comunes se pueden pre-cachear para evitar llamadas repetidas a la API de embeddings.
El resultado es un sistema que puede responder preguntas específicas sobre cualquier corpus de documentos, con información actualizada, sin alucinaciones sobre el contenido propio, y con la capacidad de indicar explícitamente cuándo algo no está en la documentación. Para cualquier caso de uso donde el conocimiento relevante vive en PDFs —desde documentación legal hasta manuales técnicos o bases de conocimiento internas— esta arquitectura es hoy la opción más pragmática y mantenible.
Related Blogs
See All Blog
Los repositorios de IA más importantes en GitHub en 2026
Los repositorios de IA más importantes en GitHub en 2026 El ecosistema open-source de inteligencia artificial está creciendo a un ritmo imp
Cómo montar un sistema RAG con LlamaParse, Voyage AI y Claude: del PDF a la respuesta inteligente
Los modelos de lenguaje como Claude o GPT son extraordinariamente capaces, pero tienen un límite fundamental: no saben lo que no estaba en s
RabbitMQ vs Kafka vs Pulsar: tres formas muy distintas de mover datos
RabbitMQ, Apache Kafka y Apache Pulsar mueven mensajes. Pero bajo esa aparente similitud, resuelven problemas muy diferentes en sistemas dis
Del concepto al impacto de negocio
No solo construimos tecnología. Construimos productos digitales que funcionan, se posicionan, se adoptan y crecen. Relación continua con visión de crecimiento.