Mantenere entro la memoria i turni dell'agente ricchi di immagini
Un turno dell'agente in produzione ha esaurito la memoria a 952 MB mentre ritagliava prodotti da sei scansioni di cataloghi e ne osservava i ritagli. Non c'era una sola perdita — ce n'erano tre. Ogni chiamata al modello rinviava le immagini del turno a dimensione piena; la libreria che usiamo per chiamare il modello teneva in vita ogni richiesta fino alla fine del turno; e il tracciamento del framework dell'agente conservava gli input di ogni chiamata, immagini comprese, per l'intero turno. Le abbiamo sistemate tutte e tre: le immagini vengono ridotte prima di essere inviate, e ogni immagine di una chiamata viene rilasciata non appena la chiamata è finita.
Come l'agente vede le immagini
Quando costruisci su Mythex, l'agente lavora dentro il sandbox cloud del tuo progetto. Può guardare immagini — uno screenshot che alleghi, un mockup, un file nel progetto o un'immagine che ha appena prodotto — inviandole al modello insieme alla conversazione.
Un turno è tutto ciò che l'agente fa in risposta a un tuo messaggio. All'interno di un turno, l'agente chiama il modello molte volte: pensa, esegue uno strumento, guarda il risultato, ripensa. Ogni chiamata invia la conversazione finora, e questo include le immagini del turno, codificate come testo (base64, che le rende circa un terzo più grandi).
Il turno che si è bloccato stava facendo qualcosa di ragionevole. Ha preso sei scansioni di cataloghi, circa 2,4 MB ciascuna, ha ritagliato singoli prodotti e ha continuato a rileggere i ritagli per verificarli. Sono molte chiamate al modello, ciascuna con diverse immagini grandi.
Causa 1: ogni chiamata rinviava immagini a dimensione piena
Ogni chiamata al modello ricostruiva il corpo della richiesta con le immagini del turno a dimensione piena — circa 20 MB di base64 per chiamata. Il corpo viene costruito come una stringa unica nella memoria dell'agente, quindi ogni chiamata allocava altri ~20 MB.
Inviare a dimensione piena non portava nemmeno vantaggi. Il modello riduce comunque le immagini a circa 1.300 pixel sul lato lungo, quindi i pixel in più costavano solo memoria e tempo di caricamento.
La soluzione: ridurre una volta, inviare piccolo
- Le immagini oltre 400 KB vengono convertite in JPEG con al massimo 1.568 pixel sul lato lungo prima di essere inviate. Le immagini più piccole partono così come sono.
- La conversione usa ImageMagick, presente in ogni sandbox di Mythex, ed esegue una volta per file. Un turno che mostra le stesse scansioni a ogni chiamata riusa la copia ridotta. La cache è indicizzata per percorso, dimensione e data di modifica del file, quindi un file modificato viene convertito di nuovo, e ha un proprio limite di dimensione così non può diventare la cosa che riempie la memoria.
- Il budget per le immagini in una singola chiamata al modello scende da 16 MB a 6 MB.
Le stesse sei scansioni ora costano 4,4 MB per chiamata invece di 19,7 MB.
Causa 2: il client del modello non lasciava mai andare una richiesta
La dimensione di ogni richiesta era solo una parte. La memoria cresceva anche di chiamata in chiamata. Nel nostro ambiente di staging, lo stesso tipo di turno tratteneva 369 MB di memoria viva anche dopo una garbage collection completa — il punto in cui tutto ciò che è irraggiungibile è stato liberato. Il campionamento dell'heap attribuiva tutta la crescita alla parte della libreria client del modello che codifica i corpi delle richieste.
La causa era un listener di abort. Quando premi Stop, il segnale di abort del turno si attiva, e ogni richiesta in ascolto viene annullata. Noi passavamo quell'unico segnale, a livello di turno, a ogni richiesta. La libreria client aggiunge un listener "abort" a qualsiasi segnale riceva — e non lo rimuove mai.
Quindi ogni richiesta di un turno restava raggiungibile dal segnale del turno, per tutta la durata del turno: il listener, che teneva il controller della richiesta, che teneva la richiesta, che teneva l'intero corpo con le sue immagini.
La soluzione: un segnale per richiesta
Ogni richiesta ora ha il proprio segnale. È collegato al segnale del turno solo mentre quella richiesta è in corso — per una risposta in streaming, finché non è stato letto l'ultimo blocco — e viene scollegato subito dopo. Stop raggiunge ancora la richiesta in volo, ma il segnale del turno tiene al massimo un listener alla volta.
I test coprono tutte e tre le parti:
- Dopo 12 chiamate, nessun listener resta sul segnale del turno. Prima della correzione, erano 12.
- Su 15 richieste da 8 MB ciascuna, la memoria resta stabile. Prima, cresceva di 128 MB.
- Premere Stop interrompe ancora una richiesta a metà del suo stream.
Causa 3: il tracciamento conservava gli input di ogni chiamata
Uno snapshot dell'heap mostrava un terzo elemento: circa 6 MB trattenuti per chiamata al modello. Questo veniva dallo streaming degli eventi del framework dell'agente. Registra ogni chiamata al modello di un turno come figlia del turno, input compresi, fino alla fine del turno. Ogni immagine allegata a ogni chiamata restava in memoria insieme a essa.
La soluzione: svuotare le immagini appena la chiamata restituisce
Le immagini sono allegate solo ai messaggi costruiti per una specifica chiamata al modello. Non vengono mai memorizzate nello stato salvato della conversazione. Quindi, una volta che una chiamata restituisce il risultato, è sicuro svuotare i dati delle immagini in quei messaggi. Il tracciamento conserva una registrazione testuale della chiamata; i pixel vengono eliminati.
Un dettaglio era importante: le immagini devono essere svuotate sul posto. Il tracciatore può raggiungere gli stessi dati dell'immagine tramite più riferimenti su un messaggio, quindi sostituire con una nuova lista vuota lascerebbe raggiungibile l'originale — pixel compresi. Lo stato salvato della conversazione non viene mai modificato.
Un test esegue il grafo dell'agente sotto lo stesso streaming di eventi con un'immagine da 4 MB in ogni chiamata. Senza la correzione, la memoria cresceva di 77 MB in 16 chiamate. Con la correzione, resta stabile.
Cosa ne abbiamo ricavato
- I problemi di memoria possono arrivare a strati. Tre cause separate trattenevano le immagini troppo a lungo, e tutte e tre dovevano essere eliminate perché il turno restasse entro la memoria.
- Qualsiasi cosa passi a una libreria può essere trattenuta. Un segnale e una traccia sopravvivevano alle chiamate a cui erano state date. Quando la memoria cresce per chiamata, guarda quali oggetti di lunga durata tocca la chiamata.
- Non inviare ciò che l'altro lato scarta comunque. I pixel che il modello avrebbe ignorato ci costavano memoria a ogni chiamata.
- Testa la memoria direttamente. Ogni correzione è arrivata con un test che misura la crescita dell'heap o il numero di listener, così una regressione si vede come un numero, non come un crash.
Questa è la seconda storia di memoria legata all'agente in poco tempo; la prima riguardava un'etichetta di avanzamento ripetuta 41.000 volte. Se vuoi sapere di più su cos'è l'agente e come funziona, vedi la nostra guida su gli agenti IA e la documentazione della chat.