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.

Recursos adicionales

  1. OpenAI Model pricing
  2. Anthropic Model pricing
  3. RouteLLM - routing library
  4. FRUGALGPT: How to Use Large Language Models While Reducing Cost and Improving Performance
  5. OpenAI Models documentation
  6. Cost optimization strategies - AWS Well-Architected ML