Inicio / Artículos

Cinco arquitecturas RAG y cuándo es adecuado utilizar cada una en entornos de producción.

Un recorrido práctico por los sistemas RAG ingenuos, híbridos, basados en grafos, avanzados y agentes con LlamaIndex y LangGraph, que aborda los modos de fallo, los costos y cómo combinarlos.

Editor: REACTAPP.TOP19 min de lectura4457 palabras

Fuentes: The 5 RAG Architectures and Exactly When to Use Each One in Production

La palabra “RAG” ahora abarca una familia de arquitecturas bastante diferentes, cada una diseñada para un tipo específico de problema de recuperación de información. Elegir la incorrecta no es solo una cuestión de rendimiento; puede marcar la diferencia entre un sistema que funciona y otro que, a gran escala, proporciona a los usuarios respuestas erróneas pero convincentes.

Este es un mapa de toda la familia. El objetivo es poder analizar cualquier problema de recuperación e indicar qué arquitectura se necesita, así como cómo construirla utilizando LangGraph para la orquestación y LlamaIndex para la capa de datos. Las cinco arquitecturas van desde la más simple hasta la más compleja: ingenua, híbrida, basada en grafos, avanzada y agente. No son rivales; son capas que se añaden progresivamente a medida que el problema exige más funcionalidades, y la mayoría de los sistemas en producción combinan al menos dos de ellas.

El problema común

A continuación, cada configuración de RAG aborda un único desafío: un modelo de lenguaje necesita datos que nunca vio durante su entrenamiento, que se le proporcionen justo cuando responde, y que estén estructurados de manera que pueda razonar sobre ellos.

Los modelos dejan de aprender en una fecha límite. Nunca han leído la wiki interna de una empresa, una hoja de especificaciones ni ningún contenido publicado después de finalizar su entrenamiento. El ajuste fino con dicho contenido es lento y costoso, y el modelo ajustado sigue quedando obsoleto cada vez que cambian los documentos.

RAG evita todo eso. En lugar de enseñar al modelo nuevos datos, el sistema recupera el conocimiento relevante en el momento de la consulta y lo incluye en el prompt como contexto. El modelo nunca tiene que memorizar nada, solo tiene que leerlo cuando sea necesario. Las cinco arquitecturas que se presentan a continuación son cinco soluciones para recuperar información de manera eficaz, y cada una se adapta a un problema diferente.

Arquitectura 1: RAG simple

Naive RAG es el punto de partida, y muchos trabajos reales nunca necesitan nada más: bots de políticas internas, asistentes de Preguntas Frecuentes, búsqueda de documentación, ayudantes para la incorporación de nuevos usuarios. A pesar del nombre, se trata de un patrón maduro que puede ejecutarse a gran escala. Se cargan los documentos, se crea un VectorStoreIndex de LlamaIndex, su motor de consultas se expone como @tool, y ese herramienta se proporciona a un agente de LangGraph.

Sus cinco pasos secuenciales se corresponden directamente con lo que LlamaIndex ofrece de forma predeterminada. Los cuatro primeros tienen lugar al crear el índice, y el quinto se ejecuta para cada pregunta:

  Stages 1–4: Indexing time (run once)
  Stage 5:    Query time (run on every user question)

La distinción más importante en todo RAG es el tiempo de indexación frente al tiempo de consulta. El costo de los embeddings se paga una sola vez, por adelantado. En el momento de la consulta, el sistema solo paga por una búsqueda de similitud y una llamada al LLM.

Por qué funciona

La pregunta del usuario se incrusta en el mismo espacio vectorial que los fragmentos de documentos, y se devuelven aquellos fragmentos cuyos vectores están geográficamente más cerca del vector de la pregunta. La suposición subyacente es que la cercanía en el espacio vectorial refleja la cercanía en el significado. En corpus limpios y basados en hechos, esa suposición se mantiene sorprendentemente bien.

El código

A continuación se presenta un template completo que abarca las importaciones y configuración, el estado, una base de conocimientos persistente, la herramienta, los nodos, el enrutamiento y el montaje del grafo:

# ============================================================
# NAIVE RAG — COMPLETE TEMPLATE
# ============================================================
# ── MODULE 1: IMPORTS & CONFIGURATION ───────────────────────
import os
from typing import Literal
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_core.tools import tool
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.prebuilt import ToolNode
from langgraph.checkpoint.memory import MemorySaver
from llama_index.core import (
    Settings, SimpleDirectoryReader, VectorStoreIndex,
    StorageContext, load_index_from_storage
)
from llama_index.llms.openai import OpenAI as LlamaOpenAI
from llama_index.embeddings.openai import OpenAIEmbedding
# LangGraph's reasoning model
llm = ChatOpenAI(model="gpt-4o", temperature=0)
# LlamaIndex's internal model - separate from the above
# These two model configs do NOT share state or conversation history
Settings.llm = LlamaOpenAI(model="gpt-4o-mini", temperature=0.1)
Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small")
Settings.chunk_size = 512
Settings.chunk_overlap = 50
# ── MODULE 2: STATE ──────────────────────────────────────────
class State(MessagesState):
    pass  # messages list inherited; extend if you need more
# ── MODULE 3: KNOWLEDGE BASE (Build once, persist, reload) ───
PERSIST_DIR = "./storage/naive"
if os.path.exists(PERSIST_DIR):
    storage_context = StorageContext.from_defaults(persist_dir=PERSIST_DIR)
    index = load_index_from_storage(storage_context)
else:
    documents = SimpleDirectoryReader("./data").load_data()
    index = VectorStoreIndex.from_documents(documents, show_progress=True)
    index.storage_context.persist(persist_dir=PERSIST_DIR)
query_engine = index.as_query_engine(similarity_top_k=3)
# ── MODULE 3 (cont.): TOOL ───────────────────────────────────
@tool
def search_knowledge_base(query: str) -> str:
    """Search internal company documents for policies, product specs,
    and procedures. Use this for any question requiring domain-specific
    knowledge rather than general world knowledge."""
    response = query_engine.query(query)
    return str(response)
tools = [search_knowledge_base]
llm_with_tools = llm.bind_tools(tools)
tool_node = ToolNode(tools)
# ── MODULE 4: NODES ──────────────────────────────────────────
def agent_node(state: State) -> dict:
    system_prompt = SystemMessage(content=(
        "You are a helpful assistant with access to an internal knowledge base. "
        "Use search_knowledge_base for company-specific questions. "
        "Answer general questions directly."
    ))
    response = llm_with_tools.invoke([system_prompt] + state["messages"])
    return {"messages": [response]}
# ── MODULE 5: ROUTING ────────────────────────────────────────
def should_continue(state: State) -> Literal["tools", "__end__"]:
    last = state["messages"][-1]
    if hasattr(last, "tool_calls") and last.tool_calls:
        return "tools"
    return "__end__"
# ── MODULE 6: GRAPH ASSEMBLY ─────────────────────────────────
builder = StateGraph(State)
builder.add_node("agent", agent_node)
builder.add_node("tools", tool_node)
builder.add_edge(START, "agent")
builder.add_conditional_edges("agent", should_continue,
    {"tools": "tools", "__end__": END})
builder.add_edge("tools", "agent")
graph = builder.compile(checkpointer=MemorySaver())

Dónde falla

El RAG ingenuo se basa en una suposición, y esa suposición falla en dos situaciones comunes.

La primera es la discrepancia en el vocabulario. Un usuario pregunta por el SLA aplicable a los clientes de nivel 1, pero el documento promete un tiempo de actividad del 99.9% a los clientes de nivel Gold. SLA, nivel 1 y nivel Gold se refieren a conceptos relacionados pero no coincidentes, por lo que la puntuación de similitud podría clasificar ese fragmento como de baja calidad y hacer que la respuesta se escape.

La segunda son las preguntas relacionales. Preguntar qué productos se verán afectados si un proveedor en particular queda fuera de servicio requiere seguir una cadena de relaciones a través de varios documentos. Ningún fragmento aislado contiene la respuesta, y el RAG ingenuo devuelve fragmentos de diferentes documentos sin posibilidad de conectarlos.

Esos dos modos de fallo son los que abordan las dos arquitecturas siguientes.

Arquitectura 2: RAG híbrido

El RAG híbrido refleja lo que los profesionales han aprendido en la práctica: la similitud semántica y la coincidencia exacta de palabras clave son señales complementarias de relevancia, y ninguna por sí sola es suficiente.

La recuperación densa, que es la búsqueda vectorial, maneja bien las paráfrasis: si se pregunta sobre un SLA, aún puede encontrar el pasaje sobre el tiempo de actividad de nivel Gold. Falla con nombres propios, códigos de producto, términos clínicos, citas legales y otras expresiones especializadas en las que la cadena exacta contiene el significado.

La recuperación dispersa (BM25 o búsqueda por palabras clave) es su opuesto. Es excelente para la coincidencia exacta de términos y siempre encontrará “SKU-4829” al buscar “SKU-4829”, pero no tiene concepto de equivalencia semántica, por lo que no encontrará “garantía de tiempo de actividad” en lugar de “SLA”.

El RAG híbrido realiza ambas búsquedas en paralelo y luego utiliza un reclasificador para convertir los resultados fusionados en una única lista ordenada. El reclasificador es el componente clave. Los modelos de embedding comparan una consulta y un fragmento de forma independiente, mientras que un reclasificador de codificador cruzado lee la consulta y cada candidato juntos y genera directamente una puntuación que refleja su relación. Es más lento que la búsqueda por similitud, pero solo procesa el conjunto de candidatos ya reducido, por lo que la latencia se mantiene manejable.

El código

La versión de LlamaIndex combina un recuperador denso, un recuperador BM25 y una fusión de rangos recíprocos:

# ============================================================
# HYBRID RAG — LLAMAINDEX IMPLEMENTATION
# pip install llama-index-retrievers-bm25 rank-bm25
# ============================================================

from llama_index.core import SimpleDirectoryReader, VectorStoreIndex
from llama_index.core.retrievers import VectorIndexRetriever
from llama_index.retrievers.bm25 import BM25Retriever
from llama_index.core.retrievers import QueryFusionRetriever
from llama_index.core.query_engine import RetrieverQueryEngine
from langchain_core.tools import tool
# Build document store and index as before
documents = SimpleDirectoryReader("./data").load_data()
index = VectorStoreIndex.from_documents(documents)
# ── DENSE RETRIEVER (vector similarity) ──────────────────────
vector_retriever = VectorIndexRetriever(
    index=index,
    similarity_top_k=5,  # Fetch more candidates pre-rerank
)
# ── SPARSE RETRIEVER (BM25 keyword match) ────────────────────
# BM25Retriever builds directly from the index's nodes
bm25_retriever = BM25Retriever.from_defaults(
    index=index,
    similarity_top_k=5,
)
# ── FUSION: Merge both retrievers with Reciprocal Rank Fusion ─
# QueryFusionRetriever is LlamaIndex's built-in hybrid combiner
# mode="reciprocal_rerank" implements the RRF algorithm:
# it combines ranked lists without needing score calibration
hybrid_retriever = QueryFusionRetriever(
    retrievers=[vector_retriever, bm25_retriever],
    similarity_top_k=3,        # Final top-k after fusion
    num_queries=1,             # No query expansion in this mode
    mode="reciprocal_rerank",  # The fusion algorithm
    use_async=True,            # Run both retrievers in parallel
)
# Build a QueryEngine on top of the hybrid retriever
hybrid_query_engine = RetrieverQueryEngine.from_args(
    retriever=hybrid_retriever,
)
# ── LANGGRAPH TOOL ────────────────────────────────────────────
@tool
def search_hybrid(query: str) -> str:
    """Search internal knowledge base using both semantic similarity
    and keyword matching. More precise than pure vector search,
    especially for technical terms, product codes, and exact names."""
    response = hybrid_query_engine.query(query)
    return str(response)
# The LangGraph graph structure is identical to Naive RAG.
# You are only swapping the tool. Everything from Module 4 onward
# stays exactly the same.

Tamaño del bloque en la recuperación híbrida

Con la búsqueda híbrida, por lo general se indexarán bloques más pequeños que en la búsqueda vectorial tradicional. BM25 recibe una señal más clara de un término repetido en un bloque corto que en uno largo. Un tamaño inicial razonable es de unos 256 tokens con 30 tokens de superposición una vez que BM25 se integra en el proceso.

Cuándo es adecuada la opción híbrida

Úsela siempre que los documentos combinen prosa libre con terminología estructurada, lo cual abarca casi todos los casos de uso empresariales importantes. Ejemplos incluyen la revisión legal, donde las formas de citación deben coincidir exactamente, los registros médicos repletos de nombres de medicamentos precisos, códigos de dosis y códigos ICD, el análisis financiero con símbolos bursátiles e identificadores de cláusulas, y la documentación técnica con códigos de error, nombres de métodos API y números de versión.

Arquitectura 3: RAG gráfico

Graph RAG redefine la unidad de conocimiento. En los enfoques RAG ingenuos e híbridos, un documento es simplemente texto y la recuperación selecciona los pasajes que se parecen más a la consulta. Aquí, los documentos se convierten en entidades conectadas por relaciones, y la recuperación recorre los caminos de esa red.

Considere la pregunta de qué clientes empresariales se verían afectados si el módulo de autenticación heredado fuera obsoleto. Un sistema ingenuo buscaría fragmentos que mencionen conjuntamente “clientes empresariales” y “módulo de autenticación” y podría encontrar algunos. Lo que realmente se necesita es una cadena: desde el módulo hasta los productos que dependen de él, y desde esos productos hasta los clientes que los utilizan. Ningún fragmento aislado contiene esa respuesta, la cual surge de la estructura del grafo de conocimiento. Graph RAG está diseñado precisamente para esta situación, ya sea que la tarea implique saltar entre varios enlaces, rastrear una relación o responder a algo que requiere datos de partes muy alejadas del corpus.

Cómo se construye el índice

En lugar de dividir los documentos en fragmentos e incrustarlos, Graph RAG comienza con un proceso de extracción de entidades en cada documento. Identifica entidades nombradas (productos, personas, organizaciones, conceptos) y las relaciones entre ellas, como “depende de”, “es cliente de”, “fue escrito por” o “fue reemplazado por”. Estas se convierten en los nodos y aristas de un grafo de conocimiento. Los algoritmos de clustering de grafos agrupan luego las entidades estrechamente relacionadas en comunidades, y un LLM escribe un resumen para cada comunidad. En el momento de la consulta, el sistema busca en los resúmenes de las comunidades y recorre el grafo para llegar a las entidades relevantes.

El código

El índice de grafo de propiedades de LlamaIndex se encarga del pipeline de extracción:

# ============================================================
# GRAPH RAG — LLAMAINDEX PROPERTY GRAPH INDEX
# pip install llama-index-core
# ============================================================
from llama_index.core import SimpleDirectoryReader, PropertyGraphIndex
from llama_index.core.indices.property_graph import (
    ImplicitPathExtractor,
    SimpleLLMPathExtractor,
)
from langchain_core.tools import tool
# Load documents
documents = SimpleDirectoryReader("./data").load_data()
# ── BUILD THE PROPERTY GRAPH INDEX ───────────────────────────
# LlamaIndex's PropertyGraphIndex handles the full extraction pipeline.
# SimpleLLMPathExtractor uses an LLM to extract (subject, relation, object)
# triples from each chunk - these become graph edges.
# ImplicitPathExtractor uses fast heuristics (cheaper, less precise).
index = PropertyGraphIndex.from_documents(
    documents,
    kg_extractors=[
        # LLM-based extraction: higher quality, higher cost
        SimpleLLMPathExtractor(
            llm=Settings.llm,
            max_paths_per_chunk=10,
        ),
        # Heuristic extraction: fast fallback
        ImplicitPathExtractor(),
    ],
    show_progress=True,
)
# ── CREATE A GRAPH-AWARE RETRIEVER ────────────────────────────
# This retriever traverses the graph rather than searching flat vectors.
kg_retriever = index.as_retriever(
    include_text=True,       # Include surrounding text with each entity
    retriever_mode="hybrid", # Combines keyword + embedding on the graph
    similarity_top_k=3,
)
from llama_index.core.query_engine import RetrieverQueryEngine
graph_query_engine = RetrieverQueryEngine.from_args(retriever=kg_retriever)
# ── LANGGRAPH TOOL ────────────────────────────────────────────
@tool
def search_knowledge_graph(query: str) -> str:
    """Search the knowledge graph for questions involving relationships
    between entities - dependencies, organizational hierarchies, impact
    analysis, and multi-hop reasoning across connected information.
    Use this when the answer requires tracing a chain of relationships
    rather than finding a single relevant document."""
    response = graph_query_engine.query(query)
    return str(response)
# ── COMBINING WITH NAIVE RAG: DUAL-TOOL AGENT ─────────────────
# In practice, Graph RAG and Naive RAG are often combined.
# The agent's LLM decides which tool fits the query.
@tool
def search_knowledge_base(query: str) -> str:
    """Search for factual information in company documents.
    Best for direct questions with answers in a single document."""
    response = query_engine.query(query)  # The naive query engine
    return str(response)
# Give the LangGraph agent BOTH tools.
# It will route to the right one based on the question type.
tools = [search_knowledge_base, search_knowledge_graph]
llm_with_tools = llm.bind_tools(tools)
tool_node = ToolNode(tools)
# Graph assembly remains identical - only the tool list changes.

Advertencia sobre los costos

La extracción de entidades realiza una llamada al LLM para cada fragmento, por lo que un conjunto grande de documentos implica miles de llamadas en el momento de la indexación. Esto es costoso y lento por diseño: el precio se paga una sola vez para obtener una estructura de datos mucho más rica. Un índice RAG basado en grafos nunca debe reconstruirse en cada inicio de sesión. Se debe persistir el grafo y volver a cargarlo, tal como ocurre con la versión tradicional.

Cuándo elegirlo

Elija el RAG basado en grafos cuando las preguntas requieran seguir cadenas de relaciones. Los casos típicos son el cumplimiento normativo y el análisis de riesgos (qué procesos afecta una normativa determinada), la inteligencia en la cadena de suministro (qué productos dependen de un proveedor), el conocimiento organizacional (quién es dueño de qué y cómo se conectan esas cadenas de propiedad) y el mapeo de dependencias de software (qué falla si se elimina un módulo).

Arquitectura 4: RAG avanzado

El RAG avanzado no es una técnica nueva única. Se trata de un conjunto estructurado de mejoras aplicadas sobre cualquier sistema de recuperación de información que ya se tenga. El RAG simple confía en la primera pasada de recuperación, mientras que el RAG avanzado la cuestiona, la refina y la valida. Estas mejoras se dividen en tres grupos que se aplican en diferentes etapas del proceso: antes de la recuperación, durante el manejo de la consulta y después de la recuperación.

Antes de la recuperación: reescritura de consultas y HyDE

Lo que escribe un usuario rara vez es la mejor consulta de búsqueda. Una pregunta casual sobre la política de devoluciones para clientes empresariales es perfectamente natural para un ser humano, pero genera resultados peores que una frase como “procedimientos de política de devoluciones y reembolsos para clientes empresariales”. La reescritura de consultas consiste en que el LLM transforme la pregunta en una consulta de búsqueda más adecuada antes de que llegue al índice.

HyDE, abreviatura de Hypothetical Document Embedding, sigue otro enfoque. En lugar de buscar con la pregunta, pide al LLM que escriba un documento hipotético que la responda y luego incrusta ese documento para la búsqueda. La idea es que el texto en forma de respuesta se encuentra más cerca en el espacio vectorial de otro texto en forma de respuesta que el texto en forma de pregunta.

Descomposición de la consulta

Las preguntas de múltiples pasos superan al RAG ingenuo porque requieren varias búsquedas. El RAG avanzado las descompone primero. Pedir que se compare la política de reembolso para clientes empresariales y minoristas y se resuman las principales diferencias en realidad son tres tareas: recuperar la política empresarial, recuperar la política minorista y compararlas. La descomposición las divide en subconsultas, realiza búsquedas individuales y fusiona los resultados antes de la síntesis.

Después de la recuperación: reclasificación y CRAG

El reclasificación, ya tratado en el contexto de la búsqueda híbrida, vuelve a ser relevante en esta etapa: un codificador cruzado recalcula la puntuación de los fragmentos obtenidos, incluso si la recuperación se realizó de forma puramente densa.

CRAG (Corrective RAG) es la técnica posterior a la recuperación más elaborada. Después de obtener los fragmentos, realiza una rápida verificación de relevancia. Si estos parecen insuficientes, recurre a una fuente diferente, como una búsqueda en la web o un índice más amplio, para que el LLM no tenga que responder a partir de un contexto débil.

El código

Una implementación combina varias de estas técnicas:

# ============================================================
# ADVANCED RAG — MULTI-TECHNIQUE IMPLEMENTATION
# ============================================================

from llama_index.core import SimpleDirectoryReader, VectorStoreIndex
from llama_index.core.query_engine import (
    SubQuestionQueryEngine,    # Handles query decomposition
    RetrieverQueryEngine,
)
from llama_index.core.tools import QueryEngineTool, ToolMetadata
from llama_index.core.postprocessor import SentenceTransformerRerank
from langchain_core.tools import tool
documents = SimpleDirectoryReader("./data").load_data()
index = VectorStoreIndex.from_documents(documents)
# ── RERANKER: Post-retrieval cross-encoder scoring ────────────
# Fetch 8 candidates, rerank down to 3
# This is the most impactful single improvement in Advanced RAG
reranker = SentenceTransformerRerank(
    model="cross-encoder/ms-marco-MiniLM-L-2-v2",
    top_n=3,
)
# ── RETRIEVER WITH RERANKING ──────────────────────────────────
base_retriever = index.as_retriever(similarity_top_k=8)
reranked_engine = RetrieverQueryEngine.from_args(
    retriever=base_retriever,
    node_postprocessors=[reranker],  # Applied after retrieval
)
# ── QUERY DECOMPOSITION with SubQuestionQueryEngine ───────────
# Wrap the base engine as a "tool" that the decomposer can call
engine_tools = [
    QueryEngineTool(
        query_engine=reranked_engine,
        metadata=ToolMetadata(
            name="company_knowledge_base",
            description=(
                "Searches company documents for policies, procedures, "
                "and product information."
            ),
        ),
    )
]
# SubQuestionQueryEngine decomposes complex questions into
# sub-questions, runs each against the available tools,
# and synthesizes a final answer from all sub-answers
decomposed_engine = SubQuestionQueryEngine.from_defaults(
    query_engine_tools=engine_tools,
    use_async=True,  # Sub-queries run in parallel when possible
)
# ── HYDE: Hypothetical Document Embedding ─────────────────────
from llama_index.core.indices.query.query_transform.base import (
    HyDEQueryTransform,
)
from llama_index.core.query_engine import TransformQueryEngine
hyde_transform = HyDEQueryTransform(include_original=True)
hyde_engine = TransformQueryEngine(
    query_engine=reranked_engine,
    query_transform=hyde_transform,
)
# ── LANGGRAPH TOOLS: Different engines for different patterns ──
@tool
def search_with_decomposition(query: str) -> str:
    """Search for answers to complex questions that may require
    combining information from multiple documents. Automatically
    breaks the question into sub-questions and merges the results.
    Best for comparison questions, multi-part questions, and anything
    requiring synthesis across different topics."""
    response = decomposed_engine.query(query)
    return str(response)
@tool
def search_with_hyde(query: str) -> str:
    """Search the knowledge base using hypothetical document embedding.
    More effective than standard search for abstract or exploratory
    questions where the exact terminology in the answer differs from
    the terminology in the question."""
    response = hyde_engine.query(query)
    return str(response)
# The LangGraph agent now has specialized retrieval tools
# and its LLM decides which retrieval strategy each question needs.
tools = [search_with_decomposition, search_with_hyde]

La regla a seguir

Resista la tentación de combinarlo todo. Elija el único cambio que tenga más probabilidades de solucionar el problema que realmente observa. La reclasificación suele ser el mejor primer paso, seguida de la descomposición; HyDE es una tercera opción adecuada para colecciones abstractas o conceptuales. Avance gradualmente y verifique la retención de información después de cada paso.

Arquitectura 5: RAG agente

El RAG agente va más allá de una recuperación mejorada: transfiere las decisiones de recuperación a otro elemento. Las cuatro arquitecturas anteriores son pipelines, secuencias fijas que se comportan de manera idéntica en cada ejecución. Aquí, la recuperación se convierte en un bucle en el que un agente basado en LLM decide qué buscar, evalúa los resultados, decide si buscar de nuevo y se detiene cuando cuenta con suficiente información, o determina que la pregunta no tiene respuesta.

Eso es exactamente para lo que fue diseñado LangGraph. El nodo de agente, el nodo de herramienta y el borde condicional ya forman un esqueleto de RAG agente. Lo que cambia es la riqueza del conjunto de herramientas y la sofisticación de la lógica de enrutamiento desarrollada alrededor de ellas.

Varias herramientas de recuperación al mismo tiempo

La verdadera ventaja del RAG agente en LangGraph es que se puede proporcionar al agente todas las estrategias de recuperación descritas anteriormente al mismo tiempo, y su LLM elige la herramienta adecuada para cada subpregunta. Este esqueleto hace que las cinco arquitecturas estén disponibles para un único agente:

# ============================================================
# AGENTIC RAG — COMPLETE MULTI-TOOL TEMPLATE
# This is the full production scaffold: all five architectures
# available to a single agent, which selects dynamically.
# ============================================================

# ── MODULE 1: IMPORTS & CONFIGURATION ───────────────────────
import os
from typing import Literal, Annotated
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage, BaseMessage
from langchain_core.tools import tool
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.prebuilt import ToolNode
from langgraph.checkpoint.memory import MemorySaver
from llama_index.core import (
    Settings, SimpleDirectoryReader, VectorStoreIndex,
    StorageContext, load_index_from_storage, PropertyGraphIndex
)
from llama_index.core.indices.property_graph import SimpleLLMPathExtractor
from llama_index.core.postprocessor import SentenceTransformerRerank
from llama_index.core.query_engine import RetrieverQueryEngine, SubQuestionQueryEngine
from llama_index.core.tools import QueryEngineTool, ToolMetadata
from llama_index.llms.openai import OpenAI as LlamaOpenAI
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.retrievers.bm25 import BM25Retriever
from llama_index.core.retrievers import QueryFusionRetriever, VectorIndexRetriever
# The agent's main reasoning model (LangGraph)
llm = ChatOpenAI(model="gpt-4o", temperature=0)
# LlamaIndex internal configuration
Settings.llm = LlamaOpenAI(model="gpt-4o-mini", temperature=0.1)
Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small")
Settings.chunk_size = 512
Settings.chunk_overlap = 50
# ── MODULE 2: STATE ──────────────────────────────────────────
class AgentState(MessagesState):
    # Extend with retrieval metadata if you need observability
    retrieval_count: int = 0  # Track how many retrieval calls were made
# ── MODULE 3: KNOWLEDGE BASES (all built at startup) ──────────
VECTOR_DIR = "./storage/vector"
GRAPH_DIR  = "./storage/graph"
documents  = SimpleDirectoryReader("./data").load_data()
# Vector index (for Naive, Hybrid, Advanced)
if os.path.exists(VECTOR_DIR):
    ctx = StorageContext.from_defaults(persist_dir=VECTOR_DIR)
    vector_index = load_index_from_storage(ctx)
else:
    vector_index = VectorStoreIndex.from_documents(documents, show_progress=True)
    vector_index.storage_context.persist(persist_dir=VECTOR_DIR)
# Property Graph index (for Graph RAG)
if os.path.exists(GRAPH_DIR):
    ctx = StorageContext.from_defaults(persist_dir=GRAPH_DIR)
    graph_index = load_index_from_storage(ctx)
else:
    graph_index = PropertyGraphIndex.from_documents(
        documents,
        kg_extractors=[SimpleLLMPathExtractor(llm=Settings.llm)],
        show_progress=True,
    )
    graph_index.storage_context.persist(persist_dir=GRAPH_DIR)
# ── Set up individual engines ─────────────────────────────────
reranker = SentenceTransformerRerank(
    model="cross-encoder/ms-marco-MiniLM-L-2-v2", top_n=3
)
# Tool 1: Naive / vector search
vector_engine = vector_index.as_query_engine(
    similarity_top_k=3,
    node_postprocessors=[reranker],
)
# Tool 2: Hybrid (vector + BM25)
hybrid_retriever = QueryFusionRetriever(
    retrievers=[
        VectorIndexRetriever(index=vector_index, similarity_top_k=5),
        BM25Retriever.from_defaults(index=vector_index, similarity_top_k=5),
    ],
    similarity_top_k=3,
    mode="reciprocal_rerank",
    use_async=True,
)
hybrid_engine = RetrieverQueryEngine.from_args(
    retriever=hybrid_retriever,
    node_postprocessors=[reranker],
)
# Tool 3: Graph traversal
graph_engine = graph_index.as_query_engine(
    include_text=True,
    retriever_mode="hybrid",
    similarity_top_k=3,
)
# Tool 4: Decomposed (for complex multi-part questions)
decomposed_engine = SubQuestionQueryEngine.from_defaults(
    query_engine_tools=[
        QueryEngineTool(
            query_engine=vector_engine,
            metadata=ToolMetadata(
                name="docs",
                description="Company documents and policies."
            )
        )
    ],
    use_async=True,
)
# ── MODULE 3 (cont.): ALL TOOLS ───────────────────────────────
@tool
def search_documents(query: str) -> str:
    """Search company documents by semantic meaning. Best for
    conceptual questions where the exact wording in the answer
    may differ from the question."""
    return str(vector_engine.query(query))
@tool
def search_exact_terms(query: str) -> str:
    """Search using both keyword and semantic matching. Best when
    the query contains specific terminology, product codes, names,
    or exact phrases that must appear in the result."""
    return str(hybrid_engine.query(query))
@tool
def search_relationships(query: str) -> str:
    """Search the knowledge graph for questions about how things
    connect: dependencies, impact chains, organizational links,
    and multi-hop reasoning. Use when the answer requires tracing
    a relationship across multiple entities."""
    return str(graph_engine.query(query))
@tool
def search_complex_question(query: str) -> str:
    """For multi-part questions requiring synthesis across several
    topics. Automatically decomposes the question into sub-queries,
    retrieves each independently, and combines the results."""
    return str(decomposed_engine.query(query))
tools = [
    search_documents,
    search_exact_terms,
    search_relationships,
    search_complex_question,
]
llm_with_tools = llm.bind_tools(tools)
tool_node = ToolNode(tools)
# ── MODULE 4: NODES ──────────────────────────────────────────
def agent_node(state: AgentState) -> dict:
    system_prompt = SystemMessage(content=(
        "You are a precise research assistant with access to four retrieval tools:\n\n"
        "1. search_documents - semantic search over company documents\n"
        "2. search_exact_terms - hybrid semantic + keyword search\n"
        "3. search_relationships - graph traversal for relationship questions\n"
        "4. search_complex_question - decomposed retrieval for multi-part questions\n\n"
        "Think step by step. Use the tool that best fits the question type. "
        "You may call multiple tools if a question has multiple parts. "
        "Only answer when you have retrieved sufficient evidence."
    ))
    response = llm_with_tools.invoke([system_prompt] + state["messages"])
    return {
        "messages": [response],
        "retrieval_count": state.get("retrieval_count", 0),
    }
# ── MODULE 5: ROUTING ────────────────────────────────────────
def should_continue(state: AgentState) -> Literal["tools", "__end__"]:
    last = state["messages"][-1]
    if hasattr(last, "tool_calls") and last.tool_calls:
        return "tools"
    return "__end__"
# ── MODULE 6: GRAPH ASSEMBLY ─────────────────────────────────
builder = StateGraph(AgentState)
builder.add_node("agent", agent_node)
builder.add_node("tools", tool_node)
builder.add_edge(START, "agent")
builder.add_conditional_edges("agent", should_continue,
    {"tools": "tools", "__end__": END})
builder.add_edge("tools", "agent")
graph = builder.compile(checkpointer=MemorySaver())
# ── MODULE 7: ENTRYPOINT ──────────────────────────────────────
if __name__ == "__main__":
    config = {"configurable": {"thread_id": "agentic-session-001"}}
    print("Agentic RAG ready. Ask anything.\n")
    while True:
        user_text = input("You: ").strip()
        if not user_text or user_text.lower() in ("exit", "quit"):
            break
        response = graph.invoke(
            {"messages": [HumanMessage(content=user_text)]},
            config=config,
        )
        print(f"\nAgent: {response['messages'][-1].content}\n")

Qué no pueden hacer los pipelines

Lo que lo diferencia del resto es la autocorrección. Un pipeline no tiene forma de darse cuenta de que ha obtenido el material incorrecto; un agente sí. Cuando los resultados iniciales son escasos, su siguiente paso de razonamiento reescribe la consulta con nuevos términos. Cuando una pregunta oculta una dependencia, el agente la detecta a mitad de proceso y obtiene más información. Cuando la pregunta es vaga, puede preguntar al usuario antes de buscar.

Utilice esta arquitectura cuando una respuesta incorrecta tiene consecuencias graves, como en entornos de cumplimiento, legales, financieros o médicos, ya que los pasos de verificación, los umbrales de confianza y los puntos de control con intervención humana pueden integrarse directamente en el grafo del agente.

El precio: la latencia

No se puede ocultar la desaceleración. Las pipelines con una sola herramienta responden en aproximadamente 200 a 500 milisegundos; un agente que realiza tres llamadas de recuperación primero puede tardar de 8 a 12 segundos, lo cual es demasiado lento para el flujo principal de muchas interfaces en tiempo real. Dos estrategias ayudan a mitigarlo: transmitir los pasos intermedios de forma continua para que los usuarios vean el progreso en lugar de esperar en silencio, y ejecutar la recuperación por agente en segundo plano para obtener de antemano las consultas posteriores probables.

Elegir y combinar

Cada problema de recuperación tiene la solución adecuada entre estas cinco opciones. Como guía aproximada para tomar decisiones: comience con RAG simple para preguntas y respuestas sobre documentos claros y factuales; pase a un enfoque híbrido cuando la terminología sea especializada o los identificadores exactos sean importantes; opte por RAG basado en grafos cuando las respuestas dependan de las relaciones entre documentos; incorpore técnicas avanzadas cuando sea necesario mejorar la calidad de la recuperación; y adopte un agente cuando la tarea sea abierta, de alto riesgo o requiera autocorrección.

Las arquitecturas también son componibles, por lo que no hay necesidad de elegir una y descartar el resto. El patrón de producción más común es una pila. En LangGraph, esa pila se traduce directamente en la lista de herramientas. Un agente con una herramienta simple, una herramienta híbrida, una herramienta de gráfico y una herramienta de consulta descompuesta representa exactamente la pila de cinco arquitecturas, donde el agente, como capa superior, selecciona y orquesta a las demás en cada turno.

Carta de referencia de palabras clave

Esta carta enumera las principales clases y configuraciones para cada capa:

NAIVE RAG
SimpleDirectoryReader           Load files into Document objects
VectorStoreIndex.from_documents Build the embed-and-store index
index.as_query_engine()         Full retrieve-and-answer pipeline
index.as_retriever()            Retrieve only (no answer generation)
Settings.chunk_size             Token size per Node
Settings.chunk_overlap          Token overlap between adjacent Nodes

HYBRID RAG
BM25Retriever                   Keyword-based sparse retriever
VectorIndexRetriever            Dense embedding retriever
QueryFusionRetriever            Merges multiple retrievers (RRF algorithm)
SentenceTransformerRerank       Cross-encoder reranker for post-retrieval

GRAPH RAG
PropertyGraphIndex              Builds a knowledge graph from documents
SimpleLLMPathExtractor          LLM-based entity and relation extraction
ImplicitPathExtractor           Heuristic-based entity extraction (fast)

ADVANCED RAG
SubQuestionQueryEngine          Decomposes complex queries into sub-queries
HyDEQueryTransform              Hypothetical Document Embedding transform
TransformQueryEngine            Wraps any engine with a query transform
node_postprocessors             Where rerankers and filters attach

AGENTIC RAG (LANGGRAPH LAYER)
@tool                           The bridge - every LlamaIndex engine becomes
                                a LangGraph tool through this decorator
ToolNode                        Executes whatever tool the agent selects
bind_tools()                    Gives the agent LLM its tool registry
MemorySaver / SqliteSaver       Thread-level memory across turns (Part 2)
interrupt()                     Human approval checkpoint before retrieval (Part 3)

La recuperación como infraestructura

Estos cinco enfoques no son cinco maneras de hacer lo mismo. Son soluciones a cinco problemas de recuperación diferentes, en una progresión clara desde lo simple hasta lo sofisticado.

  • Naive RAG: rápido, económico y adecuado para la mayoría de las preguntas y respuestas con documentos sencillos.
  • Hybrid RAG: la opción por defecto sensata cuando el vocabulario es especializado.
  • Graph RAG: la elección adecuada cuando los vínculos entre hechos son más importantes que cualquier documento individual.
  • Advanced RAG es el modelo para mejorar la precisión cuando el punto débil es la calidad de la recuperación.
  • Agentic RAG: para tareas abiertas y de alto riesgo en las que el sistema debe razonar por sí mismo.

Al utilizar LlamaIndex en la capa de datos y LangGraph para la orquestación, estos modelos cubren casi todo lo que necesitará una aplicación de recuperación en producción.

Leer más