Módulo 6: Prompt Composition y Chaining
7. Routing por Complejidad
Descripción
El routing por complejidad es una estrategia de optimización de costos que clasifica las solicitudes entrantes según su complejidad y las dirige al modelo o estrategia más apropiada. La idea central: no todas las tareas necesitan el modelo más caro o la técnica más sofisticada.
Con un buen sistema de routing, puedes reducir tus costos operativos en un 60-80% sin sacrificar calidad perceptible para el usuario, porque el 70-80% de las solicitudes en la mayoría de sistemas son tareas simples que cualquier modelo puede resolver correctamente.
El Problema sin Routing
Sistema sin routing (todo a gpt-4o):
- "¿Cuáles son sus horarios?" → gpt-4o → $0.005
- "Analiza este contrato legal de 20 páginas" → gpt-4o → $0.08
- "¿Puedo devolver un producto?" → gpt-4o → $0.003
- "¿Cómo funciona el sistema de puntos?" → gpt-4o → $0.002
- "Diseña una estrategia de pricing para nuestra empresa..." → gpt-4o → $0.12
Estimado: $0.21 para 5 requests
Sistema con routing:
- "¿Cuáles son sus horarios?" → gpt-4o-mini → $0.0001
- "Analiza este contrato legal de 20 páginas" → gpt-4o → $0.08
- "¿Puedo devolver un producto?" → gpt-4o-mini → $0.0001
- "¿Cómo funciona el sistema de puntos?" → gpt-4o-mini → $0.0001
- "Diseña una estrategia de pricing..." → gpt-4o → $0.12
Estimado: $0.2004 para 5 requests
Ahorro en este ejemplo: ~$0.01 (pequeño)
En 10,000 requests/día: ahorro de $200/día si 80% son simples
Arquitectura del Sistema de Routing
Input
│
▼
┌─────────────────────────────────┐
│ CLASIFICADOR │
│ (siempre gpt-4o-mini, rápido) │
│ │
│ Criterios de evaluación: │
│ - Longitud y complejidad │
│ - Tipo de tarea │
│ - Palabras clave de complejidad│
│ - Requiere razonamiento? │
└──────────────┬──────────────────┘
│
┌───────┴───────┐
│ │
▼ ▼
SIMPLE COMPLEJO
│ │
▼ ▼
gpt-4o-mini gpt-4o o
(0.15x/0.60x claude-opus
por 1K tokens) (2.5x/10x
por 1K tokens)
Implementación Completa
from openai import OpenAI
from dataclasses import dataclass
from enum import Enum
from typing import Optional
import time
import json
client = OpenAI()
class NivelComplejidad(Enum):
SIMPLE = "simple"
MEDIA = "media"
COMPLEJA = "compleja"
MUY_COMPLEJA = "muy_compleja"
@dataclass
class ConfigRouting:
"""Configuración del sistema de routing."""
modelo_simple: str = "gpt-4o-mini"
modelo_complejo: str = "gpt-4o"
modelo_muy_complejo: str = "gpt-4o" # Con más tokens o temperatura especial
umbral_simple: float = 0.7 # Confianza mínima para clasificar como simple
max_tokens_simple: int = 300
max_tokens_complejo: int = 1000
usar_heuristica: bool = True # Usar heurística antes del LLM
verbose: bool = False
@dataclass
class ResultadoRouting:
"""Resultado de una solicitud procesada por el router."""
query: str
complejidad: NivelComplejidad
modelo_usado: str
respuesta: str
tiempo_segundos: float
metodo_clasificacion: str # "heuristica" o "llm"
razon_clasificacion: str
confianza: float
# ============================================================
# CLASIFICADOR HEURÍSTICO (sin llamadas al LLM)
# ============================================================
class ClasificadorHeuristico:
"""
Clasificador basado en reglas sin llamadas al LLM.
Muy rápido y gratuito.
"""
PALABRAS_SIMPLE = {
"horario", "precio", "costo", "dirección", "teléfono",
"cuánto", "cuándo", "dónde", "qué es", "qué son",
"sí o no", "true or false", "confirmar", "verdadero", "falso",
"número de", "fecha de", "lista de", "hola", "gracias"
}
PALABRAS_COMPLEJO = {
"analiza", "analizar", "analiza en profundidad",
"compara", "comparar", "diferencia entre",
"diseña", "diseñar", "estrategia",
"razona", "razonar", "explica por qué",
"evalúa", "evaluar", "pros y contras",
"predice", "predecir", "proyecta",
"optimiza", "optimizar", "mejora",
"código", "code", "implementa", "implementar",
"informe completo", "análisis detallado"
}
UMBRAL_LARGO = 200 # Caracteres: por encima se considera más complejo
UMBRAL_MUY_LARGO = 800 # Texto muy largo = complejo
def clasificar(self, texto: str) -> Optional[tuple[NivelComplejidad, str, float]]:
"""
Clasifica el texto usando heurísticas.
Returns:
Tupla (nivel, razon, confianza) o None si no puede decidir
"""
texto_lower = texto.lower()
longitud = len(texto)
# Regla 1: Texto muy largo = complejo
if longitud > self.UMBRAL_MUY_LARGO:
return (
NivelComplejidad.COMPLEJA,
f"Texto largo ({longitud} chars)",
0.85
)
# Regla 2: Palabras de complejidad alta
palabras_complejas_encontradas = [
p for p in self.PALABRAS_COMPLEJO
if p in texto_lower
]
if palabras_complejas_encontradas:
return (
NivelComplejidad.COMPLEJA,
f"Palabras de alta complejidad: {palabras_complejas_encontradas[:2]}",
0.80
)
# Regla 3: Palabras de simplicidad
palabras_simples_encontradas = [
p for p in self.PALABRAS_SIMPLE
if p in texto_lower
]
if palabras_simples_encontradas and longitud < self.UMBRAL_LARGO:
return (
NivelComplejidad.SIMPLE,
f"Palabras simples: {palabras_simples_encontradas[:2]}",
0.82
)
# Regla 4: Texto corto sin palabras complejas = probablemente simple
if longitud < 100:
return (
NivelComplejidad.SIMPLE,
f"Texto corto ({longitud} chars) sin complejidad detectada",
0.70
)
# No puede decidir con heurística
return None
# ============================================================
# CLASIFICADOR LLM
# ============================================================
class ClasificadorLLM:
"""
Clasificador basado en LLM.
Más preciso pero tiene costo y latencia.
"""
PROMPT_CLASIFICACION = """Clasifica la complejidad de esta solicitud:
SOLICITUD: {texto}
Criterios:
- SIMPLE: Pregunta directa, respuesta de 1-2 oraciones, sin razonamiento complejo
Ejemplos: preguntas de sí/no, consultas de información básica, saludos
- MEDIA: Requiere algo de contexto o razonamiento, respuesta de 3-5 oraciones
Ejemplos: explicar cómo funciona algo, pasos básicos, comparaciones simples
- COMPLEJA: Análisis profundo, múltiples perspectivas, planificación, código largo
Ejemplos: diseño de sistemas, análisis de documentos, estrategias de negocio
- MUY_COMPLEJA: Máxima capacidad de razonamiento, documentos muy largos, decisiones críticas
Ejemplos: análisis financiero completo, revisión legal, arquitecturas de software
Responde en JSON:
{{"complejidad": "simple|media|compleja|muy_compleja", "confianza": 0.0-1.0, "razon": "breve explicación"}}"""
def clasificar(self, texto: str) -> tuple[NivelComplejidad, str, float]:
"""Clasifica usando gpt-4o-mini (siempre el modelo económico para clasificar)."""
response = client.chat.completions.create(
model="gpt-4o-mini", # Siempre el económico para clasificar
messages=[{
"role": "user",
"content": self.PROMPT_CLASIFICACION.format(texto=texto[:500])
}],
temperature=0,
max_tokens=100,
response_format={"type": "json_object"}
)
try:
datos = json.loads(response.choices[0].message.content)
complejidad_str = datos.get("complejidad", "media")
confianza = float(datos.get("confianza", 0.7))
razon = datos.get("razon", "")
nivel_map = {
"simple": NivelComplejidad.SIMPLE,
"media": NivelComplejidad.MEDIA,
"compleja": NivelComplejidad.COMPLEJA,
"muy_compleja": NivelComplejidad.MUY_COMPLEJA
}
nivel = nivel_map.get(complejidad_str, NivelComplejidad.MEDIA)
return nivel, razon, confianza
except Exception:
return NivelComplejidad.MEDIA, "Error en clasificación", 0.5
# ============================================================
# ROUTER PRINCIPAL
# ============================================================
class RouterComplejidad:
"""
Sistema de routing que dirige solicitudes al modelo apropiado
según su complejidad detectada.
"""
def __init__(self, config: ConfigRouting = None):
self.config = config or ConfigRouting()
self.heuristica = ClasificadorHeuristico()
self.clasificador_llm = ClasificadorLLM()
# Tracking de métricas
self.estadisticas = {
"total": 0,
"simple": 0,
"media": 0,
"compleja": 0,
"muy_compleja": 0,
"via_heuristica": 0,
"via_llm": 0,
"costo_estimado_usd": 0.0,
"ahorro_vs_gpt4o_usd": 0.0
}
# Costos estimados por 1K tokens (input/output)
self.COSTOS = {
"gpt-4o-mini": {"input": 0.00015, "output": 0.0006},
"gpt-4o": {"input": 0.0025, "output": 0.01}
}
def clasificar(self, texto: str) -> tuple[NivelComplejidad, str, str, float]:
"""
Clasifica la complejidad de la solicitud.
Primero intenta con heurística (gratis), luego con LLM si es necesario.
Returns:
Tupla (nivel, razon, metodo, confianza)
"""
# Intentar con heurística primero
if self.config.usar_heuristica:
resultado_heuristica = self.heuristica.clasificar(texto)
if resultado_heuristica:
nivel, razon, confianza = resultado_heuristica
# Solo usar heurística si la confianza es suficiente
if confianza >= self.config.umbral_simple:
return nivel, razon, "heuristica", confianza
# Si la heurística no es suficiente, usar LLM
nivel, razon, confianza = self.clasificador_llm.clasificar(texto)
return nivel, razon, "llm", confianza
def seleccionar_modelo(self, nivel: NivelComplejidad) -> tuple[str, int]:
"""
Selecciona el modelo y max_tokens según la complejidad.
Returns:
Tupla (nombre_modelo, max_tokens)
"""
if nivel in [NivelComplejidad.SIMPLE, NivelComplejidad.MEDIA]:
return self.config.modelo_simple, self.config.max_tokens_simple
elif nivel == NivelComplejidad.COMPLEJA:
return self.config.modelo_complejo, self.config.max_tokens_complejo
else: # MUY_COMPLEJA
return self.config.modelo_muy_complejo, 2000
def procesar(
self,
texto: str,
sistema_prompt: str = "Eres un asistente útil.",
temperatura: float = 0,
forzar_modelo: str = None
) -> ResultadoRouting:
"""
Procesa una solicitud con routing automático.
Args:
texto: La solicitud del usuario
sistema_prompt: System prompt del asistente
temperatura: Temperatura de la generación
forzar_modelo: Si se especifica, ignora el routing y usa este modelo
Returns:
ResultadoRouting con respuesta y metadatos
"""
t0 = time.time()
self.estadisticas["total"] += 1
# Clasificar complejidad
nivel, razon, metodo, confianza = self.clasificar(texto)
if self.config.verbose:
print(f"[Router] Complejidad: {nivel.value} ({confianza:.0%} confianza, vía {metodo})")
print(f" Razón: {razon}")
# Actualizar estadísticas de clasificación
self.estadisticas[nivel.value] += 1
self.estadisticas[f"via_{metodo}"] += 1
# Seleccionar modelo
if forzar_modelo:
modelo = forzar_modelo
max_tokens = self.config.max_tokens_complejo
else:
modelo, max_tokens = self.seleccionar_modelo(nivel)
if self.config.verbose:
print(f" Usando: {modelo} (max_tokens={max_tokens})")
# Generar respuesta
response = client.chat.completions.create(
model=modelo,
messages=[
{"role": "system", "content": sistema_prompt},
{"role": "user", "content": texto}
],
temperature=temperatura,
max_tokens=max_tokens
)
respuesta = response.choices[0].message.content
tiempo = time.time() - t0
# Calcular costos
tokens_in = response.usage.prompt_tokens
tokens_out = response.usage.completion_tokens
costos_modelo = self.COSTOS.get(modelo, self.COSTOS["gpt-4o-mini"])
costo = (tokens_in / 1000 * costos_modelo["input"]) + (tokens_out / 1000 * costos_modelo["output"])
# Calcular ahorro vs siempre usar gpt-4o
costos_gpt4o = self.COSTOS["gpt-4o"]
costo_sin_routing = (tokens_in / 1000 * costos_gpt4o["input"]) + (tokens_out / 1000 * costos_gpt4o["output"])
ahorro = costo_sin_routing - costo
self.estadisticas["costo_estimado_usd"] += costo
self.estadisticas["ahorro_vs_gpt4o_usd"] += ahorro
return ResultadoRouting(
query=texto[:100],
complejidad=nivel,
modelo_usado=modelo,
respuesta=respuesta,
tiempo_segundos=tiempo,
metodo_clasificacion=metodo,
razon_clasificacion=razon,
confianza=confianza
)
def reporte_estadisticas(self) -> str:
"""Genera un reporte de las estadísticas de uso."""
stats = self.estadisticas
total = stats["total"]
if total == 0:
return "Sin solicitudes procesadas"
pct_simple = (stats["simple"] + stats["media"]) / total * 100
pct_complejo = (stats["compleja"] + stats["muy_compleja"]) / total * 100
ahorro_pct = stats["ahorro_vs_gpt4o_usd"] / (stats["costo_estimado_usd"] + stats["ahorro_vs_gpt4o_usd"]) * 100 if stats["costo_estimado_usd"] > 0 else 0
return f"""
=== REPORTE DE ROUTING ===
Total solicitudes: {total}
Simple/Media (modelo económico): {stats['simple'] + stats['media']} ({pct_simple:.1f}%)
Compleja/Muy compleja (modelo potente): {stats['compleja'] + stats['muy_compleja']} ({pct_complejo:.1f}%)
Clasificación:
Via heurística (gratis): {stats['via_heuristica']} ({stats['via_heuristica']/total*100:.1f}%)
Via LLM: {stats['via_llm']} ({stats['via_llm']/total*100:.1f}%)
Costos:
Costo real con routing: ${stats['costo_estimado_usd']:.4f}
Costo sin routing (todo gpt-4o): ${stats['costo_estimado_usd'] + stats['ahorro_vs_gpt4o_usd']:.4f}
Ahorro: ${stats['ahorro_vs_gpt4o_usd']:.4f} ({ahorro_pct:.1f}%)
"""
# ============================================================
# EJEMPLO DE USO
# ============================================================
if __name__ == "__main__":
router = RouterComplejidad(ConfigRouting(verbose=True))
solicitudes = [
("¿Cuáles son sus horarios de atención?", "Eres un asistente de atención al cliente"),
("Analiza el impacto de los LLMs en el mercado laboral de programación", "Eres un analista de tendencias tecnológicas"),
("¿Cuánto cuesta el producto básico?", "Eres un asistente de ventas"),
("Diseña una arquitectura de microservicios para un e-commerce con 1M usuarios", "Eres un arquitecto de software"),
("¿Qué es Python?", "Eres un tutor de programación"),
("Escribe un análisis comparativo de React vs Vue vs Angular considerando rendimiento, ecosistema y curva de aprendizaje", "Eres un experto frontend")
]
print("=== DEMO DE ROUTING ===\n")
resultados = []
for solicitud, sistema in solicitudes:
print(f"\nSolicitud: {solicitud[:60]}...")
resultado = router.procesar(solicitud, sistema)
resultados.append(resultado)
print(f"Respuesta: {resultado.respuesta[:100]}...")
print(f"Tiempo: {resultado.tiempo_segundos:.2f}s")
print(router.reporte_estadisticas())
Routing con Múltiples Dimensiones
@dataclass
class AnalisisMultidimensional:
"""Análisis de múltiples factores para routing avanzado."""
complejidad_razonamiento: str # bajo/medio/alto
longitud_esperada: str # corta/media/larga
requiere_datos_frescos: bool # ¿necesita datos actuales?
riesgo_error: str # bajo/medio/alto (costo de equivocarse)
sensibilidad: str # normal/sensitivo (PII, decisiones importantes)
def clasificar_multidimensional(solicitud: str) -> AnalisisMultidimensional:
"""Análisis multidimensional para routing más preciso."""
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": f"""Analiza esta solicitud:
Solicitud: {solicitud}
JSON:
{{
"complejidad_razonamiento": "bajo|medio|alto",
"longitud_esperada": "corta|media|larga",
"requiere_datos_frescos": true|false,
"riesgo_error": "bajo|medio|alto",
"sensibilidad": "normal|sensitivo"
}}
Guías:
- riesgo_error alto: si equivocarse tiene consecuencias financieras/legales/médicas
- sensibilidad sensitivo: si involucra datos personales, decisiones importantes"""
}],
temperature=0,
response_format={"type": "json_object"}
).choices[0].message.content
datos = json.loads(response)
return AnalisisMultidimensional(**datos)
def routing_avanzado(solicitud: str, analisis: AnalisisMultidimensional) -> tuple[str, dict]:
"""
Routing avanzado que considera múltiples factores.
Retorna (modelo, parametros_adicionales)
"""
modelo = "gpt-4o-mini"
params = {"temperature": 0, "max_tokens": 300}
# Actualizar según análisis multidimensional
if analisis.complejidad_razonamiento == "alto" or analisis.riesgo_error == "alto":
modelo = "gpt-4o"
params["max_tokens"] = 1500
if analisis.longitud_esperada == "larga":
params["max_tokens"] = max(params["max_tokens"], 1000)
if analisis.sensibilidad == "sensitivo":
params["temperature"] = 0 # Máxima consistencia
# Añadir instrucción de verificación para alto riesgo
instruccion_extra = ""
if analisis.riesgo_error == "alto":
instruccion_extra = "\n\nCRÍTICO: Esta respuesta tiene alto impacto. Verifica dos veces antes de responder."
return modelo, {**params, "instruccion_extra": instruccion_extra}
# Ejemplo:
solicitud_riesgo = "¿Qué medicamento debo tomar para mi diabetes tipo 2?"
analisis = clasificar_multidimensional(solicitud_riesgo)
modelo, params = routing_avanzado(solicitud_riesgo, analisis)
print(f"Modelo: {modelo}")
print(f"Parámetros: {params}")
print(f"Análisis: riesgo={analisis.riesgo_error}, sensibilidad={analisis.sensibilidad}")
Heurísticas Avanzadas sin LLM
import re
from dataclasses import dataclass
@dataclass
class ReglaHeuristica:
nombre: str
patron: str # Regex pattern o keyword
nivel: NivelComplejidad
confianza: float
es_regex: bool = False
REGLAS_HEURISTICAS = [
# Patrones de alta complejidad
ReglaHeuristica("analisis_profundo", r"(analiz|compara|evalúa|diseña|estrategia)", NivelComplejidad.COMPLEJA, 0.85, es_regex=True),
ReglaHeuristica("codigo_complejo", r"(implementa|arquitectura|microservicio|refactor)", NivelComplejidad.COMPLEJA, 0.88, es_regex=True),
ReglaHeuristica("documento_largo", r"(?:análisis|revisión|evaluación).+(?:completo|detallado|exhaustivo)", NivelComplejidad.COMPLEJA, 0.90, es_regex=True),
# Patrones de baja complejidad
ReglaHeuristica("pregunta_si_no", r"^(¿puedo|puedo|¿es|¿tiene|tiene|¿hay|hay)", NivelComplejidad.SIMPLE, 0.82, es_regex=True),
ReglaHeuristica("horario_precio", r"(horario|precio|costo|cuánto cuesta|dirección)", NivelComplejidad.SIMPLE, 0.90, es_regex=False),
ReglaHeuristica("saludo", r"^(hola|buenos|buenas|hi|hello)", NivelComplejidad.SIMPLE, 0.95, es_regex=True),
]
def aplicar_reglas_heuristicas(texto: str) -> Optional[tuple[NivelComplejidad, str, float]]:
"""Aplica reglas heurísticas en orden de confianza."""
texto_lower = texto.lower().strip()
resultados = []
for regla in REGLAS_HEURISTICAS:
if regla.es_regex:
if re.search(regla.patron, texto_lower):
resultados.append((regla.confianza, regla.nivel, regla.nombre))
else:
if regla.patron in texto_lower:
resultados.append((regla.confianza, regla.nivel, regla.nombre))
if not resultados:
return None
# Tomar la regla con mayor confianza
confianza, nivel, nombre = max(resultados, key=lambda x: x[0])
if confianza >= 0.80:
return nivel, f"Regla: {nombre}", confianza
return None
A/B Testing del Router
import random
class RouterABTest:
"""
Sistema de A/B testing para comparar estrategias de routing.
Permite evaluar si el routing mejora la calidad vs. solo usar gpt-4o.
"""
def __init__(self, porcentaje_control: float = 0.2):
"""
Args:
porcentaje_control: % de solicitudes que van a grupo de control (gpt-4o siempre)
"""
self.porcentaje_control = porcentaje_control
self.router_experimental = RouterComplejidad(ConfigRouting(verbose=False))
self.resultados_ab = {
"control": {"llamadas": 0, "costo": 0, "satisfaccion": []},
"experimental": {"llamadas": 0, "costo": 0, "satisfaccion": []}
}
def procesar(self, solicitud: str, feedback_fn=None) -> tuple[str, str]:
"""
Procesa con A/B testing.
Args:
solicitud: La solicitud del usuario
feedback_fn: Función opcional que evalúa la calidad de la respuesta (0-1)
Returns:
Tupla (respuesta, grupo)
"""
grupo = "control" if random.random() < self.porcentaje_control else "experimental"
self.resultados_ab[grupo]["llamadas"] += 1
if grupo == "control":
# Siempre gpt-4o (control)
respuesta = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": solicitud}],
temperature=0, max_tokens=500
).choices[0].message.content
else:
# Router inteligente (experimental)
resultado = self.router_experimental.procesar(solicitud)
respuesta = resultado.respuesta
if feedback_fn:
score = feedback_fn(solicitud, respuesta)
self.resultados_ab[grupo]["satisfaccion"].append(score)
return respuesta, grupo
def reporte_ab(self) -> dict:
"""Genera reporte comparativo del A/B test."""
for grupo in self.resultados_ab:
sats = self.resultados_ab[grupo]["satisfaccion"]
self.resultados_ab[grupo]["satisfaccion_promedio"] = sum(sats) / len(sats) if sats else None
return self.resultados_ab
Troubleshooting
Problema 1: El clasificador envía solicitudes complejas al modelo simple
Síntoma: La calidad de las respuestas baja para algunas consultas porque el clasificador las marcó como "simple" incorrectamente.
Diagnóstico:
def calibrar_clasificador(
solicitudes_etiquetadas: list[tuple[str, NivelComplejidad]],
router: RouterComplejidad
) -> dict:
"""
Evalúa la precisión del clasificador en un conjunto de pruebas.
"""
correctas = 0
errores = []
for solicitud, nivel_real in solicitudes_etiquetadas:
nivel_pred, razon, metodo, conf = router.clasificar(solicitud)
if nivel_pred == nivel_real:
correctas += 1
else:
errores.append({
"solicitud": solicitud[:60],
"real": nivel_real.value,
"predicho": nivel_pred.value,
"confianza": conf,
"metodo": metodo
})
accuracy = correctas / len(solicitudes_etiquetadas)
return {"accuracy": accuracy, "errores": errores[:5]}
# Ajuste: Si hay muchos falsos simples, bajar el umbral
# config.umbral_simple = 0.85 # Más estricto para clasificar como simple
Problema 2: La latencia del clasificador LLM aumenta el tiempo total
Síntoma: El routing añade 500ms-1s de latencia por la llamada de clasificación.
Solución: Optimizar la clasificación:
def clasificacion_rapida(texto: str) -> NivelComplejidad:
"""
Clasificación ultra-rápida usando solo heurísticas + análisis de longitud.
Sin llamadas al LLM.
"""
longitud = len(texto)
texto_lower = texto.lower()
# Heurísticas ordenadas de más a menos confiables
if longitud > 500:
return NivelComplejidad.COMPLEJA
palabras_complejas = ["analiz", "diseñ", "estrategi", "implement", "arquitect"]
if any(p in texto_lower for p in palabras_complejas):
return NivelComplejidad.COMPLEJA
if longitud < 80:
return NivelComplejidad.SIMPLE
return NivelComplejidad.MEDIA # Default
Problema 3: Over-routing (demasiadas solicitudes van al modelo costoso)
Síntoma: El ahorro esperado no se materializa porque el 60%+ va a gpt-4o.
Diagnóstico y ajuste:
def analizar_distribucion(router: RouterComplejidad, solicitudes: list[str]) -> dict:
"""Analiza la distribución de complejidad sin ejecutar las respuestas."""
conteo = {"simple": 0, "media": 0, "compleja": 0, "muy_compleja": 0}
for s in solicitudes:
nivel, _, _, _ = router.clasificar(s)
conteo[nivel.value] += 1
total = len(solicitudes)
print("Distribución de complejidad:")
for nivel, n in conteo.items():
print(f" {nivel}: {n} ({n/total*100:.1f}%)")
return conteo
# Si hay demasiadas "complejas", revisar las reglas heurísticas
# o bajar el umbral: config.umbral_simple = 0.65
Ejercicios
Ejercicio 1: Routing para un sistema de e-commerce
Diseña un sistema de routing para un chatbot de e-commerce. Define reglas heurísticas para:
- Preguntas simples de catálogo (precio, disponibilidad)
- Consultas de estado de pedido (necesita datos externos)
- Reclamaciones o disputas (requiere manejo cuidadoso)
- Consultas técnicas sobre productos
Ver solución
REGLAS_ECOMMERCE = [
# Simple: consultas de catálogo
ReglaHeuristica("precio", r"(precio|costo|cuánto vale|cuánto cuesta)", NivelComplejidad.SIMPLE, 0.9, True),
ReglaHeuristica("disponibilidad", r"(disponible|en stock|hay|tienes)", NivelComplejidad.SIMPLE, 0.88, True),
# Compleja: datos externos (pedidos)
ReglaHeuristica("pedido", r"(pedido|orden|envío|entrega|tracking|dónde está)", NivelComplejidad.COMPLEJA, 0.92, True),
ReglaHeuristica("devolucion", r"(devolver|reembolso|garantía|reclamo|queja)", NivelComplejidad.COMPLEJA, 0.90, True),
# Media: consultas técnicas
ReglaHeuristica("tecnica", r"(compatib|especificacion|medida|talla|funciona con)", NivelComplejidad.MEDIA, 0.82, True),
]
config_ecommerce = ConfigRouting(
modelo_simple="gpt-4o-mini",
modelo_complejo="gpt-4o",
umbral_simple=0.85,
verbose=True
)
router_ecommerce = RouterComplejidad(config_ecommerce)
# Reemplazar las reglas heurísticas:
router_ecommerce.heuristica = ClasificadorHeuristico()
# Idealmente: extender la clase para usar REGLAS_ECOMMERCE
Ejercicio 2: Medir el ahorro real
Implementa un sistema que ejecute 20 solicitudes de prueba con y sin routing, y calcule el ahorro real en costos de API.
Ver solución
def benchmark_routing_vs_sin_routing(solicitudes: list[str]) -> dict:
"""Compara costos con y sin routing."""
router = RouterComplejidad(ConfigRouting(verbose=False))
costos = {"con_routing": 0, "sin_routing": 0}
COSTO_MINI = {"input": 0.00015, "output": 0.0006}
COSTO_4O = {"input": 0.0025, "output": 0.01}
for solicitud in solicitudes:
# Con routing
resultado = router.procesar(solicitud, verbose=False)
modelo = resultado.modelo_usado
tokens_in = len(solicitud.split()) * 1.3
tokens_out = len(resultado.respuesta.split()) * 1.3
costos_modelo = COSTO_MINI if "mini" in modelo else COSTO_4O
costos["con_routing"] += (tokens_in/1000 * costos_modelo["input"] + tokens_out/1000 * costos_modelo["output"])
costos["sin_routing"] += (tokens_in/1000 * COSTO_4O["input"] + tokens_out/1000 * COSTO_4O["output"])
ahorro_pct = (costos["sin_routing"] - costos["con_routing"]) / costos["sin_routing"] * 100
print(f"Con routing: ${costos['con_routing']:.4f}")
print(f"Sin routing (todo gpt-4o): ${costos['sin_routing']:.4f}")
print(f"Ahorro: {ahorro_pct:.1f}%")
return costos
Resumen
- Routing por complejidad: Clasificar solicitudes → dirige al modelo apropiado. Puede ahorrar 60-80% de costos.
- Clasificación en dos pasos: Heurística primero (gratis, instantáneo) → LLM solo si la heurística no es suficiente.
- Heurísticas efectivas: Longitud del texto, palabras clave de complejidad, patrones de pregunta.
- Routing multidimensional: Considerar riesgo de error, sensibilidad, y datos frescos además de complejidad.
- A/B testing: Para validar que el routing no degrada la calidad percibida.
- Calibración: Ajustar umbrales y reglas con solicitudes etiquetadas reales.