Enrutamiento de modelos: deja de usar un solo modelo para todo

«El modelo adecuado para la tarea adecuada».

Índice

Ejecutar un modelo de 70 mil millones de parámetros para resumir un correo electrónico de 200 palabras es un desperdicio. Ejecutar un modelo de 3 mil millones de parámetros para revisar código en producción es imprudente. La mayoría de los sistemas se encuentran en algún punto intermedio, y ahí es donde entra la enrutación de modelos.

Esta técnica adapta la complejidad de la tarea a la capacidad del modelo. Los compromisos son reales, pero los ahorros también.

Diagrama de estrategias de enrutación de modelos LLM

El problema de la enrutación

La gente suele comenzar con un solo modelo y mantenerlo. Eso funciona hasta que te das cuenta del costo, de la latencia, o de ambos. La alternativa es construir un enrutador, algo que decida qué modelo maneja cada solicitud.

Cuatro estrategias funcionan en la práctica:

  1. Basada en capacidades: enrutar según lo que el modelo puede hacer.
  2. Consciente del costo: enrutar según lo que estás dispuesto a gastar.
  3. Consciente de la latencia: enrutar según qué tan rápido lo necesitas.
  4. Híbrida: combinarlos.

Cada una optimiza algo diferente. Elegir una suele ser una decisión sobre qué duele más.

Enrutación basada en capacidades

El enfoque más simple. Clasifica la tarea y envíala al modelo que la maneja.

Tarea Tamaño del modelo Ejemplos
Clasificación, etiquetado 1-3B Qwen3-1.7B, Gemma-2-2B
Resumen, extracción 3-7B Qwen3-8B, Llama-3.1-8B
Generación de código 7-14B Qwen2.5-Coder-7B, DeepSeek-Coder-V2
Razonamiento complejo 14-32B Qwen3-32B, Llama-3.1-70B
Escritura creativa, análisis 32B+ Qwen2.5-72B, Claude, GPT-4

Si la tarea no necesita el modelo más grande, no lo uses. Un modelo de 1.5B maneja bien la clasificación de sentimiento. Simplemente no escribirá un ensayo coherente.

La implementación es sencilla:

ROUTING_RULES = {
    "classify": {"model": "qwen3-1.7B", "max_tokens": 100},
    "summarize": {"model": "qwen3-8B", "max_tokens": 500},
    "code_review": {"model": "qwen2.5-coder-7b", "max_tokens": 2000},
    "reason": {"model": "qwen3-32b", "max_tokens": 4000},
    "creative": {"model": "claude-sonnet-4", "max_tokens": 8000},
}

def route_request(task_type: str) -> dict:
    return ROUTING_RULES.get(task_type, ROUTING_RULES["reason"])

La trampa es la clasificación en sí. Si te equivocas en el tipo de tarea, enrutarás al modelo incorrecto. He visto sistemas clasificar la revisión de código como “resumen” y perder calidad silenciosamente.

Enrutación consciente del costo

La inferencia local brilla aquí. Los modelos locales son prácticamente gratuitos después de amortizar el hardware. Una RTX 5080 se paga sola en aproximadamente seis meses con un uso moderado de la API.

Modelo Entrada ($/M tokens) Salida ($/M tokens) Costo local/hora
GPT-4o $2.50 $10.00
Claude Sonnet 4 $3.00 $15.00
Qwen2.5-72B (API) $0.50 $2.00
Qwen3-32B (local) $0.00 $0.00 ~$0.10
Qwen3-8B (local) $0.00 $0.00 ~$0.05

Si procesas miles de solicitudes por sesión, incluso $0.05 en electricidad supera a $15/M tokens.

La enrutación basada en presupuesto retrocede a medida que gastas:

class CostAwareRouter:
    def __init__(self, budget_per_session: float = 0.10):
        self.budget = budget_per_session
        self.spent = 0.0
        self.models = {
            "cheap": {"model": "qwen3-8B", "cost": 0.0},
            "medium": {"model": "qwen3-32b", "cost": 0.0},
            "expensive": {"model": "claude-sonnet-4", "cost": 0.000015},
        }

    def route(self, task: str) -> str:
        ratio = self.spent / self.budget
        if ratio < 0.5:
            return self.models["expensive"]["model"]
        elif ratio < 0.8:
            return self.models["medium"]["model"]
        return self.models["cheap"]["model"]

La calidad se degrada a medida que retrocedes. Comienzas con Claude, pasas a Qwen3-32B y luego a Qwen3-8B. Al final de una sesión larga, la salida es notablemente peor. Si eso importa depende de lo que estés construyendo.

Enrutación consciente de la latencia

Las herramientas interactivas necesitan primeros tokens rápidos. Las tareas por lotes pueden esperar. La diferencia suele ser un factor de cinco en el tamaño del modelo.

Caso de uso Primer token Completo Tamaño máximo del modelo
Chat en tiempo real < 200ms < 2s < 7B
Herramientas interactivas < 500ms < 5s < 14B
Procesamiento por lotes < 1s < 30s Cualquiera
Investigación/análisis < 2s < 60s Cualquiera

Cuando estás transmitiendo tokens a un usuario, la latencia del primer token es lo que sienten. Un modelo de 32B que tarda medio segundo en comenzar se siente lento en comparación con un modelo de 1.5B que responde al instante.

class LatencyAwareRouter:
    def __init__(self):
        self.model_latencies = {
            "qwen3-1.7b": {"first_token": 0.05, "complete": 0.5},
            "qwen3-8B": {"first_token": 0.15, "complete": 2.0},
            "qwen3-32b": {"first_token": 0.5, "complete": 10.0},
            "claude-sonnet-4": {"first_token": 0.3, "complete": 5.0},
        }

    def route(self, target_latency: float) -> str:
        for model, latencies in sorted(
            self.model_latencies.items(),
            key=lambda x: x[1]["complete"]
        ):
            if latencies["complete"] <= target_latency:
                return model
        return "qwen3-1.7b"

Los números de latencia son aproximados, dependen de tu hardware, cuantización y tamaño del lote. Mide en tu propia configuración.

Estrategias de respaldo

Los modelos fallan. Las APIs limitan las tasas. Ocurren tiempos de espera. El patrón que funciona es una cadena de respaldo, ordenada desde la mejor hasta la más confiable:

class FallbackRouter:
    def __init__(self):
        self.fallback_chain = [
            {"model": "claude-sonnet-4", "timeout": 30},
            {"model": "qwen2.5-72b", "timeout": 60},
            {"model": "qwen3-32b", "timeout": 120},
            {"model": "qwen3-8b", "timeout": 300},
        ]

    def route_with_fallback(self, prompt: str) -> str:
        for config in self.fallback_chain:
            try:
                return self.call_model(
                    config["model"], prompt,
                    timeout=config["timeout"]
                )
            except (TimeoutError, APIError) as e:
                log.warning(f"Model {config['model']} failed: {e}")
                continue
        raise RuntimeError("All fallback models failed")

El último modelo de la cadena debería ser local. Es más lento, pero no fallará debido a un problema de red o a una clave de API.

Cuando la enrutación ayuda

La enrutación tiene sentido cuando tu carga de trabajo es mixta. Si estás haciendo clasificación, resumen y razonamiento en el mismo sistema, un enrutador ahorra dinero y latencia.

No tiene sentido cuando todo lo que haces tiene la misma complejidad. Simplemente usa el modelo que es bueno en esa tarea. El enrutador añade complejidad que no necesitas.

El prototipado temprano es otra razón para omitirlo. Haz que la tarea funcione con un modelo, luego añade enrutación cuando el costo o la latencia realmente se conviertan en un problema.

Compromisos

Cada estrategia de enrutación optimiza algo y sacrifica otro:

  • Modelo único: más simple, más caro, calidad constante.
  • Basada en capacidades: mejor costo, mayor calidad por tarea, complejidad moderada.
  • Consciente del costo: más barato, la calidad varía, complejidad moderada.
  • Consciente de la latencia: más rápido, puede sacrificar calidad, complejidad moderada.
  • Híbrida: lo mejor de todo, la más compleja de implementar.

Los sistemas en producción suelen converger en híbrido. Comienza con enrutación basada en capacidades, añade conciencia de costos cuando llegue la factura y añade conciencia de latencia cuando los usuarios se quejen de la lentitud.

Relacionado

Suscribirse

Recibe nuevas publicaciones sobre sistemas, infraestructura e ingeniería de IA.