Garder les tours d'agent riches en images dans la mémoire
En production, un tour d'agent a manqué de mémoire à 952 Mo pendant qu'il découpait des produits dans six numérisations de catalogue et examinait ses découpes. Il n'y avait pas une seule fuite, mais trois. Chaque appel au modèle renvoyait les images du tour en pleine taille ; la bibliothèque que nous utilisons pour appeler le modèle conservait chaque requête en vie jusqu'à la fin du tour ; et le traçage du framework d'agent conservait les entrées de chaque appel, images comprises, pendant tout le tour. Nous avons corrigé les trois : les images sont réduites avant d'être envoyées, et les images de chaque appel sont libérées dès que l'appel se termine.
Comment l'agent voit les images
Lorsque vous créez sur Mythex, l'agent travaille dans le bac à sable infonuagique de votre projet. Il peut examiner des images, qu'il s'agisse d'une capture d'écran que vous joignez, d'une maquette, d'un fichier de votre projet ou d'une image qu'il vient de produire lui-même, en les envoyant au modèle avec la conversation.
Un tour correspond à tout ce que fait l'agent en réponse à un message de votre part. Au cours d'un tour, l'agent appelle le modèle plusieurs fois : il réfléchit, exécute un outil, examine le résultat, puis réfléchit à nouveau. Chacun de ces appels envoie la conversation jusqu'à présent, y compris les images du tour, encodées en texte (base64, ce qui les rend environ un tiers plus volumineuses).
Le tour qui a planté faisait quelque chose de raisonnable. Il a pris six numérisations de catalogue, d'environ 2,4 Mo chacune, a découpé des produits individuels et a relu ses découpes pour les vérifier. Cela représente beaucoup d'appels au modèle, chacun transportant plusieurs grandes images.
Cause 1 : chaque appel renvoyait les images en pleine taille
Chaque appel au modèle reconstruisait le corps de sa requête avec les images du tour en pleine taille, soit environ 20 Mo de base64 par appel. Ce corps est construit comme une seule chaîne dans la mémoire de l'agent, donc chaque appel allouait encore environ 20 Mo.
Envoyer la pleine taille ne rapportait même rien. Le modèle réduit lui-même les images à environ 1 300 pixels sur le grand côté, donc les pixels supplémentaires ne coûtaient que de la mémoire et du temps de téléversement.
La correction : réduire une fois, envoyer petit
- Les images de plus de 400 Ko sont maintenant converties en JPEG d'au plus 1 568 pixels sur le grand côté avant l'envoi. Les images plus petites sont envoyées telles quelles.
- La conversion utilise ImageMagick, présent dans chaque bac à sable Mythex, et s'exécute une fois par fichier. Un tour qui montre les mêmes numérisations à chaque appel réutilise la copie réduite. Ce cache est indexé par le chemin, la taille et la date de modification du fichier ; un fichier modifié est donc reconverti, et le cache a sa propre limite de taille pour ne pas devenir lui-même ce qui remplit la mémoire.
- Le budget d'images dans un seul appel au modèle passe de 16 Mo à 6 Mo.
Les mêmes six numérisations coûtent maintenant 4,4 Mo par appel au lieu de 19,7 Mo.
Cause 2 : le client du modèle ne libérait jamais une requête
La taille de chaque requête n'était qu'une partie du problème. La mémoire augmentait aussi d'appel en appel. Dans notre environnement de préproduction, le même type de tour gardait 369 Mo de mémoire vive même après un ramasse-miettes complet, c'est-à-dire le moment où tout ce qui est inatteignable a été libéré. Un échantillonnage du tas a attribué toute la croissance à la partie de la bibliothèque cliente du modèle qui encode les corps de requête.
La cause était un écouteur d'abandon. Lorsque vous appuyez sur Stop, le signal d'abandon du tour se déclenche, et chaque requête à l'écoute s'annule. Nous transmettions ce signal unique, propre au tour, à chaque requête. La bibliothèque cliente ajoute un écouteur « abort » au signal qu'on lui donne, et ne le retire jamais.
Ainsi, chaque requête d'un tour restait accessible depuis le signal du tour, pendant toute la durée du tour : l'écouteur, qui retenait le contrôleur de la requête, qui retenait la requête, qui retenait tout le corps de la requête avec ses images.
La correction : un signal par requête
Chaque requête reçoit maintenant son propre signal. Il n'est relié au signal du tour que pendant l'exécution de cette requête (pour une réponse diffusée en continu, jusqu'à la lecture du dernier fragment), puis il est délié aussitôt. Stop atteint toujours la requête en cours, mais le signal du tour ne contient jamais plus d'un écouteur à la fois.
Les tests couvrent les trois volets :
- Après 12 appels, aucun écouteur ne reste sur le signal du tour. Avant la correction, il y en avait 12.
- Sur 15 requêtes de 8 Mo chacune, la mémoire reste stable. Avant, elle augmentait de 128 Mo.
- Appuyer sur Stop interrompt toujours une requête en plein milieu de son flux.
Cause 3 : le traçage conservait les entrées de chaque appel
Un instantané du tas a révélé un troisième détenteur : environ 6 Mo conservés par appel au modèle. Celui-ci venait de la diffusion d'événements du framework d'agent. Elle enregistre chaque appel au modèle d'un tour comme enfant du tour, entrées comprises, jusqu'à la fin du tour. Chaque image jointe à chaque appel restait donc en mémoire.
La correction : vider les images une fois l'appel terminé
Les images ne sont jamais attachées qu'aux messages construits pour un appel précis au modèle. Elles ne sont jamais stockées dans l'état enregistré de la conversation. Une fois l'appel terminé, il est donc sûr de vider les données d'image de ces messages. La trace conserve un enregistrement textuel de l'appel ; les pixels disparaissent.
Un détail était important : les images doivent être vidées sur place. Le traceur peut atteindre les mêmes données d'image par plus d'une référence sur un message ; remplacer la liste par une nouvelle liste vide laisserait l'original, pixels compris, toujours accessible. L'état de conversation enregistré n'est jamais modifié.
Un test exécute le graphe de l'agent sous la même diffusion d'événements avec une image de 4 Mo à chaque appel. Sans la correction, la mémoire augmentait de 77 Mo sur 16 appels. Avec elle, la mémoire reste stable.
Ce que nous en avons retenu
- Les problèmes de mémoire peuvent être superposés. Trois causes distinctes gardaient les images trop longtemps, et les trois devaient disparaître pour que le tour reste dans la mémoire.
- Tout ce que vous remettez à une bibliothèque peut être conservé. Un signal et une trace survivaient aux appels auxquels on les avait confiés. Lorsque la mémoire augmente par appel, examinez les objets de longue durée que l'appel touche.
- N'envoyez pas ce que l'autre côté jette de toute façon. Les pixels que le modèle écarterait de toute façon nous coûtaient de la mémoire à chaque appel.
- Testez la mémoire directement. Chaque correction s'accompagne d'un test qui mesure la croissance du tas ou le nombre d'écouteurs, afin qu'une régression apparaisse sous forme de chiffre et non de plantage.
C'est le deuxième récit de mémoire lié à la sortie de l'agent en peu de temps ; le premier était une étiquette de progression qui renvoyait 41 000 fois. Pour en savoir plus sur ce qu'est l'agent et son fonctionnement, voir notre guide sur les agents IA et la documentation du clavardage.