¿Por qué falla la versión simple?

A estas alturas tenemos todas las piezas (fragmentación, indexación, recuperación y reranking), y la forma más simple de conectarlas es embeber la consulta del usuario, recuperar los top-$k$ fragmentos, concatenarlos en el contexto del LLM y generar. Este pipeline ingenuo funciona bien como línea base, pero falla de maneras predecibles que se vuelven obvias una vez que miramos consultas reales.

Primero, la consulta misma puede ser ambigua o mal formulada. "Dime sobre el modelo" podría significar un modelo de machine learning, un modelo de moda o una maqueta, así que el recuperador devuelve una mezcla dispersa de fragmentos y el LLM alucina una respuesta coherente a partir de contexto incoherente. Segundo, la respuesta a veces requiere síntesis de múltiples documentos. Una consulta como "Compara el rendimiento de BM25 y ColBERT en BEIR" necesita información de diferentes artículos combinada, y un solo paso de recuperación top-$k$ puede devolver solo un lado de la comparación. Tercero, el contexto recuperado puede ser irrelevante o contradictorio, pero el LLM lo usa de todos modos y produce una respuesta incorrecta con confianza.

Los sistemas RAG de producción añaden pasos antes, durante y después de la generación para abordar cada una de estas fallas. El resto de este artículo recorre esos pasos.

¿Podemos corregir la consulta antes de buscar?

La primera falla (consultas ambiguas o mal formuladas) sugiere una corrección obvia: transformar la consulta sin procesar antes de que llegue al recuperador. Hay varias formas de hacer esto, cada una sacrificando un poco de latencia por mejor recall.

La reescritura de consultas usa un LLM para reformular la consulta en una forma que mejor coincida con el estilo de los documentos del corpus. Cuando los usuarios hacen preguntas conversacionales pero los documentos son formales, un reescritor cierra la brecha (por ejemplo, convirtiendo "¿qué onda con la atención?" en "¿Cómo funciona el mecanismo de auto-atención en modelos transformer?").

La expansión de consultas toma un ángulo diferente. En lugar de reescribir una consulta, generamos $n$ variantes de la consulta y unimos o fusionamos los resultados de recuperación, lo cual tiende a mejorar el recall a costa de $n imes$ latencia de recuperación. Una técnica relacionada es Step-Back Prompting (Zheng et al., 2023) , que pide a un LLM generar una pregunta "step-back" más general junto con la específica y recupera para ambas.

Hypothetical Document Embeddings (HyDE) (Gao et al., 2022) aborda el desajuste consulta-documento desde otra dirección. En lugar de embeber la consulta directamente, le pedimos al LLM que genere un documento hipotético que respondería la consulta, luego embebemos ese documento y lo usamos como vector de consulta. La razón por la que esto ayuda es que un documento de respuesta hipotético tiende a estar más cerca en el espacio de embeddings de los documentos de respuesta reales que la pregunta original, ya que comparte vocabulario y estructura con el corpus.

$$\mathbf{e}_{ ext{query}} = ext{Encode}( ext{LLM}( ext{``Write a document that answers: ''} + q))$$

HyDE tiende a ayudar más cuando las consultas son muy cortas (palabras clave individuales) o estilísticamente lejanas de los documentos del corpus. El compromiso es una llamada adicional al LLM antes de la recuperación, que puede añadir unos cientos de milisegundos de latencia, por lo que vale la pena medir si la ganancia en recall justifica el costo para un caso de uso dado.

¿Qué pasa si un solo recuperador no es suficiente?

No todas las consultas necesitan la misma estrategia de recuperación. Un sistema de producción puede tener múltiples fuentes de datos (un almacén de vectores para búsqueda semántica sobre documentación, una base de datos SQL para datos estructurados de productos, un índice BM25 sobre tickets de soporte al cliente), y un enrutador implementado como un clasificador LLM o un pequeño modelo fine-tuned puede dirigir cada consulta a la fuente apropiada.

Para consultas complejas, una sola fuente frecuentemente no es suficiente, por lo que el sistema recupera de múltiples fuentes en paralelo y fusiona los resultados. El enrutador puede no elegir una fuente exclusivamente sino asignar pesos o instruir a la capa de fusión para que utilice fuentes específicas.

La recuperación multi-paso aborda el problema de síntesis que identificamos antes (consultas que necesitan información de varios documentos). El LLM descompone la consulta en sub-preguntas, recupera para cada una, lee los resultados, y luego decide si recuperar más o generar. Este es el patrón central detrás de ReAct (Yao et al., 2022) (Reason + Act), donde el modelo intercala pasos de razonamiento con acciones de recuperación para construir contexto iterativamente.

¿Cómo debemos ensamblar el contexto?

Una vez que tenemos los fragmentos recuperados, necesitamos ensamblarlos en un prompt, y varias decisiones aquí pueden determinar la calidad de la respuesta.

  • El orden de los fragmentos importa porque los LLMs tienden a ser menos influenciados por la información en el medio de un contexto largo que por la información al inicio o al final (Liu et al., 2023) . Colocar los fragmentos más relevantes primero ayuda a mitigar este efecto de "perdidos en el medio".
  • Los fragmentos superpuestos de pasajes cercanos pueden añadir texto casi idéntico al contexto, por lo que debemos deduplicar por similitud de embeddings antes de insertarlos en el prompt.
  • Incluir metadatos de la fuente (título del documento, sección, URL) junto a cada fragmento ayuda al LLM a atribuir afirmaciones y permite a los usuarios rastrear las respuestas hasta sus fuentes.
  • Incluso con ventanas de contexto de 128K tokens, los contextos largos tienden a ralentizar la generación y diluir la atención. En la práctica, muchos equipos encuentran que entre 5 y 10 fragmentos de aproximadamente 500 tokens cada uno es un punto de partida razonable, aunque el número óptimo depende de la tarea.

El prompt de sistema debe instruir al LLM para que responda basándose en el contexto proporcionado, diga "No lo sé" cuando el contexto no contenga la respuesta, y cite fuentes. Sin estas instrucciones, el LLM tiende a recurrir a su conocimiento paramétrico para llenar las brechas de recuperación, lo cual puede producir respuestas incorrectas pero con confianza.

¿Qué pasa si la respuesta es incorrecta?

Incluso después de una cuidadosa optimización de consulta y ensamblaje de contexto, la respuesta generada aún puede ser incorrecta. Un paso de autorreflexión aborda esto pidiendo al LLM (o a un modelo crítico separado) que evalúe su propia salida. ¿Está esta respuesta fundamentada en el contexto recuperado? ¿Es el contexto realmente relevante? Si no, ¿debemos recuperar de nuevo con una consulta diferente?

SELF-RAG (Asai et al., 2023) formaliza esta idea entrenando un modelo con tokens de reflexión especiales. Durante la generación, el modelo emite tokens como [Retrieve], [IsRel], [IsSup] e [IsUse] para señalar cuándo necesita recuperar, si el documento recuperado es relevante, si una afirmación está respaldada y si la generación es útil. Debido a que el modelo se entrena de extremo a extremo para emitir estos tokens, la reflexión está integrada en la generación en lugar de añadida como un paso separado.

Corrective RAG (CRAG) (Yan et al., 2024) toma un enfoque más ligero añadiendo un evaluador que puntúa la relevancia de los documentos recuperados. Si todos los documentos recuperados puntúan por debajo de un umbral, CRAG activa una búsqueda web para obtener documentos frescos; si solo algunos son relevantes, descompone y filtra el conjunto recuperado antes de la generación. Esto funciona bien en la práctica porque la calidad de la recuperación varía de consulta en consulta, y detectar recuperación de baja calidad es mucho más barato que siempre recurrir a búsqueda web.

El siguiente pseudocódigo muestra cómo estas ideas encajan en un bucle de recuperar-leer-reflexionar. En cada iteración, el sistema recupera, filtra por relevancia, genera, y luego verifica si la respuesta está fundamentada antes de decidir devolverla o intentar de nuevo con una consulta reescrita.

# Skeleton of a self-reflective RAG pipeline
# (pseudocode — illustrates the control flow)

def rag_with_reflection(query, retriever, llm, max_iterations=3):
    context = []
    for iteration in range(max_iterations):
        # Optionally rewrite the query on subsequent iterations
        effective_query = llm.rewrite(query, context) if iteration > 0 else query

        # Retrieve candidates
        candidates = retriever.retrieve(effective_query, top_k=10)

        # Score relevance of each candidate
        relevant = [c for c in candidates if llm.is_relevant(query, c) > 0.5]

        if not relevant:
            # No relevant docs found — try web search or return "I don't know"
            if iteration == max_iterations - 1:
                return "I could not find relevant information to answer this question."
            continue  # retry with rewritten query

        context = relevant[:5]
        answer = llm.generate(query, context)

        # Check if the answer is grounded in the context
        if llm.is_grounded(answer, context):
            return answer
        # If not grounded, retry
    return answer  # Return best attempt
💡 El bucle de recuperar-leer-reflexionar añade latencia proporcional al número de iteraciones. En producción, un solo paso de reflexión (verificar si los documentos recuperados son relevantes, regenerar una vez si no lo son) tiende a dar la mayor parte del beneficio con una sobrecarga de latencia aceptable.

Quiz

Pon a prueba tu comprensión del pipeline RAG de extremo a extremo.

¿Cómo mejora HyDE la recuperación?

¿A qué se refiere el problema de 'perdidos en el medio'?

¿Por qué el modelo SELF-RAG emite tokens especiales como [Retrieve] e [IsRel]?

¿Por qué es importante incluir 'di que no sé cuando el contexto no contiene la respuesta' en el prompt de sistema?

¿Cuál es el patrón central de flujo de control de los agentes ReAct en recuperación multi-paso?