Blogs / RAG en Producción: Por Qué tu Pipeline Funciona en la Demo pero No en Producción

RAG en Producción: Por Qué tu Pipeline Funciona en la Demo pero No en Producción

Publicado
26 de julio de 2026
Autor
Faizan Nadeem
Etiquetas
RAG AI Engineering Backend Development LLM
Visualización abstracta de nodos de datos interconectados que representan una pipeline de recuperación
Foto de Steve A Johnson en Unsplash

La demo siempre funciona. Insertas embeddings para unas pocas docenas de documentos, haces una pregunta y el modelo la responde correctamente, al instante, con una cita limpia. Se lo muestras a tu equipo. Todos quedan impresionados. Luego lo apuntas al conjunto real de documentos, unos pocos miles de páginas en lugar de unas pocas docenas, y lo pones frente a usuarios reales, y todo se desmorona. Las respuestas se vuelven vagas. La latencia supera lentamente los tres segundos. El modelo cita con seguridad la sección equivocada. Alguien hace una pregunta con un código de producto exacto y el sistema devuelve cinco chunks que están todos relacionados temáticamente y son completamente inútiles.

La mayoría de los desarrolladores tratan RAG como una sola casilla de verificación: “¿agregamos una base de datos vectorial?” Ese es el error. Una base de datos vectorial es un componente dentro de una pipeline, no la pipeline en sí. La brecha entre una demo funcional y un sistema de producción no es un modelo más inteligente ni un mejor prompt; casi siempre es la capa de retrieval que está debajo, y en concreto tres cosas que la mayoría de los tutoriales omiten: cómo divides tus documentos en chunks, cómo buscas en ellos y cómo evitas hacer dos veces el trabajo costoso. Corrige esas tres cosas y todo lo que viene después se vuelve más fácil.

Aprenderás:

  • Por qué el chunking de tamaño fijo corrompe silenciosamente la calidad del retrieval, y qué corrige el chunking semántico
  • Por qué la búsqueda puramente vectorial falla en consultas de coincidencia exacta, y cómo el retrieval híbrido (vector + BM25) cierra esa brecha
  • Cómo el caché de consultas reduce tanto la latencia como el gasto en LLM sin servir respuestas obsoletas
  • Cómo trazar una sola consulta a través de una pipeline de nivel producción, de extremo a extremo
  • Qué es lo primero que se rompe cuando tu conjunto de documentos y tu tráfico realmente escalan
  • Los errores de retrieval que son fáciles de cometer y costosos de dejar sin corregir

Tabla de contenidos

  1. Lo básico
  2. La arquitectura completa
  3. Capas principales explicadas
  4. Recorrido de extremo a extremo
  5. Casos especiales
  6. Escalado y desafíos de producción
  7. Ejemplos de código
  8. Errores comunes
  9. Mejores prácticas de producción

Lo básico

Qué cubre realmente RAG

Retrieval-Augmented Generation tiene que responder correctamente cuatro preguntas, en orden, para cada consulta: ¿qué quiere decir realmente el usuario? ¿Qué partes de tus documentos son relevantes para ese significado? ¿Cómo haces para que solo las partes útiles quepan en una ventana de contexto limitada? ¿Y cómo generas una respuesta que esté fundamentada en lo que recuperaste en lugar de en lo que el modelo ya “sabe”?

La mayoría de las personas que construyen su primera funcionalidad RAG solo resuelven la tercera y la cuarta pregunta: conectan una llamada de embeddings, una búsqueda de similitud vectorial y una plantilla de prompt, y asumen que las dos primeras se resuelven solas. No es así. La comprensión de la consulta y la calidad del retrieval son los problemas realmente difíciles. La generación es la parte fácil; un LLM capaz puede escribir una respuesta coherente a partir de casi cualquier contexto que le des, incluido un mal contexto. Esa es exactamente la trampa: un mal retrieval no lanza un error, solo produce una respuesta segura, fluida e incorrecta.

Por qué es un problema de alto valor resolverlo bien

La calidad del retrieval no es un extra opcional; es el techo de todo lo demás que construyas encima:

  • Las alucinaciones amplifican la pérdida de confianza. Una respuesta incorrecta que suena correcta es peor que no tener respuesta, especialmente en herramientas de soporte o de conocimiento interno donde la gente deja de verificar dos veces después de unas pocas buenas experiencias.
  • La latencia tiene un piso duro de UX. Pasados aproximadamente dos segundos, los usuarios asumen que algo está roto, independientemente de lo buena que sea la respuesta final.
  • El costo escala con el mal retrieval, no con el buen retrieval. Los equipos que no pueden recuperar con precisión compensan metiendo más chunks en la ventana de contexto, lo que significa más tokens de entrada en cada llamada.
  • Los fallos son silenciosos. No hay stack trace para “recuperó la sección equivocada”. Solo te enteras por un ticket de soporte o una captura en Slack, lo que significa que tienes que incorporar visibilidad deliberadamente; no ocurrirá por accidente.

La arquitectura completa

Documents ─▶ Chunking ─▶ Embedding ─┬─▶ Vector Index
                                     └─▶ BM25 Index

User Query ─▶ Query Processing ─▶ Hybrid Retrieval ─▶ Rerank ─▶ Cache Check
                                                     ┌───────────────┴───────────────┐
                                                  cache hit                     cache miss
                                                     │                                │
                                              return cached                Context Assembly
                                                answer                            │
                                                                              LLM Call
                                                                          Cache + Return

El principio rector que sustenta todo esto: la calidad del retrieval es un techo duro para la calidad de la generación. Ninguna cantidad de prompt engineering rescata una respuesta construida a partir del contexto equivocado; si acaso, un mejor prompt solo hace que la respuesta equivocada resulte más convincente.

Capas principales explicadas

1. Chunking semántico

Qué es: Dividir documentos siguiendo límites naturales de significado —encabezados de sección, saltos de párrafo, cambios de tema detectados por similitud a nivel de oración— en lugar de cortar cada N caracteres o tokens sin importar lo que haya allí.

Por qué importa: El chunking de tamaño fijo es la fuente más común de pérdida silenciosa de calidad en una pipeline RAG. Un límite de chunk de 500 tokens no se preocupa por si cae en medio de una oración, separa una lista numerada de su introducción o aísla una salvedad de la regla a la que matiza. El embedding de ese chunk termina representando media idea, lo que significa que o bien no se recupera cuando debería, o se recupera sin el contexto que lo hacía correcto en primer lugar.

def semantic_chunks(sentences, embed_fn, threshold=0.75, max_tokens=400):
    chunks, current, current_tokens = [], [], 0
    prev_vec = None
    for sentence in sentences:
        vec = embed_fn(sentence)
        sim = cosine_similarity(prev_vec, vec) if prev_vec else 1.0
        too_big = current_tokens + count_tokens(sentence) > max_tokens
        if (sim < threshold or too_big) and current:
            chunks.append(" ".join(current))
            current, current_tokens = [], 0
        current.append(sentence)
        current_tokens += count_tokens(sentence)
        prev_vec = vec
    if current:
        chunks.append(" ".join(current))
    return chunks

Consejo de producción: Los límites semánticos por sí solos aún pueden producir chunks demasiado grandes o demasiado pequeños. Mantén siempre un techo duro de tokens como respaldo y agrega un pequeño solapamiento (10–15%) entre chunks adyacentes para que un límite que divide una cláusula dependiente siga dejando suficiente contexto en ambos lados.

2. Retrieval híbrido (Vector + BM25)

Qué es: Ejecutar dos métodos de retrieval en paralelo: búsqueda de similitud vectorial densa para significado semántico y un método disperso basado en palabras clave como BM25 para coincidencia exacta de términos; luego fusionar las dos listas ordenadas.

Por qué importa: Los embeddings son excelentes para “estas cosas significan algo parecido” y silenciosamente malos para “esto contiene exactamente esta cadena”. Pregúntale a un bot de soporte sobre el código de error ERR_4402 o un nombre de función específico, y una búsqueda puramente vectorial devolverá felizmente chunks relacionados por tema pero que no contienen el identificador literal, porque el embedding lo suavizó hacia un concepto general. BM25 captura exactamente los casos que la búsqueda vectorial pasa por alto y, a la inversa, la búsqueda vectorial captura consultas parafraseadas o conceptuales que no comparten vocabulario con el texto fuente.

def hybrid_retrieve(query, vector_index, bm25_index, k=10, rrf_k=60):
    vec_results = vector_index.search(query, top_k=k)
    bm25_results = bm25_index.search(query, top_k=k)
    scores = {}
    for rank, doc_id in enumerate(r.id for r in vec_results):
        scores[doc_id] = scores.get(doc_id, 0) + 1 / (rrf_k + rank)
    for rank, doc_id in enumerate(r.id for r in bm25_results):
        scores[doc_id] = scores.get(doc_id, 0) + 1 / (rrf_k + rank)
    return sorted(scores.items(), key=lambda x: -x[1])[:k]

Eso es fusión recíproca por rango, una forma simple y efectiva de fusionar dos rankings sin necesidad de normalizar escalas de puntuación incompatibles.

Consejo de producción: Da más peso a BM25 cuando la consulta contenga frases entre comillas, números o tokens con aspecto de identificador (cualquier cosa que coincida con un patrón de código o SKU). Una verificación rápida con regex antes del retrieval basta para detectar la mayoría de estos casos.

3. Caché de consultas

Qué es: Almacenar en caché tanto el embedding de una consulta como la respuesta generada final, usando como clave la consulta (o un casi duplicado de ella), de modo que las preguntas repetidas o muy similares se salten por completo las partes costosas de la pipeline.

Por qué importa: En la mayoría de los sistemas RAG en producción, un pequeño conjunto de preguntas representa una proporción desmedida del tráfico: el mismo puñado de consultas del estilo “cómo restablezco mi contraseña” o “cuál es nuestra política de reembolso”, formuladas de manera ligeramente distinta por diferentes usuarios. Volver a generar embeddings y volver a generar respuestas para cada una de ellas es puro desperdicio: es la misma latencia y el mismo costo en tokens para una respuesta que ya produjiste.

def get_cached_or_generate(query, redis_client, embed_fn, generate_fn, sim_threshold=0.95):
    query_vec = embed_fn(query)
    cache_key = f"rag:cache:{hash_vector(query_vec)}"
    cached = redis_client.get(cache_key)
    if cached and cosine_similarity(query_vec, cached["vec"]) > sim_threshold:
        return cached["answer"]
    answer = generate_fn(query)
    redis_client.setex(cache_key, 3600, {"vec": query_vec, "answer": answer})
    return answer

En una pipeline en la que trabajé, agregar solo esta capa redujo la latencia media de consulta de aproximadamente 3.2 segundos a cerca de 1.1 segundos durante las horas pico, puramente porque una gran proporción de consultas repetidas nunca tocó el LLM. Tus cifras dependerán de cuán repetitivo sea realmente tu tráfico de consultas, pero el patrón se mantiene casi en todas partes.

Consejo de producción: Nunca uses caché sin un TTL y una ruta de invalidación vinculada a las actualizaciones de documentos. Un caché que sobrevive más que el documento del que se construyó es la forma en que terminas sirviendo con total seguridad una respuesta que era correcta el mes pasado.

4. Reranking

Qué es: Una segunda pasada de puntuación, más costosa, normalmente con un modelo cross-encoder, aplicada a los 20–50 mejores candidatos del retrieval híbrido antes de que el top-k final pase a la ventana de contexto.

Por qué importa: Tanto la búsqueda vectorial como BM25 son aproximaciones rápidas. Son buenas para meter candidatos “probablemente relevantes” en una lista corta, pero no son lo bastante precisas como para confiar en ellas como ranking final. Un cross-encoder que realmente analiza juntos la consulta y cada candidato, en lugar de comparar vectores precalculados, es sustancialmente mejor ordenando correctamente esa lista corta; simplemente cuesta demasiado ejecutarlo sobre todo tu corpus.

Consejo de producción: Reordena la lista corta, nunca el corpus completo. Ejecutar un cross-encoder sobre unos pocos cientos de candidatos ya añade latencia real; sobre todo tu conjunto de documentos ni siquiera es una opción.

5. Ensamblado de contexto y presupuestación de tokens

Qué es: Empaquetar deliberadamente los chunks rerankeados dentro de tu ventana de contexto, eliminando contenido solapado, ordenando por relevancia y deteniéndote al alcanzar un presupuesto de tokens, en lugar de volcar cada chunk recuperado en el prompt.

Por qué importa: “Más contexto” no es lo mismo que “mejor contexto”. A partir de cierto punto, los chunks extra diluyen la señal con la que el modelo tiene que trabajar, elevan la latencia y el costo, e incrementan la probabilidad de que el modelo se enganche a un pasaje irrelevante. Un presupuesto de tokens rígido obliga a las etapas previas de retrieval y reranking a hacer realmente su trabajo en lugar de depender del volumen para compensar.

Recorrido de extremo a extremo

Sigue una sola consulta a través de la pipeline completa: “What’s the timeout on the payment webhook retry?”

  1. Consulta recibida. La capa API acepta la cadena en bruto y normaliza espacios en blanco y mayúsculas/minúsculas.
  2. Verificación de caché. Se genera el embedding de la consulta y se compara con vectores de consultas almacenados en caché por encima del umbral de similitud. En un caché frío, esto da un miss.
  3. Procesamiento de la consulta. Una verificación liviana marca esto como una consulta que contiene un término técnico específico (“webhook retry”), lo que inclina la ponderación híbrida hacia BM25.
  4. Retrieval híbrido. La búsqueda vectorial y la búsqueda BM25 devuelven cada una sus mejores candidatos; la fusión recíproca por rango los combina en una única lista corta ordenada.
  5. Reranking. El cross-encoder puntúa la lista corta contra el texto real de la consulta y la reordena. Si el reranking no devuelve nada por encima de una puntuación mínima de relevancia, la pipeline hace short-circuit hacia una respuesta de respaldo (“No pude encontrar nada específico sobre eso; intenta reformularlo”) en lugar de forzar al LLM a generar a partir de un contexto débil.
  6. Ensamblado de contexto. Los chunks con mayor puntuación se desduplican, se ordenan y se empaquetan en el prompt hasta alcanzar el presupuesto de tokens.
  7. Llamada al LLM. El modelo genera una respuesta fundamentada en el contexto ensamblado, con instrucciones para citar el chunk fuente.
  8. Escritura en caché. El embedding de la consulta y la respuesta generada se escriben en caché con un TTL y una etiqueta de versión ligada a la marca de tiempo de última actualización del documento fuente.
  9. Respuesta devuelta. El usuario recibe una respuesta en aproximadamente uno o dos segundos, con una cita hacia la sección específica del documento.

Cada paso de esa cadena tiene una ruta de fallo explícita. Esa es la diferencia entre una pipeline que se degrada con elegancia y una que se cuelga o alucina cuando algo aguas arriba no devuelve lo que esperaba.

Casos especiales

Tablas y datos estructurados. El chunking ingenuo, semántico o no, tiende a destrozar las tablas, separando encabezados de filas o dividiendo una tabla a mitad de fila. Detecta contenido tabular durante la ingesta y divídelo como una unidad, convirtiéndolo a una representación en markdown o clave-valor que sobreviva al proceso de embeddings como texto.

Documentos que se actualizan con frecuencia. Los documentos que cambian a menudo (páginas de precios, documentos de políticas, changelogs) necesitan invalidación de caché vinculada a un hash de contenido o a un número de versión, no solo a un TTL. Un caché basado únicamente en tiempo servirá respuestas obsoletas durante todo el tiempo que dure la ventana del TTL, incluso si el documento fuente cambió cinco minutos después de que el caché se escribiera.

Conversaciones de múltiples turnos. Una pregunta de seguimiento como “¿y qué hay de staging?” no tiene sentido para un sistema de retrieval sin el contexto del turno previo. Agrega un paso de reescritura de consulta que incorpore el historial reciente de la conversación en una consulta autocontenida antes de llegar al retrieval; de lo contrario, tu búsqueda híbrida estará recuperando contra media pregunta.

Escalado y desafíos de producción

La generación de embeddings se convierte en un cuello de botella a escala de ingesta. Generar embeddings de un puñado de documentos de forma síncrona está bien; generar embeddings de decenas de miles no. Mueve la ingesta a una cola de tareas async; Celery con Redis como broker es una opción natural si ya está en tu stack, y agrupa llamadas de embeddings en lotes en lugar de emitir una solicitud por chunk.

La búsqueda en el índice vectorial se ralentiza a medida que crece el corpus. La búsqueda de similitud por fuerza bruta funciona bien a pequeña escala y se desmorona después de alrededor de 100k vectores. Pásate a un índice de approximate nearest neighbor (HNSW es el valor por defecto más común) y acepta una pequeña compensación de precisión, ajustable, a cambio de una gran mejora en latencia.

La invalidación de caché se vuelve más difícil, no más fácil, a escala. Con más documentos cambiando con mayor frecuencia, un único TTL global deja de ser suficiente. Vincula las claves de caché a un hash de versión de los documentos fuente que contribuyeron a una respuesta dada, para que una actualización en cualquiera de ellos invalide de forma natural cada respuesta en caché que dependía de él.

El costo aumenta silenciosamente. Sin seguimiento de tokens por consulta, la primera señal de un problema de costos es la factura mensual, no una consulta concreta. Registra el conteo de tokens de entrada y salida por solicitud y alerta sobre valores atípicos; una consulta que incorpora mucho más contexto de lo habitual suele ser una señal de que tu presupuesto de tokens o tu umbral de reranking necesitan ajustarse más.

Ejemplos de código

El chunking semántico, el retrieval híbrido y el caché semántico se cubren por completo arriba, en Capas principales. Una pieza más que vale la pena tener a mano: una protección mínima de fallback para que la pipeline nunca genere a partir de un contexto vacío o casi vacío:

def generate_with_guard(query, context_chunks, generate_fn, min_score=0.3):
    if not context_chunks or context_chunks[0].score < min_score:
        return "I couldn't find anything specific on that in the documentation."
    context = "\n\n".join(c.text for c in context_chunks)
    return generate_fn(query=query, context=context)

Esta única función es responsable de convertir “respuesta incorrecta silenciosa” en “no lo sé” honesto, un pequeño cambio que hace mucho por la confianza del usuario.

Errores comunes

Error: tratar la búsqueda vectorial como la solución completa. Una pipeline solo vectorial omitirá sistemáticamente consultas de coincidencia exacta, IDs, códigos y términos entrecomillados. Solución: ejecuta retrieval híbrido por defecto, no como una optimización posterior. Es barato añadirlo temprano y caro adaptarlo después, una vez que tu conjunto de evaluación ya esté construido alrededor de un comportamiento solo vectorial.

Error: dividir en chunks con un conteo fijo de tokens sin considerar los límites del contenido. Esta es la causa más común de “la respuesta es casi correcta pero le falta un detalle clave”. Solución: divide semánticamente, limita el tamaño como respaldo y agrega un pequeño solapamiento entre chunks adyacentes.

Error: usar caché sin una estrategia de invalidación. Un caché sin camino de regreso al documento fuente servirá felizmente la respuesta del mes pasado como si fuera actual. Solución: versiona las claves de caché con un hash de contenido de los documentos contribuyentes, no solo con un TTL basado en tiempo.

Error: asumir que más chunks recuperados significan una mejor respuesta. Atiborrar contexto incrementa costo y latencia y a menudo empeora las respuestas, no las mejora, al diluir la señal. Solución: aplica reranking de forma agresiva y haz cumplir un presupuesto duro de tokens sobre lo que realmente llega al prompt.

Error: lanzar a producción sin un conjunto de evaluación. Sin un conjunto fijo de consultas representativas y fuentes esperadas, no tienes forma de saber si un cambio en el chunking o en el retrieval ayudó o perjudicó. Solución: crea incluso un conjunto pequeño (30–50 consultas) antes de lanzar, y vuelve a ejecutarlo en cada cambio de la capa de retrieval.

Mejores prácticas de producción

  • Mide la calidad del retrieval por separado de la calidad de la generación. Precision@k y recall frente a un conjunto de evaluación fijo te dicen si el problema está en el retrieval o en el prompt; no depures la llamada al LLM cuando el problema real está aguas arriba.
  • Usa caché con claves versionadas, no solo TTLs. Vincula cada entrada del caché a un hash de los documentos a partir de los que se construyó, para que las actualizaciones de documentos invaliden automáticamente las respuestas correctas.
  • Haz cumplir un presupuesto duro de tokens en el ensamblado de contexto. Más chunks no es una estrategia; una ventana de contexto ajustada y rerankeada supera a una grande y sin ranking casi siempre.
  • Registra cada retrieval, no solo cada error. Qué chunks se recuperaron, qué puntuó dónde y qué se descartó por el presupuesto de tokens: estos son los datos que necesitarás la primera vez que alguien reporte una respuesta incorrecta.
  • Construye una ruta de fallback para retrieval débil. Un “no lo sé” honesto a partir de una puntuación baja de relevancia siempre es mejor que una respuesta fluida construida a partir de un contexto que apenas coincidía con la pregunta.

Cierre

Nada de esto es exótico: el chunking semántico, el retrieval híbrido y una capa de caché son cosas que la mayoría de los ingenieros backend ya saben construir. La brecha entre la demo y producción no es un avance revolucionario ausente; es que la demo nunca tuvo suficientes documentos ni suficiente variedad de consultas para exponer dónde el chunking de tamaño fijo y la búsqueda solo vectorial se quedan silenciosamente cortos. Corrige esas tres cosas antes de tocar el prompt, y la mayoría de los tickets de “ayer funcionaba, ¿por qué hoy está mal?” dejan de aparecer.

¿Cuál fue la primera cosa que se rompió cuando sacaste tu pipeline RAG de la demo y la llevaste a producción? Sinceramente me gustaría saber qué fue lo que te sorprendió.

Más artículos