La fuita de memòria que s'amagava en una etiqueta de progrés
Una nit de finals de setembre, l'agent d'IA d'un espai de treball d'un usuari va fallar amb un error de memòria insuficient a mig torn. Els sospitosos òbvis (massa subagents executant-se alhora, o un context que havia crescut massa) estaven tots equivocats. La causa real era una etiqueta d'estat: mentre el model transmetia una ordre de shell gran a trossos de pocs caràcters, reenviàvem tota l'ordre fins aquell moment com a etiqueta de progrés a cada tros. Una ordre de 124 KB es va convertir en uns 41.000 esdeveniments i al voltant de 690 MB de cadenes en cua.
Com transmet la feina l'agent
Quan construeixes a Mythex, l'agent s'executa dins del sandbox privat al nùvol del teu projecte. Mentre el model escriu, la seva sortida arriba en flux al navegador: el text apareix paraula a paraula, i quan executa una eina (per exemple, una ordre de shell que escriu un fitxer) veus una etiqueta per a ella que s'actualitza en directe, perquè sàpigues què fa.
El model transmet en trossos petits. Amb el model que fèiem servir, sovint eren només tres caràcters.
Les primeres hipòtesis eren errònies
L'error semblava un problema d'escala, així que hi vam començar a mirar.
- Subagents en paral·lel? El torn només n'havia fet servir dos, cadascun amb uns 1,5 MB d'estat. No n'hi havia prou.
- Traces retingudes? El nostre traçat mantenia un arbre d'execució a memòria, però no era ni de lluny la mida de l'heap en el moment de la caiguda.
Les dues eren històries plausibles, i les dues eren fàcils de creure. Cap no reproduïa la caiguda. La lliçó que tornem a aprendre: una hipòtesi no és una causa fins que reprodueix la falla.
Reproduir-ho de debò
Vam construir un sandbox de prova amb els mateixos límits de memòria que producció i un model fals que transmetia un fitxer gran en trossos realistes de tres caràcters. També vam fer que el lector de l'altre extrem del socket anés lent, com una xarxa real.
Això ho va fer aparèixer. L'heap pujava i el procés moria amb el mateix error.
El mecanisme era senzill quan el podíem observar. Mentre el model escrivia una ordre gran (en aquest cas un heredoc que escrivia un fitxer de 124 KB), el nostre codi de transmissió actualitzava l'etiqueta de l'eina a cada tros. L'etiqueta no era els tres caràcters nous; era tota l'ordre fins aquell moment. Així, els esdeveniments creixien: 3 caràcters, després 6, després 9… fins a 124 KB, unes 41.000 vegades. El socket no els podia enviar tan ràpid com es produïen, de manera que s'acumulaven al seu buffer d'escriptura com a cadenes: uns 690 MB.
La solució
- Limita l'etiqueta. Una etiqueta d'estat només ha de dir què està passant. Ara està limitada a 200 caràcters, i limitem la freqüència amb què tornem a escanejar l'ordre parcial.
- Elimina la concatenació quadràtica. Unir trossos transmesos amb concatenació repetida de cadenes és O(n²) per a sortides grans. Hem passat a un enfocament per blocs que afegeix en temps lineal.
Després vam buscar qualsevol altra cosa que pogués créixer sense límit amb un allau de sortida, i la vam acotar:
- La sortida d'ordres en directe que es mostra al xat s'agrupa i es limita als últims 8.000 caràcters.
- Les línies d'esdeveniments individuals estan limitades a 4 MB.
- El buffer de repetició que fa servir un navegador per recuperar-se després de reconnectar-se està limitat a 8 MB per projecte.
- Els punts de control antics s'eliminen a mesura que avança un torn, guardant l'estat més recent i els ancoratges que necessiten els torns anteriors.
Provar les allaus amb honestedat
Dues coses van fer que això fos més difícil de provar del que hauria de ser:
- Els models reals no col·laboren. Si demanes a un model que escrigui un fitxer enorme, normalment es nega, resumeix o l'escriu de manera més sensata. Per provar allaus cal un model guionitzat que faci exactament el que li diguis, executant-se a través del codi real de l'agent.
- Els scripts han de comportar-se com models. El nostre primer model guionitzat reutilitzava el mateix ID de resposta a cada resposta, de manera que el framework de l'agent tractava cada resposta com a substitució de l'anterior i tancava els torns abans d'hora. Semblava un error d'actualització de la IU. No ho era: era l'arnès de prova que ens enganyava. També vam haver d'aplicar el límit real de context del model dins del script, o les xifres de memòria no eren realistes.
Què ens hem endut
- Les IU de flux multipliquen els petits errors. Qualsevol cosa que s'enviï «per cada tros» s'envia desenes de milers de vegades. Envia deltes, o limita la càrrega.
- Limita cada buffer que travessa una frontera lenta. Sockets, registres de repetició i punts de control necessiten un sostre.
- Reprodueix abans de corregir. Les nostres dues primeres explicacions semblaven correctes i no ho eren.
Si has vist fa poc un fitxer molt llarg aparèixer al xat de Mythex sense que l'agent es bloquegi, aquesta és part de la raó.