← Todo el material
Curso Avanzado 20 de agosto de 2026

LangGraph — Módulo 9: sistemas multi-agente

Noveno módulo del curso de LangGraph: cuándo dividir un agente en varios. Subgrafos como nodos, el patrón supervisor con subagentes como tools, y handoffs con Command(goto, graph=Command.PARENT).

☰ Índice del contenido

Arranca el bloque avanzado del Curso completo de LangGraph. Hasta aquí construimos un agente cada vez mejor equipado. Hoy vemos qué hacer cuando ese agente se vuelve demasiado grande — y, tanto o más importante, cómo darte cuenta de que todavía no lo es.

La tesis del módulo

Multi-agente no es una mejora: es una decisión de arquitectura con un costo. Divides cuando un solo agente ya no rinde —demasiadas tools, demasiadas instrucciones contradictorias— no cuando suena sofisticado. Y cuando divides, en LangGraph los agentes no son un concepto nuevo: son grafos dentro de grafos.

Primero: no dividas todavía

El síntoma real que justifica dividir es concreto: el agente elige mal la tool. Con 25 herramientas en el prompt, el modelo empieza a confundir buscar_cliente con buscar_pedido. El prompt de sistema se llena de excepciones (“si es de facturación, entonces… salvo que…”). La precisión cae y cada cambio rompe otra cosa.

Antes de llegar a multi-agente, prueba lo barato: mejores docstrings, menos tools, un prompt más corto. Un solo agente bien afinado le gana a tres agentes mal coordinados casi siempre.

Lo que un sistema multi-agente te cuesta: más llamadas al modelo (más latencia y más plata), un flujo más difícil de depurar, y un problema nuevo — decidir qué información se pasa entre agentes.

La pieza base: el subgrafo

Un subgrafo es un grafo compilado que usas como nodo dentro de otro grafo. Eso es todo el concepto. Y como un agente ya es un grafo compilado, un agente es un subgrafo.

Si el padre y el subgrafo comparten claves de estado, lo pasas directo a add_node:

from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START

class State(TypedDict):
    tema: str

# El subgrafo
sub = StateGraph(State)
sub.add_node("investigar", investigar)
sub.add_edge(START, "investigar")
subgrafo = sub.compile()

# El padre lo usa como un nodo más
padre = StateGraph(State)
padre.add_node("investigacion", subgrafo)   # ← un grafo compilado como nodo
padre.add_edge(START, "investigacion")
graph = padre.compile()

Si los esquemas de estado no comparten claves, no puedes pasarlo directo: necesitas una función que traduzca en ambas direcciones.

class EstadoPadre(TypedDict):
    tema: str

class EstadoSub(TypedDict):
    consulta: str

def llamar_subgrafo(state: EstadoPadre) -> dict:
    salida = subgrafo.invoke({"consulta": state["tema"]})   # traducir hacia adentro
    return {"tema": salida["consulta"]}                      # y de vuelta hacia afuera

padre.add_node("investigacion", llamar_subgrafo)

Esa función traductora no es burocracia: es el contrato entre los dos agentes. Escribirla te obliga a decidir explícitamente qué información cruza la frontera — que es justamente la decisión difícil de un sistema multi-agente.

Para depurar: graph.get_state(config, subgraphs=True) te muestra también el estado interno de los subgrafos, y en el streaming puedes filtrar por namespace para saber de qué subgrafo viene cada evento. Sin esto, un sistema multi-agente es una caja negra dentro de otra caja negra.

Patrón 1: supervisor con subagentes como tools

El patrón más usado y el más fácil de razonar. Un agente central —el supervisor— tiene como herramientas a los demás agentes. Decide a quién llamar, recibe el resultado y sigue.

from langchain.tools import tool
from langchain.agents import create_agent

agente_investigador = create_agent(model=MODELO, tools=[buscar_web, leer_pagina])
agente_redactor = create_agent(model=MODELO, tools=[revisar_estilo])

@tool("investigar", description="Investiga un tema y devuelve los hallazgos con fuentes.")
def llamar_investigador(consulta: str) -> str:
    r = agente_investigador.invoke({"messages": [{"role": "user", "content": consulta}]})
    return r["messages"][-1].content

@tool("redactar", description="Redacta un texto final a partir de hallazgos ya investigados.")
def llamar_redactor(instruccion: str) -> str:
    r = agente_redactor.invoke({"messages": [{"role": "user", "content": instruccion}]})
    return r["messages"][-1].content

supervisor = create_agent(
    model=MODELO,
    tools=[llamar_investigador, llamar_redactor],
)
                 ┌──────────────┐
                 │  supervisor  │
                 └──┬────────┬──┘
          investigar│        │redactar
                    ▼        ▼
            investigador   redactor

Fíjate en lo que realmente pasa: es el ciclo ReAct del Módulo 5. No hay maquinaria nueva; lo que cambió es que detrás de la tool hay un agente completo en vez de una función.

Dos propiedades importantes de este patrón:

  • Los subagentes son efímeros. Cada llamada empieza limpia; no recuerdan turnos anteriores. La memoria de la conversación vive en el supervisor.
  • Solo cruza texto. El subagente devuelve su última respuesta como string. Eso es una ventaja (aísla el ruido: el supervisor no ve los 30 mensajes internos del investigador) y una limitación (si necesitas estructura, devuélvela como JSON y parséala).

Si tienes muchos agentes, en vez de una tool por cada uno usa una sola tool parametrizada — así el prompt no crece:

SUBAGENTES = {"investigador": agente_investigador, "redactor": agente_redactor}

@tool
def delegar(agente: str, tarea: str) -> str:
    """Delega una tarea a un agente especialista: 'investigador' o 'redactor'."""
    r = SUBAGENTES[agente].invoke({"messages": [{"role": "user", "content": tarea}]})
    return r["messages"][-1].content

Patrón 2: handoffs (transferencia entre pares)

En el supervisor, el control siempre vuelve al centro. En un handoff el control se transfiere: el agente de ventas le pasa la conversación al de soporte y se sale del camino. Es el modelo de un call center.

La pieza clave es que una tool devuelva un Command que redirige en el grafo padre:

from langchain.messages import AIMessage, ToolMessage
from langchain.tools import tool, ToolRuntime
from langgraph.types import Command

@tool
def transferir_a_soporte(runtime: ToolRuntime) -> Command:
    """Transfiere la conversación al agente de soporte técnico."""
    ultimo_ai = next(
        m for m in reversed(runtime.state["messages"]) if isinstance(m, AIMessage)
    )
    aviso = ToolMessage(
        content="Transferido al agente de soporte.",
        tool_call_id=runtime.tool_call_id,
    )
    return Command(
        goto="agente_soporte",                       # a qué nodo ir
        update={"messages": [ultimo_ai, aviso]},     # qué contexto llevar
        graph=Command.PARENT,                        # el destino está en el grafo padre
    )

Tres cosas que importan aquí:

  1. Command es un objeto que un nodo (o tool) devuelve para hacer dos cosas a la vez: actualizar el estado (update) y decidir a dónde ir (goto). Es la alternativa a las aristas condicionales cuando la decisión nace dentro del nodo.
  2. graph=Command.PARENT es lo que hace posible el handoff: sin eso, goto buscaría el nodo dentro del propio agente, no en el grafo que lo contiene.
  3. El par AIMessage + ToolMessage. Toda tool call necesita su respuesta (lo vimos en el Módulo 7). Si transfieres sin cerrar la tool call, el historial queda inválido y el proveedor devuelve error.

El grafo padre coordina a los agentes como nodos hermanos, y una clave del estado —agente_activo— recuerda quién tiene el control:

builder = StateGraph(EstadoMulti)
builder.add_node("agente_ventas", llamar_ventas)
builder.add_node("agente_soporte", llamar_soporte)
builder.add_conditional_edges(START, ruta_inicial, ["agente_ventas", "agente_soporte"])
builder.add_conditional_edges("agente_ventas", tras_agente, ["agente_ventas", "agente_soporte", END])
builder.add_conditional_edges("agente_soporte", tras_agente, ["agente_ventas", "agente_soporte", END])

Cuál usar

SupervisorHandoff
ControlVuelve siempre al centroSe transfiere
ContextoAislado (solo pasa texto)Compartido (el historial viaja)
DepurarFácil: un solo coordinadorMás difícil: control distribuido
CuándoTareas que se descomponen (investigar → redactar)Conversaciones que cambian de dominio

Empieza siempre por supervisor. Es más fácil de razonar, más fácil de depurar y cubre la mayoría de los casos. Pasa a handoffs cuando de verdad necesites que el contexto conversacional completo siga con el usuario a través de especialistas.

Errores comunes

  • Dividir antes de tiempo. Tres agentes mediocres son peores que uno bueno. Divide por síntoma medido, no por estética.
  • Pasar demasiado contexto entre agentes. Si le mandas el historial completo a cada subagente, pagas el aislamiento con tokens y confusión.
  • Pasar muy poco. El subagente responde algo inútil porque no sabía para qué se lo preguntaban. Ese equilibrio es el trabajo de diseño.
  • Handoff sin cerrar la tool call. Falta el ToolMessage y la siguiente llamada al modelo falla.
  • Olvidar graph=Command.PARENT. El goto no encuentra el nodo y el handoff no ocurre.
  • Ciclos entre agentes. Ventas transfiere a soporte, soporte devuelve a ventas, infinito. Aplica el recursion_limit del Módulo 4 — pero la solución real es una condición de salida.

La frase para llevarse

“Un agente no se divide porque sea complejo, sino porque ya se equivoca de forma medible. Y cuando lo divides, el trabajo difícil no es separarlo: es decidir qué se dicen entre ellos.”

Qué viene

En el Módulo 10, la memoria que falta. El checkpointer del Módulo 6 recuerda dentro de un thread; el Store recuerda entre threads: lo que sabes de un usuario y que sigue siendo cierto la próxima semana.

Material relacionado

LangGraphPythonMulti-agenteSubgrafosSupervisor

Preguntas sobre este curso

Regístrate gratis para hacer preguntas y guardar tu progreso.

✉ Entérate de lo nuevo

Recibe un aviso cuando publique nuevos cursos, workshops o investigaciones. Sin spam.