18. Sistemas multiagente — El patrón supervisor
Recuerda el agente de atención al cliente que construimos en el capítulo 16. Tenía exactamente una herramienta —consulta de pedidos— así que cuando un cliente preguntaba por un pedido, informaba del estado del envío. Con una sola herramienta, sencillamente no había forma de que eligiera la incorrecta.
Ahora supongamos que hacemos crecer ese agente hasta convertirlo en un servicio de producción real. La consulta de pedidos por sí sola no basta. Necesitaríamos cancelación de pedidos, seguimiento de envíos, cambios de dirección, solicitudes de cambio, solicitudes de devolución, comprobaciones de elegibilidad de reembolso, procesamiento de reembolsos, comprobaciones de inventario, emisión de cupones, consulta de puntos, creación de tickets de soporte y más. La mecánica es simple: seguir añadiendo herramientas y agregar reglas de negocio al prompt del sistema.
Pero a medida que las herramientas y las reglas se acumulan, surgen tres problemas.
-
La selección de herramientas se vuelve menos precisa. En cada turno el modelo lee todas las descripciones de herramientas y decide cuál llamar. A medida que añades herramientas que reciben las mismas entradas y se solapan en propósito —como solicitud de cambio y solicitud de devolución— aumentan las probabilidades de elegir la incorrecta.
-
El contexto se llena de información que no necesitas en ese momento. Incluso mientras se procesa un reembolso, el esquema de la herramienta de comprobación de inventario, las reglas de emisión de cupones, el procedimiento de solicitud de cambio y todo lo demás van a cuestas en cada llamada. Estás enviando el conjunto completo de esquemas de herramientas y las reglas de negocio de cada dominio en cada ocasión. Cuando el contexto está abarrotado de material irrelevante para la tarea actual, las partes que importan quedan sepultadas y la precisión sufre.
-
Se vuelve difícil de cambiar. Ajustar una sola regla de reembolso significa editar un prompt donde las reglas de todos los dominios están enredadas entre sí, y no puedes estar seguro de que el cambio no repercuta en los cambios o los envíos. Si distintos dominios tienen distintos responsables, el problema solo crece.
Este capítulo enseña una forma de lidiar con esto. En lugar de amontonar más herramientas y reglas en un único agente, dividimos el trabajo en un agente por dominio. Continuando con el escenario de atención al cliente del capítulo 16, construiremos un equipo formado por un agente que solo se ocupa de las consultas de pedidos y un agente que solo se ocupa de los reembolsos. Cada uno tiene sus propias herramientas y su propio prompt. Luego añadimos un agente más para dirigirlos —a este director se le llama supervisor—. Una configuración con varios agentes como esta es un sistema multiagente, y la disposición donde un supervisor comanda al resto es el patrón supervisor.
Los sistemas multiagente tienen un coste. Como el supervisor debe decidir a qué trabajador delegar en cada paso, hay más llamadas al LLM, y eso significa más latencia y más gasto. Así que si no tienes tantas herramientas y reglas, no hay ninguna necesidad de recurrir a un sistema multiagente.
Este es el plan. En 18.1 vemos qué es un sistema multiagente y cómo funciona. En 18.2 construimos los agentes trabajadores. En 18.3 construimos un supervisor a mano con el objeto Command de LangGraph. En 18.4 envolvemos los trabajadores como herramientas y reconstruimos el mismo equipo con muchísimo menos código.
18.1) Comprender los sistemas multiagente
18.1.1) Qué es un sistema multiagente
Diseñemos un agente capaz de gestionar la solicitud: "Mi pedido nunca llegó — si algo va mal, reembólsalo". Podríamos construirlo con todo lo aprendido hasta ahora: adjuntar una herramienta de consulta de pedidos y una herramienta de reembolso a un único agente, y dejar que el agente itere hasta terminar el trabajo. Un único agente consulta el pedido, confirma que la entrega falló, ve ese resultado, solicita el reembolso y redacta la respuesta final.
Un sistema multiagente es una estructura donde varios agentes dividen este trabajo entre ellos. Divides los agentes por dominio y colocas un supervisor por encima de ellos, y cada agente tiene solo las herramientas que necesita. El agente que acabamos de esbozar, por ejemplo, se divide de forma natural en un agente de consulta de pedidos y un agente de reembolso. El supervisor primero llama al agente de consulta de pedidos para comprobar el estado del envío; cuando recibe la respuesta de que la entrega falló, llama al agente de reembolso para procesar el reembolso; luego reúne ambos resultados para responder al cliente. Lo que antes era un único agente llamando a herramientas en secuencia se ha convertido en un supervisor llamando a agentes en secuencia.
Si solo hubiera dos herramientas, no habría razón para dividir así. Un único agente bastaría, y añadir un supervisor solo añadiría llamadas al LLM. Pero como vimos en la introducción, una vez que hay muchas herramientas, un agente puede elegir la incorrecta, su contexto se llena de información no relacionada con la tarea que tiene entre manos, y su prompt se vuelve difícil de cambiar.
Dividir resuelve estos problemas. El agente de consulta de pedidos ve solo unas pocas herramientas relacionadas con pedidos, así que elegir de una lista de docenas se reduce a elegir entre un puñado. Su prompt contiene solo reglas de consulta de pedidos, así que la política de reembolsos, las condiciones de cupones y otros asuntos no relacionados con la consulta de pedidos no llenan su contexto. Y cuando necesitas cambiar una regla de reembolso, solo tocas el agente de reembolso, así que el cambio no llega a los demás.
El agente de consulta de pedidos y el agente de reembolso aquí no tienen nada de especial. Son el mismo tipo de agente que construiste en el capítulo 16. Los creas pasando un modelo, herramientas y un prompt a create_agent, y los llamas con invoke. Simplemente abarcan menos terreno.
Entonces, ¿qué hace el supervisor? El agente de consulta de pedidos y el agente de reembolso no saben que el otro existe. Cada uno solo hace su propio trabajo; ninguno sabe quién debería ir primero. En el ejemplo anterior, llamar primero a la consulta de pedidos y —solo después de ver su resultado— llamar al reembolso fue el criterio del supervisor.
Quién llama a quién, y cuándo. A esto lo llamamos orquestación.
18.1.2) El patrón supervisor
El patrón supervisor es una estructura en la que un único supervisor central orquesta a varios agentes trabajadores. Sigue estas reglas:
- El supervisor nunca hace el trabajo él mismo. No consulta pedidos ni procesa reembolsos. Solo decide a quién delegar y luego reúne los resultados devueltos en una respuesta final.
- Los trabajadores nunca se llaman entre sí. El agente de consulta de pedidos nunca llama directamente al agente de reembolso. Todo camino pasa por el supervisor.
- Solo el supervisor habla con el cliente. Los trabajadores reportan al supervisor, no al cliente.
Entonces, ¿cómo decide el supervisor a qué agente llamar? Lo decide el LLM. El supervisor lee toda la conversación hasta ese momento y juzga. Si aún no conoce el estado del pedido, llama al agente de consulta de pedidos; una vez confirmado que la entrega falló y que se necesita un reembolso, llama al agente de reembolso.
El supervisor repite este juicio hasta que la solicitud del usuario está completa. Llama a un agente, recibe un informe, vuelve a leer la conversación ahora que el informe se ha añadido, y decide a qué agente llamar a continuación.
Este bucle tiene la misma estructura que el que construiste en el capítulo 14.
- Pensar — leer la conversación hasta ahora y decidir a qué agente llamar.
- Actuar — ejecutar el agente elegido.
- Observar — tomar el informe del agente y añadirlo a la conversación.
En el capítulo 14 llamabas a herramientas; aquí llamas a agentes. Eso es lo único que cambia.
Fíjate en cómo las flechas vuelven en bucle al supervisor. Cuando un trabajador termina, reporta al supervisor, y el supervisor lee ese informe y decide el siguiente movimiento.
El supervisor es quien termina el bucle. Una vez que juzga que la solicitud del cliente está completamente atendida, produce la respuesta final y se detiene.
18.2) Construir los agentes trabajadores
Construyamos los dos agentes trabajadores que diseñamos en 18.1: un trabajador de consulta de pedidos que comprueba el estado del pedido, y un trabajador de reembolso que procesa los reembolsos. Construiremos el supervisor en 18.3.
Un agente trabajador no es más que el agente corriente que construiste en el capítulo 16. Los construiremos rápidamente con create_agent.
Primero, configuremos los datos de pedidos que los dos trabajadores compartirán.
ORDERS = {
"12345": {"item": "Wireless Earbuds", "amount": 89,
"status": "in_transit", "status_text": "En tránsito (llega mañana)"},
"67890": {"item": "Mechanical Keyboard", "amount": 129,
"status": "delivered", "status_text": "Entregado"},
"24680": {"item": "Noise-Cancelling Headphones", "amount": 249,
"status": "delivery_failed", "status_text": "Entrega fallida (devuelto — destinatario no encontrado)"},
}El trabajador de consulta de pedidos tiene solo una herramienta.
from langchain.tools import tool
from langchain.agents import create_agent
@tool
def get_order_status(order_id: str) -> str:
"""Busca el artículo, el importe pagado y el estado del envío para un número de pedido."""
order = ORDERS.get(order_id)
if order is None:
return f"Pedido {order_id} no encontrado."
return (f"Pedido {order_id}: {order['item']}, "
f"${order['amount']:,}, estado: {order['status_text']}")
order_agent = create_agent(
name="order_expert",
model="openai:gpt-5.4-mini",
tools=[get_order_status],
system_prompt=(
"Eres un especialista en consulta de pedidos. Busca el estado del pedido y responde.\n"
"Incluye el número de pedido, el artículo, el importe pagado y el estado del envío en tu respuesta final.\n"
"No juzgues si procede un reembolso ni menciones reembolsos de ninguna manera. Tu rol es únicamente la consulta de pedidos y el informe de estado."
),
)El trabajador de reembolso tiene dos herramientas: una que decide si un pedido es elegible para reembolso, y otra que efectivamente procesa el reembolso.
@tool
def check_refund_eligibility(order_id: str) -> str:
"""Determina si un pedido es elegible para un reembolso. Solo califican los pedidos con entrega fallida."""
order = ORDERS.get(order_id)
if order is None:
return f"Pedido {order_id} no encontrado."
if order["status"] == "delivery_failed":
return f"El pedido {order_id} es elegible para un reembolso (motivo: entrega fallida)."
return f"El pedido {order_id} no es elegible para un reembolso (estado actual: {order['status_text']})."
@tool
def issue_refund(order_id: str) -> str:
"""Procesa un reembolso. Confirma siempre la elegibilidad con check_refund_eligibility antes de llamar a esto."""
order = ORDERS.get(order_id)
if order is None:
return f"Pedido {order_id} no encontrado."
return (f"Reembolso completo: ${order['amount']:,} para el pedido {order_id} "
f"se reembolsará en un plazo de 3 a 5 días hábiles. (número de aprobación: RF-{order_id})")
refund_agent = create_agent(
name="refund_expert",
model="openai:gpt-5.4-mini",
tools=[check_refund_eligibility, issue_refund],
system_prompt=(
"Eres un especialista en procesamiento de reembolsos.\n"
"Confirma siempre la elegibilidad con check_refund_eligibility primero, y solo "
"entonces llama a issue_refund.\n"
"Si procesaste un reembolso, incluye el importe y el número de aprobación en tu respuesta final.\n"
"Si el pedido no es elegible, no lo proceses — informa del motivo en su lugar."
),
)Dimos a ambos trabajadores un name. Este nombre es lo que create_supervisor en 18.5 usa como nombre del nodo y como nombre de la herramienta de handoff.
Cuatro cosas que necesita el prompt del sistema de un trabajador
El prompt del sistema de un trabajador se construye a partir de cuatro elementos —principios que Anthropic destiló mientras construía su propio sistema multiagente de investigación—. Cuando estos son débiles, los trabajadores duplican trabajo, dejan tareas sin hacer o no logran encontrar la información que necesitan.
| Elemento | order_agent | refund_agent |
|---|---|---|
| Rol | Eres un especialista en consulta de pedidos | Eres un especialista en procesamiento de reembolsos |
| Guía de herramientas | Confirma siempre con check_refund_eligibility primero, y solo entonces llama a issue_refund | |
| Formato de salida | Incluye el número de pedido, el artículo, el importe pagado y el estado del envío en tu respuesta final | Si procesaste un reembolso, incluye el importe y el número de aprobación en tu respuesta final |
| Límite de tarea | No juzgues si procede un reembolso ni menciones reembolsos de ninguna manera. Tu rol es únicamente la consulta de pedidos y el informe de estado. | Si el pedido no es elegible, no lo proceses — informa del motivo |
El rol fija, en una sola frase, quién es este trabajador. Determinar su identidad con "Eres un especialista en consulta de pedidos" mantiene al modelo centrado en su propio trabajo y con menos probabilidad de meterse en el de otro.
Guía de herramientas. Escribe esto cuando haya algo que el esquema de la herramienta por sí solo no pueda transmitir —el orden y las condiciones en que deben usarse las herramientas, por ejemplo—. Si no hay nada que añadir más allá del esquema, puedes omitirlo.
El formato de salida y el límite de tarea importan muchísimo en un entorno multiagente.
Formato de salida. La respuesta final de un trabajador no es una respuesta al cliente —es un informe presentado al supervisor—. Cualquier cosa que no esté escrita ahí nunca llega al supervisor. Si un trabajador busca un importe con una herramienta pero lo deja fuera de su respuesta final, el supervisor no tiene forma de saberlo.
Límite de tarea. Este es el alcance que define hasta dónde puede llegar un trabajador y qué no debe hacer. El trabajador de consulta de pedidos solo debe consultar —nunca reembolsar—. Por eso no le dimos una herramienta de reembolso. Pero retener la herramienta no basta por sí solo, porque el modelo todavía puede decir "La entrega falló, así que te emitiré un reembolso" sin herramienta alguna. Si esa frase llega al supervisor, el supervisor puede suponer que un reembolso ya está en marcha y nunca llamar al trabajador de reembolso. Por eso el prompt también dice "No juzgues si procede un reembolso ni menciones reembolsos de ninguna manera", impidiéndole siquiera sacar el tema de los reembolsos en palabras.
Elegir modelos
Estamos usando gpt-5.4-mini para los trabajadores y gpt-5.4 para el supervisor. Un trabajador hace el trabajo simple de llamar a unas pocas herramientas en un orden fijo, así que un modelo pequeño es más que suficiente. El supervisor tiene que leer toda la conversación y juzgar a quién llamar a continuación, así que necesita un modelo más grande. Poder elegir un modelo por agente, ajustado a la dificultad de su trabajo, es otro beneficio de dividir.
Ahora podemos añadir el supervisor.
18.3) Construir el supervisor a mano
Construyamos un supervisor a mano. En la práctica usarás sobre todo el enfoque donde el framework se encarga de esto por ti (que veremos en la siguiente sección), pero para entender qué está pasando bajo el capó, necesitas construirlo tú mismo al menos una vez.
Como vimos en 18.1, lo que hace el supervisor es un único bucle: llamar a un trabajador, leer el informe y decidir a quién llamar a continuación —o si detenerse— una y otra vez.
18.3.1) Handoffs y Command
Para que este bucle gire, el control tiene que pasar de un lado a otro entre el supervisor y los trabajadores. El supervisor cede el control —"este trabajador va a continuación"— y cuando el trabajador termina, devuelve el control al supervisor. Este paso de control de un nodo a otro se llama handoff.
Un handoff necesita dos piezas de información: adónde ir (el destino) y qué pasar (la carga útil). El destino es siempre obligatorio; la carga útil se incluye solo cuando hay algo que pasar. En LangGraph, un nodo especifica ambas devolviendo un Command.
from typing import Literal
from langgraph.graph import MessagesState
from langgraph.types import Command
def some_node(state: MessagesState) -> Command[Literal["refund_expert_proxy"]]:
return Command(
goto="refund_expert_proxy", # adónde: el nodo a ejecutar a continuación
update={"messages": [...]}, # qué: el informe del trabajador (la carga útil añadida al State)
)goto es el destino; update es la carga útil. La anotación de tipo de retorno Command[Literal["refund_expert_proxy"]] enumera, por adelantado, los destinos a los que este nodo puede ir. Veremos cómo se usa realmente este Command en la siguiente sección, donde construimos el nodo supervisor y los proxies de trabajadores.
18.3.2) Construir el bucle del supervisor
La estructura en sí es simple. Creamos un nodo supervisor y tantos proxies de trabajadores como necesitemos. Un proxy de trabajador es un nodo que llama a su agente trabajador asignado en nombre del supervisor. El punto de entrada es el supervisor, y cada proxy de trabajador, una vez terminado, vuelve al supervisor —formando el bucle—. El Command que devuelve cada nodo es lo que decide adónde va el control a continuación.
Las líneas continuas son handoffs entre nodos (goto); las líneas punteadas son un proxy de trabajador llamando a su agente trabajador (invoke). El supervisor cede el control a un proxy de trabajador, y el proxy de trabajador, una vez terminado, vuelve al supervisor. Cuando el supervisor decide FINISH, sale a END.
Ahora convirtamos esta imagen en código. Primero, la clase Route. Route es el esquema para recibir la respuesta del supervisor como una respuesta estructurada cuando le preguntamos al LLM a qué proxy de trabajador llamar a continuación. Si el LLM respondiera en lenguaje natural de forma libre, sería difícil saber qué proxy de trabajador ejecutar. Route contiene qué proxy de trabajador llamar a continuación (next) y el motivo por el que lo decidió así (reason).
from typing import Literal
from pydantic import BaseModel, Field
class Route(BaseModel):
reason: str = Field(description="El motivo de esta decisión.")
next: Literal["order_expert_proxy", "refund_expert_proxy", "FINISH"] = Field(
description="El nodo trabajador a ejecutar a continuación. FINISH si la solicitud está completamente atendida."
)Hay una razón por la que reason se declara antes que next. La salida estructurada se genera en el orden en que aparecen los campos en el esquema, así que con reason primero, el LLM escribe su razonamiento antes de elegir un trabajador. Razonar primero y decidir después produce una mejor elección. Invierte el orden —pon next primero— y el LLM elige un trabajador antes de haber razonado siquiera, y luego rellena una justificación para encajar con una elección que quizá ya haya errado.
A continuación, el nodo supervisor.
from typing import Literal
from langgraph.graph import MessagesState, StateGraph, START, END
from langgraph.types import Command
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
supervisor_llm = ChatOpenAI(model="gpt-5.4")
SUPERVISOR_PROMPT = (
"Eres el supervisor de un equipo de atención al cliente. Gestionas dos trabajadores:\n"
"- order_expert_proxy: consulta el estado de los pedidos.\n"
"- refund_expert_proxy: comprueba la elegibilidad de reembolso y procesa reembolsos.\n"
"Para decidir si se necesita un reembolso, primero debes comprobar el estado del pedido.\n"
"Asigna a un trabajador a la vez, y responde con FINISH una vez que la solicitud esté completamente atendida."
)
def supervisor(
state: MessagesState,
) -> Command[Literal["order_expert_proxy", "refund_expert_proxy", "__end__"]]:
messages = [{"role": "system", "content": SUPERVISOR_PROMPT}, *state["messages"]]
decision = supervisor_llm.with_structured_output(Route).invoke(messages)
print(f"[supervisor] → {decision.next} ({decision.reason})")
if decision.next == "FINISH":
final = supervisor_llm.invoke(
[{"role": "system", "content": "Basándote en la conversación hasta ahora, escribe una respuesta al cliente."},
*state["messages"]]
)
return Command(goto=END, update={"messages": [final]}) # ceder el control a END
return Command(goto=decision.next) # ceder el control al proxy de trabajadorHasta ahora, los nodos devolvían solo el State modificado. Pero el nodo supervisor devuelve un Command. Cuando un nodo devuelve un Command, LangGraph hace dos cosas: aplica el contenido de update al State, y ejecuta a continuación el nodo nombrado en goto. En el código anterior, ponemos el nombre del proxy de trabajador extraído de decision.next en goto, así que el nodo que el LLM eligió —decision.next— es el que se ejecuta.
A continuación, los proxies de trabajadores. Un proxy de trabajador hace su trabajo y devuelve el control al supervisor —por eso cede el control con goto="supervisor"—.
El trabajo de un proxy de trabajador es simple: llamar a su agente trabajador con invoke.
def order_expert_proxy(state: MessagesState) -> Command[Literal["supervisor"]]:
result = order_agent.invoke(state)
last = result["messages"][-1]
return Command(
goto="supervisor",
update={"messages": [HumanMessage(content=last.content, name="order_expert")]},
)
def refund_expert_proxy(state: MessagesState) -> Command[Literal["supervisor"]]:
result = refund_agent.invoke(state)
last = result["messages"][-1]
return Command(
goto="supervisor",
update={"messages": [HumanMessage(content=last.content, name="refund_expert")]},
)Fíjate en que extraemos solo el mensaje final del trabajador y se lo pasamos al supervisor. El supervisor solo necesita la conclusión; no necesita saber cuántas veces llamó el trabajador a las herramientas internamente.
18.3.3) Cablear y ejecutar el grafo
Registramos los tres nodos y conectamos solo el punto de entrada al supervisor. Todo otro movimiento lo decide el Command de cada nodo, así que no se necesitan más aristas.
builder = StateGraph(MessagesState)
builder.add_node("supervisor", supervisor)
builder.add_node("order_expert_proxy", order_expert_proxy)
builder.add_node("refund_expert_proxy", refund_expert_proxy)
builder.add_edge(START, "supervisor")
team = builder.compile()
result = team.invoke(
{"messages": [HumanMessage(
content="El pedido 24680 todavía no ha llegado. Si algo va mal, reembólsalo por favor."
)]},
config={"recursion_limit": 15},
)Salida:
[supervisor] → order_expert_proxy (Es necesario comprobar el estado del pedido antes de decidir sobre un reembolso.)
[supervisor] → refund_expert_proxy (La entrega fallida está confirmada, así que comprueba la elegibilidad de reembolso y procésalo.)
[supervisor] → FINISH (La comprobación del pedido y el procesamiento del reembolso están ambos completos.)El supervisor primero enrutó a la consulta de pedidos. Cuando volvió el informe de que la entrega había fallado, enrutó al reembolso, y cuando llegó el informe de reembolso completo, terminó. Eligió cada destino siguiente leyendo el informe del agente anterior.
El proxy de trabajador pasa todo el State compartido a su trabajador vía invoke(state), así que cada trabajador ve toda la conversación hasta ese momento. Con solo dos trabajadores esto está bien, pero a medida que crecen los trabajadores y la conversación, cada trabajador termina leyendo mensajes que no tienen nada que ver con su propio trabajo. Resolveremos esto de otra forma en la siguiente sección.
18.4) Delegar a los trabajadores mediante herramientas
Tras construir las tripas del supervisor a mano en 18.3, reconstruyamos ahora el mismo equipo de la forma recomendada para proyectos reales. Este enfoque no necesita ninguna API nueva. Conviertes cada trabajador en una herramienta con @tool, y das esas herramientas a un agente supervisor. Construiremos el agente supervisor simplemente con create_agent.
La idea clave cabe en una sola frase: el supervisor es en sí mismo solo un agente, y cada trabajador se convierte en una herramienta que el supervisor llama.
Visto así, el supervisor tiene la misma estructura que el agente que llama a herramientas que construiste en el capítulo 16. Simplemente contiene herramientas de alto nivel que llaman a agentes, en lugar de herramientas de bajo nivel como get_order_status.
18.4.1) Envolver los trabajadores como herramientas
Usamos el order_agent y el refund_agent de 18.2 sin cambios. Todo lo que hacemos es envolver cada uno en una función @tool.
from langchain.tools import tool
# order_agent y refund_agent son los trabajadores de 18.2
@tool
def lookup_order(request: str) -> str:
"""Busca el artículo, el importe pagado y el estado del envío de un pedido. Usa esto cuando necesites conocer el estado de un pedido.
Entrada: una solicitud de consulta en lenguaje natural (p. ej., 'Dime el estado del envío del pedido 24680').
"""
print("[llamada a herramienta] lookup_order")
print(f" request: {request}")
result = order_agent.invoke({"messages": [{"role": "user", "content": request}]})
return result["messages"][-1].content
@tool
def handle_refund(request: str) -> str:
"""Comprueba la elegibilidad de reembolso y procesa un reembolso. Usa esto cuando el cliente quiera un reembolso y ya hayas confirmado el estado del pedido.
Entrada: una solicitud de reembolso en lenguaje natural. Incluye el número de pedido y el estado del envío confirmado por la consulta.
(p. ej., 'El pedido 24680 está en estado de entrega fallida. Procesa un reembolso si es elegible.')
"""
print("[llamada a herramienta] handle_refund")
print(f" request: {request}")
result = refund_agent.invoke({"messages": [{"role": "user", "content": request}]})
return result["messages"][-1].contentCambiaron tres cosas.
-
Las descripciones de las herramientas reemplazan la lógica de enrutamiento. En 18.3 escribimos
SUPERVISOR_PROMPTy el esquemaRoutea mano para decirle al supervisor su lista de trabajadores y sus opciones. Aquí los docstrings de las herramientas hacen ese trabajo. El LLM del supervisor lee las descripciones de las herramientas y decide cuándo llamar a qué. -
Cada trabajador parte de un contexto limpio. Puesta al lado de 18.3, la diferencia es clara.
python# 18.3 (grafo manual): pasa todo el State compartido result = refund_agent.invoke(state) # 18.4 (delegación mediante herramientas): pasa solo la descripción de la tarea que el supervisor escribió result = refund_agent.invoke({"messages": [{"role": "user", "content": request}]})En 18.4 el trabajador de reembolso recibe solo una frase que describe su tarea. Nunca ve la redacción original del cliente, el razonamiento del supervisor, ni el historial de llamadas a herramientas de otro trabajador. Incluso con diez trabajadores y cien turnos de conversación, el contexto de cada trabajador sigue siendo solo esa única descripción de tarea.
-
A cambio, el supervisor asume el trabajo de pasar la información. Como un trabajador no puede ver el historial de conversación, lo que necesite tiene que empaquetarlo el supervisor en la cadena
request. Por eso el docstring dehandle_refunddeletrea: "Incluye el número de pedido y el estado del envío confirmado por la consulta". Sin esa instrucción, el supervisor podría pasar solo"Procesa un reembolso", dejando al trabajador de reembolso sin saber siquiera de qué pedido se trata.
18.4.2) Ensamblar y ejecutar el supervisor
En este enfoque el supervisor, también, es solo un agente. No se necesita ningún Command, ni ningún esquema Route.
from langchain.agents import create_agent
from langchain_core.messages import HumanMessage
TOOL_SUPERVISOR_PROMPT = (
"Eres el supervisor de un equipo de atención al cliente.\n"
"Para decidir si se necesita un reembolso, primero debes comprobar el estado del pedido.\n"
"No hagas el trabajo tú mismo — delega en los trabajadores.\n"
"Los trabajadores no pueden ver esta conversación. Cuando delegues, pon todo lo que necesiten en la solicitud.\n"
"Cuando todo el trabajo esté hecho, sintetiza los resultados de los trabajadores en una respuesta al cliente."
)
supervisor_agent = create_agent(
model="openai:gpt-5.4",
tools=[lookup_order, handle_refund],
system_prompt=TOOL_SUPERVISOR_PROMPT,
)
result = supervisor_agent.invoke(
{"messages": [HumanMessage(
content="El pedido 24680 todavía no ha llegado. Si algo va mal, reembólsalo por favor."
)]}
)
print("\n\n[respuesta final]")
print(result["messages"][-1].content)El resultado final es el mismo que en 18.3.
[llamada a herramienta] lookup_order
request: El cliente dice que el pedido 24680 aún no ha llegado. Para decidir si se necesita
un reembolso, dame por favor el artículo, el importe pagado y el estado actual del envío del pedido 24680.
[llamada a herramienta] handle_refund
request: El pedido 24680 es unos Noise-Cancelling Headphones, $249, y su estado de envío está
confirmado como 'Entrega fallida (devuelto — destinatario no encontrado)'. El cliente está
solicitando un reembolso, así que comprueba la elegibilidad y procésalo si es elegible.
[respuesta final]
He comprobado, y el pedido 24680 estaba en estado de entrega fallida (devuelto — destinatario no encontrado).
Calificaba para un reembolso, y he completado el reembolso.
- Artículo: Noise-Cancelling Headphones
- Importe del reembolso: $249
- Número de aprobación del reembolso: RF-24680
Dependiendo de tu método de pago, normalmente tarda unos días hábiles en aparecer el reembolso.Pero mira las llamadas a herramientas que el supervisor hizo por el camino —ahí es donde se muestra la diferencia con 18.3—. Mira el request de handle_refund. El supervisor resumió el resultado anterior de la consulta y escribió él mismo la descripción de la tarea. El trabajador de reembolso recibe solo esta única frase. Donde en 18.3 se le entregaba al trabajador toda la conversación y se le dejaba hurgar en ella, aquí el supervisor selecciona justo lo necesario y lo pasa.
18.4.3) Añadir memoria al supervisor con un checkpointer
Decir que el supervisor es un agente corriente significa que el checkpointing que aprendiste en el capítulo 17 funciona sobre él tal cual.
from langgraph.checkpoint.memory import InMemorySaver
supervisor_agent = create_agent(
model="openai:gpt-5.4",
tools=[lookup_order, handle_refund],
system_prompt=TOOL_SUPERVISOR_PROMPT,
checkpointer=InMemorySaver(), # el checkpointer va solo en el agente de nivel superior
)
config = {"configurable": {"thread_id": "cs-1"}}
supervisor_agent.invoke(
{"messages": [HumanMessage(content="¿Dónde está mi pedido 12345?")]},
config,
)
follow_up = supervisor_agent.invoke(
{"messages": [HumanMessage(content="¿Cuánto costó ese?")]},
config,
)
print(follow_up["messages"][-1].content)Salida:
Tu pedido 12345, los Wireless Earbuds, costaba $89.El supervisor lee correctamente ese en el mensaje de seguimiento como el pedido 12345 del turno anterior. El checkpointer funciona sobre el supervisor exactamente igual.
No adjuntes un checkpointer a los agentes trabajadores (
order_agent,refund_agent). Si lo haces, un trabajador arrastrará los resultados de su llamada anterior a la actual, lo que puede interferir con la tarea que tiene entre manos. Sin uno, un trabajador se ejecuta con nada más que la solicitud del supervisor. Para los subagentes, este es el valor por defecto recomendado.
18.5) create_supervisor: la forma que encontrarás en código heredado
En bases de código existentes y tutoriales más antiguos, te toparás con el ayudante create_supervisor del paquete langgraph-supervisor. Le das una lista de agentes y un prompt, y construye por ti todo el grafo del supervisor.
from langgraph_supervisor import create_supervisor
from langchain_openai import ChatOpenAI
workflow = create_supervisor(
agents=[order_agent, refund_agent], # cada agente debe tener un name establecido
model=ChatOpenAI(model="gpt-5.4"),
prompt="Asigna las comprobaciones de pedidos a order_expert y los reembolsos a refund_expert.",
)
app = workflow.compile()create_supervisor es un ayudante que ensambla un equipo supervisor en una única llamada de función (el enfoque de handoff de 18.3). Sin embargo, no lo uses en proyectos nuevos. Es código heredado que LangChain ya no recomienda, e internamente depende de create_react_agent, que fue marcado como obsoleto en v1 (su eliminación está prevista para v2). LangChain recomienda el supervisor basado en herramientas que aprendiste en 18.4.
En este capítulo tomamos un agente que había vivido dentro de un único grafo y lo expandimos en un equipo —dividido por dominio, orquestado por un supervisor—. Construimos el mismo equipo de tres formas: el grafo manual con Command en 18.3, el enfoque de delegación mediante herramientas en 18.4, y el ayudante heredado create_supervisor en 18.5. Para el trabajo real, haz del enfoque de delegación mediante herramientas tu opción por defecto.