Módulo 1: Por qué multi-agente (y cuándo no)
Un agente con muchas tools vs. muchos agentes
Descripción
agent-fundamentals M5 lección 06 midió, ejecutando, qué pasa cuando el registro de tools de
un agente crece sin disciplina: hizo crecer un set de cuatro tools canónicas a ocho —cuatro
sintéticas que reimplementaban las mismas capacidades con nombres distintos— y encontró dos
costos reales: el peso en bytes del arreglo tools (crece más o menos lineal) y la cantidad de
pares de tools con el mismo input_schema, la señal de duplicación (crece de forma
combinatoria: 0, 1, 3, 6, 7). Esta lección retoma exactamente esos números y les da una
salida que esa lección no exploró: en vez de seguir agregando tools al mismo agente, repartir
esas ocho tools entre dos agentes especializados.
Vas a ejecutar la comparación directa: el costo de ocho tools declaradas juntas en un solo registro, contra el costo de las mismas ocho tools repartidas en dos registros más chicos. Vas a ver que repartir sí reduce el peso por request y la confusión que un especialista tiene que resolver — pero también vas a ver, con el mismo ejemplo, el límite exacto de ese argumento: no toda tarea deja repartirse sin pagar el costo de coordinación de la lección 03.
Conexión con el módulo
Esta lección conecta dos piezas construidas por separado: el costo de un set de tools grande
(agent-fundamentals M5 L06, medido allá) y el costo de coordinar varios agentes (lección 03 de
este módulo, medido acá). El resultado es el árbitro entre dos formas de crecer — más tools en un
agente, o más agentes con menos tools cada uno — y prepara el terreno exacto para la lección 05,
que aplica este árbitro a una tarea real de Reservo.
Analogía: el llavero que ya no cabe, repartido en dos llaveros
agent-fundamentals M5 usó la imagen de un llavero que crece hasta que ya no cabe en una mano —
treinta llaves, varias casi idénticas, y encontrar la correcta toma revisar rótulo por rótulo. Una
solución obvia es repartir esas treinta llaves en dos llaveros más chicos, cada uno con quince —
pero si las llaves se repartieron al azar, cada llavero sigue teniendo el mismo problema de
confusión, solo que a menor escala, y ahora además hay que decidir cuál de los dos llaveros
llevar a cada puerta. La solución de fondo nunca fue "menos llaves por llavero" — fue agrupar
las llaves por el edificio al que abren, de modo que cada llavero sea coherente por sí mismo
y nunca necesites los dos al mismo tiempo para abrir una sola puerta.
Esa es la pregunta que esta lección responde con números: cuándo repartir tools entre agentes de verdad reduce la confusión (porque el reparto sigue una frontera de dominio real), y cuándo solo mueve el problema de lugar sin resolverlo.
Ejemplo trabajado: las mismas ocho tools, declaradas juntas o repartidas
Partimos exactamente del set de agent-fundamentals M5 L06: las cuatro tools canónicas de
Reservo (CANONICAL) más cuatro tools sintéticas (SPRAWL) que reimplementan las mismas
capacidades con nombres distintos —check_price, get_room_cost y quote_room copian el
input_schema de get_quote; make_reservation copia el de book_room—.
import json
import itertools
CANONICAL = [
{"name": "list_rooms", "description": (
"Lista todas las salas de Reservo con su tarifa base por hora en "
"centavos. Usa esta tool cuando el usuario pregunta qué salas hay "
"disponibles, sin haber elegido todavía una sala específica."),
"input_schema": {"type": "object", "properties": {}}},
{"name": "get_quote", "description": (
"Cotiza el precio de una sala para un tier y una cantidad de horas, "
"sin reservar nada. Usa esta tool cuando el usuario pregunta cuánto "
"cuesta una reserva."),
"input_schema": {"type": "object", "properties": {
"room": {"type": "string", "enum": ["Focus", "Studio", "Boardroom"]},
"tier": {"type": "string", "enum": ["basic", "pro"]},
"hours": {"type": "integer"}}, "required": ["room", "tier", "hours"]}},
{"name": "book_room", "description": (
"Crea una reserva CONFIRMADA para una sala, un tier y una cantidad "
"de horas, a nombre de un miembro. Tiene efectos reales: genera una "
"reserva de verdad. Usa esta tool solo cuando el usuario pide "
"reservar explícitamente, no cuando solo pregunta el precio."),
"input_schema": {"type": "object", "properties": {
"room": {"type": "string", "enum": ["Focus", "Studio", "Boardroom"]},
"tier": {"type": "string", "enum": ["basic", "pro"]},
"hours": {"type": "integer"}, "member": {"type": "string"}},
"required": ["room", "tier", "hours", "member"]}},
{"name": "cancel_booking", "description": (
"Cancela una reserva existente a partir de su id. Es una acción "
"destructiva e irreversible. Usa esta tool cuando el usuario pide "
"cancelar o anular una reserva que ya hizo."),
"input_schema": {"type": "object", "properties": {"id": {"type": "integer"}},
"required": ["id"]}},
]
# Sintéticas -- reusadas tal cual de agent-fundamentals M5 L06, sin cambios.
SPRAWL = [
{"name": "check_price", "description": "Revisa el precio de una sala.",
"input_schema": CANONICAL[1]["input_schema"]},
{"name": "get_room_cost", "description": "Devuelve el costo de una sala reservada por horas.",
"input_schema": CANONICAL[1]["input_schema"]},
{"name": "quote_room", "description": "Genera una cotización para una sala de Reservo.",
"input_schema": CANONICAL[1]["input_schema"]},
{"name": "make_reservation", "description": "Reserva una sala para un miembro.",
"input_schema": CANONICAL[2]["input_schema"]},
]
def schema_collisions(tools):
"""La misma prueba de agent-fundamentals M5 L06: pares con input_schema
BYTE-IDÉNTICO."""
pairs = itertools.combinations(tools, 2)
return sum(1 for a, b in pairs if a["input_schema"] == b["input_schema"])
def measure(label, tools):
size = len(json.dumps(tools, ensure_ascii=False))
collisions = schema_collisions(tools)
print(f"{label:38} {len(tools):2} tools | {size:5} bytes | {collisions} pares colisionando")
print("--- un agente, las 8 tools juntas (el resultado de agent-fundamentals M5 L06) ---")
measure("agente único (8 tools)", CANONICAL + SPRAWL)
print()
print("--- dos agentes, cada uno con SU subset declarado ---")
measure("booking_agent (4 canónicas)", CANONICAL)
measure("legacy_agent (4 de SPRAWL)", SPRAWL)
Qué esperar:
--- un agente, las 8 tools juntas (el resultado de agent-fundamentals M5 L06) ---
agente único (8 tools) 8 tools | 2982 bytes | 7 pares colisionando
--- dos agentes, cada uno con SU subset declarado ---
booking_agent (4 canónicas) 4 tools | 1621 bytes | 0 pares colisionando
legacy_agent (4 de SPRAWL) 4 tools | 1361 bytes | 3 pares colisionando
Los números de la primera fila —2982 bytes, 7 pares colisionando— son exactamente los que
agent-fundamentals M5 L06 midió para el set completo de ocho tools. Repartirlas en dos agentes
de cuatro tools cada uno cambia la foto de dos formas distintas, y vale la pena separarlas:
booking_agentqueda perfectamente sano: 0 colisiones. Las cuatro tools canónicas nunca compartieroninput_schemaentre sí —eso ya era cierto en M5, con o sin las sintéticas—, así que aislarlas en su propio registro no cambia nada sobre ellas mismas.legacy_agentsigue teniendo 3 colisiones, no 0. Repartir NO arregló el problema de fondo deSPRAWL—tres tools que reimplementanget_quote—; solo lo contuvo dentro de un registro más chico. El agente que reciba una tarea delegacy_agentsigue viendo tres formas distintas de pedir lo mismo, aunque ya no vea las cuatro canónicas mezcladas encima.
Lo que sí gana cada request individual
Hay un beneficio real, medible, en el reparto — pero está en el peso por request, no en
arreglar el problema de fondo de SPRAWL:
bytes_all = len(json.dumps(CANONICAL + SPRAWL, ensure_ascii=False))
bytes_booking = len(json.dumps(CANONICAL, ensure_ascii=False))
print(f"bytes que viajan si UN agente declara las 8: {bytes_all}")
print(f"bytes que viaja booking_agent en una request que solo necesita reservar: {bytes_booking}")
print(f"reducción para esa request: {(1 - bytes_booking / bytes_all) * 100:.0f}%")
Qué esperar:
bytes que viajan si UN agente declara las 8: 2982
bytes que viaja booking_agent en una request que solo necesita reservar: 1621
reducción para esa request: 46%
Cuando una tarea solo necesita reservar una sala, un booking_agent que solo declara sus
cuatro tools canónicas le ahorra al modelo tener que leer, y potencialmente confundirse con, las
cuatro tools de legacy_agent — un 46% menos de bytes en esa request puntual. Ese ahorro es
real, pero fíjate en la condición: se cumple solo si la tarea nunca necesita las tools del otro
agente. El día que una tarea necesite tanto book_room como algo de legacy_agent, hay que
consultar a los dos agentes — y ahí entra, de lleno, el costo de coordinación que mediste en la
lección 03.
El árbitro: cuándo repartir vale la pena
Con los dos ejemplos ejecutados —el de agent-fundamentals M5 L06 (un agente que crece) y el de
esta lección (el mismo set repartido)— el criterio se puede escribir en una frase: repartir
tools entre agentes reduce el peso y la confusión por request SOLO cuando la mayoría de las
tareas se resuelven completas dentro de un solo agente. Si las tareas típicas necesitan tools
de ambos lados con la misma frecuencia, repartir no ahorra nada — mueve el costo de "un registro
grande" al costo de "coordinar entre dos registros chicos", que la lección 03 ya mostró que no es
gratis.
Nota algo importante sobre el ejemplo de esta lección: repartir CANONICAL de SPRAWL no
está motivado por una frontera de dominio real —es una frontera arbitraria, "las que ya existían"
vs. "las que se agregaron después"—. La lección 06 va a mostrar el caso opuesto: un reparto que sí
sigue una frontera de dominio genuina (reservas vs. políticas), donde el argumento de "menos
confusión por request" se sostiene con mucha más fuerza porque las dos mitades casi nunca hacen
falta juntas por la misma razón.
Errores comunes
-
Pensar que repartir tools automáticamente arregla la duplicación.
legacy_agentsigue teniendo 3 pares colisionando después del reparto — repartir aisló el problema, no lo resolvió. La única forma de llegar a 0 en ese grupo es la Técnica 1 deagent-fundamentalsM5 L04 (eliminar las duplicadas), no un reparto entre agentes. -
Medir solo el ahorro de bytes y olvidar el costo de coordinación. El 46% de reducción por request es real, pero solo aplica cuando la tarea se resuelve dentro de un agente. Contar ese ahorro sin restar el costo de las tareas que sí necesitan a los dos agentes es una comparación incompleta.
-
Repartir tools por una frontera arbitraria (como "viejas vs. nuevas") en vez de por dominio real. El reparto
CANONICAL/SPRAWLde esta lección es intencionalmente el ejemplo débil — sirve para medir el efecto del reparto en sí, pero un reparto real de producción debería seguir una frontera de expertise genuina, como la lección 06 va a mostrar. -
Asumir que "menos tools por agente" siempre es la meta. No lo es —
agent-fundamentalsM5 L06 ya mostró que un set de 6 tools bien diferenciadas (0 colisiones) puede ser más sano que uno de 4 con colisiones. El número de tools por sí solo nunca es la métrica; los bytes y las colisiones sí lo son. -
Olvidar que "el ahorro de bytes" y "el costo de coordinación" se miden en unidades distintas. Uno es bytes por request; el otro es llamadas al modelo y hops. No se pueden sumar ni restar directamente — hay que pesar los dos, caso por caso, como hace la lección 05.
Ejercicios
Ejercicio 1: Lee los números sin ejecutar (Fácil)
Mirando la salida del ejemplo trabajado: (a) ¿cuántos bytes se ahorran si legacy_agent, en vez
de declarar sus 4 tools, declarara solo make_reservation (la única que no colisiona con las
otras tres de su propio grupo)? (b) ¿cuántas colisiones quedarían en ese caso? (c) ¿por qué esa
reducción no es la misma solución que "repartir en dos agentes" de esta lección?
Ver solución
(a) legacy_agent con las 4 tools pesa 1361 bytes; con solo make_reservation (367 bytes) el
ahorro es 1361 - 367 = 994 bytes — casi tres cuartos del peso original, porque make_reservation
es la más liviana de las cuatro (su description es una sola frase corta).
(b) Cero. make_reservation sola, sin check_price, get_room_cost ni quote_room en el
mismo registro, no tiene con quién colisionar dentro de legacy_agent.
(c) Porque esa reducción es la Técnica 1 de agent-fundamentals M5 L04 —eliminar tools
duplicadas—, no el reparto entre agentes de esta lección. Repartir mueve tools de un registro a
otro sin borrar ninguna; eliminar duplicadas borra las que no aportan capacidad real. Son dos
técnicas distintas, y esta lección muestra que se pueden combinar: primero limpiar la duplicación
dentro de cada grupo (M5 L04), y solo después, si todavía hace falta, repartir por dominio.
Ejercicio 2: Mide un reparto de tres agentes (Medio)
Reservo decide repartir sus ocho tools en tres agentes: booking_agent (las 4 CANONICAL),
quote_variants_agent (check_price, get_room_cost, quote_room — las tres que colisionan
entre sí) y reservation_variant_agent (solo make_reservation). Mide bytes y colisiones de
cada uno con measure, y confirma que la suma de colisiones de los tres es menor que las 7 del
agente único original.
Ver solución
QUOTE_VARIANTS = SPRAWL[:3] # check_price, get_room_cost, quote_room
RESERVATION_VARIANT = SPRAWL[3:] # make_reservation
measure("booking_agent (4 canónicas)", CANONICAL)
measure("quote_variants_agent (3 variantes de get_quote)", QUOTE_VARIANTS)
measure("reservation_variant_agent (1 tool)", RESERVATION_VARIANT)
Salida esperada:
booking_agent (4 canónicas) 4 tools | 1621 bytes | 0 pares colisionando
quote_variants_agent (3 variantes de get_quote) 3 tools | 994 bytes | 3 pares colisionando
reservation_variant_agent (1 tool) 1 tools | 367 bytes | 0 pares colisionando
Explicación: la suma de colisiones de los tres agentes es 0 + 3 + 0 = 3, menos que las 7
del agente único —porque las 7 originales contaban pares entre grupos que ya no comparten
registro (por ejemplo, check_price ya no colisiona con nada de booking_agent porque nunca
vuelven a declararse juntas)—. Pero fíjate que quote_variants_agent sigue con sus 3 colisiones
internas intactas: repartir en más agentes reduce el número total de pares visibles en cualquier
request dada, pero no toca el problema real de que esas tres tools siguen siendo la misma
capacidad con tres nombres. Ese problema solo se resuelve con la Técnica 1 o 2 de
agent-fundamentals M5 L04, sin importar en cuántos agentes repartas las tools.
Ejercicio 3: Diseña el criterio de "vale la pena repartir" (Difícil)
Escribe una función worth_splitting(bytes_together, bytes_specialist_a, bytes_specialist_b, fraction_needs_both) que decida si vale la pena repartir un set de tools en dos agentes,
comparando el peso esperado por request de las dos estrategias: (a) un agente único que
siempre declara todo (bytes_together en el 100% de las requests), y (b) dos agentes separados,
donde una fracción fraction_needs_both de las requests necesita a los dos (pagando
bytes_specialist_a + bytes_specialist_b, más el costo de coordinar) y el resto solo necesita a
uno (pagando el menor peso de los dos). Para simplificar, ignora el costo de coordinación en
bytes y compara solo peso; ejecútala con los números de booking_agent/legacy_agent de esta
lección para fraction_needs_both = 0.1 y fraction_needs_both = 0.6, e interpreta el resultado.
Ver solución
def worth_splitting(bytes_together, bytes_specialist_a, bytes_specialist_b, fraction_needs_both):
expected_together = bytes_together # siempre paga el total, sea cual sea la tarea
avg_single_specialist = (bytes_specialist_a + bytes_specialist_b) / 2
expected_split = (
fraction_needs_both * (bytes_specialist_a + bytes_specialist_b)
+ (1 - fraction_needs_both) * avg_single_specialist
)
return expected_split < expected_together, expected_together, expected_split
for fraction in (0.1, 0.6):
worth_it, together, split = worth_splitting(2982, 1621, 1361, fraction)
print(f"fraction_needs_both={fraction}: "
f"agente único={together:.0f} bytes, repartido={split:.0f} bytes, "
f"¿vale la pena repartir? {worth_it}")
Salida esperada:
fraction_needs_both=0.1: agente único=2982 bytes, repartido=1640 bytes, ¿vale la pena repartir? True
fraction_needs_both=0.6: agente único=2982 bytes, repartido=2386 bytes, ¿vale la pena repartir? True
Explicación: en este modelo simplificado —que ignora a propósito el costo de coordinación en
llamadas y hops, medido aparte en la lección 03— repartir siempre sale más liviano en bytes,
incluso cuando el 60% de las tareas necesita a los dos agentes, porque declarar solo un
subconjunto nunca pesa más que declarar el conjunto completo. Ese resultado es exactamente la
trampa que la sección "El árbitro" de esta lección advierte: mirar solo el peso en bytes siempre
favorece repartir, así que la decisión real nunca puede tomarse solo con esta función — hay que
sumarle el costo de coordinación de la lección 03 (llamadas y hops), que sí crece con
fraction_needs_both y sí puede revertir la conclusión. Este ejercicio, a propósito, es una
función incompleta — su valor pedagógico es mostrar por qué el criterio final de la lección 05 no
puede basarse solo en bytes.
Resumen y siguiente paso
- Retomamos, ejecutados, los números de
agent-fundamentalsM5 L06 —8 tools juntas: 2982 bytes, 7 pares colisionando— y los repartimos en dos registros de 4:booking_agent(0 colisiones, 1621 bytes) ylegacy_agent(3 colisiones, 1361 bytes). - Repartir contuvo la confusión (7 pares visibles bajaron a 3, dentro del grupo que
realmente la tenía) pero no la resolvió — la duplicación de fondo en
SPRAWLsigue intacta; eso requiere la Técnica 1/2 deagent-fundamentalsM5 L04, no un reparto entre agentes. - El ahorro real y medible del reparto está en el peso por request —46% menos bytes cuando una tarea se resuelve dentro de un solo agente— pero esa ganancia solo se sostiene si la mayoría de las tareas no necesitan a los dos agentes a la vez; el momento en que sí los necesitan, entra en juego el costo de coordinación de la lección 03.
- El reparto de esta lección (
CANONICALvs.SPRAWL) fue deliberadamente arbitrario — la lección 06 muestra el caso donde repartir sigue una frontera de dominio real, y el argumento se sostiene con mucha más fuerza.
Siguiente lección: 05 — La comparación ejecutada. Aplicamos todo lo medido hasta aquí a una tarea real de Reservo: la misma tarea, resuelta por un agente único y por un sistema de dos agentes, con las llamadas y los mensajes contados de verdad, no estimados con una fórmula.
Recursos adicionales
- Anthropic — Tool use (function calling) overview — La referencia oficial que recomienda mantener el conjunto de tools de cada agente acotado y bien diferenciado, la base de esta comparación.
- Anthropic — Building effective agents — Por qué agregar complejidad (más tools o más agentes) solo se justifica cuando el problema real lo exige, no como regla general.
- Python —
itertools.combinations— La función detrás deschema_collisions, reusada sin cambios deagent-fundamentalsM5 L06. - Python —
json.dumps— Usado para medir, en bytes reales, el peso de cada registro de tools de esta lección.