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.
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.
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 ENDDevolví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".
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 ENDDe 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.
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.
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: 3El 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":
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.
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.
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 comoChatOpenAI(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 unToolNodeinternamente.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 conInMemorySaver()ythread_iden 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 enresult["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 registrartrim_old_messagesen el Capítulo 11. Lo explicaremos en detalle a continuación.
Veamos cómo funciona response_format en la práctica.
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.
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_agenten el lado de LangGraph, y ahora está en desuso. Si vesfrom langgraph.prebuilt import create_react_agenten tutoriales o publicaciones de blog más antiguos, entiende que es una versión anterior delcreate_agentque 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:
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".
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.
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.
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.
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.
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.
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.
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.
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.