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

El modelo adecuado para la tarea correcta.

Índice

Ejecutar un modelo de 70 mil millones de parámetros para resumir un correo electrónico de 200 palabras es un desperdicio. Utilizar 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 el enrutamiento de modelos.

Este enfoque adapta la complejidad de la tarea a la capacidad del modelo. Los compromisos (trade-offs) son reales, pero los ahorros también lo son.

Diagrama de estrategias de enrutamiento de modelos LLM

El problema del enrutamiento

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

En la práctica, funcionan cuatro estrategias:

  1. Basada en capacidades — enrutar según lo que el modelo puede hacer.
  2. Consciente del coste — enrutar según lo que estás dispuesto a gastar.
  3. Consciente de la latencia — enrutar según la velocidad requerida.
  4. Híbrida — combinarlas.

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

Enrutamiento basado en capacidades

Es el enfoque más sencillo. Clasifica la tarea y envíala al modelo que la maneje.

Tarea Tamaño del modelo Ejemplos
Clasificación, etiquetado 1-3B Qwen2.5-1.5B, Gemma-2-2B
Resumen, extracción 3-7B Qwen2.5-7B, Llama-3.1-8B
Generación de código 7-14B Qwen2.5-Coder-7B, DeepSeek-Coder-V2
Razonamiento complejo 14-32B Qwen2.5-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 sentimientos. Simplemente no escribirá un ensayo coherente.

La implementación es directa:

ROUTING_RULES = {
    "classify": {"model": "qwen2.5-1.5b", "max_tokens": 100},
    "summarize": {"model": "qwen2.5-7b", "max_tokens": 500},
    "code_review": {"model": "qwen2.5-coder-7b", "max_tokens": 2000},
    "reason": {"model": "qwen2.5-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"])

El problema es la propia clasificación. Si te equivocas en el tipo de tarea, enrutarás al modelo incorrecto. He visto sistemas que clasificaban la revisión de código como “resumen” y perdían calidad silenciosamente.

Enrutamiento consciente del coste

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 unos seis meses con un uso moderado de la API.

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

Si procesas miles de solicitudes por sesión, incluso $0.05 en electricidad superan a los $15 por millón de tokens.

El enrutamiento basado 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": "qwen2.5-7b", "cost": 0.0},
            "medium": {"model": "qwen2.5-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 Qwen-32B y luego a Qwen-7B. Al final de una sesión larga, la salida es notablemente peor. Si eso importa depende de lo que estés construyendo.

Enrutamiento 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 Completado 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 transmites tokens a un usuario, la latencia del primer token es lo que perciben. Un modelo de 32B que tarda medio segundo en empezar se siente lento en comparación con un modelo de 1.5B que responde al instante.

class LatencyAwareRouter:
    def __init__(self):
        self.model_latencies = {
            "qwen2.5-1.5b": {"first_token": 0.05, "complete": 0.5},
            "qwen2.5-7b": {"first_token": 0.15, "complete": 2.0},
            "qwen2.5-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 "qwen2.5-1.5b"

Los números de latencia son aproximados: dependen de tu hardware, cuantización y tamaño de 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 de la mejor a la más fiable:

class FallbackRouter:
    def __init__(self):
        self.fallback_chain = [
            {"model": "claude-sonnet-4", "timeout": 30},
            {"model": "qwen2.5-72b", "timeout": 60},
            {"model": "qwen2.5-32b", "timeout": 120},
            {"model": "qwen2.5-7b", "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.

Cuándo ayuda el enrutamiento

El enrutamiento 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 sea bueno para esa tarea. El enrutador añade complejidad que no necesitas.

El prototipado inicial es otra razón para omitirlo. Consigue que la tarea funcione con un modelo y luego añade enrutamiento cuando el coste o la latencia se conviertan realmente en un problema.

Compromisos (Trade-offs)

Cada estrategia de enrutamiento optimiza algo y sacrifica otra cosa:

  • Modelo único — más sencillo, más caro, calidad consistente.
  • Basado en capacidades — mejor coste, mayor calidad por tarea, complejidad moderada.
  • Consciente del coste — más barato, la calidad varía, complejidad moderada.
  • Consciente de la latencia — más rápido, puede sacrificar calidad, complejidad moderada.
  • Híbrido — lo mejor de todo, más complejo de implementar.

Los sistemas en producción suelen converger hacia la opción híbrida. Comienza con el enrutamiento basado en capacidades, añade la conciencia del coste cuando llegue la factura y añade la conciencia de la latencia cuando los usuarios se quejen de la lentitud.

Relacionado

Suscribirse

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