Módulo 1: Por qué multi-agente (y cuándo no)
Cuándo multi-agente SÍ ayuda
Descripción
La lección 05 midió el peor caso: una tarea que un solo agente ya resuelve bien, resuelta también por un sistema de dos agentes — y el sistema multi-agente perdió, con números reales, sin ganar nada a cambio. Sería un error leer esa lección como "nunca uses multi-agente". Esta lección construye el caso contrario, con el mismo rigor: una tarea que sí justifica repartir el trabajo, y por qué el mismo costo de coordinación que perdió en la lección 05 esta vez vale la pena pagarlo.
Vas a ejecutar tres señales concretas, cada una con su propia medición: separabilidad genuina
(dos sub-preguntas que no dependen la una de la otra, y por lo tanto pueden resolverse en
paralelo, no en fila), expertise distinto (tools cuyo input_schema nunca puede colisionar,
porque viven en dominios de datos completamente distintos), y aislamiento de contexto
(nombrado, no construido — la razón por la que dos dominios muy distintos conviene que no
compartan la misma ventana). El caso: booking_agent y un policy_agent nuevo, con su tool
search_docs — el stub mínimo que representa, sin reconstruirlo, el índice real de
production-rag-and-document-ingestion-guide.
Conexión con el módulo
Esta lección cierra el círculo abierto por la 04: ahí, repartir CANONICAL de SPRAWL no seguía
ninguna frontera de dominio real, y el ahorro de bytes no alcanzaba a justificar, por sí solo, el
costo de coordinación de la lección 03. Acá, el reparto entre booking_agent y policy_agent sí
sigue una frontera de dominio genuina, y vas a ver cómo eso cambia la cuenta completa. La lección
07 toma este mismo par de agentes como el caso de entrada al patrón de fan-out del Módulo 4.
Analogía: el especialista de logística y el abogado de contratos
Vuelve al equipo de personas de la lección 01. Organizar un evento necesita, entre otras cosas, reservar el salón y revisar la cláusula de cancelación del contrato con el proveedor. Ninguna de las dos tareas depende del resultado de la otra —puedes reservar el salón mientras alguien más revisa el contrato, al mismo tiempo, sin esperarse—. Y ninguna de las dos personas necesita saber lo que la otra sabe: quien reserva el salón no necesita entender cláusulas legales, y el abogado de contratos no necesita saber cuántas sillas caben en el salón. Cuando las dos condiciones se cumplen a la vez —el trabajo se separa limpio, y cada parte pide un conocimiento que la otra no tiene—, contratar a dos especialistas no agrega una reunión innecesaria: evita que una sola persona tenga que dominar dos oficios distintos, y deja que las dos partes avancen a la vez.
Señal 1: separabilidad — la tarea se reparte en sub-preguntas independientes
La pregunta clave para esta señal: ¿el resultado de una sub-tarea depende del resultado de la
otra? En la tarea de la lección 05 ("cotiza y resérvala"), la respuesta era sí — book_room
necesitaba el precio de get_quote antes de poder ejecutarse, así que las dos tool calls tenían
que ir en secuencia. En la tarea de esta lección, la respuesta es no.
import reservo_tools as rt
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):
"""STUB mínimo -- 2 entradas fijas por coincidencia de palabra clave.
Sustituto EXPLÍCITO de la tool real de
production-rag-and-document-ingestion-guide (índice BM25 sobre 57
chunks/13 docs); aquí solo alcanza para que la orquestación de este
módulo se ejecute de punta a punta."""
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."
print(search_docs("¿cuál es la política de cancelación?"))
print(search_docs("¿qué pasa si no me presento a mi reserva?"))
print(search_docs("¿aceptan tarjeta corporativa?"))
Qué esperar:
[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.
[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.
No se encontró una política relevante para esa pregunta.
Con search_docs funcionando, ahora despachamos la tarea compuesta "Cotiza Focus pro 3h y dime
la política de cancelación" — get_quote y search_docs en el mismo turno, en paralelo,
igual que el fan-out con hilos de agent-fundamentals M5 L05.
def dispatch_parallel(tool_use_blocks, tools):
import concurrent.futures
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)
]
TOOLS_FANOUT = {"get_quote": rt.get_quote, "search_docs": search_docs}
turn = [
{"type": "tool_use", "id": "toolu_01", "name": "get_quote",
"input": {"room": "Focus", "tier": "pro", "hours": 3}},
{"type": "tool_use", "id": "toolu_02", "name": "search_docs",
"input": {"query": "política de cancelación"}},
]
results = dispatch_parallel(turn, TOOLS_FANOUT)
for r in results:
print(r)
Qué esperar:
{'type': 'tool_result', 'tool_use_id': 'toolu_01', 'content': "{'price_cents': 6000}"}
{'type': 'tool_result', 'tool_use_id': 'toolu_02', 'content': '[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.'}
Compara con la lección 05: ahí, get_quote y book_room no podían despacharse juntos en un
mismo turno —book_room necesitaba el price_cents que get_quote todavía no había devuelto—.
Acá, get_quote y search_docs sí, porque ninguno de los dos necesita nada del otro. Esta es la
separabilidad genuina: no es que "hay dos preguntas", es que ninguna depende del resultado de
la otra. El Módulo 4 de esta guía construye el patrón de fan-out completo sobre exactamente
esta propiedad.
Señal 2: expertise distinto — tools que nunca pueden colisionar
La lección 04 midió el costo de tools que colisionan en input_schema — el problema de fondo de
SPRAWL. Con booking_agent y policy_agent, el riesgo de colisión desaparece por diseño,
no por cuidado adicional: los input_schema de los dos dominios son estructuralmente distintos.
import json
import itertools
BOOKING_TOOLS = [
{"name": "get_quote", "description": "Cotiza el precio de una sala.",
"input_schema": {"type": "object", "properties": {
"room": {"type": "string"}, "tier": {"type": "string"},
"hours": {"type": "integer"}}, "required": ["room", "tier", "hours"]}},
{"name": "book_room", "description": "Reserva una sala.",
"input_schema": {"type": "object", "properties": {
"room": {"type": "string"}, "tier": {"type": "string"},
"hours": {"type": "integer"}, "member": {"type": "string"}},
"required": ["room", "tier", "hours", "member"]}},
]
SEARCH_DOCS_TOOL = {
"name": "search_docs",
"description": "Busca en la base de políticas de Reservo un fragmento relevante.",
"input_schema": {
"type": "object", "properties": {"query": {"type": "string"}},
"required": ["query"],
},
}
def schema_collisions(tools):
pairs = itertools.combinations(tools, 2)
return sum(1 for a, b in pairs if a["input_schema"] == b["input_schema"])
merged = BOOKING_TOOLS + [SEARCH_DOCS_TOOL]
print("colisiones si se declaran juntas en un agente:", schema_collisions(merged))
print("input_schema de get_quote:", set(BOOKING_TOOLS[0]["input_schema"]["properties"]))
print("input_schema de search_docs:", set(SEARCH_DOCS_TOOL["input_schema"]["properties"]))
Qué esperar:
colisiones si se declaran juntas en un agente: 0
input_schema de get_quote: {'room', 'tier', 'hours'}
input_schema de search_docs: {'query'}
Cero colisiones, incluso fusionadas en un solo agente. Esta es una diferencia importante con
la lección 04: ahí, el argumento para repartir era reducir colisiones que ya existían. Acá,
nunca hubo ni va a haber colisión posible —get_quote pide números estructurados
(room/tier/hours); search_docs pide una sola cadena de texto libre (query)—. El
argumento para repartir booking_agent de policy_agent no es "menos confusión de schema" —
ya viste que es cero de cualquier forma—. Es la separabilidad de la Señal 1, y la Señal 3, que
sigue.
Señal 3: aislamiento de contexto (nombrada, no construida aquí)
La tercera señal no se mide con bytes ni con conteo de colisiones — se nombra. policy_agent
podría, en una versión de producción real, tener acceso al contenido completo de los
documentos de política de Reservo —no solo las 2-3 entradas de este stub, sino el corpus real,
los 57 chunks de production-rag-and-document-ingestion-guide—. Si esa cantidad de texto viviera
en el mismo contexto que booking_agent, cada llamada de booking_agent —incluso una tan
simple como "cotiza Focus pro 3h"— cargaría con documentos de política que nunca va a usar.
Aislar cada especialista en su propio agente, con su propia ventana de contexto, evita ese
"ruido" — cada agente solo ve lo que su expertise necesita.
Presupuestar y comprimir el contenido dentro de esa ventana —qué entra exactamente en el
system/messages de policy_agent, cómo se resume un corpus grande, cuánto cuesta en tokens—
es terreno de context-engineering-guide, no de esta lección: aquí la ventana aislada ya existe
por construcción (cada agente es un proceso lógico separado, con su propio historial), pero cómo
se llena esa ventana con cuidado es una guía hermana completa.
La cuenta completa: costo medido, beneficio real
Con las tres señales sobre la mesa, la cuenta de esta tarea se ve distinta a la de la lección 05. El costo de coordinación sigue siendo el mismo tipo de costo —más llamadas, más hops— pero ahora se compara contra beneficios reales, no contra cero:
Sistema A (1 agente, 5 tools) Sistema B (2 agentes)
llamadas al modelo ~3 ~5-6
bytes declarados por request 1986 1621 (booking) / 365 (policy)
sub-tareas en paralelo no aplica SÍ -- get_quote y search_docs
riesgo de colisión de schema 0 (ya sano) 0 (estructuralmente imposible)
contexto de policy_agent en booking_agent SIEMPRE presente NUNCA presente
La fila que cambia la decisión frente a la lección 05 es la de paralelo: en la tarea de la
lección 05, no había nada que paralelizar —book_room dependía de get_quote—, así que el costo
de coordinación no compraba ninguna reducción de latencia. Acá, las dos sub-tareas SÍ corren en
paralelo (lo ejecutaste arriba, con dispatch_parallel), así que parte del costo de coordinación
se paga a cambio de no esperar en fila — un beneficio que la lección 05 nunca tuvo la
oportunidad de ofrecer.
Errores comunes
-
Pensar que "menos colisiones de schema" es la razón para separar
booking_agentdepolicy_agent. Ya viste que las colisiones son cero de cualquier forma, fusionadas o no. La razón real es la separabilidad (Señal 1) y el aislamiento de contexto (Señal 3) — no la Señal 2, que en este caso concreto no aporta nada nuevo por sí sola. -
Confundir "el stub de
search_docsfunciona" con "elpolicy_agentestá completo". Este stub tiene 2-3 entradas fijas por palabra clave — es un sustituto explícito y mínimo del índice BM25 real deproduction-rag-and-document-ingestion-guide, no una reimplementación. Sirve para que la orquestación se ejecute de punta a punta; no sirve para responder preguntas de política que no calcen con esas palabras clave. -
Pensar que toda tarea con dos sub-preguntas es automáticamente separable. La tarea de la lección 05 ("cotiza Y reserva") también tenía dos partes, pero NO eran independientes — la señal correcta no es "cuántas partes tiene la tarea", es "¿el resultado de una depende del resultado de la otra?".
-
Construir el aislamiento de contexto en vez de solo nombrarlo. Esta lección deliberadamente no presupuesta ni comprime nada dentro de la ventana de
policy_agent— eso escontext-engineering-guide. Confundir "cada agente tiene su propia ventana" (aquí, gratis, por construcción) con "gestionar el contenido de esa ventana con cuidado" (allá, una guía entera) es perder la frontera. -
Olvidar que las tres señales no siempre aparecen juntas. El escenario de esta lección tiene las tres a la vez —caso fuerte—. Un escenario real puede tener solo una o dos; el Módulo 1 no exige las tres para justificar multi-agente, pero cuantas más señales aparezcan, más fuerte es el caso (retomado con una función de decisión en la lección 08).
Ejercicios
Ejercicio 1: Identifica las señales en un escenario nuevo (Fácil)
Reservo agrega un feedback_agent (el del Ejercicio 2 de la lección 02) que recopila la
satisfacción de un socio después de su reserva, con una tool record_feedback(booking_id, stars, comment). Para una tarea compuesta "reserva Boardroom pro 2h para Sofía y, cuando termine,
pregúntale su feedback": (a) ¿la Señal 1 (separabilidad) se cumple entre book_room y
record_feedback? (b) ¿la Señal 2 (expertise distinto) se cumple? Justifica comparando los
input_schema de las dos tools.
Ver solución
(a) No se cumple — al menos no en el sentido de "independientes". record_feedback necesita
el booking_id que solo existe después de que book_room se ejecutó — hay una dependencia
de datos real entre las dos, igual que book_room dependía del precio de get_quote en la
lección 05. No pueden despacharse en el mismo turno en paralelo; tienen que ir en secuencia (una
señal de que este caso, más que fan-out, podría ser un candidato para el patrón pipeline del
Módulo 3 — la salida de una alimenta directamente a la siguiente, en orden fijo).
(b) Sí se cumple. book_room pide {room, tier, hours, member}; record_feedback pide
{booking_id, stars, comment} — ningún campo se repite entre los dos, y las propiedades no
colisionan. Que la Señal 2 se cumpla no alcanza sola, sin embargo: como muestra este ejercicio,
sin la Señal 1 (separabilidad/paralelismo) la razón para repartir en dos agentes es más débil que
la del booking_agent/policy_agent de esta lección.
Ejercicio 2: Ejecuta el fan-out con tres tools distintas (Medio)
Extiende el dispatch_parallel de esta lección para que la tarea compuesta sea "Cotiza Focus pro
3h, dime la política de no-presentación, y lista todas las salas disponibles" — tres tool_use en
el mismo turno: get_quote, search_docs y list_rooms. Ejecútalo y confirma que las tres
respuestas llegan, sin importar el orden real en que los hilos terminaron.
Ver solución
import reservo_tools as rt
TOOLS_THREE_WAY = {
"get_quote": rt.get_quote,
"search_docs": search_docs,
"list_rooms": rt.list_rooms,
}
turn = [
{"type": "tool_use", "id": "toolu_01", "name": "get_quote",
"input": {"room": "Focus", "tier": "pro", "hours": 3}},
{"type": "tool_use", "id": "toolu_02", "name": "search_docs",
"input": {"query": "¿qué pasa si no me presento?"}},
{"type": "tool_use", "id": "toolu_03", "name": "list_rooms", "input": {}},
]
results = dispatch_parallel(turn, TOOLS_THREE_WAY)
# Ordenamos por tool_use_id antes de imprimir -- el orden de FINALIZACIÓN de
# los hilos no es determinista, pero el orden de los resultados sí lo es,
# porque dispatch_parallel arma la lista final en el orden de los blocks de
# entrada (zip(tool_use_blocks, results)), no en el orden en que terminan.
for r in results:
print(r["tool_use_id"], "->", r["content"][:60])
Salida esperada:
toolu_01 -> {'price_cents': 6000}
toolu_02 -> [no-show-policy] Si un miembro no se presenta a una reserva
toolu_03 -> [{'room': 'Focus', 'rate_cents': 2500}, {'room': 'Studio', '
Explicación: dispatch_parallel construye la lista final con zip(tool_use_blocks, results) — es decir, empareja cada resultado con el bloque que lo pidió por posición en la
lista de entrada, no por el momento en que el hilo terminó. Por eso, aunque los tres hilos
puedan terminar en cualquier orden real (search_docs y list_rooms son casi instantáneos;
get_quote también, en este caso, porque ninguno tiene latencia simulada), la salida siempre
sigue el orden toolu_01, toolu_02, toolu_03 — el mismo requisito de determinismo de salida
que exige la Regla dura de esta guía.
Ejercicio 3: Diseña un cuarto escenario con las tres señales completas (Difícil)
Diseña, en prosa y con el input_schema de al menos una tool nueva, un escenario de Reservo
distinto al de esta lección que cumpla las TRES señales a la vez (separabilidad, expertise
distinto, necesidad de aislar contexto). No hace falta que lo ejecutes con el runner completo —
eso es el Módulo 4—; alcanza con: (a) nombrar los dos (o más) agentes involucrados y su tool
característica, (b) explicar por qué las dos sub-tareas son independientes entre sí, y (c)
explicar qué contenido, si viviera en el mismo agente, generaría "ruido" de contexto.
Ver solución
Escenario: un socio corporativo pregunta, en un solo mensaje: "¿Cuál es la política de facturación mensual de mi empresa, y cuántas salas tenemos disponibles para reservar hoy?"
(a) Los agentes: billing_agent, con una tool nueva get_invoice_policy(company_id) -> str (input_schema: {"type": "object", "properties": {"company_id": {"type": "string"}}, "required": ["company_id"]}) que consulta un documento de facturación corporativa —un dominio
completamente distinto al de reservas—; y booking_agent, con list_rooms(), sin cambios.
(b) Separabilidad: la política de facturación de una empresa no depende de qué salas están
disponibles hoy, y viceversa — ninguna de las dos respuestas necesita el resultado de la otra.
Se pueden despachar en el mismo turno, en paralelo, exactamente como get_quote y search_docs
en el ejemplo trabajado de esta lección.
(c) El ruido de contexto: un documento de facturación corporativa real —términos de pago,
ciclos de facturación, condiciones por volumen de reservas— puede ser largo y específico de cada
empresa cliente. Si ese documento viviera en el mismo contexto que booking_agent, cada
pregunta de reserva —incluso una tan simple como "¿hay salas libres?"— cargaría con términos de
facturación que nunca necesita para responder. Peor: si Reservo atiende a varias empresas, el
contexto de booking_agent tendría que decidir CUÁL política de facturación es relevante para
cada socio que pregunta, un problema que no tiene nada que ver con su expertise real (reservar
salas). Aislar billing_agent en su propio agente evita ese cruce por completo — el patrón
exacto que context-engineering-guide desarrolla a fondo, nombrado aquí sin construirse.
Resumen y siguiente paso
- Multi-agente se justifica cuando aparecen, con evidencia concreta, alguna de estas tres señales: separabilidad genuina (las sub-tareas no dependen entre sí — pueden correr en paralelo), expertise distinto (tools de dominios de datos que nunca colisionan), y necesidad de aislar contexto (un dominio que, mezclado, contaminaría el de otro).
- Ejecutamos el caso Reservo que las tiene las tres:
booking_agent+policy_agentconsearch_docs(el stub mínimo, sustituto explícito deproduction-rag-and-document-ingestion-guide), resolviendo "cotiza Focus pro 3h y dime la política de cancelación" conget_quoteysearch_docsen el mismo turno, en paralelo — algo que la tarea secuencial de la lección 05 no podía ofrecer. - La colisión de
input_schema(Señal 2, medida en la lección 04) resultó ser cero incluso fusionadas en un agente — el argumento real para separarbooking_agentdepolicy_agentno es la limpieza de schema, es la separabilidad y el aislamiento. - El aislamiento de contexto se nombró, no se construyó: presupuestar y comprimir lo que
entra en la ventana de cada especialista es terreno de
context-engineering-guide.
Siguiente lección: 07 — Preview de los patrones. Con el criterio completo del módulo ya construido —costo medido, señales que sí lo justifican— vemos un mapa de los cinco patrones que los Módulos 2 a 6 construyen: supervisor, pipeline, fan-out, handoff y blackboard.
Recursos adicionales
- Anthropic — Multi-agent research system — El caso real de Anthropic donde separar agentes por dominio (y correrlos en paralelo) sí compensó el costo de coordinación, la misma lógica que esta lección aplica a Reservo.
- Anthropic — Building effective agents — "Workflows" con pasos paralelos vs. secuenciales — la base conceptual de la Señal 1 (separabilidad) de esta lección.
- Anthropic — Tool use (function calling) overview — La forma de varios
tool_useen un mismo turno, la pieza del protocolo que hace posible el fan-out ejecutado en esta lección. - Python —
concurrent.futures— El módulo detrás dedispatch_parallel, reusado deagent-fundamentalsM5 L05 sin cambios.