Módulo 8: Project The Reservo Multi Agent System
Midiendo el costo de coordinación del sistema completo
Descripción
Cuatro lecciones de conteo, cuatro fórmulas distintas: M1 L05 midió una tarea resuelta por un agente único contra un supervisor con un solo especialista (3 vs. 5 llamadas, 0 vs. 2 hops). M3 L05 midió un pipeline de tres etapas contra un supervisor que decidiera en cada una (7 vs. 10 llamadas, 2 vs. 6 hops). M4 L07 midió un fan-out secuencial contra uno paralelo (6 = 6 llamadas, 4 = 4 hops, pero 6 vs. 4 rondas). M5 L07 midió un handoff directo contra una vuelta al supervisor (4 vs. 6 llamadas, 1 vs. 3 hops). Cada una midió su propio patrón, aislado.
Esta lección construye la pieza que falta: coordination_cost, una función que generaliza las
tres convenciones —N - 1 para un pipeline (M3), HOPS_PER_SPECIALIST = 2 para un track
consultado por el supervisor (M4), el hop directo de un handoff (M5)— en una sola fórmula que
funciona sobre cualquier PLAN de tracks, sin importar cuántos ni de qué patrón. Es la única
pieza de código genuinamente nueva de todo este módulo, y la vas a probar contra las Demos A y B ya
construidas —confirmando, con una comparación honesta, en qué se parece y en qué se diferencia de
las mediciones aisladas de M3 y M4.
Conexión con el módulo
Esta lección reusa, sin cambios, count_model_calls y count_tool_calls de M1 L05, y las
convenciones de hops de M2 L06/M4 L07 (HOPS_PER_SPECIALIST = 2) y M5 L07 (el hop directo de un
handoff). Lo único nuevo es track_calls_and_hops y coordination_cost, que aplican esas
convenciones ya conocidas sobre un PLAN con varios tracks a la vez — el conteo que el Módulo 1
prometió medir para un sistema completo, y que M7 L01 explícitamente dejó pendiente para acá. La
lección 08 reusa esta misma función, sin cambios, sobre la Demo C.
Analogía: el libro de cuentas, no solo la cocina
Las lecciones 03, 04, 05 y 06 de este módulo mostraron que la cocina funciona —cada mecanismo resuelve su parte—. Pero un dueño de restaurante que quiere vender el negocio no entrega solo una cocina que funciona: entrega un libro de cuentas que dice, con números, cuánto cuesta atender cada tipo de mesa. Esta lección es ese libro de cuentas, generalizado: no un renglón distinto por cada tipo de mesa (como hicieron M3, M4 y M5, cada uno con su propia hoja de cálculo), sino una sola fórmula que funciona sin importar cuántos platos —o patrones— tenga la mesa que estás cobrando.
Ejemplo trabajado: coordination_cost, generalizando M3, M4 y M5
import ast
import concurrent.futures
from dataclasses import dataclass
import reservo_tools as rt
# ---- SPECIALISTS, run_specialist, run_pipeline, run_tracks_parallel,
# Track: idénticos a los Módulos 2, 3, 4/7 -- las mismas funciones que ya
# probaste en las lecciones 02, 03 y 04 de este módulo ----
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 search_docs(query):
q = query.lower()
if "cancela" in q:
return "[cancellation-policy] Puedes cancelar sin cargo hasta 2 horas antes del horario reservado."
if "no" in q and ("present" in q or "show" in q or "llega" in q):
return ("[no-show-policy] 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.")
return "No se encontró una política relevante para esa pregunta."
SPECIALISTS = {
"booking_agent": {"tools": {"get_quote": rt.get_quote, "book_room": rt.book_room}},
"policy_agent": {"tools": {"search_docs": search_docs}},
"pricing_agent": {"tools": {"get_quote": rt.get_quote}},
}
def run_specialist(name, task, model_script):
tools = SPECIALISTS[name]["tools"]
return run_agent_parallel(task, model_script, tools)
@dataclass
class PipelineStage:
kind: str
name: str
label: str
def last_tool_result(history):
for m in reversed(history):
if isinstance(m["content"], list):
for b in m["content"]:
if b["type"] == "tool_result":
try:
return ast.literal_eval(b["content"])
except (ValueError, SyntaxError):
return b["content"]
return None
def build_stage_task(stage, payload):
if stage.kind == "quote":
return f"Cotiza {payload['room']} {payload['tier']} {payload['hours']}h."
if stage.kind == "validate_policy":
return (f"¿Cuál es la política de cancelación para una reserva de "
f"{payload['room']} de {payload['hours']}h, antes de confirmarla?")
if stage.kind == "confirm":
if payload.get("cleared_to_book"):
return (f"Reserva {payload['room']} {payload['tier']} {payload['hours']}h "
f"para {payload['member']} -- la política de cancelación ya se validó.")
return (f"Reserva {payload['room']} {payload['tier']} {payload['hours']}h "
f"para {payload['member']}.")
raise ValueError(f"no sé armar la tarea de la etapa {stage.kind!r}")
def extract_payload(stage, history, payload):
new_payload = dict(payload)
result = last_tool_result(history)
if stage.kind == "quote":
new_payload["price_cents"] = result["price_cents"]
elif stage.kind == "validate_policy":
new_payload["cancellation_policy"] = result
new_payload["cleared_to_book"] = True
elif stage.kind == "confirm":
new_payload["booking_id"] = result["booking_id"]
new_payload["confirmed"] = result["confirmed"]
return new_payload
def run_pipeline(stages, model_scripts, initial_payload):
payload = dict(initial_payload)
trace = []
for i, stage in enumerate(stages):
task = build_stage_task(stage, payload)
final, history = run_specialist(stage.name, task, model_scripts[i])
payload = extract_payload(stage, history, payload)
trace.append({"stage": i + 1, "label": stage.label, "history": history})
return payload, trace
def run_tracks_parallel(jobs):
results = {}
with concurrent.futures.ThreadPoolExecutor(max_workers=len(jobs)) as pool:
future_to_key = {pool.submit(fn): key for key, fn in jobs.items()}
for future in concurrent.futures.as_completed(future_to_key):
key = future_to_key[future]
results[key] = future.result()
return results
@dataclass
class Track:
key: str
pattern: str # "pipeline" | "fanout" | "handoff"
description: str
def count_model_calls(history):
"""M1 L05, sin cambios: cada turno 'assistant' es una llamada al
modelo consumida del guion (concepto) -- pedir una tool, o el texto final."""
return sum(1 for m in history if m["role"] == "assistant")
def count_tool_calls(history):
"""M1 L05, sin cambios: cuenta los bloques tool_use reales despachados."""
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
PLAN_CALLS = 1 # concepto: el supervisor lee la petición y arma el PLAN de tracks
COMPOSE_CALLS = 1 # concepto: la síntesis final para el socio, combinando todos los tracks
DISPATCH_HOPS = 2 # convención M2 L06 / M4 L07: ida y vuelta del supervisor a CADA track
def track_calls_and_hops(track, result):
"""Generaliza, por patrón, cómo se cuentan las llamadas y los hops
INTERNOS de un track -- reusando exactamente las fórmulas ya medidas
en M3 (pipeline_hops = N - 1), M4 (HOPS_PER_SPECIALIST = 2) y M5 (el
hop directo de un handoff = 1)."""
if track.pattern == "pipeline":
_, trace = result
calls = sum(count_model_calls(s["history"]) for s in trace)
tool_calls = sum(count_tool_calls(s["history"]) for s in trace)
hops = DISPATCH_HOPS + (len(trace) - 1)
elif track.pattern == "fanout":
_, history = result
calls = count_model_calls(history)
tool_calls = count_tool_calls(history)
hops = DISPATCH_HOPS
elif track.pattern == "handoff":
_, trace = result
calls = sum(count_model_calls(step["history"]) for step in trace)
tool_calls = sum(count_tool_calls(step["history"]) for step in trace)
hops = DISPATCH_HOPS + 1
else:
raise ValueError(f"patrón desconocido: {track.pattern!r}")
return calls, tool_calls, hops
def coordination_cost(plan, track_results):
"""El conteo prometido en el Módulo 1, generalizado a una petición con
N tracks de patrones distintos. Devuelve llamadas al modelo, tool
calls, hops entre agentes, y rondas de coordinación (secuencial vs.
paralelo -- max() en vez de sum(), la fórmula de M4 L07, porque los
tracks corren a la vez con run_tracks_parallel)."""
per_track = {}
for t in plan:
calls, tool_calls, hops = track_calls_and_hops(t, track_results[t.key])
per_track[t.key] = {"pattern": t.pattern, "calls": calls, "tools": tool_calls, "hops": hops}
call_counts = [v["calls"] for v in per_track.values()]
total_specialist_calls = sum(call_counts)
total_tool_calls = sum(v["tools"] for v in per_track.values())
total_hops = sum(v["hops"] for v in per_track.values())
total_model_calls = PLAN_CALLS + total_specialist_calls + COMPOSE_CALLS
surrounding = PLAN_CALLS + COMPOSE_CALLS
sequential_rounds = sum(call_counts) + surrounding
parallel_rounds = (max(call_counts) if call_counts else 0) + surrounding
return {
"per_track": per_track, "total_model_calls": total_model_calls,
"total_tool_calls": total_tool_calls, "total_hops": total_hops,
"sequential_rounds": sequential_rounds, "parallel_rounds": parallel_rounds,
}
# --- Reconstruir los jobs de la Demo A (Luis, lección 03) y la Demo B
# (Marta, lección 04) -- los mismos guiones, sin ningún cambio ---
PIPELINE_STAGES = [
PipelineStage(kind="quote", name="booking_agent", label="cotizar"),
PipelineStage(kind="validate_policy", name="policy_agent", label="validar la política de cancelación"),
PipelineStage(kind="confirm", name="booking_agent", label="confirmar la reserva"),
]
INITIAL_PAYLOAD_A = {"room": "Studio", "tier": "pro", "hours": 3, "member": "Luis"}
script_quote_a = [
{"stop_reason": "tool_use", "content": [{"type": "tool_use", "id": "toolu_01", "name": "get_quote",
"input": {"room": "Studio", "tier": "pro", "hours": 3}}]},
{"stop_reason": "end_turn", "content": [{"type": "text", "text": "Studio pro 3h cuesta 9600 centavos."}]},
]
script_policy_a = [
{"stop_reason": "tool_use", "content": [{"type": "tool_use", "id": "toolu_01", "name": "search_docs",
"input": {"query": "política de cancelación"}}]},
{"stop_reason": "end_turn", "content": [{"type": "text", "text": (
"Puedes cancelar sin cargo hasta 2 horas antes del horario reservado. "
"No hay ningún impedimento para confirmar.")}]},
]
script_confirm_a = [
{"stop_reason": "tool_use", "content": [{"type": "tool_use", "id": "toolu_01", "name": "book_room",
"input": {"room": "Studio", "tier": "pro", "hours": 3, "member": "Luis"}}]},
{"stop_reason": "end_turn", "content": [{"type": "text", "text": "Reservé Studio pro 3h para Luis (confirmación #1)."}]},
]
def job_book_studio():
return run_pipeline(PIPELINE_STAGES, [script_quote_a, script_policy_a, script_confirm_a], INITIAL_PAYLOAD_A)
script_pricing_b = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "get_quote", "input": {"room": "Studio", "tier": "pro", "hours": 2}},
{"type": "tool_use", "id": "toolu_02", "name": "get_quote", "input": {"room": "Boardroom", "tier": "pro", "hours": 2}},
]},
{"stop_reason": "end_turn", "content": [{"type": "text", "text": (
"Studio pro 2h: 6400 centavos. Boardroom pro 2h: 12800 centavos. Studio es la opción más barata de las dos.")}]},
]
script_no_show_b = [
{"stop_reason": "tool_use", "content": [{"type": "tool_use", "id": "toolu_01", "name": "search_docs",
"input": {"query": "qué pasa si un socio no se presenta a una 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.")}]},
]
def job_compare_rooms():
return run_specialist("pricing_agent", "Compara Studio y Boardroom pro 2h.", script_pricing_b)
def job_no_show_policy():
return run_specialist("policy_agent", "¿Qué pasa si un socio no se presenta a una reserva confirmada?", script_no_show_b)
# --- Probar la función contra la Demo A (Luis, 1 track, pipeline) ---
PLAN_A = [Track(key="book_studio", pattern="pipeline", description="cotizar, validar, confirmar")]
results_a = run_tracks_parallel({"book_studio": job_book_studio})
cost_a = coordination_cost(PLAN_A, results_a)
print("=== costo de coordinación: Demo A (Luis, 1 track, pipeline) ===")
for t in PLAN_A:
d = cost_a["per_track"][t.key]
print(f" {t.key:<14} patrón={d['pattern']:<9} llamadas={d['calls']} tools={d['tools']} hops={d['hops']}")
print(f" TOTAL llamadas al modelo: {cost_a['total_model_calls']} "
f"(1 PLAN + {cost_a['per_track']['book_studio']['calls']} especialistas + 1 síntesis)")
print(f" TOTAL hops: {cost_a['total_hops']}")
print(f" rondas secuencial={cost_a['sequential_rounds']} vs. paralelo={cost_a['parallel_rounds']}")
# --- Probar la función contra la Demo B (Marta, 2 tracks, fan-out) ---
PLAN_B = [
Track(key="compare_rooms", pattern="fanout", description="comparar Studio y Boardroom pro 2h"),
Track(key="no_show_policy", pattern="fanout", description="pregunta general de no-presentación"),
]
results_b = run_tracks_parallel({"compare_rooms": job_compare_rooms, "no_show_policy": job_no_show_policy})
cost_b = coordination_cost(PLAN_B, results_b)
print()
print("=== costo de coordinación: Demo B (Marta, 2 tracks, fan-out) ===")
for t in PLAN_B:
d = cost_b["per_track"][t.key]
print(f" {t.key:<14} patrón={d['pattern']:<9} llamadas={d['calls']} tools={d['tools']} hops={d['hops']}")
print(f" TOTAL llamadas al modelo: {cost_b['total_model_calls']}")
print(f" TOTAL hops: {cost_b['total_hops']}")
rounds_saved = cost_b['sequential_rounds'] - cost_b['parallel_rounds']
pct = round(100 * rounds_saved / cost_b['sequential_rounds'])
print(f" rondas secuencial={cost_b['sequential_rounds']} vs. paralelo={cost_b['parallel_rounds']} "
f"({rounds_saved} menos, {pct}% menos)")
Qué esperar:
=== costo de coordinación: Demo A (Luis, 1 track, pipeline) ===
book_studio patrón=pipeline llamadas=6 tools=3 hops=4
TOTAL llamadas al modelo: 8 (1 PLAN + 6 especialistas + 1 síntesis)
TOTAL hops: 4
rondas secuencial=8 vs. paralelo=8
=== costo de coordinación: Demo B (Marta, 2 tracks, fan-out) ===
compare_rooms patrón=fanout llamadas=2 tools=2 hops=2
no_show_policy patrón=fanout llamadas=2 tools=1 hops=2
TOTAL llamadas al modelo: 6
TOTAL hops: 4
rondas secuencial=6 vs. paralelo=4 (2 menos, 33% menos)
Dos resultados para leer con cuidado. La Demo A, con un solo track, no ahorra nada de rondas
(8 = 8) — tiene sentido: max() y sum() de una lista de un solo elemento dan el mismo número,
así que correr un único track "en paralelo" no tiene ningún otro track con el que compartir el
tiempo de coordinación. La Demo B, con dos tracks, sí ahorra 2 rondas (33% menos) — el mismo
33% que ya viste en el ejemplo canónico de M4 L07, porque la estructura es idéntica: dos tracks,
dos llamadas internas cada uno.
Una comparación honesta: esta función NO reduce exactamente a M3 L05
Vale la pena ser preciso acá, porque sería fácil (y engañoso) afirmar que coordination_cost
"reproduce" los números de M3 L05 cuando se le pasa un solo track de pipeline. No lo hace, y hay
una razón real:
m3_pipeline_total = 0 + cost_a["per_track"]["book_studio"]["calls"] + 1 # 0 ruteo (M3) + especialistas + 1 síntesis
m3_pipeline_hops = 3 - 1 # N - 1, la fórmula original de M3 L05
print(f"M3 aislado: {m3_pipeline_total} llamadas, {m3_pipeline_hops} hops "
f"(sin PLAN_CALLS -- el patrón ya se sabía de antemano)")
print(f"M8 sistema: {cost_a['total_model_calls']} llamadas, {cost_a['total_hops']} hops "
f"(+1 PLAN_CALLS, +2 DISPATCH_HOPS: el supervisor SÍ tuvo que decidir el plan)")
M3 aislado: 7 llamadas, 2 hops (sin PLAN_CALLS -- el patrón ya se sabía de antemano)
M8 sistema: 8 llamadas, 4 hops (+1 PLAN_CALLS, +2 DISPATCH_HOPS: el supervisor SÍ tuvo que decidir el plan)
La diferencia es real, no un error de conteo: la lección de M3 midió un pipeline aislado,
donde el patrón ya se sabía de antemano —nadie tuvo que decidir "¿esto es un pipeline o alguna
otra cosa?"—. El sistema completo de este módulo es distinto: recibe una petición arbitraria y
tiene que decidir, primero, cuántos tracks tiene y con qué patrón se resuelve cada uno —eso es
exactamente PLAN_CALLS—, y tiene que despachar y recibir de vuelta cada track, aunque sea
uno solo —eso es DISPATCH_HOPS—. Ese costo de "decidir el plan" no existía en la lección aislada
de M3 porque, en esa lección, el plan ya venía dado. Es la misma clase de hallazgo que ya viste en
M1 L05 (coordinar cuesta, incluso cuando el trabajo real es idéntico) — ahora aplicado al costo de
decidir qué mecanismo usar, no solo al de ejecutarlo.
De dónde sale cada pieza de la fórmula
PLAN_CALLS = 1 -- el supervisor SIEMPRE arma un PLAN, sin importar
cuántos tracks resulten (0, 1, 2, o más).
COMPOSE_CALLS = 1 -- la síntesis final SIEMPRE combina lo que
produjeron los tracks, sin importar cuántos haya.
DISPATCH_HOPS = 2 -- CADA track, sin importar su patrón interno, es
despachado por el supervisor y le devuelve un
resultado -- ida y vuelta, la convención de M4 L07.
Hops INTERNOS por patrón (adentro de DISPATCH_HOPS, no en vez de):
pipeline (N etapas) -> N - 1 (traspaso directo etapa a etapa, M3 L05)
fanout (1 especialista) -> 0 (nada que traspasar puertas adentro)
handoff -> 1 (el traspaso directo emisor -> receptor, M5 L07)
Con esta tabla, verificar hops=4 de book_studio en la Demo A es directo:
DISPATCH_HOPS(2) + (3 etapas - 1) = 2 + 2 = 4. Y verificar hops=2 de cada track de la Demo B:
DISPATCH_HOPS(2) + 0 = 2, por track — 2 + 2 = 4 en total para los dos tracks de Marta.
Errores comunes
-
Esperar que
coordination_costde exactamente los mismos números que M3/M4/M5 aislados. No lo hace, a propósito — la sección anterior de esta lección lo explica: el sistema completo paga un costo real de decisión (PLAN_CALLS) que un patrón aislado, con el mecanismo ya decidido de antemano, no pagaba. -
Olvidar sumar
DISPATCH_HOPSa los hops internos de cada track. Un error común es calcular soloN - 1para un pipeline y olvidar los 2 hops de despacho/retorno del supervisor — el resultado saldría2en vez de4para la Demo A, un número que no coincide con la salida real ejecutada. -
Aplicar
max()en vez desum()para las rondas secuenciales, o viceversa. La convención de M4 L07 es clara: secuencial suma (sum(), cada track espera al anterior), paralelo acota al más largo (max(), todos corren a la vez). Invertirlas produciría un ahorro de rondas negativo, una señal inmediata de que algo está mal.
Ejercicios
Ejercicio 1: Calcula a mano el costo de un PLAN de 3 tracks, antes de ejecutar (Fácil)
Sin ejecutar código todavía, calcula a mano total_model_calls y total_hops para un PLAN con
tres tracks: uno pipeline de 4 etapas, uno fanout, y uno handoff, donde las llamadas internas
de cada uno son 8, 2 y 3 respectivamente. Después, verifica tu cálculo con código.
Ver solución
A mano: total_model_calls = PLAN_CALLS(1) + (8 + 2 + 3) + COMPOSE_CALLS(1) = 1 + 13 + 1 = 15.
total_hops = [DISPATCH_HOPS(2) + (4-1)] + [DISPATCH_HOPS(2) + 0] + [DISPATCH_HOPS(2) + 1] = 5 + 2 + 3 = 10.
per_track_hardcoded = {
"pipeline_track": {"calls": 8, "hops": 2 + (4 - 1)},
"fanout_track": {"calls": 2, "hops": 2 + 0},
"handoff_track": {"calls": 3, "hops": 2 + 1},
}
total_calls = PLAN_CALLS + sum(v["calls"] for v in per_track_hardcoded.values()) + COMPOSE_CALLS
total_hops = sum(v["hops"] for v in per_track_hardcoded.values())
print("total_model_calls:", total_calls)
print("total_hops:", total_hops)
Salida esperada:
total_model_calls: 15
total_hops: 10
Explicación: coincide con el cálculo a mano — la fórmula no hace nada distinto de sumar las
piezas ya conocidas de cada patrón, más los dos costos fijos del sistema (PLAN_CALLS,
COMPOSE_CALLS).
Ejercicio 2: ¿Cuántas rondas ahorra un PLAN de 3 tracks desparejos? (Medio)
Usando los mismos tres tracks del Ejercicio 1 (llamadas internas 8, 2 y 3), calcula las
rondas secuencial y paralelo, y el porcentaje de ahorro.
Ver solución
call_counts = [8, 2, 3]
surrounding = PLAN_CALLS + COMPOSE_CALLS
sequential_rounds = sum(call_counts) + surrounding
parallel_rounds = max(call_counts) + surrounding
saved = sequential_rounds - parallel_rounds
pct = round(100 * saved / sequential_rounds)
print(f"secuencial={sequential_rounds} paralelo={parallel_rounds} ahorro={saved} ({pct}%)")
Salida esperada:
secuencial=15 paralelo=10 ahorro=5 (33%)
Explicación: con tracks de tamaños muy desparejos (8 llamadas contra 2 y 3), el ahorro de
rondas depende del track MÁS GRANDE —el que domina el max()—. Acá, aunque hay tres tracks, el
ahorro real viene de que los tracks de 2 y 3 llamadas "se esconden" detrás del de 8, sin sumar
tiempo de coordinación extra — el mismo principio de M4 L07, ahora con tres tracks en vez de dos.
Ejercicio 3: ¿Por qué coordination_cost no necesita saber CUÁNTOS especialistas distintos hay? (Difícil)
Sin ejecutar código, explica por qué coordination_cost calcula el mismo DISPATCH_HOPS = 2 para
un track sin importar si su especialista interno es booking_agent, policy_agent o
pricing_agent — y por qué eso es una ventaja de diseño, no una limitación.
Ver solución
Porque el costo que DISPATCH_HOPS mide no depende de QUIÉN resuelve el track, depende de que
hay un track que el supervisor tuvo que despachar y recibir de vuelta — el mismo costo de
coordinación existe sin importar si el especialista detrás es simple o complejo. Es una ventaja de
diseño, no una limitación, por la misma razón que run_tracks_parallel (M4/M7) no necesita saber
qué hace cada job por dentro: la función de costo, igual que la función de despacho, opera al
nivel de "cuántos tracks hay y qué patrón resuelve cada uno" —información que SIEMPRE está
disponible en el PLAN— sin necesitar bajar al detalle de qué tools tiene cada especialista. Si
coordination_cost tuviera que conocer el registro interno de cada especialista para calcular
hops, dejaría de generalizar: cada especialista nuevo que Reservo agregara en el futuro obligaría a
tocar la función de costo, exactamente el tipo de acoplamiento que esta guía evitó desde que M2
separó el registro (SPECIALISTS) del mecanismo de despacho (run_specialist).
Resumen y siguiente paso
coordination_costgeneraliza las tres fórmulas de costo de M3, M4 y M5 en una sola función que funciona sobre cualquierPLANde tracks, sin importar cuántos ni de qué patrón.- Probada contra la Demo A: 8 llamadas, 4 hops, 0 rondas ahorradas (un solo track no tiene con quién compartir tiempo de coordinación).
- Probada contra la Demo B: 6 llamadas, 4 hops, 2 rondas ahorradas (33% menos, el mismo patrón que M4 L07 ya midió).
- El sistema completo paga un costo real —
PLAN_CALLSyDISPATCH_HOPS— que las lecciones aisladas de M3/M4/M5 no pagaban, porque en el sistema completo el patrón correcto se decide, no se asume de antemano.
Siguiente lección: 08 — Proyecto: el sistema multi-agente de Reservo. El capstone: las seis piezas de este módulo, ensambladas en un solo sistema, con la tercera demo (Valentina, los tres patrones combinados) y el cierre de toda la guía.
Recursos adicionales
- Anthropic — Multi-agent research system — Anthropic documenta ahí mismo que sus sistemas multi-agente consumen considerablemente más tokens que una conversación de un solo agente — la misma clase de costo de coordinación que esta lección cuantifica, a escala de tokens en vez de llamadas.
- Python — Funciones integradas
sum()ymax()— Las dos funciones detrás desequential_roundsyparallel_rounds, sin cambios desde M4 L07. - Python — Dataclasses — La estructura detrás de
Track, el tipo quecoordination_costrecibe como suPLAN. - Anthropic — Building effective agents — La advertencia explícita de Anthropic sobre el costo de latencia y llamadas de los sistemas multi-agente — el mismo principio que esta lección convierte en una función reusable.