Módulo 2: El patrón supervisor/router
El dispatcher ejecutado de punta a punta
Descripción
Las cinco lecciones anteriores construyeron cada pieza por separado: la anatomía de un supervisor
(02), el ruteo determinista (03), el ruteo por decisión del modelo (04), y el tercer especialista
completo (05). Esta lección las junta todas sobre un caso real, de punta a punta: tres peticiones
distintas, cada una con una intención distinta, ruteadas por route_deterministic al especialista
correcto, despachadas con run_specialist, y con el costo de coordinación de cada camino contado y
citado.
No hay ninguna pieza nueva en esta lección — es la primera vez que ves el supervisor completo del Módulo 2 funcionando de principio a fin, con las tres peticiones del ejemplo trabajado de lecciones anteriores corriendo, una tras otra, contra el registro completo de especialistas.
Conexión con el módulo
Esta lección es la síntesis ejecutada de las lecciones 02 a 05: usa SPECIALISTS y
run_specialist (02), route_deterministic (03) sin ningún cambio, y el pricing_agent recién
construido (05). No usa el ruteo por decisión del modelo (04) — las tres peticiones de esta lección
son de intención clara, exactamente el caso donde las reglas alcanzan solas. La lección 07 retoma
el caso en que no alcanzan, y construye el router híbrido que combina los dos mecanismos.
Analogía: la recepción, un día completo de trabajo
Las lecciones anteriores mostraron piezas sueltas de la recepción: el cartel de reglas, un caso donde confundió al visitante, un puesto nuevo recién habilitado. Esta lección es la recepción funcionando un día entero: tres visitantes distintos llegan, cada uno con una necesidad distinta, y el cartel de reglas los dirige a los tres pisos correctos, sin que nadie tenga que intervenir.
Ejemplo trabajado: el dispatcher completo
import concurrent.futures
import reservo_tools as rt
def dispatch_parallel(tool_use_blocks, tools):
with concurrent.futures.ThreadPoolExecutor(max_workers=len(tool_use_blocks)) as pool:
futures = [pool.submit(tools[b["name"]], **b["input"]) for b in tool_use_blocks]
results = [f.result() for f in futures]
return [
{"type": "tool_result", "tool_use_id": b["id"], "content": str(r)}
for b, r in zip(tool_use_blocks, results)
]
def run_agent_parallel(question, model_script, tools, max_iterations=10):
messages = [{"role": "user", "content": question}]
for step in range(max_iterations):
turn = model_script[step]
messages.append({"role": "assistant", "content": turn["content"]})
if turn["stop_reason"] != "tool_use":
return turn, messages
tool_result_blocks = dispatch_parallel(turn["content"], tools)
messages.append({"role": "user", "content": tool_result_blocks})
raise RuntimeError(f"max_iterations alcanzado ({max_iterations})")
def count_model_calls(history):
return sum(1 for m in history if m["role"] == "assistant")
def count_tool_calls(history):
total = 0
for m in history:
if isinstance(m["content"], list):
total += sum(1 for b in m["content"] if b["type"] == "tool_use")
return total
POLICY_DOCS = {
"no-show-policy": (
"Si un miembro no se presenta a una reserva confirmada y no cancela "
"con al menos 2 horas de anticipación, Reservo cobra el 50% del "
"precio cotizado como cargo por no-presentación."
),
"cancellation-policy": (
"Las reservas se pueden cancelar sin cargo hasta 2 horas antes del "
"horario reservado. Cancelaciones dentro de esas 2 horas aplican "
"el cargo de no-presentación."
),
}
def search_docs(query):
q = query.lower()
if "cancela" in q:
return f"[cancellation-policy] {POLICY_DOCS['cancellation-policy']}"
if "no" in q and ("present" in q or "show" in q):
return f"[no-show-policy] {POLICY_DOCS['no-show-policy']}"
return "No se encontró una política relevante para esa pregunta."
SPECIALISTS = {
"booking_agent": {
"tools": {
"list_rooms": rt.list_rooms, "get_quote": rt.get_quote,
"book_room": rt.book_room, "cancel_booking": rt.cancel_booking,
},
"expertise": "cotizar, reservar y cancelar salas",
},
"policy_agent": {
"tools": {"search_docs": search_docs},
"expertise": "responder preguntas de política (cancelación, no-presentación)",
},
"pricing_agent": {
"tools": {"get_quote": rt.get_quote},
"expertise": "comparar el costo de varias salas/tiers en una sola respuesta",
},
}
def run_specialist(name, task, model_script):
tools = SPECIALISTS[name]["tools"]
return run_agent_parallel(task, model_script, tools)
POLICY_KEYWORDS = ("política", "no-show", "no me presento", "no llego", "cargo por")
PRICING_KEYWORDS = ("compara", " vs ", "más barata", "conviene")
BOOKING_KEYWORDS = ("cotiza", "reserva", "resérva", "agenda", "cancela")
def route_deterministic(text):
t = text.lower()
if any(kw in t for kw in POLICY_KEYWORDS):
return "policy_agent"
if any(kw in t for kw in PRICING_KEYWORDS):
return "pricing_agent"
if any(kw in t for kw in BOOKING_KEYWORDS):
return "booking_agent"
return None
model_script_booking = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "get_quote",
"input": {"room": "Focus", "tier": "pro", "hours": 3}}]},
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_02", "name": "book_room",
"input": {"room": "Focus", "tier": "pro", "hours": 3, "member": "Ana"}}]},
{"stop_reason": "end_turn", "content": [
{"type": "text", "text": (
"Focus pro 3h cuesta 6000 centavos. Reservé la sala para Ana "
"(confirmación #1)."
)}]},
]
model_script_policy = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "search_docs",
"input": {"query": "qué pasa si no me presento a mi reserva"}}]},
{"stop_reason": "end_turn", "content": [
{"type": "text", "text": (
"Si no te presentas a una reserva confirmada y no cancelas con "
"al menos 2 horas de anticipación, Reservo cobra el 50% del "
"precio cotizado como cargo por no-presentación."
)}]},
]
model_script_pricing = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "get_quote",
"input": {"room": "Focus", "tier": "pro", "hours": 3}},
{"type": "tool_use", "id": "toolu_02", "name": "get_quote",
"input": {"room": "Studio", "tier": "pro", "hours": 3}},
{"type": "tool_use", "id": "toolu_03", "name": "get_quote",
"input": {"room": "Boardroom", "tier": "pro", "hours": 3}},
]},
{"stop_reason": "end_turn", "content": [
{"type": "text", "text": (
"Focus pro 3h: 6000 centavos. Studio pro 3h: 9600 centavos. "
"Boardroom pro 3h: 19200 centavos. Focus es la opción más "
"barata de las tres."
)}]},
]
REQUESTS = [
("Cotiza Focus pro 3h y resérvala para Ana.", model_script_booking),
("¿Qué pasa si no me presento a mi reserva?", model_script_policy),
("Compara el precio de Focus, Studio y Boardroom, todos pro, 3h.", model_script_pricing),
]
ROUTE_CALLS = 0 # determinista: cero llamadas al modelo para rutear
COMPOSE_CALLS = 1 # concepto: el supervisor sintetiza la respuesta final
summary = []
for text, script in REQUESTS:
target = route_deterministic(text)
print(f"petición: {text!r}")
print(f" ruteo determinista (0 llamadas al modelo) -> {target}")
final, history = run_specialist(target, text, script)
for i, m in enumerate(history):
role, content = m["role"], m["content"]
if isinstance(content, str):
continue
for block in content:
if block["type"] == "tool_use":
print(f" [{i}] {role:<9} tool_use({block['name']}): {block['input']}")
elif block["type"] == "tool_result":
print(f" [{i}] {role:<9} tool_result: {block['content']}")
elif block["type"] == "text":
print(f" [{i}] {role:<9} texto final: {block['text']!r}")
calls = count_model_calls(history)
tools_ = count_tool_calls(history)
total_calls = ROUTE_CALLS + calls + COMPOSE_CALLS
print(f" respuesta ({target}): {final['content'][0]['text']}")
print(f" llamadas al modelo: {total_calls} ({ROUTE_CALLS} ruteo + {calls} {target} + {COMPOSE_CALLS} síntesis)")
print(f" llamadas a tools: {tools_}")
print(f" hops entre agentes: 2")
print()
summary.append((text, target, total_calls, tools_, 2))
print("--- resumen ---")
labels = ["cotiza + reserva Focus pro 3h", "política de no-presentación", "compara Focus/Studio/Boardroom"]
print(f"{'petición':32}{'agente':16}{'modelo':>8}{'tools':>7}{'hops':>6}")
for (text, target, calls, tools_, hops), label in zip(summary, labels):
print(f"{label:32}{target:16}{calls:>8}{tools_:>7}{hops:>6}")
Qué esperar:
petición: 'Cotiza Focus pro 3h y resérvala para Ana.'
ruteo determinista (0 llamadas al modelo) -> booking_agent
[1] assistant tool_use(get_quote): {'room': 'Focus', 'tier': 'pro', 'hours': 3}
[2] user tool_result: {'price_cents': 6000}
[3] assistant tool_use(book_room): {'room': 'Focus', 'tier': 'pro', 'hours': 3, 'member': 'Ana'}
[4] user tool_result: {'booking_id': 1, 'confirmed': True}
[5] assistant texto final: 'Focus pro 3h cuesta 6000 centavos. Reservé la sala para Ana (confirmación #1).'
respuesta (booking_agent): Focus pro 3h cuesta 6000 centavos. Reservé la sala para Ana (confirmación #1).
llamadas al modelo: 4 (0 ruteo + 3 booking_agent + 1 síntesis)
llamadas a tools: 2
hops entre agentes: 2
petición: '¿Qué pasa si no me presento a mi reserva?'
ruteo determinista (0 llamadas al modelo) -> policy_agent
[1] assistant tool_use(search_docs): {'query': 'qué pasa si no me presento a mi reserva'}
[2] user tool_result: [no-show-policy] Si un miembro no se presenta a una reserva confirmada y no cancela con al menos 2 horas de anticipación, Reservo cobra el 50% del precio cotizado como cargo por no-presentación.
[3] assistant texto final: 'Si no te presentas a una reserva confirmada y no cancelas con al menos 2 horas de anticipación, Reservo cobra el 50% del precio cotizado como cargo por no-presentación.'
respuesta (policy_agent): Si no te presentas a una reserva confirmada y no cancelas con al menos 2 horas de anticipación, Reservo cobra el 50% del precio cotizado como cargo por no-presentación.
llamadas al modelo: 3 (0 ruteo + 2 policy_agent + 1 síntesis)
llamadas a tools: 1
hops entre agentes: 2
petición: 'Compara el precio de Focus, Studio y Boardroom, todos pro, 3h.'
ruteo determinista (0 llamadas al modelo) -> pricing_agent
[1] assistant tool_use(get_quote): {'room': 'Focus', 'tier': 'pro', 'hours': 3}
[1] assistant tool_use(get_quote): {'room': 'Studio', 'tier': 'pro', 'hours': 3}
[1] assistant tool_use(get_quote): {'room': 'Boardroom', 'tier': 'pro', 'hours': 3}
[2] user tool_result: {'price_cents': 6000}
[2] user tool_result: {'price_cents': 9600}
[2] user tool_result: {'price_cents': 19200}
[3] assistant texto final: 'Focus pro 3h: 6000 centavos. Studio pro 3h: 9600 centavos. Boardroom pro 3h: 19200 centavos. Focus es la opción más barata de las tres.'
respuesta (pricing_agent): Focus pro 3h: 6000 centavos. Studio pro 3h: 9600 centavos. Boardroom pro 3h: 19200 centavos. Focus es la opción más barata de las tres.
llamadas al modelo: 3 (0 ruteo + 2 pricing_agent + 1 síntesis)
llamadas a tools: 3
hops entre agentes: 2
--- resumen ---
petición agente modelo tools hops
cotiza + reserva Focus pro 3h booking_agent 4 2 2
política de no-presentación policy_agent 3 1 2
compara Focus/Studio/Boardroom pricing_agent 3 3 2
Tres peticiones, tres agentes distintos, tres rutas correctas — todas decididas sin gastar una sola
llamada al modelo en el Paso 2 (decidir). El único costo de coordinación en este dispatcher es la
síntesis final (COMPOSE_CALLS = 1, concepto) más el trabajo interno de cada especialista, que
varía según cuántos pasos necesita: 3 para booking_agent (cotizar, reservar, responder), 2 para
policy_agent y pricing_agent (un turno de tools, un turno de texto).
El ahorro concreto: compara con el Módulo 1, lección 05
El Módulo 1, lección 05, midió el mismo tipo de tarea —"Cotiza Focus pro 3h y resérvala para Ana"— con un supervisor que siempre ruteaba por decisión del modelo, porque solo existía un especialista posible:
M1 L05 (ruteo SIEMPRE por decisión del modelo): 1 ruteo + 3 booking_agent + 1 síntesis = 5 llamadas
M2 L06 (ruteo determinista, esta misma tarea): 0 ruteo + 3 booking_agent + 1 síntesis = 4 llamadas
Una llamada menos, exactamente el costo de rutear, ahorrado — sin perder nada de exactitud, porque la tarea es de intención clara y las reglas la resuelven perfecto. Esta es la primera vez en la guía que un router determinista se compara, número contra número, con un router por decisión del modelo sobre la misma tarea exacta. El resultado confirma lo que la lección 03 ya adelantaba: cuando las reglas alcanzan, cuestan menos sin perder nada.
Errores comunes
-
Pensar que el dispatcher de esta lección maneja cualquier petición. Solo maneja las que
route_deterministicsabe rutear — una petición como la del "wifi" de la lección 03 rompería este dispatcher tal como está, porqueSPECIALISTS[None]no existe. La lección 07 lo resuelve. -
Correr las tres peticiones en el mismo proceso sin reiniciar el estado entre lecciones. Si ejecutas el código de esta lección después de haber corrido otro ejemplo de la guía en el mismo intérprete,
book_roompuede devolver unbooking_iddistinto a1— un desajuste real con el texto del guion, no un error del dispatcher. Cada lección asume su propio Reservo desechable, recién iniciado. -
Sumar mal el costo total.
ROUTE_CALLS + calls + COMPOSE_CALLSno es "cuántas tool calls hizo el especialista" — mezclar esos dos números lleva a comparaciones erróneas entre peticiones. Este dispatcher los reporta por separado a propósito (llamadas al modelovs.llamadas a tools). -
Generalizar el ahorro de "1 llamada menos" a cualquier volumen de tráfico. Esta comparación es para una petición. La lección 07 escala el cálculo a un volumen diario real, donde el ahorro puede ser mucho mayor —o, si la mayoría de las peticiones son ambiguas, mucho menor.
-
Olvidar que estos tres agentes siguen corriendo, cada uno, el mismo runner sin cambios. Nada en
booking_agent,policy_agentnipricing_agentsabe que está siendo ruteado por un supervisor — cada uno simplemente recibe una tarea y un guion, y ejecuta su loop de siempre. La novedad de este módulo vive enteramente en lo que rodea a cada agente, no dentro de ellos.
Ejercicios
Ejercicio 1: Confirma tu propia ejecución (Fácil)
Ejecuta el dispatcher completo de esta lección tú mismo y confirma, línea por línea, que tu salida
coincide con el "Qué esperar" de arriba. Presta especial atención a los tres booking_id/precios y
a los totales de llamadas de la tabla resumen.
Ver solución
No hay una única "solución de código" para este ejercicio — es una verificación: si tu salida coincide exactamente con el bloque "Qué esperar" del ejemplo trabajado, tu Reservo desechable arrancó limpio y el dispatcher corrió sin desvíos.
Ejercicio 2: ¿Qué pasa con una petición que no rutea? (Medio)
Ejecuta route_deterministic("Hola, ¿tienen wifi en las salas?") y explica, sin ejecutar el
dispatcher completo sobre esa petición, qué pasaría si intentaras despacharla tal como está escrito
el código de esta lección.
Ver solución
task_none = "Hola, ¿tienen wifi en las salas?"
target_none = route_deterministic(task_none)
print(f"route_deterministic({task_none!r}) -> {target_none!r}")
Salida esperada:
route_deterministic('Hola, ¿tienen wifi en las salas?') -> None
Explicación: el dispatcher de esta lección no tiene ningún caso para target = None —
intentar run_specialist(None, ...) fallaría con un KeyError, porque SPECIALISTS[None] no
existe en el registro. Este dispatcher, tal como está construido acá, asume implícitamente que
route_deterministic siempre encuentra un especialista — una suposición que la lección 07 corrige
con el router híbrido.
Ejercicio 3: El mismo dispatcher, otra sala y otras horas (Difícil)
Agrega una cuarta petición al dispatcher: "Cotiza Boardroom pro 4h y resérvala para Sofía." con su guion correspondiente. Ejecutala a través del mismo flujo de ruteo + despacho + conteo, y confirma el precio calculado a mano.
Ver solución
task_ex3 = "Cotiza Boardroom pro 4h y resérvala para Sofía."
script_ex3 = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "get_quote",
"input": {"room": "Boardroom", "tier": "pro", "hours": 4}}]},
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_02", "name": "book_room",
"input": {"room": "Boardroom", "tier": "pro", "hours": 4, "member": "Sofía"}}]},
{"stop_reason": "end_turn", "content": [
{"type": "text", "text": "Boardroom pro 4h cuesta 25600 centavos. Reservé la sala para Sofía (confirmación #1)."}]},
]
target_ex3 = route_deterministic(task_ex3)
final_ex3, hist_ex3 = run_specialist(target_ex3, task_ex3, script_ex3)
calls_ex3 = count_model_calls(hist_ex3)
tools_ex3 = count_tool_calls(hist_ex3)
print(f"ruteo -> {target_ex3}")
print("respuesta:", final_ex3["content"][0]["text"])
print(f"llamadas al modelo: {0 + calls_ex3 + 1} (0 ruteo + {calls_ex3} {target_ex3} + 1 síntesis)")
print(f"llamadas a tools: {tools_ex3}")
print("cálculo a mano Boardroom pro 4h:", 8000 * 4 * 80 // 100)
Salida esperada:
ruteo -> booking_agent
respuesta: Boardroom pro 4h cuesta 25600 centavos. Reservé la sala para Sofía (confirmación #1).
llamadas al modelo: 4 (0 ruteo + 3 booking_agent + 1 síntesis)
llamadas a tools: 2
cálculo a mano Boardroom pro 4h: 25600
Explicación: la estructura de costo es idéntica a la de "Cotiza Focus pro 3h" del ejemplo
trabajado —4 llamadas al modelo, 2 tool calls— porque el número de pasos internos de
booking_agent (cotizar, reservar, responder) no depende de qué sala ni de cuántas horas se pidan,
solo de la forma de la tarea. 25600 = 8000 * 4 * 80 // 100 confirma el precio.
Resumen y siguiente paso
- Ejecutamos el dispatcher completo del Módulo 2: tres peticiones, cada una ruteada por
route_deterministic, despachada conrun_specialist, sobre los tres especialistas terminados de las lecciones anteriores. - Los números reales:
booking_agent(cotizar + reservar) costó 4 llamadas al modelo y 2 tool calls;policy_agentypricing_agentcostaron 3 llamadas al modelo cada uno, con 1 y 3 tool calls respectivamente. Las tres, 0 llamadas al modelo para rutear. - Comparado con el supervisor del Módulo 1, lección 05 —que siempre ruteaba por decisión del modelo— este dispatcher ahorra 1 llamada exacta en la misma tarea de reservar, sin perder nada de exactitud.
- El dispatcher de esta lección asume que
route_deterministicsiempre encuentra un especialista — una petición que devuelveNonelo rompe. La lección 07 lo arregla.
Siguiente lección: 07 — Eligiendo tu router. Comparamos determinista y decisión del modelo con números, y construimos un router híbrido — con sus límites reales, no idealizados.
Recursos adicionales
- Anthropic — Multi-agent research system — Un orquestador real decidiendo, sin llamadas de más, a qué sub-agente delegar cada parte de una tarea — la misma disciplina de costo medido de esta lección.
- Anthropic — Building effective agents — El principio de usar el mecanismo más simple que resuelva la tarea, antes de sumar complejidad — el resultado de esta lección lo confirma con números.
- Anthropic — Messages API reference — La forma exacta de
tool_use/tool_result/stop_reasonque cada especialista de este dispatcher respeta, sin cambios. - Python —
concurrent.futures— El módulo detrás dedispatch_parallel, corriendo sin cambios dentro de cada uno de los tres especialistas.