Por qué RAG falla en producción: fragmentación, embeddings, recuperación y evaluación.
Por qué RAG falla en producción una vez terminada la demostración: límites entre los fragmentos, puntos ciegos en los embeddings, límites de top-K, datos obsoletos y el paso de evaluación que los equipos omiten.
Fuentes: RAG Without the Hype: What Retrieval-Augmented Generation Actually Takes to Work in Production
El tutorial estándar de RAG sigue un proceso lineal: se carga un PDF y se divide en fragmentos, estos se almacenan en una base de datos vectorial, luego se incluye una pregunta del usuario, se busca la coincidencia con los fragmentos más cercanos y se integra en un prompt. La respuesta que se obtiene es precisa, y parece que la parte más difícil ya ha terminado.
Pero en el entorno de producción la situación es diferente. Las preguntas abarcan varios documentos, y el contexto relevante se pierde en los bordes de los fragmentos. La recuperación de información devuelve pasajes que, aunque están cercanos en términos semánticos a la consulta, son incorrectos en el contexto, con las palabras adecuadas pero el significado equivocado. El modelo luego responde con confianza basándose en material obsoleto, contradictorio o que carece de información esencial que se encontraba tres oraciones más allá del límite del fragmento. Lo peor es que la salida suena correcta, y no hay señales de que la recuperación de información haya fallado.
Es fácil crear una demostración de RAG, pero no lo es desarrollar una versión que funcione de manera fiable con un corpus real y usuarios reales. La brecha se debe a tres factores: cómo se dividen los documentos en fragmentos, cómo los embeddings manejan las matices y cómo el sistema de recuperación decide qué se considera relevante. Cada uno de estos aspectos tiene modos de fallo que son fáciles de pasar por alto y cuyo depuración resulta costosa una vez lanzado el sistema.
División en fragmentos: donde se pierde la información
La opción predeterminada en la guía de inicio es la división recursiva de caracteres con un tamaño fijo y cierta superposición.
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200
)
chunks = splitter.split_documents(documents)
Eso es suficiente para una demostración. Falla cuando la estructura del documento contiene significado que los recuentos de caracteres ignoran, de tres maneras comunes.
- Contexto dividido. Una especificación podría indicar que el valor máximo soportado es 500, y que ese límite solo se aplica en modo de alta disponibilidad con la configuración de conmutación por falla activa. Al cortar en 1,000 caracteres, la cifra podría quedar en un bloque y la condición en el siguiente. Quien pregunte sobre el límite de alta disponibilidad obtendrá el bloque que indica 500 sin ningún matiz. La respuesta existe en el corpus, pero el particionamiento interrumpió la relación entre ambos.
- Títulos separados. Un título como “Configuración de seguridad” y el primer párrafo debajo de él, que indica que se requiere TLS para cada conexión, pueden terminar en fragmentos diferentes. Un sistema que encuentra el párrafo no cuenta con el contexto del título, y la inserción de la oración no indica que pertenezca a la configuración de seguridad.
- Tablas y listas dañadas. Los divisores de tamaño fijo suelen cortar una tabla a mitad de fila o una lista a mitad de elemento. Un encabezado de columna en un fragmento y los datos en otro impiden que cualquiera de ellos sea legible por sí solo.
La división del contenido en fragmentos para uso en producción suele basarse en tres técnicas.
División consciente de la estructura implica cortar en los límites propios del documento, es decir, en títulos, párrafos y secciones, en lugar de según el recuento de caracteres. Markdown puede dividirse en títulos de segundo o tercer nivel, y HTML en sus etiquetas de título, párrafo o sección.
from langchain.text_splitter import MarkdownHeaderTextSplitter
headers_to_split_on = [
("#", "document_title"),
("##", "section"),
("###", "subsection"),
]
splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on)
chunks = splitter.split_text(markdown_document)
# Each chunk now includes header metadata: {"section": "Security Configuration"}
Enriquecimiento del contexto padre almacena con cada fragmento la ruta del encabezado que conduce hasta él. Cuando se recupera el fragmento, la instrucción recibe los títulos de sección y subsección junto al texto, proporcionando al modelo la estructura que el espacio de embeddings no incluye.
Fragmentación padre-hijo mantiene fragmentos pequeños para mayor precisión en la recuperación y fragmentos padre más grandes que los contienen. La recuperación se realiza con el fragmento pequeño, y la instrucción recibe al fragmento padre. Los fragmentos pequeños incrustan ideas específicas con mayor precisión, mientras que los fragmentos padre aportan la información circundante.
# Small child chunk for retrieval
child_chunk = "The maximum supported value is 500."
# Parent chunk injected into prompt when child is retrieved
parent_chunk = """
## High Availability Configuration
When operating in high-availability mode with primary failover enabled,
the following limits apply:
- Maximum supported value: 500
- Minimum heartbeat interval: 100ms
- Failover timeout: 30 seconds
"""
Este enfoque cuesta más en implementación y almacenamiento, pero ofrece respuestas mucho mejores para preguntas que requieren contexto circundante, lo cual abarca la mayoría de las preguntas reales.
Embebidos y los límites de la similitud
Los embebidos colocan el texto en un espacio vectorial donde los significados similares se encuentran cerca unos de otros, y la recuperación devuelve los fragmentos más cercanos a la consulta. Esto funciona siempre que la cercanía semántica sea un sustituto adecuado de la relevancia. Falla cuando ambas se separan.
- Negación. Las oraciones que indican que un sistema no admite IPv6 y las que afirman que sí lo admite obtienen vectores casi idénticos, ya que ambas tratan sobre la compatibilidad con IPv6. La negación apenas afecta al vector. Una pregunta sobre IPv6 podría recuperar ambos fragmentos o el incorrecto, y el modelo debe reconciliar una contradicción o responder con el pasaje equivocado sin darse cuenta.
- Desequilibrio en el vocabulario. Un usuario escribe “login” mientras que la documentación habla de “authentication” y “sign-in”. Por lo general, estas expresiones están lo suficientemente cerca como para que la búsqueda funcione, pero surgen casos extremos cuando el jerga del usuario difiere del uso indicado en la documentación.
- Especificidad. Una pregunta sobre cómo configurar el pool de conexiones y otra sobre el tiempo de espera predeterminado del pool aparecen muy cerca una de la otra, por lo que ambas devuelven el mismo conjunto de fragmentos. Cuando el valor del tiempo de espera se encuentra en un párrafo entre varios sobre los pools, el mejor fragmento no siempre ocupa el primer lugar.
Tres soluciones abordan estos limites.
Búsqueda híbrida combina la recuperación densa, que captura el significado, con la recuperación dispersa como BM25 o TF-IDF, que captura términos exactos. Una consulta para un número de versión, una clave de configuración o un código de error se resuelve mejor mediante la coincidencia de palabras clave que mediante la similitud semántica. Muchas bases de datos vectoriales ofrecen ahora búsqueda híbrida de forma nativa, y el ejemplo a continuación utiliza una combinación de tipo disperso y denso en Qdrant.
from qdrant_client import QdrantClient
from qdrant_client.models import SparseVector, NamedSparseVector
# Many vector databases now support hybrid search natively
# This example uses Qdrant's sparse + dense hybrid search
results = client.query_points(
collection_name="docs",
prefetch=[
models.Prefetch(
query=sparse_query_vector, # BM25 sparse vector
using="sparse",
limit=20,
),
models.Prefetch(
query=dense_query_vector, # Embedding dense vector
using="dense",
limit=20,
),
],
query=models.FusionQuery(fusion=models.Fusion.RRF), # Reciprocal rank fusion
limit=5,
)
Expansión de consultas enriquece la pregunta antes de que sea incrustada. Una consulta sobre autenticación puede ampliarse con términos como inicio de sesión, autenticación y verificación de identidad, lo que la sitúa en una zona más rica del espacio de representación. Una pequeña llamada a un LLM puede generar estas variantes, o bien un mapa estático de sinónimos puede cubrir los términos específicos del dominio.
Elección del modelo de incrustación también merece atención. Muchos equipos optan por modelos generales de OpenAI como text-embedding-ada-002 o text-embedding-3-small. Para corpus médicos, legales o técnicos, un modelo específico del dominio o ajustado mediante fine-tuning suele rendir mucho mejor. Cuando la recuperación sigue siendo deficiente a pesar de un buen particionamiento de los datos, comparar alternativas en una muestra representativa de consultas resulta útil.
Problemas de recuperación que aparecen a gran escala
El fragmentado y las representaciones vectoriales determinan si se puede encontrar el contenido, mientras que la recuperación decide si se localiza para una consulta determinada. Algunos fallos solo se manifiestan con un corpus grande o consultas variadas.
Elegir K. La mayoría de los sistemas obtienen los K fragmentos más similares y los incluyen todos en el prompt. Un valor pequeño de K deja de lado hechos cuando una respuesta necesita el apoyo de varios fragmentos. Un valor grande de K llena la ventana de contexto con fragmentos poco relevantes que diluyen la información clave y, en prompts largos, pueden hacer que el fragmento verdaderamente importante quede fuera del alcance de la atención efectiva del modelo.
El patrón práctico consiste en obtener una cantidad excesiva de resultados, digamos los 20 o 30 mejores candidatos, y luego volver a clasificarlos. Un codificador cruzado combina la consulta con los fragmentos y evalúa directamente su relevancia, en lugar de comparar dos embeddings calculados de forma independiente. Es más lento porque los embeddings de los fragmentos no pueden calcularse por adelantado, pero separa con mucha mayor precisión los resultados relevantes de aquellos casi relevantes. La recuperación aproximada rápida reduce el campo de resultados, y el codificador cruzado elige el conjunto final.
from sentence_transformers import CrossEncoder
reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")
# Retrieve broad candidate set
candidates = vector_store.similarity_search(query, k=20)
# Re-rank with cross-encoder
pairs = [(query, chunk.page_content) for chunk in candidates]
scores = reranker.predict(pairs)
# Sort by re-ranking score, take top 5
reranked = sorted(zip(scores, candidates), reverse=True)[:5]
final_chunks = [chunk for _, chunk in reranked]
Obsolescencia. Un almacén de vectores implementado hace seis meses contiene documentos que desde entonces han cambiado, y el proceso de incrustación no tiene conocimiento de ello. La recuperación devuelve el fragmento desactualizado con alta confianza, y el modelo responde a partir de él. La solución es el filtrado de metadatos: se debe almacenar la fecha de última modificación de cada fragmento y filtrar según un rango de fechas antes o después de la búsqueda de similitud. En el caso de documentación mantenida activamente, los fragmentos más antiguos que un umbral determinado se descartan o se les asigna un ranking más bajo.
from datetime import datetime, timedelta
# Filter to chunks updated in the last 90 days
cutoff = datetime.now() - timedelta(days=90)
results = vector_store.similarity_search_with_filter(
query=query,
filter={"last_modified": {"$gte": cutoff.isoformat()}},
k=10
)
Confianza falsa. Un modelo responde basándose en lo que recibe, incluso cuando los fragmentos obtenidos no están relacionados con la pregunta. No sabe que la recuperación de información fue deficiente. Esto causa el mayor daño a la confianza del usuario, ya que la respuesta parece autoritaria cuando en realidad es incorrecta. La solución es implementar una verificación de relevancia antes de generar la respuesta. O bien se califican los fragmentos obtenidos y se evita la generación cuando la mejor puntuación está por debajo de un umbral, o bien se indica al modelo en el prompt del sistema que carece de información suficiente, y luego se verifica que utilice adecuadamente esa opción de escape.
SYSTEM_PROMPT = """
You are a documentation assistant. Answer questions using only the provided context.
If the context does not contain enough information to answer the question confidently,
respond with: "I don't have sufficient information in the available documentation to
answer this question reliably."
Do not speculate or answer from general knowledge when documentation context is insufficient.
"""
Evaluación, el paso que los equipos omiten
Un sistema RAG sin evaluación tiene una calidad desconocida. Puede rendir bien en algunas consultas y mal en otras, y la única forma de averiguarlo es a través de quejas de los usuarios. La evaluación consta de tres partes.
Calidad de la recuperación. Para consultas de prueba con documentos de origen correctos conocidos, se mide con qué frecuencia aparece el documento adecuado entre los K mejores recuperados. El índice de recuperación en 5 consultas indica si el fragmento correcto está entre los primeros cinco resultados, mientras que el índice de rango recíproco promedio muestra su posición relativa. Se debe ejecutar un conjunto de prueba con entre 100 y 200 consultas representativas antes del lanzamiento.
Fidelidad de la respuesta. ¿La respuesta generada refleja el contexto recuperado sin añadir afirmaciones que los fragmentos no contienen? Un sistema con un LLM como juez puede calificar esto al recibir la consulta, los fragmentos recuperados y la respuesta, y evaluar hasta qué punto el contexto respalda la respuesta.
FAITHFULNESS_PROMPT = """
Given the following context and answer, rate whether the answer is fully
supported by the context on a scale of 1-5.
Context: {context}
Answer: {answer}
Score (1=not supported, 5=fully supported):
"""
# Automate across your evaluation set
scores = []
for query, context, answer in eval_set:
score = judge_model.complete(
FAITHFULNESS_PROMPT.format(context=context, answer=answer)
)
scores.append(score)
print(f"Average faithfulness: {sum(scores)/len(scores):.2f}")
Corrección de extremo a extremo. En el caso de consultas con respuestas conocidas, ¿el sistema genera la correcta? Es la parte más difícil de automatizar y también la más importante. Incluso un conjunto manual de 50 preguntas con respuestas verificadas, ejecutado antes y después de cada cambio en los parámetros de segmentación, incrustación o recuperación, permite detectar regresiones.
El conjunto de evaluación funciona como un conjunto de pruebas de regresión. Un nuevo modelo de incrustación, un tamaño de segmento diferente o un prompt del sistema modificado deberían producir un cambio medible en las puntuaciones. Cuando un cambio mejora la fidelidad promedio pero disminuye la capacidad de recuperación, o viceversa, ese equilibrio debe ser una decisión consciente y no un despliegue silencioso.
El pipeline es el producto
El pipeline de tutoriales tiene cuatro pasos y no incluye manejo de errores. El pipeline de producción tiene seis pasos, con detección de fallos en cada uno, metadatos adjuntos en todo momento y evaluación antes de cada lanzamiento.
Los fallos son predecibles. El procesamiento en fragmentos que ignora la estructura pierde el contexto en los bordes. La recuperación basada únicamente en embeddings no encuentra coincidencias exactas y maneja mal las negaciones. La recuperación sin reclasificación devuelve un conjunto de los K mejores diluido, que oculta al mejor fragmento. La generación sin mecanismo de relevancia produce respuestas incorrectas pero seguras a partir de un contexto débil.
Ninguno de estos problemas es irresoluble, y cada solución tiene un costo conocido. El procesamiento en fragmentos que considera la estructura requiere comprender el formato del documento. La búsqueda híbrida implica utilizar índices tanto densos como dispersos. La reclasificación aumenta la latencia en la recuperación. La evaluación necesita crear y mantener un conjunto de pruebas.
Los equipos cuyos sistemas RAG funcionan bien toman esas decisiones de forma intencionada antes del lanzamiento, y no como reacción a respuestas incorrectas. Las tutoriales omiten las partes más complejas porque una demostración no las necesita, pero un sistema fiable sí las requiere.