Blog

Mantener los turnos del agente con muchas imágenes dentro de la memoria

Un turno del agente en producción se quedó sin memoria con 952 MB mientras recortaba productos de seis escaneos de catálogo y revisaba sus recortes. No hubo una sola fuga: hubo tres. Cada llamada al modelo volvía a enviar las imágenes del turno a tamaño completo; la biblioteca que usamos para llamar al modelo mantenía vivas todas las peticiones hasta que terminaba el turno; y el trazado del framework del agente conservaba las entradas de cada llamada, con sus imágenes, durante todo el turno. Lo arreglamos los tres: las imágenes se reducen antes de enviarse, y las imágenes de cada llamada se liberan en cuanto esa llamada termina.

Cómo ve las imágenes el agente

Cuando construyes en Mythex, el agente trabaja dentro del sandbox en la nube de tu proyecto. Puede mirar imágenes (una captura que adjuntas, una maqueta, un archivo de tu proyecto o una imagen que él mismo acaba de producir) enviándolas al modelo junto con la conversación.

Un turno es todo lo que hace el agente en respuesta a un mensaje tuyo. Dentro de un turno, el agente llama al modelo muchas veces: piensa, ejecuta una herramienta, mira el resultado, vuelve a pensar. Cada una de esas llamadas envía la conversación hasta ese momento, incluidas las imágenes del turno, codificadas como texto (base64, que las hace alrededor de un tercio más grandes).

El turno que se cayó hacía algo razonable. Tomó seis escaneos de catálogo, de unos 2,4 MB cada uno, recortó productos individuales de ellos y siguió leyendo sus recortes para comprobarlos. Eso son muchas llamadas al modelo, cada una con varias imágenes grandes.

Causa 1: cada llamada volvía a enviar imágenes a tamaño completo

Cada llamada al modelo reconstruía el cuerpo de su petición con las imágenes del turno a tamaño completo: unos 20 MB de base64 por llamada. El cuerpo se construía como una sola cadena en la memoria del agente, así que cada llamada reservaba otros ~20 MB.

Enviar a tamaño completo ni siquiera aportaba nada. El modelo reduce las imágenes a unos 1.300 píxeles en el lado largo por sí mismo, así que los píxeles extra solo costaban memoria y tiempo de subida.

La solución: reducir una vez, enviar pequeño

  • Las imágenes de más de 400 KB se convierten ahora a JPEG de como máximo 1.568 píxeles en el lado largo antes de enviarse. Las más pequeñas van tal como están.
  • La conversión usa ImageMagick, que viene en cada sandbox de Mythex, y se ejecuta una vez por archivo. Un turno que muestra los mismos escaneos en cada llamada reutiliza la copia reducida. Esa caché se indexa por la ruta del archivo, su tamaño y su fecha de modificación, así que un archivo editado se vuelve a convertir, y tiene su propio tope de tamaño para que no pueda ser lo que llene la memoria.
  • El presupuesto de imágenes en una sola llamada al modelo baja de 16 MB a 6 MB.

Los mismos seis escaneos ahora cuestan 4,4 MB por llamada en lugar de 19,7 MB.

Causa 2: el cliente del modelo nunca soltaba una petición

El tamaño de cada petición era solo una parte. La memoria también crecía llamada tras llamada. En nuestro entorno de staging, el mismo tipo de turno retenía 369 MB de memoria activa incluso después de una recolección de basura completa, el punto en el que se libera todo lo inalcanzable. El muestreo del heap situó todo el crecimiento en la parte de la biblioteca cliente del modelo que codifica los cuerpos de las peticiones.

La causa era un listener de aborto. Cuando pulsas Detener, se dispara la señal de aborto del turno, y cada petición que escucha se cancela. Nosotros pasábamos esa única señal de todo el turno a cada petición. El cliente de la biblioteca añade un listener «abort» a cualquier señal que reciba, y nunca lo quita.

Así que cada petición de un turno seguía alcanzable desde la señal del turno, mientras durara el turno: el listener, que retenía el controlador de la petición, que retenía la petición, que retenía todo el cuerpo con sus imágenes.

La solución: una señal por petición

Ahora cada petición tiene su propia señal. Se vincula a la señal del turno solo mientras esa petición está en curso (en una respuesta en streaming, hasta que se ha leído el último fragmento) y se desvincula justo después. Detener sigue llegando a la petición en vuelo, pero la señal del turno solo tiene un listener a la vez.

Las pruebas cubren las tres partes:

  • Tras 12 llamadas, no queda ningún listener en la señal del turno. Antes del arreglo había 12.
  • En 15 peticiones de 8 MB cada una, la memoria se mantiene estable. Antes crecía 128 MB.
  • Pulsar Detener sigue abortando una petición a mitad de su stream.

Causa 3: el trazado guardaba las entradas de cada llamada

Una instantánea del heap mostró un tercer retenedor: unos 6 MB retenidos por cada llamada al modelo. Esto venía del streaming de eventos del framework del agente. Registra cada llamada al modelo de un turno como hija del turno, con sus entradas incluidas, hasta que termina el turno. Cada imagen adjunta a cada llamada se quedaba en memoria con ella.

La solución: vaciar las imágenes cuando la llamada retorna

Las imágenes solo se adjuntan a los mensajes construidos para una llamada concreta al modelo. Nunca se guardan en el estado guardado de la conversación. Así que, una vez que una llamada retorna, es seguro vaciar los datos de imagen de esos mensajes. El trazado conserva un registro de texto de la llamada; los píxeles se van.

Un detalle fue crucial: las imágenes tienen que vaciarse en el sitio. El trazador puede llegar a los mismos datos de imagen por más de una referencia en un mensaje, así que sustituir por una lista nueva y vacía dejaría el original (píxeles incluidos) todavía alcanzable. El estado de la conversación guardada nunca se modifica.

Una prueba ejecuta el grafo del agente bajo el mismo streaming de eventos con una imagen de 4 MB en cada llamada. Sin el arreglo, la memoria crecía 77 MB en 16 llamadas. Con él, se mantiene estable.

Qué aprendimos

  • Los problemas de memoria pueden venir en capas. Tres causas distintas retenían imágenes demasiado tiempo, y las tres tenían que desaparecer para que el turno se mantuviera dentro de la memoria.
  • Cualquier cosa que pases a una biblioteca puede retenerse. Una señal y un trazado sobrevivieron a las llamadas a las que se les dieron. Cuando la memoria crece por llamada, mira qué objetos de larga vida toca la llamada.
  • No envíes lo que el otro lado va a descartar. Los píxeles que el modelo desecharía de todos modos nos costaban memoria en cada llamada.
  • Prueba la memoria directamente. Cada arreglo llegó con una prueba que mide el crecimiento del heap o el número de listeners, así que una regresión aparece como un número, no como una caída.

Esta es la segunda historia de memoria del agente en poco tiempo; la primera fue una etiqueta de progreso reenviada 41.000 veces. Si quieres saber más sobre qué es el agente y cómo funciona, consulta nuestra guía sobre agentes de IA y la documentación del chat.

Mythex · Pricing · Crea lo que quieras con IA · Plantillas de sitios web y apps · Mythex comparado · Glosario para crear apps con IA · Herramientas gratis para crear apps con IA · English

English አማርኛ العربية Български বাংলা Bosanski Català Čeština Dansk Deutsch Ελληνικά Español (España) Eesti فارسی Suomi Français Français (Canada) ગુજરાતી हिन्दी Hrvatski Magyar Հայերեն Bahasa Indonesia Íslenska Italiano 日本語 ქართული Қазақ тілі ಕನ್ನಡ 한국어 Lietuvių Latviešu Македонски മലയാളം Монгол मराठी Bahasa Melayu မြန်မာ Norsk bokmål Nederlands ਪੰਜਾਬੀ Polski Português (Brasil) Português (Portugal) Română Русский Slovenčina Slovenščina Soomaali Shqip Srpski Svenska Kiswahili தமிழ் తెలుగు ไทย Tagalog Türkçe Українська اردو Tiếng Việt 简体中文 繁體中文(香港) 繁體中文(台灣)