Saltar al contenido
Lección 8 de 12

Patrones Avanzados de RAG

8 min read

Por qué el RAG Básico No es Suficiente

El pipeline estándar de recuperar-luego-generar funciona bien para preguntas directas con respuestas claras en tus documentos. Pero se descompone en varios escenarios comunes:

  • La consulta del usuario es vaga o usa terminología diferente a la de los documentos.
  • La respuesta requiere sintetizar información dispersa en múltiples documentos.
  • Los fragmentos recuperados son marginalmente relevantes pero no realmente útiles.
  • La pregunta requiere razonamiento o lógica de múltiples pasos, no solo búsqueda.

Los patrones avanzados de RAG abordan estos modos de fallo agregando inteligencia a las etapas de recuperación y generación.

RAG Multi-Consulta

El problema: una única consulta del usuario podría no ser la mejor consulta de búsqueda para encontrar toda la información relevante. Diferentes formulaciones de la misma pregunta pueden recuperar fragmentos diferentes y complementarios.

RAG multi-consulta usa el LLM para generar múltiples reformulaciones de la pregunta original, ejecuta cada una como una consulta de recuperación separada y fusiona los resultados.

from langchain.retrievers.multi_query import MultiQueryRetriever
from langchain_openai import ChatOpenAI
from langchain_community.vectorstores import Chroma

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.3)

# Envolver tu retriever base con multi-consulta
multi_retriever = MultiQueryRetriever.from_llm(
    retriever=vectorstore.as_retriever(search_kwargs={"k": 5}),
    llm=llm,
)

# El retriever genera multiples consultas internamente
# Para "Cuales son los beneficios del trabajo remoto?", podria generar:
# 1. "Que ventajas ofrece trabajar desde casa a los empleados?"
# 2. "Como beneficia la politica de trabajo remoto a la organizacion?"
# 3. "Cuales son las ventajas del teletrabajo?"
results = multi_retriever.invoke("Cuales son los beneficios del trabajo remoto?")

Multi-consulta es uno de los patrones avanzados más simples de implementar y frecuentemente proporciona una mejora inmediata en el recall de recuperación. El tradeoff es latencia incrementada (una llamada extra al LLM) y costo.

HyDE: Hypothetical Document Embeddings

HyDE aborda una asimetría fundamental en RAG: la consulta es corta (una pregunta) mientras que los documentos son largos (párrafos de información). Sus embeddings viven en regiones diferentes del espacio vectorial, lo cual puede perjudicar la calidad de recuperación.

HyDE funciona pidiendo al LLM que genere una respuesta hipotética a la consulta, luego embebe esa respuesta hipotética en lugar de la consulta misma. Como la respuesta hipotética se parece más a un documento que a una pregunta, coincide con documentos reales más efectivamente.

from langchain.chains import HypotheticalDocumentEmbedder
from langchain_openai import OpenAIEmbeddings, ChatOpenAI

# Crear embeddings HyDE
base_embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

hyde_embeddings = HypotheticalDocumentEmbedder.from_llm(
    llm=llm,
    base_embeddings=base_embeddings,
    prompt_key="web_search",  # Prompt incorporado para generar docs hipoteticos
)

# Usar embeddings HyDE para recuperacion
# Cuando embedes "Cual es la politica de reembolso?", HyDE primero genera
# un documento hipotetico como "Nuestra politica de reembolso permite a los
# clientes devolver productos dentro de 30 dias..." luego embebe ESE texto
results = vectorstore.similarity_search(
    query="Cual es la politica de reembolso?",
    k=5,
    embedding_function=hyde_embeddings,
)

Cuándo HyDE ayuda: Preguntas sobre temas donde el LLM tiene algún conocimiento general (para que el documento hipotético sea razonable). Preguntas formuladas muy diferente a los documentos reales.

Cuándo HyDE perjudica: Preguntas altamente específicas del dominio donde el LLM genera un documento hipotético inexacto, desviando la recuperación.

Self-RAG: Recuperación Auto-Reflexiva

Self-RAG agrega una capa de pensamiento crítico al pipeline RAG. Después de generar una respuesta, el sistema evalúa si la respuesta realmente está respaldada por los documentos recuperados. Si no, puede re-recuperar, reformular o marcar la respuesta como incierta.

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate

llm = ChatOpenAI(model="gpt-4o", temperature=0)

def self_rag(query: str, retriever, llm) -> dict:
    """RAG con auto-reflexion sobre calidad de respuesta."""

    # Paso 1: Recuperar
    docs = retriever.invoke(query)
    context = "\n\n".join(d.page_content for d in docs)

    # Paso 2: Generar respuesta inicial
    answer_prompt = ChatPromptTemplate.from_messages([
        ("system", "Responde basandote en el contexto. Contexto:\n{context}"),
        ("human", "{query}"),
    ])
    answer = (answer_prompt | llm).invoke({"context": context, "query": query})

    # Paso 3: Auto-reflexion -- esta la respuesta fundamentada?
    reflection_prompt = ChatPromptTemplate.from_messages([
        ("system", """Evalua si la respuesta esta completamente respaldada
por el contexto. Responde con un objeto JSON:
- "is_grounded": true/false
- "confidence": 0.0 a 1.0
- "unsupported_claims": lista de afirmaciones no presentes en el contexto
- "suggestion": que hacer si no esta fundamentada"""),
        ("human", "Contexto:\n{context}\n\nPregunta: {query}\n\nRespuesta: {answer}"),
    ])
    reflection = (reflection_prompt | llm).invoke({
        "context": context,
        "query": query,
        "answer": answer.content,
    })

    return {
        "answer": answer.content,
        "reflection": reflection.content,
        "sources": docs,
    }

Self-RAG es particularmente valioso en aplicaciones de alto riesgo (médico, legal, financiero) donde una respuesta no fundamentada puede tener consecuencias serias.

RAG Correctivo (CRAG)

RAG Correctivo evalúa la calidad de los documentos recuperados antes de generar una respuesta. Si los documentos no son lo suficientemente relevantes, recurre a búsqueda web o admite que no puede responder.

def corrective_rag(query: str, retriever, llm) -> str:
    """Evaluar calidad de recuperacion antes de generar."""

    # Paso 1: Recuperar
    docs = retriever.invoke(query)

    # Paso 2: Calificar cada documento por relevancia
    grading_prompt = ChatPromptTemplate.from_messages([
        ("system", """Califica si este documento es relevante para la consulta.
Responde con solo "relevant" o "not_relevant"."""),
        ("human", "Consulta: {query}\n\nDocumento: {document}"),
    ])

    relevant_docs = []
    for doc in docs:
        grade = (grading_prompt | llm).invoke({
            "query": query,
            "document": doc.page_content,
        })
        if "relevant" in grade.content.lower() and "not_relevant" not in grade.content.lower():
            relevant_docs.append(doc)

    # Paso 3: Decidir basandose en relevancia
    if len(relevant_docs) == 0:
        return "No pude encontrar informacion relevante para responder esta pregunta."
    elif len(relevant_docs) < 2:
        # Opcionalmente: complementar con busqueda web
        context = relevant_docs[0].page_content
    else:
        context = "\n\n".join(d.page_content for d in relevant_docs)

    # Paso 4: Generar con solo documentos relevantes
    answer_prompt = ChatPromptTemplate.from_messages([
        ("system", "Responde basandote en el contexto.\n\nContexto:\n{context}"),
        ("human", "{query}"),
    ])
    answer = (answer_prompt | llm).invoke({"context": context, "query": query})
    return answer.content

RAG Agéntico

RAG agéntico le da al LLM la capacidad de decidir cuándo, qué y cómo recuperar. En lugar de siempre recuperar antes de responder, un agente puede elegir recuperar de diferentes fuentes, realizar múltiples pasos de recuperación, u omitir la recuperación completamente para preguntas que puede responder directamente.

from langchain.agents import AgentExecutor, create_openai_tools_agent
from langchain_core.tools import Tool
from langchain_openai import ChatOpenAI

# Definir recuperacion como una herramienta que el agente puede elegir usar
retrieval_tool = Tool(
    name="knowledge_base_search",
    description="Buscar en la base de conocimiento de la empresa para politicas, "
                "procedimientos y documentacion. Usar cuando la pregunta "
                "es sobre informacion especifica de la empresa.",
    func=lambda query: "\n\n".join(
        d.page_content for d in retriever.invoke(query)
    ),
)

code_search_tool = Tool(
    name="codebase_search",
    description="Buscar en el repositorio de codigo para ejemplos de codigo, "
                "documentacion de funciones y detalles de implementacion tecnica.",
    func=lambda query: "\n\n".join(
        d.page_content for d in code_retriever.invoke(query)
    ),
)

# El agente decide que herramienta usar (o ninguna)
llm = ChatOpenAI(model="gpt-4o", temperature=0)
tools = [retrieval_tool, code_search_tool]

prompt = ChatPromptTemplate.from_messages([
    ("system", "Eres un asistente util con acceso a bases de conocimiento "
               "de la empresa. Usa las herramientas cuando necesites informacion "
               "especifica de la empresa. Para conocimiento general, responde directamente."),
    ("human", "{input}"),
    ("placeholder", "{agent_scratchpad}"),
])

agent = create_openai_tools_agent(llm, tools, prompt)
executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

result = executor.invoke({"input": "Como funciona nuestro middleware de autenticacion?"})

RAG agéntico es el patrón más flexible pero también el más difícil de controlar. El agente puede tomar malas decisiones sobre cuándo recuperar, llevando a información perdida o recuperaciones innecesarias. Úsalo cuando el espacio de consultas es diverso y diferentes preguntas genuinamente necesitan diferentes estrategias de recuperación.

RAPTOR: Procesamiento Abstractivo Recursivo para Recuperación Organizada en Árbol

RAPTOR construye un árbol jerárquico de resúmenes de tus documentos. Los nodos hoja son los fragmentos originales. Los nodos padre son resúmenes de grupos de fragmentos. Los niveles superiores son resúmenes de resúmenes. En tiempo de consulta, el sistema puede recuperar en cualquier nivel del árbol, obteniendo fragmentos detallados o resúmenes amplios dependiendo de lo que la pregunta necesite.

# Implementacion conceptual de RAPTOR
def build_raptor_tree(chunks: list, llm, max_levels: int = 3) -> dict:
    """Construir un arbol jerarquico de resumenes."""
    tree = {"level_0": chunks}  # Nodos hoja son fragmentos originales

    current_level = chunks
    for level in range(1, max_levels + 1):
        # Agrupar fragmentos (e.g., por clustering de sus embeddings)
        groups = cluster_chunks(current_level, n_clusters=len(current_level) // 5)

        # Resumir cada grupo
        summaries = []
        for group in groups:
            combined = "\n\n".join(c.page_content for c in group)
            summary = llm.invoke(
                f"Resume los siguientes documentos de forma concisa:\n\n{combined}"
            )
            summaries.append(summary)

        tree[f"level_{level}"] = summaries
        current_level = summaries

    return tree

RAPTOR es particularmente útil para preguntas que necesitan una comprensión amplia de un tema en lugar de un detalle específico. Por ejemplo, "¿Cuáles son los temas principales de nuestro roadmap de producto?" se beneficia de resúmenes de nivel superior, mientras que "¿Cuál es la fecha límite para la característica X?" necesita detalle a nivel de hoja.

Eligiendo el Patrón Correcto

| Patrón | Mejor Para | Complejidad | Costo Extra | |--------|-----------|-------------|-------------| | Multi-Consulta | Consultas vagas o amplias | Baja | 1 llamada extra al LLM | | HyDE | Consultas cortas, desajuste de terminología | Baja | 1 llamada extra al LLM | | Self-RAG | Aplicaciones de alto riesgo | Media | 1-2 llamadas extra al LLM | | RAG Correctivo | Bases de conocimiento ruidosas | Media | N llamadas de calificación | | RAG Agéntico | Tipos de consulta diversos, múltiples fuentes | Alta | Variable | | RAPTOR | Preguntas de multi-granularidad | Alta | Resumen offline |

Comienza con multi-consulta o HyDE -- son simples de implementar y proporcionan mejoras inmediatas. Agrega self-RAG o RAG correctivo cuando la confiabilidad de las respuestas es crítica. Pasa a RAG agéntico cuando tu sistema necesita manejar tipos fundamentalmente diferentes de preguntas.

En la siguiente lección, aplicaremos RAG específicamente al código -- un dominio con desafíos únicos de chunking, embedding y recuperación.