Python & AI Tutorials Logo
LangChain & LangGraph

16. Componentes prediseñados y enrutamiento multirrama

En el Capítulo 15 ensamblamos un grafo de agente a mano: un nodo de modelo, un nodo de herramienta y una arista condicional que decide si seguir iterando o detenerse. Esta es esencialmente la estructura estándar para agentes que llaman a herramientas, por lo que LangChain y LangGraph la incluyen como componentes prediseñados(prebuilt components) que puedes usar en lugar de escribir el mismo andamiaje desde cero cada vez.

En la primera mitad de este capítulo, reconstruiremos el agente del Capítulo 15 usando componentes prediseñados. Cambiaremos el nodo de ejecución de herramientas y la función de enrutamiento por ToolNode y tools_condition, y finalmente reemplazaremos todo el ensamblaje del grafo por una sola llamada a create_agent. Verás que el comportamiento se mantiene idéntico al del Capítulo 15, mientras que el código se reduce considerablemente.

En la segunda mitad, combinaremos componentes prediseñados con el enfoque de ensamblaje manual de grafos del Capítulo 15 para construir un agente más complejo. El agente que construiremos enruta cada solicitud a un manejador diferente: las consultas complejas van a un modelo de alto rendimiento, mientras que las preguntas simples las responde un modelo más pequeño y económico. Esta es una estructura multirrama en la que la ruta de procesamiento se bifurca según el tipo de solicitud.

16.1) Componentes prediseñados y create_agent

En esta sección reemplazaremos la función tool_node y la función should_continue del grafo del Capítulo 15 por los componentes prediseñados ToolNode y tools_condition. Después de eso, omitiremos por completo el ensamblaje manual y crearemos todo el grafo con una sola llamada a create_agent. Lo que debes observar en cada paso es que el código se acorta mientras que el comportamiento del agente se mantiene idéntico al del Capítulo 15.

16.1.1) ToolNode y tools_condition

ToolNode es un nodo prediseñado que se encarga de la ejecución de herramientas por ti. Cuando el último mensaje del State (el AIMessage devuelto por el LLM) contiene tool_calls, ejecuta las herramientas solicitadas y añade los resultados a messages como objetos ToolMessage. Hace el mismo trabajo que la función tool_node que escribimos en el Capítulo 15. Además, cuando el LLM solicita varias herramientas a la vez, las ejecuta en paralelo.

También admite el manejo de excepciones durante la ejecución de herramientas. Si estableces ToolNode(tools, handle_tool_errors=True), el grafo no fallará incluso cuando una herramienta lance una excepción. La excepción se convierte en un ToolMessage que lleva los detalles del error, que se pasa al LLM para que pueda ver el fallo y reintentar con argumentos corregidos.

Creas un ToolNode pasándole una lista de herramientas. Usaremos las mismas dos herramientas del Capítulo 15.

python
from langgraph.prebuilt import ToolNode
from langchain.tools import tool
 
@tool
def get_weather(city: str) -> str:
    """Obtiene el clima actual de una ciudad."""
    fake_data = {"Tokyo": "18°C, nublado", "Cairo": "31°C, soleado"}
    return fake_data.get(city, f"No hay datos meteorológicos para {city}.")
 
@tool
def calculate(expression: str) -> str:
    """Evalúa una expresión aritmética simple. Ejemplo: '3 * 21'."""
    return str(eval(expression))  # Advertencia: eval() es un riesgo de seguridad. No lo uses en producción.
 
tools = [get_weather, calculate]
 
# El tool_map + la función tool_node del Capítulo 15 se reemplazan por esta única línea
tool_node = ToolNode(tools)

ToolNode construye internamente un mapeo de nombre a herramienta a partir de la lista de herramientas, igual que el tool_map del Capítulo 15. En tiempo de ejecución busca cada herramienta por el nombre que solicitó el LLM y la llama. En otras palabras, el diccionario tool_map, el bucle for sobre tool_calls y el código que construye y recopila los objetos ToolMessage: todo eso ahora vive dentro de ToolNode.

tools_condition es la función de enrutamiento prediseñada que reemplaza la función should_continue del Capítulo 15. Veamos de nuevo el should_continue del Capítulo 15.

python
def should_continue(state: AgentState) -> Literal["tool_node", "__end__"]:
    """Decide si ejecutar herramientas o terminar el grafo."""
    last_message = state["messages"][-1]
    if last_message.tool_calls:
        return "tool_node"
    return END

Devolvía "tool_node" (el nombre registrado de nuestro nodo de herramienta) cuando el último mensaje tenía tool_calls, y END en caso contrario. tools_condition funciona exactamente igual, con una diferencia en el nombre que devuelve. Mientras que should_continue estaba escrito para devolver "tool_node" —el nombre que registramos en nuestro grafo—, tools_condition está codificado para devolver "tools".

Como aprendimos en la Sección 15.2.4, el valor que devuelve una función de enrutamiento es el nombre del siguiente nodo a ejecutar. Si no existe un nodo con ese nombre en el grafo, el enrutamiento falla. Por lo tanto, al usar tools_condition, el nodo de herramienta debe registrarse con el nombre "tools".

python
from langgraph.prebuilt import ToolNode, tools_condition
 
builder.add_node("tools", ToolNode(tools))                  # Registrar con el nombre "tools"
builder.add_conditional_edges("llm_call", tools_condition)  # Enruta a "tools" o END

De esta manera, cuando tools_condition devuelve "tools", se conecta exactamente al ToolNode que acabamos de registrar.

Ahora reensamblemos el grafo completo del Capítulo 15, incluyendo todas las piezas restantes. Las herramientas, el State y el nodo llm_call no cambian respecto al Capítulo 15.

python
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.prebuilt import ToolNode, tools_condition
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from langchain.tools import tool
 
@tool
def get_weather(city: str) -> str:
    """Obtiene el clima actual de una ciudad."""
    fake_data = {"Tokyo": "18°C, nublado", "Cairo": "31°C, soleado"}
    return fake_data.get(city, f"No hay datos meteorológicos para {city}.")
 
@tool
def calculate(expression: str) -> str:
    """Evalúa una expresión aritmética simple. Ejemplo: '3 * 21'."""
    return str(eval(expression))  # Advertencia: eval() es un riesgo de seguridad. No lo uses en producción.
 
tools = [get_weather, calculate]
 
class AgentState(MessagesState):
    llm_calls: int
 
llm = ChatOpenAI(model="gpt-5-mini")
model_with_tools = llm.bind_tools(tools)
 
def llm_call(state: AgentState):
    """Llama al LLM y devuelve su respuesta."""
    response = model_with_tools.invoke(state["messages"])
    return {
        "messages": [response],
        "llm_calls": state.get("llm_calls", 0) + 1,
    }
 
builder = StateGraph(AgentState)
 
builder.add_node("llm_call", llm_call)
builder.add_node("tools", ToolNode(tools))  # ToolNode en lugar de la función tool_node del Capítulo 15
 
builder.add_edge(START, "llm_call")
builder.add_conditional_edges("llm_call", tools_condition)  # tools_condition en lugar del should_continue del Capítulo 15
builder.add_edge("tools", "llm_call")
 
agent = builder.compile()

Compara esto con el código del Capítulo 15. El diccionario tool_map, la función tool_node y la función should_continue han desaparecido. El trabajo que hacían ahora lo manejan ToolNode(tools) y tools_condition. El nodo de herramienta se registra como "tools" para coincidir con el nombre al que enruta tools_condition. Ejecutémoslo con la misma pregunta del Capítulo 15.

python
result = agent.invoke({
    "messages": [HumanMessage(content="Obtén la temperatura en El Cairo y luego multiplica el número por 3.")],
    "llm_calls": 0,
})
 
print(result["messages"][-1].content)
print(f"\nTotal de llamadas al LLM: {result['llm_calls']}")

Salida:

Temperatura actual en El Cairo: 31°C. Multiplicada por 3 = 93.
 
Total de llamadas al LLM: 3

El resultado es idéntico al del Capítulo 15. El agente consulta el clima, realiza el cálculo y produce la respuesta final: se conserva el mismo comportamiento, mientras que el código que necesitamos escribir y mantener se ha reducido.

¿Y si quieres registrar el nodo de herramienta con un nombre distinto de "tools"? En ese caso, pasa un diccionario de mapeo como tercer argumento a add_conditional_edges, especificando a qué nodo debe conectar cada valor de retorno de tools_condition. Dado que tools_condition devuelve "tools" o END, los usas como claves y los mapeas a los nodos de destino. Por ejemplo, si registras el nodo de herramienta como "run_tools":

python
builder.add_node("run_tools", ToolNode(tools))
builder.add_conditional_edges(
    "llm_call",
    tools_condition,
    {"tools": "run_tools", END: END}   # retorno "tools" → nodo run_tools, retorno END → terminar
)

ToolNode y tools_condition reemplazan partes individuales del grafo —las tediosas—, pero añadir los nodos y conectarlos entre sí sigue siendo tarea nuestra. ¿Podríamos delegar también ese ensamblaje? Eso es exactamente lo que hace create_agent.

16.1.2) create_agent

create_agent es una función de fábrica de LangChain que se encarga de todo el ensamblaje del grafo para un agente que llama a herramientas. Pásale un modelo y una lista de herramientas, y construye un grafo con la misma estructura que ensamblamos en 16.1.1, ya compilado y listo para ejecutarse. Probémoslo.

python
from langchain.agents import create_agent
from langchain.tools import tool
from langchain_core.messages import HumanMessage
 
@tool
def get_weather(city: str) -> str:
    """Obtiene el clima actual de una ciudad."""
    fake_data = {"Tokyo": "18°C, nublado", "Cairo": "31°C, soleado"}
    return fake_data.get(city, f"No hay datos meteorológicos para {city}.")
 
@tool
def calculate(expression: str) -> str:
    """Evalúa una expresión aritmética simple. Ejemplo: '3 * 21'."""
    return str(eval(expression))  # Advertencia: eval() es un riesgo de seguridad. No lo uses en producción.
 
agent = create_agent(
    model="openai:gpt-5-mini",
    tools=[get_weather, calculate],
    system_prompt="Eres un asistente útil.",
)
 
result = agent.invoke({
    "messages": [HumanMessage(content="Obtén la temperatura en El Cairo y luego multiplica el número por 3.")],
})
print(result["messages"][-1].content)

Salida:

Temperatura actual en El Cairo: 31°C. Multiplicada por 3 = 93.

Sin definición de State, sin funciones de nodo, sin add_node ni add_edge. Una sola llamada a create_agent hizo todo eso, y el resultado es idéntico al de 16.1.1.

Lo que ocurre internamente es exactamente lo que ya sabemos. create_agent crea un nodo de llamada al LLM a partir del modelo que le pasas, construye un ToolNode a partir de la lista de herramientas, y los conecta con una arista tools_condition y una arista de retorno al bucle. El resultado es un grafo con estructura de bucle idéntico al que ensamblamos en 16.1.1.

tool_calls presentes

sin tool_calls

START

Nodo de llamada al LLM

ToolNode

END

Veamos los parámetros. En realidad create_agent no es nuevo para nosotros: lo usamos brevemente en el Capítulo 11 al construir un RAG conversacional, pero no entramos en detalle en los parámetros. Repasémoslos uno por uno.

  • model: El LLM que usará el agente. El enfoque más simple es pasar una cadena de proveedor como "openai:gpt-5-mini". Si necesitas configurar los parámetros del modelo directamente, pasa una instancia de modelo inicializada como ChatOpenAI(model="gpt-5-mini"). Internamente, el nodo de llamada al LLM usa este modelo.
  • tools: La lista de herramientas que el agente puede usar. A partir de ellas se construye un ToolNode internamente.
  • system_prompt: Instrucciones de comportamiento para el agente. Esto se antepone como mensaje de sistema a la lista de mensajes en cada llamada al LLM.
  • checkpointer: Guarda el estado de la conversación para que el agente pueda recordar turnos anteriores. Es el mismo parámetro que usamos con InMemorySaver() y thread_id en el Capítulo 11 para implementar conversaciones multiturno. Cubriremos cómo funciona en detalle en el Capítulo 17.
  • response_format: Úsalo cuando quieras la respuesta final del agente como salida estructurada. Pasa un modelo Pydantic (el mismo concepto del Capítulo 7), y el objeto validado estará disponible en result["structured_response"].
  • middleware: Registra funciones para ejecutarse en puntos específicos del bucle de ejecución del agente. Es el parámetro que usamos para registrar trim_old_messages en el Capítulo 11. Lo explicaremos en detalle a continuación.

Veamos cómo funciona response_format en la práctica.

python
from pydantic import BaseModel
from langchain.agents import create_agent
 
class WeatherReport(BaseModel):
    city: str
    temperature: str
    condition: str
 
agent = create_agent(
    model="openai:gpt-5-mini",
    tools=[get_weather, calculate],
    response_format=WeatherReport,
)
 
result = agent.invoke({
    "messages": [HumanMessage(content="¿Qué tiempo hace en Tokio?")],
})
print(result["structured_response"])

Salida:

city='Tokyo' temperature='18°C' condition='cloudy'

El agente llamó a la herramienta get_weather y luego organizó la información en un objeto WeatherReport que coincide con el esquema.

middleware

El bucle del agente tiene etapas distintas. Llama al LLM, ejecuta herramientas, vuelve a llamar al LLM: estas etapas se repiten. El middleware te permite insertar tus propias funciones antes o después de estas etapas. Especificas el momento con un decorador: @before_model significa justo antes de la llamada al LLM, y @after_model significa justo después de que el LLM responde. Registra la función en el parámetro middleware, y se ejecuta en el punto designado cada vez.

Ya usamos middleware en el Capítulo 11. Decoramos una función trim_old_messages con @before_model y la registramos; como se ejecutaba antes de cada llamada al LLM, podía recortar la lista de mensajes cada vez.

Construyamos un middleware que imprima el recuento de mensajes justo antes de cada llamada al LLM, para que podamos ver exactamente cuándo se ejecuta.

python
from langchain.agents import create_agent, AgentState
from langchain.agents.middleware import before_model
 
@before_model
def log_llm_call(state: AgentState, runtime) -> None:
    """Imprime el recuento de mensajes justo antes de cada llamada al LLM."""
    print(f"[before_model] A punto de llamar al LLM, mensajes actuales: {len(state['messages'])}")
 
agent = create_agent(
    model="openai:gpt-5-mini",
    tools=[get_weather, calculate],
    middleware=[log_llm_call],
)
 
result = agent.invoke({
    "messages": [HumanMessage(content="Obtén la temperatura en El Cairo y luego multiplica el número por 3.")],
})
print(result["messages"][-1].content)

Salida:

[before_model] A punto de llamar al LLM, mensajes actuales: 1
[before_model] A punto de llamar al LLM, mensajes actuales: 3
[before_model] A punto de llamar al LLM, mensajes actuales: 5
Temperatura actual en El Cairo: 31°C. Multiplicada por 3 = 93.

El middleware log_llm_call se ejecutó tres veces. El LLM fue llamado tres veces mientras procesaba la solicitud del usuario, y el middleware se ejecutó justo antes de cada llamada. Los recuentos de mensajes nos indican el State en cada punto: antes de la primera llamada solo estaba la pregunta del usuario (HumanMessage) — 1 mensaje. Después de cada iteración del bucle, se añadieron el AIMessage que solicita una llamada a herramienta y el ToolMessage con el resultado, creciendo a 3 y luego a 5.

Algo que debes saber: antes de LangChain 1.0, este papel lo cumplía una función llamada create_react_agent en el lado de LangGraph, y ahora está en desuso. Si ves from langgraph.prebuilt import create_react_agent en tutoriales o publicaciones de blog más antiguos, entiende que es una versión anterior del create_agent que estás aprendiendo ahora.

Hemos reconstruido de forma concisa el agente del Capítulo 15 usando ToolNode, tools_condition y create_agent. En la siguiente sección, combinaremos estos componentes prediseñados con el ensamblaje manual de grafos para construir un agente más complejo.

16.2) Construir un agente multirrama

El agente multirrama(multi-branch agent) que construiremos en esta sección primero determina con qué tipo de solicitud está tratando, y luego maneja cada tipo con un modelo diferente o un conjunto diferente de herramientas. Ensamblaremos el grafo general manualmente usando el enfoque del Capítulo 15, y usaremos create_agent para las partes que necesitan un bucle de llamada a herramientas.

16.2.1) Requisitos y diseño

Construiremos el agente de atención al cliente mencionado en la introducción de este capítulo. Estos son los requisitos:

  • Consultas simples ("¿Cuál es su horario de atención?") → Un modelo pequeño de bajo costo responde directamente.
  • Consultas complejas ("Mi pedido llegó dañado, ¿debería obtener un cambio o un reembolso?") → Un modelo de alto rendimiento responde.
  • Búsquedas de pedidos ("¿Cuál es el estado de entrega del pedido #12345?") → Un agente con una herramienta de búsqueda de pedidos se encarga de ello.

Cada tipo de consulta necesita una configuración diferente de modelo y herramientas, así que necesitamos un grafo que primero clasifique cada solicitud y luego la enrute al manejador correcto. Esta es la estructura:

consulta simple

consulta compleja

búsqueda de pedido

START

classify

¿tipo de consulta?

simple_handler

complex_handler

order_agent

END

Cuando llega una solicitud, el nodo classify determina de qué tipo de consulta se trata y registra el resultado en el State. Una arista condicional lee entonces el tipo registrado del State y enruta al nodo apropiado. simple_handler maneja las consultas simples, y complex_handler se ocupa de las consultas complejas. order_agent usa una herramienta de búsqueda de pedidos para comprobar el estado de entrega y responder. Dado que order_agent es un nodo estándar de llamada a herramientas, lo construiremos con create_agent. Ahora construyamos cada pieza en orden.

16.2.2) El nodo Classify y la función de enrutamiento

Primero, definamos el State. Añadiremos un campo intent para almacenar el resultado de la clasificación. Los tres tipos se representarán con los valores "simple", "complex" y "order".

python
from langgraph.graph import MessagesState
 
class State(MessagesState):
    intent: str    # Resultado de la clasificación: "simple", "complex", "order"

A continuación está el nodo classify. Usa un LLM para determinar a cuál de los tres tipos pertenece la solicitud del usuario, y registra el resultado en intent. Recibimos el resultado de la clasificación usando salida estructurada, que aprendimos en el Capítulo 7. Cuando declaramos el campo intent del esquema con un tipo Literal, la respuesta del LLM se restringe a uno de los valores declarados.

python
from typing import Literal
from pydantic import BaseModel, Field
from langchain_openai import ChatOpenAI
 
class IntentRoute(BaseModel):
    """Resultado de la clasificación de una consulta de cliente."""
    intent: Literal["simple", "complex", "order"] = Field(
        description=(
            "simple: preguntas generales como horarios de atención o saludos. "
            "complex: consultas que requieren un razonamiento cuidadoso, como disputas o reembolsos. "
            "order: solicitudes para buscar un pedido específico."
        )
    )
 
classifier_llm = ChatOpenAI(model="gpt-5.4-nano").with_structured_output(IntentRoute)
 
def classify(state: State):
    """Clasifica el tipo de consulta del cliente."""
    question = state["messages"][-1].content
    result = classifier_llm.invoke(
        f"Clasifica la solicitud del cliente.\n\nSolicitud: {question}"
    )
    return {"intent": result.intent}

Usamos el modelo más pequeño (gpt-5.4-nano) para la clasificación. Decidir "¿de qué tipo es esta consulta?" es una tarea simple que no necesita un modelo de alto rendimiento. Y dado que el nodo classify es una puerta de entrada por la que pasa cada solicitud, es preferible un modelo económico y rápido.

A continuación está la función de enrutamiento. Simplemente devuelve el resultado de la clasificación almacenado en el State.

python
def route_by_intent(state: State) -> Literal["simple", "complex", "order"]:
    """Determina el siguiente nodo según el resultado de la clasificación."""
    return state["intent"]

El nodo classify ya ha decidido qué nodo debe ejecutarse a continuación y lo ha registrado en intent, por lo que la función de enrutamiento simplemente devuelve ese valor tal cual.

16.2.3) Nodos manejadores por tipo

Ahora construyamos los manejadores para cada uno de los tres tipos de consulta.

El manejador de consultas simples llama a un modelo pequeño una vez. En un agente de atención al cliente real aplicarías RAG para buscar respuestas en documentos internos, pero hemos mantenido el manejador simple para centrarnos en el tema de este capítulo.

python
simple_llm = ChatOpenAI(model="gpt-5.4-mini")
 
def simple_handler(state: State):
    """Responde consultas simples con un modelo pequeño."""
    response = simple_llm.invoke(state["messages"])
    return {"messages": [response]}

El manejador de consultas complejas usa un modelo de alto rendimiento. Por la misma razón que el manejador de consultas simples, lo hemos mantenido simple: solo genera una respuesta.

python
complex_llm = ChatOpenAI(model="gpt-5.4")
 
def complex_handler(state: State):
    """Responde consultas complejas con un modelo de alto rendimiento."""
    response = complex_llm.invoke(state["messages"])
    return {"messages": [response]}

El manejador de búsqueda de pedidos necesita usar una herramienta de búsqueda de pedidos, lo que significa que requiere un bucle de llamada a herramientas. Dado que su estructura es idéntica a la de un agente estándar de llamada a herramientas, lo construiremos con create_agent.

python
from langchain.tools import tool
from langchain.agents import create_agent
 
@tool
def get_order_status(order_id: str) -> str:
    """Busca el estado de entrega de un pedido por número de pedido."""
    fake_data = {"12345": "En tránsito, se espera mañana", "67890": "Entregado"}
    return fake_data.get(order_id, f"Pedido {order_id} no encontrado.")
 
order_agent = create_agent(
    model="openai:gpt-5.4-mini",
    tools=[get_order_status],
)

16.2.4) Ensamblar y ejecutar el grafo

Conectemos todos los nodos que hemos construido en un grafo. Colocaremos classify en el punto de inicio, lo conectaremos a los tres manejadores mediante una arista condicional, y configuraremos cada manejador para que termine cuando acabe.

python
from langgraph.graph import StateGraph, START, END
 
builder = StateGraph(State)
 
builder.add_node("classify", classify)
builder.add_node("simple", simple_handler)
builder.add_node("complex", complex_handler)
builder.add_node("order", order_agent)      # Registrar el grafo de create_agent como un nodo
 
builder.add_edge(START, "classify")
builder.add_conditional_edges("classify", route_by_intent)
builder.add_edge("simple", END)
builder.add_edge("complex", END)
builder.add_edge("order", END)
 
agent = builder.compile()

Ejecutemos una consulta de cada tipo y veamos qué manejador la procesa.

python
from langchain_core.messages import HumanMessage
 
for question in [
    "¿Cuál es su horario de atención?",
    "Mi pedido llegó dañado. ¿Debería obtener un cambio o un reembolso?",
    "¿Cuál es el estado de entrega del pedido 12345?",
]:
    result = agent.invoke({"messages": [HumanMessage(content=question)]})
    print(f"P: {question}")
    print(f"[{result['intent']}] R: {result['messages'][-1].content}\n")

Salida:

P: ¿Cuál es su horario de atención?
[simple] R: No tengo un horario de atención fijo: estoy disponible las 24 horas, los 7 días de la semana.
...
 
P: Mi pedido llegó dañado. ¿Debería obtener un cambio o un reembolso?
[complex] R: Si tu pedido llegó dañado, generalmente deberías tener derecho a un **reemplazo/cambio o un reembolso completo**.
...
 
P: ¿Cuál es el estado de entrega del pedido 12345?
[order] R: El pedido 12345 está **en tránsito** y **se espera para mañana**.

Cada consulta se procesó a través de una ruta diferente. simple_handler respondió la consulta simple con un modelo pequeño, complex_handler respondió la consulta compleja con un modelo de alto rendimiento, y order_agent llamó a la herramienta get_order_status para responder la búsqueda de pedido.