Módulo 2: El patrón supervisor/router
Ruteo determinista con reglas
Descripción
La lección 02 dejó el Paso 2 del supervisor —decidir a quién delegar— como concepto, escrito a
mano. Esta lección lo resuelve por primera vez con código real: una función de Python,
route_deterministic, que lee el texto de la petición y decide, por coincidencia de palabras
clave, cuál de los tres especialistas debe atenderla. Cero llamadas al modelo. Cero red. Una
comparación de substrings, tan determinista como cualquier otra función que ya escribiste en esta
guía.
Vas a ejecutarla contra tres peticiones de intención clara —una por especialista— y vas a ver que acierta las tres. Después vas a encontrarle el límite: una petición donde el orden de las reglas importa, una que ninguna regla cubre, y —la más importante— una donde la función devuelve una respuesta equivocada con total confianza, no un "no sé". Esa última es la que motiva la lección 04.
Conexión con el módulo
Esta lección resuelve, con código ejecutado, la primera mitad del "cartel de reglas fijas" de la
analogía de la recepción (lección 01): rápido, gratuito, perfecto para el vocabulario que
anticipaste. La lección 04 construye la "recepcionista humana" —la otra mitad de esa analogía— para
los casos donde el cartel no alcanza. La lección 06 combina esta función, sin cambios, con
SPECIALISTS y run_specialist de la lección 02 para armar el dispatcher completo.
Analogía retomada: el cartel de reglas
El cartel de la recepción no necesita entender español, ni contexto, ni intención — solo necesita comparar palabras. "Si la frase contiene 'reserva', tercer piso." Esa simplicidad es exactamente su fuerza: es instantánea, cuesta cero, y es perfectamente auditable — cualquiera puede leer el cartel completo y saber, de antemano, exactamente qué va a pasar con cualquier frase que lo mencione. Esta lección construye ese cartel en código.
Ejemplo trabajado: route_deterministic
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):
"""Enruta por reglas: coincidencia de palabra clave sobre el texto de la
petición. El orden de los `if` importa -- se revisa primero la señal
más específica (política) para no perderla frente a una palabra más
genérica (p. ej. "reserva" dentro de una pregunta de política).
Devuelve el nombre del especialista, o None si ninguna regla matchea."""
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
REQUESTS = [
"Cotiza Focus pro 3h y resérvala para Ana.",
"¿Qué pasa si no me presento a mi reserva?",
"Compara el precio de Focus, Studio y Boardroom, todos pro, 3h.",
]
print("--- ruteo determinista sobre 3 peticiones de intención única ---")
for text in REQUESTS:
print(f"{route_deterministic(text):15} <- {text!r}")
Qué esperar:
--- ruteo determinista sobre 3 peticiones de intención única ---
booking_agent <- 'Cotiza Focus pro 3h y resérvala para Ana.'
policy_agent <- '¿Qué pasa si no me presento a mi reserva?'
pricing_agent <- 'Compara el precio de Focus, Studio y Boardroom, todos pro, 3h.'
Tres peticiones, tres aciertos, cero llamadas al modelo. Esta es la mejor cara del ruteo determinista: cuando el vocabulario es predecible y cada petición tiene una sola intención clara, una función de Python resuelve el Paso 2 del supervisor tan bien como un modelo — y más barato.
El orden de las reglas importa
route_deterministic revisa POLICY_KEYWORDS antes que BOOKING_KEYWORDS a propósito. Esta
petición muestra por qué:
edge_case = "¿Cuál es la política de cancelación?"
print(f"texto: {edge_case!r}")
print(f" contiene 'política': {'política' in edge_case.lower()}")
print(f" contiene 'cancela': {'cancela' in edge_case.lower()}")
print(f" route_deterministic -> {route_deterministic(edge_case)}")
Qué esperar:
texto: '¿Cuál es la política de cancelación?'
contiene 'política': True
contiene 'cancela': True
route_deterministic -> policy_agent
La frase contiene las dos palabras clave a la vez: "política" (de POLICY_KEYWORDS) y
"cancela" —oculta dentro de "cancelación"— (de BOOKING_KEYWORDS). Si route_deterministic
revisara BOOKING_KEYWORDS primero, esta pregunta de política terminaría mal enrutada a
booking_agent. El orden de los if no es un detalle cosmético — es la única razón por la que
esta frase se resuelve bien. Es exactamente el mismo principio que ya viste en el Módulo 1, lección
07, con el orden de los if de choose_pattern: la señal más específica se revisa primero.
Una petición que ninguna regla cubre
uncovered = "Hola, ¿tienen wifi en las salas?"
print(f"texto: {uncovered!r}")
print(f" route_deterministic -> {route_deterministic(uncovered)!r}")
Qué esperar:
texto: 'Hola, ¿tienen wifi en las salas?'
route_deterministic -> None
Ninguna de las tres listas de palabras clave anticipó "wifi". route_deterministic devuelve
None — un resultado honesto: la función sabe que no sabe. Este es el tipo de falla más fácil
de manejar, porque es visible: un supervisor que recibe None puede tener un plan B explícito (la
lección 07 construye uno).
El caso que de verdad importa: una regla equivocada, con confianza
ambiguous = "Necesito cancelar porque no voy a poder llegar a mi reserva de mañana, ¿me cobran algo?"
print(f"texto: {ambiguous!r}")
print(f" contiene 'cancela': {'cancela' in ambiguous.lower()}")
print(f" contiene 'reserva': {'reserva' in ambiguous.lower()}")
print(f" contiene alguna POLICY_KEYWORD: {[kw for kw in POLICY_KEYWORDS if kw in ambiguous.lower()]}")
print(f" route_deterministic -> {route_deterministic(ambiguous)!r}")
Qué esperar:
texto: 'Necesito cancelar porque no voy a poder llegar a mi reserva de mañana, ¿me cobran algo?'
contiene 'cancela': True
contiene 'reserva': True
contiene alguna POLICY_KEYWORD: []
route_deterministic -> 'booking_agent'
Este es el caso que la lección 01 anticipó con el visitante que confunde al cartel. La frase
contiene "cancela" y "reserva" — dos palabras que BOOKING_KEYWORDS reconoce con seguridad—, así
que route_deterministic devuelve booking_agent sin dudar. Pero la intención real del socio no
es "cancela mi reserva ahora" — es "¿me van a cobrar algo?", una pregunta sobre el cargo de
no-presentación, que vive en el dominio de policy_agent. Ninguna POLICY_KEYWORD de la lista
aparece en el texto —ni "no llego" (dice "no voy a poder llegar"), ni "cargo por" (dice "me
cobran")—, así que la regla no tiene ninguna señal para dudar de sí misma.
Esta es la diferencia clave con el caso del wifi. Ahí, la función devolvió None — un "no sé"
honesto. Acá, devuelve una respuesta equivocada con la misma confianza que las tres peticiones
correctas del principio de la lección. Un sistema que solo revisa "¿la función devolvió None?"
para decidir si confiar en ella no detecta este caso — la lección 07 construye el router
híbrido y muestra, ejecutado, exactamente por qué ese chequeo no alcanza acá.
Cuándo el ruteo determinista alcanza
Con los cuatro casos ejecutados —tres aciertos, un None honesto, una falla confiada—, el criterio
queda concreto:
- Vocabulario acotado y predecible. Si las formas en que un socio real pide cada cosa son un conjunto chico y conocido, las reglas las cubren todas.
- Intenciones que no se superponen en las mismas palabras. "Cotiza"/"reserva" vs. "política"/"cargo" vs. "compara"/"conviene" casi nunca comparten vocabulario — el riesgo de colisión es bajo quando las categorías son, de entrada, semánticamente distintas.
- Costo cero, auditable al 100%. Sin llamadas al modelo, sin latencia de red, y cada regla se puede leer, probar y versionar como cualquier otra función de Python — el Ejercicio 3 de esta lección construye exactamente ese tipo de prueba.
- Reproducible siempre. La misma entrada produce siempre la misma salida — una propiedad que ninguna decisión de un modelo puede garantizar con la misma certeza.
Lo que el ruteo determinista no ofrece es comprensión del sentido completo de una frase — solo ve palabras sueltas, nunca la intención detrás de ellas. La lección 04 construye la alternativa para exactamente ese hueco.
Errores comunes
-
Agregar palabras clave sin pensar en el orden de las reglas. Sumar una keyword nueva a
BOOKING_KEYWORDSsin revisar si colisiona con una dePOLICY_KEYWORDSpuede romper un caso que antes funcionaba bien — el Ejercicio 2 de esta lección lo ejercita. -
Confundir
Nonecon "la función está rota".Nonees el resultado correcto para una petición que las reglas no anticiparon — es información útil, no un error. El error real es no tener un plan para ese caso (la lección 07 lo resuelve). -
Pensar que un router determinista que pasa tus pruebas nunca falla. El caso ambiguo de esta lección pasa "las pruebas" en el sentido de que no lanza ninguna excepción y devuelve algo — solo que ese algo está mal. Probar un router determinista exige un conjunto de casos etiquetados con la respuesta correcta, no solo confirmar que no explota (Ejercicio 3).
-
Suponer que más palabras clave siempre mejoran la cobertura. Cada palabra nueva que se agrega a una lista es una oportunidad más de matchear algo que no debía — el Ejercicio 2 muestra una keyword que sí ayuda, pero el principio general exige revisar cada adición contra los casos ya cubiertos, no solo contra el caso que la motivó.
-
Olvidar que "cero llamadas al modelo" no significa "cero costo de mantenimiento". Las reglas hay que escribirlas, probarlas y actualizarlas a mano cada vez que el vocabulario real de los socios cambia — un costo de ingeniería que no aparece en ningún conteo de llamadas, pero que existe igual.
Ejercicios
Ejercicio 1: Rutea tres peticiones nuevas (Fácil)
Ejecuta route_deterministic sobre estas tres peticiones, distintas a las del ejemplo trabajado, y
confirma que cada una acierta el especialista esperado: (a) "Necesito agendar el Studio basic 4h
para Carlos."; (b) "¿Cuál es la política de no-presentación?"; (c) "¿Conviene más el Studio o el
Boardroom para 3 personas?".
Ver solución
tests_1 = [
"Necesito agendar el Studio basic 4h para Carlos.",
"¿Cuál es la política de no-presentación?",
"¿Conviene más el Studio o el Boardroom para 3 personas?",
]
for t in tests_1:
print(f"{route_deterministic(t):15} <- {t!r}")
Salida esperada:
booking_agent <- 'Necesito agendar el Studio basic 4h para Carlos.'
policy_agent <- '¿Cuál es la política de no-presentación?'
pricing_agent <- '¿Conviene más el Studio o el Boardroom para 3 personas?'
Explicación: (a) matchea "agenda" de BOOKING_KEYWORDS (dentro de "agendar"); (b) matchea
"política" de POLICY_KEYWORDS; (c) matchea "conviene" de PRICING_KEYWORDS. Las tres son
peticiones de intención única, igual que las del ejemplo trabajado — el router las resuelve sin
ambigüedad.
Ejercicio 2: Agrega una keyword y confirma que arregla un caso descubierto (Medio)
La petición "¿Qué salas están disponibles hoy?" devuelve None con el router actual. Agrega
"disponible" a BOOKING_KEYWORDS (en una copia nueva de la tupla, sin modificar la original) y
confirma que ahora rutea correctamente a booking_agent.
Ver solución
uncovered = "¿Qué salas están disponibles hoy?"
print(f"antes: route_deterministic({uncovered!r}) -> {route_deterministic(uncovered)!r}")
BOOKING_KEYWORDS_V2 = BOOKING_KEYWORDS + ("disponible",)
def route_deterministic_v2(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_V2):
return "booking_agent"
return None
print(f"después: route_deterministic_v2({uncovered!r}) -> {route_deterministic_v2(uncovered)!r}")
Salida esperada:
antes: route_deterministic('¿Qué salas están disponibles hoy?') -> None
después: route_deterministic_v2('¿Qué salas están disponibles hoy?') -> 'booking_agent'
Explicación: "disponibles" contiene la substring "disponible", así que la nueva keyword
matchea. Ampliar la cobertura de un router determinista es exactamente este proceso: identificar un
caso descubierto (un None en producción, por ejemplo) y agregar la palabra clave que lo cierra —
siempre revisando, como advierte el error común 4, que la palabra nueva no abra una colisión con
otra categoría.
Ejercicio 3: Mide la cobertura contra un set etiquetado (Difícil)
Escribe route_coverage(labeled), que recibe una lista de tuplas (texto, especialista_esperado)
y devuelve (aciertos, total, porcentaje). Ejecutala sobre un set de 5 casos que incluya las tres
peticiones limpias del ejemplo trabajado, el caso del wifi (None esperado), y el caso ambiguo de
esta lección (con policy_agent como el especialista correcto, no el que la función devuelve).
Ver solución
LABELED = [
("Cotiza Focus pro 3h y resérvala para Ana.", "booking_agent"),
("¿Qué pasa si no me presento a mi reserva?", "policy_agent"),
("Compara el precio de Focus, Studio y Boardroom, todos pro, 3h.", "pricing_agent"),
("Hola, ¿tienen wifi en las salas?", None),
("Necesito cancelar porque no voy a poder llegar a mi reserva de mañana, ¿me cobran algo?", "policy_agent"),
]
def route_coverage(labeled):
correct = sum(1 for text, expected in labeled if route_deterministic(text) == expected)
return correct, len(labeled), correct / len(labeled)
correct, total, pct = route_coverage(LABELED)
print(f"correctas: {correct}/{total} ({pct:.0%})")
for text, expected in LABELED:
got = route_deterministic(text)
mark = "OK" if got == expected else "MISS"
print(f" [{mark}] esperado={expected!r:16} obtenido={got!r:16} <- {text[:50]}")
Salida esperada:
correctas: 4/5 (80%)
[OK] esperado='booking_agent' obtenido='booking_agent' <- Cotiza Focus pro 3h y resérvala para Ana.
[OK] esperado='policy_agent' obtenido='policy_agent' <- ¿Qué pasa si no me presento a mi reserva?
[OK] esperado='pricing_agent' obtenido='pricing_agent' <- Compara el precio de Focus, Studio y Boardroom, to
[OK] esperado=None obtenido=None <- Hola, ¿tienen wifi en las salas?
[MISS] esperado='policy_agent' obtenido='booking_agent' <- Necesito cancelar porque no voy a poder llegar a m
Explicación: este set etiquetado, chico pero honesto, es exactamente el tipo de prueba que el error común 3 pide: no basta con confirmar que la función no explota, hace falta confirmar que cada resultado coincide con el especialista correcto. El 80% de cobertura de este set concreto es un número real, medible y reproducible — la única forma seria de decidir, después, si conviene sumar más reglas (Ejercicio 2) o si el caso que falla necesita el router de la lección 04.
Resumen y siguiente paso
route_deterministicresuelve el Paso 2 del supervisor con una función de Python: coincidencia de palabras clave, cero llamadas al modelo, resultado reproducible siempre.- Ejecutamos cuatro casos: 3 aciertos sobre peticiones de intención clara, 1
Nonehonesto sobre vocabulario no anticipado ("wifi"), y 1 falla confiada sobre una petición donde "cancela" y "reserva" aparecen sin que la intención real sea reservar. - El orden de los
ifimporta: revisar la señal más específica primero (política antes que booking) resuelve casos que, en el orden inverso, se enrutarían mal. - El caso más importante de la lección es la falla confiada:
route_deterministicno siempre devuelveNonecuando se equivoca — a veces devuelve una respuesta segura y equivocada, sin ninguna señal de alerta.
Siguiente lección: 04 — Ruteo por decisión del modelo. Tomamos la misma petición ambigua que esta lección enrutó mal, y la resolvemos con el modelo leyendo el sentido completo de la frase — concepto, con la extracción de la decisión ejecutada de verdad.
Recursos adicionales
- Anthropic — Building effective agents — El patrón "routing" descrito con reglas simples de clasificación, la forma general que esta lección implementa a mano.
- Python — Métodos de strings —
str.lower()y el operadorin, la base entera deroute_deterministic. - Python — Funciones de orden superior con
any()— La función que evalúa cada lista de palabras clave sin escribir un loop explícito. - Anthropic — Tool use (function calling) overview — El protocolo que sigue corriendo, sin cambios, dentro de cada especialista al que este router despacha.