En mi organización he trabajado como ingeniero backend y arquitecto. Mi principal responsabilidad es garantizar que los servicios que diseñamos cumplan con sus requisitos funcionales, pero también escale a millones de solicitudes por minuto, mantenga un tiempo de actividad del 99,99 % y siga siendo lo suficientemente rentable como para mantener los OPEX bajo control. Durante la mayor parte de mi carrera, el tráfico que llegó a esos servicios siguió la tendencia sobre la que podía razonar: impulsado por humanos, predecible y autolimitado. Eso ha cambiado. El tráfico de agentes no se comporta de esa manera, y los dos modelos de escalamiento dominantes (bajo demanda y sin servidor) que hemos creado fracasan. En este artículo, analizo el cambio de mentalidad necesario al escalar para el tráfico de agentes y los patrones específicos que vale la pena considerar.
Si ha trabajado en ingeniería de inteligencia artificial, habrá visto este cambio. El tráfico que llega a sus puntos finales, sus puertas de enlace modelo, no se ven como solía verse el tráfico o como solía comportarse el tráfico humano. El tráfico agente llega en ráfagas impredecibles. Se repite y lo vuelve a intentar sin descanso. Reduce significativamente sus costos de escalado en comparación con el tráfico con forma humana.
Quiero explicar por qué esa suposición ahora no se cumple, por qué los dos modelos de escalamiento dominantes (bajo demanda y sin servidor) que hemos creado heredan el problema y cómo debería ser la solución al problema.
La suposición subyacente a todo
Cada marco de escalamiento que hemos creado supone que el tráfico se ve tal como lo generan las personas. Compare lo que está a la izquierda de esta tabla con lo que está ahora a la derecha
El tráfico agente viola estos siete supuestos a la vez. Es por eso que la respuesta no es una versión mejor de ninguno de los modelos que ya tenemos: es un lugar fundamentalmente diferente para colocar la inteligencia. Permítanme mostrarles lo que quiero decir al explicar cómo ha evolucionado la disciplina del escalamiento.
Generación 1: Anticipación (instancias bajo demanda)
Al principio de mi carrera, cuando trabajaba en backends de transmisión de video a gran escala que atendían a millones de espectadores simultáneos, la planificación de la capacidad era un ejercicio humano. Cuando estaba a punto de comenzar un gran evento en vivo, sabíamos exactamente cuándo se produciría el pico, aproximadamente qué tan pronunciado sería y cuándo se aplanaría. El trabajo se realizó con anticipación: precalentar las flotas EC2 con días de anticipación, establecer límites de escala automática mínima y máxima, dotar de personal a una sala de guerra durante el evento.
El tráfico tenía forma humana. Tenía una curva, un pico, una cola. Podrías razonar al respecto y prepararte para ello.
Solía pasar horas en salas de guerra. Solía comenzar horas antes del evento, asegurándose de que todas las flotas de EC2 estuvieran preconfiguradas, que los tipos de instancias estuvieran actualizados, que las comprobaciones de estado funcionaran bien, que los balanceadores de carga estuvieran todos bien y que la red estuviera en buen estado. Solíamos monitorear constantemente los picos en el volumen de llamadas y las fallas. Hubo casos en los que la anticipación de la demanda no encajaba bien debido a volúmenes de llamadas del lado del cliente mal configurados, lo que nos llevó a cambiar la política del grupo de escalamiento automático durante el evento e incluso a absorber un breve período de fallas. Una vez finalizado el evento, veríamos caer el tráfico y luego, después del evento, tendríamos que volver a la capacidad anterior que se había incrementado, para ahorrar en OPEX.
El supuesto central de la Generación 1: el pico es predecible, por lo que hay que anticiparse a él.
Generación 2: confianza reactiva (sin servidor)
Luego comenzamos a construir sistemas sin servidor, lo que cambió las reglas del juego: API Gateway, Lambda, Step Functions y la conversación sobre aprovisionamiento desaparecieron en gran medida. Dejaste de precalentar y comenzaste a confiar en que la plataforma reaccionaría. Eso funcionó porque el tráfico todavía estaba impulsado principalmente por humanos: los usuarios abren la aplicación, navegan, interactúan y la cierran. La demanda todavía era predecible y el tiempo de reacción de la plataforma fue lo suficientemente rápido porque el inicio fue gradual.
El supuesto central de la Generación 2: no es necesario anticiparse, porque la plataforma reacciona más rápido que los aumentos de la demanda.
El pivote: por qué la orquestación de las máquinas rompe ambas cosas a la vez
La orquestación de máquinas no determinista (agentes autónomos, cadenas de llamada de herramientas de varios pasos, bucles de recuperación) rompe ambos modelos simultáneamente.
Derrota a Gen 1 porque no hay un cronograma que anticipar. El tráfico de agentes no tiene reloj y no se puede precalentar para un pico que no se puede predecir. Derrota a Gen 2 porque el escalado reactivo es una señal retrasada. El tráfico de agentes alcanza su velocidad máxima en milisegundos; cuando se activa el escalado automático basado en CPU, ya estás degradado. Peor aún, la tecnología sin servidor ejecuta fielmente cada llamada redundante en un bucle de agente fuera de control y le factura por la disfunción.
Para crear y responder a solicitudes de agente, la respuesta de cuatro capas a continuación cubre los patrones clave.
Capa 1: escalamiento basado en el comportamiento
Debe dejar de aumentar el uso de la CPU. Es una señal retrasada y cuando cruza un umbral, un bucle de agente ya ha drenado el grupo o ha aumentado la factura por disfunción. La señal que desea monitorear es la velocidad y la forma de solicitud. Las solicitudes casi idénticas de una persona que llama son un indicador de un bucle de agente que debe verificarse mucho antes de que aparezca en las métricas agregadas de CPU. Un agente (reintentado, mal configurado) puede enviar cientos de solicitudes casi idénticas en segundos. Para cuando la CPU capta la señal, es posible que los ciclos de bucle ya hayan degradado el rendimiento o hayan desperdiciado dinero real en atender esas llamadas duplicadas.
El siguiente patrón utiliza velocidad de solicitud + diversidad de carga útil para determinar si la persona que llama X está en un bucle, de modo que pueda ponerla en cuarentena antes de que agote los ciclos de la CPU.
tiempo de importación desde colecciones import defaultdict, clase deque AgentLoopDetector: """Marca los bucles de agente fuera de control según la velocidad de la solicitud y la repetición de la carga útil, mucho antes de que la CPU agregada refleje la carga.""" def __init__(self, window_s=10, rate_threshold=50, serious_threshold=0.2): self.window_s = window_s self.rate_threshold = rate_threshold self.diversity_threshold = diversidad_threshold self.events = defaultdict(deque) # caller_id -> deque[(ts, payload_hash)] def is_looping(self, caller_id: str, payload_hash: str) -> bool: now = time.monotonic() q = self.events[caller_id] q.append((now, payload_hash)) while q y now – q[0][0]> self.window_s: q.popleft() tasa = len(q) if tasa < self.rate_threshold: return False único = len({h for _, h in q}) diversidad = único / tasa de retorno diversidad < self.diversity_threshold # Introduzca el valor booleano en un detector de decisión de aislamiento = AgentLoopDetector() if detector.is_looping(caller_id="agent-1", payload_hash="test1"): cuarentena(caller_id="agente-1")
Capa 2: La puerta de enlace de la IA como amortiguador
Una puerta de enlace tradicional cuenta el tráfico de solicitud-respuesta HTTP/REST. Una puerta de enlace de IA mide el costo de las interacciones LLM y el manejo rápido: valora cada llamada en tokens o calcula y acelera una conexión específica que excede su presupuesto antes de que llegue a sus sistemas de inferencia centrales.
Dos capacidades que importan aquí: limitación del costo por conexión (basada en la cuota de token) y almacenamiento en caché semántico: almacenamiento en caché según la similitud inmediata, de modo que las consultas repetitivas de los agentes nunca lleguen al modelo en absoluto. El almacenamiento en caché semántico es poderoso para absorber los impactos del tráfico, pero necesita barreras de seguridad a su alrededor: un caché puede devolver respuestas obsoletas o alucinadas y, peor aún, puede filtrar la respuesta privada de un usuario a otro si las claves del caché no tienen el alcance adecuado.
El siguiente ejemplo es la capa de middleware que se encuentra entre la persona que llama y el modelo. Ante cada solicitud entrante, decide:
¿Puedo saltarme el modelo? (golpe de caché) ¿Puede permitírselo la persona que llama? (error de caché y ejecución) Hazlo y recuérdalo (construye el caché)
Si el caché llega, devuelve la respuesta almacenada: eso es costo de modelo cero, sin uso de token, sin latencia.
def gateway(request, ctx): # Caché semántico: coincide con la similitud incrustada, no con la URL hit = semantic_cache.lookup(request.prompt, umbral=0.95) if hit: return hit # absorbido en el borde, costo cero del modelo # Ponle precio a la llamada y verifica el presupuesto de costos restante de la persona que llama est_cost = estima_token_cost(request.prompt, request.model) si no ctx.budget.can_afford(request.caller_id, est_cost): return Respuesta( status=429, headers={"Retry-After": ctx.budget.reset_in(request.caller_id)}, body="presupuesto de costo de conexión excedido", ) resp = forward_to_model(solicitud) ctx.budget.debit(request.caller_id, resp.usage.total_tokens) semantic_cache.store(request.prompt, resp, ttl=3600) devolver resp
El almacenamiento en caché semántico intercambia exactitud por absorción: necesita un umbral de similitud lo suficientemente alto como para evitar devolver respuestas incorrectas y una ruta de derivación para las llamadas que deben ser recientes. En entornos empresariales, donde varias personas de un equipo a menudo trabajan en proyectos similares y terminan enviando mensajes similares, esto es un gran beneficio. El almacenamiento en caché semántico con las medidas de seguridad adecuadas puede ahorrar una cantidad significativa de dinero y aun así mantener rápidos los tiempos de respuesta.
Capa 3: cola asíncrona
Las interacciones humanas esperan respuestas en menos de un segundo. Los agentes autónomos normalmente no lo hacen. Un humano en una pasarela de pago espera que la transacción se complete inmediatamente cuando se realiza el pedido; esa no es la misma expectativa que una API orientada al agente, que puede ejecutarse en segundo plano como colas asíncronas. Los SLA para las API humanas y las API de agentes son fundamentalmente diferentes, y cambiar a patrones asíncronos ayuda a aplanar los picos y eliminar la expectativa de un martillo sincrónico.
El siguiente ejemplo muestra el patrón de cola asíncrona. Tiene un lado remitente y un lado trabajador, que funcionan de forma desacoplada. Una vez que se alcanza la señal de contrapresión, admitir más empeoraría las cosas. Cada solicitud entrante verifica primero cuántos trabajos hay en la cola. Si la cola ya está llena, rechaza la solicitud con la señal (HTTP 429). Es una señal importante para que el cliente reduzca el ritmo con una estrategia de retroceso en lugar de ponerlo en cola.
QUEUE_HIGH_WATERMARK = 10000 def enviar(solicitud): profundidad = queue.approx_profundidad() si profundidad > QUEUE_HIGH_WATERMARK: # Contrapresión: decirle a la persona que llama que reduzca la velocidad en lugar de hacer cola infinitamente return Respuesta( estado=429, encabezados={"Reintentar-después": contrapresión_delay(profundidad)}, cuerpo="sistema saturado, reintentar más tarde", ) job_id = queue.enqueue(request.payload, caller_id=request.caller_id) return Response(status=202, body={"job_id": job_id, "poll": f"/result/{job_id}"}) # Lado del trabajador: tire a un ritmo controlado; los límites de concurrencia protegen el flujo descendente def work_loop(): para el trabajo en queue.consume(max_concurrency=200): resultado = proceso(trabajo) resultados.put(trabajo.id, resultado)
Capa 4: control de admisión basado en tokens
En lugar de contar las solicitudes en conjunto, la intención es cambiar la unidad de admisión del recuento de solicitudes al costo de los recursos. No limite las llamadas por minuto; limitar el cálculo que puede consumir una sesión.
Un depósito de tokens ingresado en la sesión (debitado por los tokens reales o el cómputo utilizado, no por el recuento de llamadas) permite cortar una cadena de razonamiento pesada que consume un cómputo desproporcionado, mientras que las personas que llaman con poco peso pasan libremente.
A continuación se muestra un ejemplo de SessionTokenBucket, que implementa el control de admisión por sesión según el costo del token, no por el recuento de solicitudes. Cada sesión obtiene su propio depósito de capacidad_tokens (predeterminado 100 000) que se recarga a refill_per_s (predeterminado 1000 tokens/segundo). El método admit() estima el costo de una llamada entrante y carga el depósito si hay suficientes tokens disponibles o rechaza la llamada.
El mecanismo principal es la recarga basada en el tiempo: cuando una sesión intenta admitir, _tokens() calcula cuántos tokens se han acumulado desde su última actividad, con un límite del tamaño del depósito. Esto permite que una sesión inactiva acumule capacidad, mientras que una sesión activa se acelera proporcionalmente a su consumo.
El uso es sencillo: si admit() devuelve False, responde con un HTTP 429 (Demasiadas solicitudes) que le indica a la persona que llama que su presupuesto de sesión está agotado. Las personas que llaman con poco peso siguen pasando sin verse afectadas; una sesión que ejecuta una cadena de razonamiento pesada se interrumpe antes de que agote los recursos que todos los demás necesitan.
importar clase de tiempo SessionTokenBucket: """Admisión por costo de recursos. La capacidad y la recarga están en tokens (cálculo), no en solicitudes, por lo que una cadena de razonamiento pesada puede rechazarse mientras pasan muchas llamadas ligeras.""" def __init__(self, capacidad_tokens=100_000, recarga_por_s=1_000): self.capacity = capacidad_tokens self.refill = recarga_per_s self.state = {} # session_id -> [tokens_available, last_refill_ts] def _tokens(self, session_id): ahora = time.monotonic() disponible, último = self.state.get(session_id, (self.capacity, now)) disponible = min(self.capacity, disponible + (ahora – último) * self.refill) self.state[session_id] = [avail, now] return disponible def admitir(self, session_id, est_tokens) -> bool: si self._tokens(session_id) < est_tokens: devuelve False self.state[session_id][0]-= est_tokens devuelve True bucket = SessionTokenBucket() si no es bucket.admit(session_id="s-7", est_tokens=40_000): elevar Reject(429, "presupuesto de cómputo de sesión agotado")
La verdadera respuesta: mover la inteligencia hacia arriba
Para manejar el patrón no determinista de tráfico de agencia, las cuatro capas anteriores ayudan. Son necesarios. Pero observe lo que tienen en común: todas son válvulas en la entrada de la tubería. Lambda aún ejecuta y factura la llamada redundante. La puerta de enlace todavía tiene que inspeccionar y rechazar. La cola todavía tiene que aguantar la inundación. Absorber una carga no determinista en la capa de infraestructura es un juego que sólo se puede perder lentamente.
Por eso la inteligencia tiene que ir contracorriente. El control de admisión y la contrapresión no pueden vivir sólo en la puerta de entrada: el cliente tiene que ser lo suficientemente inteligente como para saber cuándo dejar de preguntar. Un cliente agente con buen comportamiento:
Lleva un presupuesto de reintento y lo gasta: no hay reintentos infinitos. Respeta `429` y `Retry-After` en lugar de atravesarlos. Ejecuta un disyuntor del lado del cliente que se abre en caso de falla sostenida. Trata las señales de contrapresión como tiempo de importación cooperativo, no adversario, clase aleatoria. BackpressionAwareClient: """Un cliente agente cooperativo. El acelerador más efectivo vive aquí, en la fuente, no en la puerta de enlace.""" def __init__(self, retry_budget=3, breaker_threshold=5, cooldown_s=30): self.retry_budget = retry_budget self.failures = 0 self.breaker_threshold = breaker_threshold self.cooldown_s = cooldown_s self.open_until = 0 def call(self, fn): if time.monotonic() < self.open_until: rise CircuitOpen("interruptor abierto; sin preguntar") para intento en rango(self.retry_budget + 1): resp = fn() if resp.status == 429: self.failures += 1 if self.failures >= self.breaker_threshold: self.open_until = time.monotonic() + self.cooldown_s rise CircuitOpen("disyuntor disparado") retraso = resp.headers.get("Retry-After") o (2 ** intento + random.random()) time.sleep(float(delay)) # coopera, no golpees continuar self.failures = 0 return resp rise RetryBudgetExhausted("dejó de preguntar") # el cliente decide detenerse
donde esto nos deja
Pasamos la Generación 1 con la carga anticipándonos. Pasamos la Generación 2 confiando en la plataforma para reaccionar. La Generación 3 pide algo más difícil: construir clientes e infraestructura lo suficientemente inteligentes como para no generar la carga en primer lugar. Incluso con la Generación 1 y la Generación 2 para la carga determinista impulsada por humanos, he visto clientes que se portan mal y generan problemas. El cliente debe ser lo suficientemente inteligente como para comprender la petición en primer lugar y respetar todas las señales de contrapresión.
Si está diseñando una arquitectura de agente hoy (orquestando llamadas de LLM, ejecutando canales de recuperación, permitiendo que los modelos de lenguaje planifiquen y actúen), cree el presupuesto de reintento, el disyuntor y la contrapresión cooperativa en el cliente desde el primer día. No lo deje como una ocurrencia tardía que surge cuando comienza a costarle dinero.
Una vez más, la válvula más inteligente no está en la entrada de la tubería. Está en la fuente.