Saltar al contenido
Lección 8 de 12

Patrones Avanzados de RAG

8 min read

Por que el RAG Basico No es Suficiente

El pipeline estandar 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 terminologia diferente a la de los documentos.
  • La respuesta requiere sintetizar informacion dispersa en multiples documentos.
  • Los fragmentos recuperados son marginalmente relevantes pero no realmente utiles.
  • La pregunta requiere razonamiento o logica de multiples pasos, no solo busqueda.

Los patrones avanzados de RAG abordan estos modos de fallo agregando inteligencia a las etapas de recuperacion y generacion.

RAG Multi-Consulta

El problema: una unica consulta del usuario podria no ser la mejor consulta de busqueda para encontrar toda la informacion relevante. Diferentes formulaciones de la misma pregunta pueden recuperar fragmentos diferentes y complementarios.

RAG multi-consulta usa el LLM para generar multiples reformulaciones de la pregunta original, ejecuta cada una como una consulta de recuperacion 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 mas simples de implementar y frecuentemente proporciona una mejora inmediata en el recall de recuperacion. El tradeoff es latencia incrementada (una llamada extra al LLM) y costo.

HyDE: Hypothetical Document Embeddings

HyDE aborda una asimetria fundamental en RAG: la consulta es corta (una pregunta) mientras que los documentos son largos (parrafos de informacion). Sus embeddings viven en regiones diferentes del espacio vectorial, lo cual puede perjudicar la calidad de recuperacion.

HyDE funciona pidiendo al LLM que genere una respuesta hipotetica a la consulta, luego embebe esa respuesta hipotetica en lugar de la consulta misma. Como la respuesta hipotetica se parece mas a un documento que a una pregunta, coincide con documentos reales mas 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,
)

Cuando HyDE ayuda: Preguntas sobre temas donde el LLM tiene algun conocimiento general (para que el documento hipotetico sea razonable). Preguntas formuladas muy diferente a los documentos reales.

Cuando HyDE perjudica: Preguntas altamente especificas del dominio donde el LLM genera un documento hipotetico inexacto, desviando la recuperacion.

Self-RAG: Recuperacion Auto-Reflexiva

Self-RAG agrega una capa de pensamiento critico al pipeline RAG. Despues de generar una respuesta, el sistema evalua si la respuesta realmente esta 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 (medico, legal, financiero) donde una respuesta no fundamentada puede tener consecuencias serias.

RAG Correctivo (CRAG)

RAG Correctivo evalua la calidad de los documentos recuperados antes de generar una respuesta. Si los documentos no son lo suficientemente relevantes, recurre a busqueda 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 Agentico

RAG agentico le da al LLM la capacidad de decidir cuando, que y como recuperar. En lugar de siempre recuperar antes de responder, un agente puede elegir recuperar de diferentes fuentes, realizar multiples pasos de recuperacion, u omitir la recuperacion 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 agentico es el patron mas flexible pero tambien el mas dificil de controlar. El agente puede tomar malas decisiones sobre cuando recuperar, llevando a informacion perdida o recuperaciones innecesarias. Usalo cuando el espacio de consultas es diverso y diferentes preguntas genuinamente necesitan diferentes estrategias de recuperacion.

RAPTOR: Procesamiento Abstractivo Recursivo para Recuperacion Organizada en Arbol

RAPTOR construye un arbol jerarquico de resumenes de tus documentos. Los nodos hoja son los fragmentos originales. Los nodos padre son resumenes de grupos de fragmentos. Los niveles superiores son resumenes de resumenes. En tiempo de consulta, el sistema puede recuperar en cualquier nivel del arbol, obteniendo fragmentos detallados o resumenes 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 util para preguntas que necesitan una comprension amplia de un tema en lugar de un detalle especifico. Por ejemplo, "Cuales son los temas principales de nuestro roadmap de producto?" se beneficia de resumenes de nivel superior, mientras que "Cual es la fecha limite para la caracteristica X?" necesita detalle a nivel de hoja.

Eligiendo el Patron Correcto

| Patron | Mejor Para | Complejidad | Costo Extra | |--------|-----------|-------------|-------------| | Multi-Consulta | Consultas vagas o amplias | Baja | 1 llamada extra al LLM | | HyDE | Consultas cortas, desajuste de terminologia | 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 calificacion | | RAG Agentico | Tipos de consulta diversos, multiples 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 critica. Pasa a RAG agentico cuando tu sistema necesita manejar tipos fundamentalmente diferentes de preguntas.

En la siguiente leccion, aplicaremos RAG especificamente al codigo -- un dominio con desafios unicos de chunking, embedding y recuperacion.