Krashen av slut på minne som gömde sig i en förloppsetikett
En kväll i slutet av september kraschade AI-agenten i en användares arbetsyta med ett fel om slut på minne mitt i en tur. De uppenbara misstänkta, nämligen för många underagenter som körs samtidigt eller en kontext som vuxit för stor, var båda fel. Den verkliga orsaken var en statusetikett. Medan modellen strömmade ett stort skalkommando några tecken i taget skickade vi om hela kommandot hittills som förloppsetikett vid varje bit. Ett kommando på 124 KB blev cirka 41 000 händelser och ungefär 690 MB köade strängar.
Hur agenten strömmar sitt arbete
När du bygger på Mythex körs agenten i ditt projekts privata molnsandlåda. Medan modellen skriver strömmar utdata tillbaka till din webbläsare. Text dyker upp ord för ord, och när agenten kör ett verktyg, till exempel ett skalkommando som skriver en fil, ser du en etikett för det som uppdateras live. Då vet du vad den gör.
Modellen strömmar i små bitar. Med modellen vi körde var de ofta bara tre tecken långa.
De första hypoteserna var fel
Krashen såg ut som ett skalningsproblem, så vi började där.
- Parallella underagenter? Turen hade bara använt två, var och en med cirka 1,5 MB tillstånd. Inte tillräckligt.
- Kvarhållna spår? Vår spårning höll ett körningsträd i minnet, men det var inte i närheten av storleken på heapen vid krashen.
Båda var rimliga förklaringar, och båda var billiga att tro på. Ingen av dem reproducerade krashen. Lärdomen vi fortsätter att lära om är att en hypotes inte är en orsak förrän den reproducerar felet.
Reproduktion på riktigt
Vi byggde en tillfällig sandlåda med samma minnesgränser som i produktion och en falsk modell som strömmade en stor fil i realistiska bitar om tre tecken. Vi gjorde också läsaren i andra änden av socketen långsam, som ett riktigt nätverk.
Det gjorde susen. Heapen växte och processen dog med samma fel.
Mekanismen var enkel när vi kunde se den. Medan modellen skrev ett stort kommando, i det här fallet en heredoc som skrev en fil på 124 KB, uppdaterade vår strömningskod verktygets etikett vid varje bit. Etiketten var inte de nya tre tecknen. Den var hela kommandot hittills. Så händelserna växte: 3 tecken, sedan 6, sedan 9, och så vidare upp till 124 KB, cirka 41 000 gånger. Socketen kunde inte skicka dem lika snabbt som de producerades, så de samlades i dess skrivbuffert som strängar, ungefär 690 MB av dem.
Åtgärden
- Begränsa etiketten. En statusetikett behöver bara säga vad som händer. Den är nu begränsad till 200 tecken, och vi stryper hur ofta det delvisa kommandot skannas om.
- Sluta med kvadratisk sammanfogning. Att foga ihop strömmade bitar med upprepad strängsammanfogning är O(n²) för stora utdata. Vi gick över till ett chunk-baserat upplägg som lägger till i linjär tid.
Sedan letade vi efter allt annat som kunde växa obegränsat vid en flod av utdata, och begränsade det:
- Live-utdata från kommandon som visas i chatten samlas i batchar och behålls till de sista 8 000 tecknen.
- Enskilda händelserader är begränsade till 4 MB.
- Replay-bufferten som en webbläsare använder för att komma ikapp efter återanslutning är begränsad till 8 MB per projekt.
- Gamla checkpoints rensas allteftersom turen går, och de behåller det nyaste tillståndet samt de ankare som tidigare turer behöver.
Att testa översvämningar ärligt
Två saker gjorde det svårare att testa än det borde vara:
- Riktiga modeller samarbetar inte. Ber du en modell skriva ut en gigantisk fil vägrar den oftast, sammanfattar den eller skriver den mer förnuftigt. För att testa översvämningar behöver du en skriptad modell som gör exakt det du säger, körd genom den riktiga agentkoden.
- Skript måste bete sig som modeller. Vår första skriptade modell återanvände samma svars-ID vid varje svar. Agentramverket behandlade då varje svar som en ersättning för det förra och avslutade turer i förtid. Det såg ut som en bugg i UI-uppdateringen. Det var det inte. Det var testramverket som ljög för oss. Vi var också tvungna att tillämpa modellens riktiga kontextgräns i skriptet, annars var minnessiffrorna orealistiska.
Det vi tog med oss
- Strömmande UI:n förstärker små misstag. Allt du skickar “per bit” skickas tiotusentals gånger. Skicka deltan, eller begränsa nyttolasten.
- Begränsa varje buffert som passerar en långsam gräns. Sockets, replay-loggar och checkpoints behöver alla ett tak.
- Reproducera innan du åtgärdar. Våra två första förklaringar lät rimliga och var fel.
Om du nyligen har sett en mycket lång fil dyka upp i Mythex-chatten utan att agenten stannat, är det en del av förklaringen.