Essä · AI-utveckling
Vad jag lärde mig av att bygga ctx
Johan Öhman Saldes

Vad ctx skulle bevisa
Jag började bygga ctx utifrån en enkel tes. En liten modell som läser rätt 2 000 rader i ett repo borde klara sig bättre än en som läser fel 200 000. ctx indexerar en kodbas till symboler, anropskanter och mätvärden, och använder sedan den modellen för att välja kontext och granska vad en agent ändrar.
Tesen behövde belägg, så jag byggde benchmarks. Under flera månader körde jag retrieval-studier, ett kompilatorkontrollerat orakel för namnbyten, agentkörningar i två harnesses, modelljämförelser och experiment med belöningssignaler. Flera gav svar jag inte väntade mig. En del talade emot mitt eget verktyg.
Den första rubriken var en artefakt av packningen
Min första retrieval-studie såg ut som en klar vinst. På 36 verkliga buggfix-commits lade en vandring över ctx anropsgraf filen med rättningen i en kontext på 6k tokens betydligt oftare än BM25 (28 av 36 mot 12, p ≤ 0,004).
Vinsten kom mest från hur kontexterna packades. BM25 lade sin budget på testfiler, eftersom uppgiftsbeskrivningarna nämnde tester som fallerade. Grafvandringarna uteslöt tester redan i sin konstruktion. Med samma regel steg BM25:s andel från 33 % till 52 %, och vandringens försprång krympte till p ≈ 0,05 för den Jev-bedömda grafen. För de andra graferna var skillnaden inte längre signifikant.
En andra baslinje var ännu mer ödmjukande. En filnamnsregel helt utan index, som gissar context.go utifrån context_test.go, fick med rätt fil på 29 av 36 uppgifter. Regeln kombinerad med valfri grafvandring, eller med en rättvist packad BM25, nådde 33. På den här uppgiftsmängden tillförde grafen nästan ingenting utöver filnamnet.
Jag drog två lärdomar av det. Hur kontexten packas kan väga tyngre än själva retrieval-signalen, så en baslinje måste packas på samma sätt som metoden den jämförs med. Och en benchmark vars prompt avslöjar svaret kan inte mäta navigering.
Harnessen betydde mer än verktyget
Den största effekt jag mätte hade inget med ctx att göra. Med samma modell (gpt-oss-20b) och samma 36 uppgifter löste min första egenbyggda harness 28 % av episoderna. pi utan tillägg, en liten kodagent med öppen källkod, löste 51 %.
Skillnaden handlade om redigering. Min harness lät modellen ersätta radintervall den inte kunde se. I 353 av 537 sådana ändringar skilde sig den nya texten från det ersatta stycket med tre rader eller mer, så kod dubblerades eller klipptes bort. Episoder med radintervallsändringar löstes i 12 % av fallen, de utan i 53 %. pi:s redigeringsverktyg kräver den exakta gamla texten, så det felet kan inte uppstå där.
Inne i pi gjorde ctx-tillägget ingen mätbar skillnad. Nyckelordsroutning löste 56 % och Jev-routning 45 %, mot 51 % utan tillägg, och båda konfidensintervallen korsade noll med god marginal. Briefen gjorde sitt jobb: den visade en fil som rättningen berör redan före första turen i 96 av 108 episoder. Men pi utan tillägg hittade filen senast andra turen ändå, och i alla lägen kom första ändringen i den runt tur 5.
Körningarna visade också hur sällan små modeller använder verktygen de får. get_callers och get_callees fanns tillgängliga, och modellen anropade dem inte en enda gång. Publicerade studier av harnesses rapporterar samma mönster. Det fick mig att ändra riktning: ctx ska visa graffakta när modellen läser en fil eller söker, i stället för att vänta på att den frågar.
Grafen är exakt, inte fullständig
För att bedöma grafen utan en modell i loopen använde jag Go-kompilatorn som facit. Jag döpte om en funktion med enbart det grafen visste, alltså definitionen och anropsställena, och kontrollerade sedan om gin och dess tester fortfarande kompilerade.
På 150 symboler i gin kompilerade namnbyten från den Jev-bedömda grafen i 90 % av fallen. En grep efter anropsställen, name(, nådde också 90 %. Skillnaden låg i misstagen: grafen gjorde 14 felaktiga ändringar, grep 56. Ren namnupplösning utan Jev var sämre än grep, med 84 %. Grafens styrka var precision, inte täckning.
Tvetydiga namn visade vad som saknades. På 60 metoder som delar namn med ungefär nio andra symboler gav Jev-grafen rena namnbyten i 20 % av fallen och grep i 58 %. De flesta misslyckanden hade en och samma orsak. I Go kan en metod som uppfyller ett interface inte döpas om ensam; interface-deklarationen och varje syskonimplementation måste ändras samtidigt. ctx hade bara calls-kanter och kunde därför inte se dessa familjer. Med en familjeslutning fördubblades andelen rena namnbyten till 40 %.
Vissa familjer går aldrig att döpa om. En metod som heter Error eller String uppfyller oftast ett interface i standardbiblioteket, och att döpa om den bryter antingen bygget eller ändrar i det tysta hur värdet skrivs ut. Ett säkert verktyg måste känna igen de fallen och avstå.
Att vägra är en funktion
Arbetet med namnbyten ändrade frågan jag ställde. "Hur stor andel kompilerar efter ändringen?" belönar ett verktyg som gissar. En bättre fråga är hur stor andel verktyget kan genomföra på egen hand och få rätt, medan resten lämnas över till en människa.
Planeraren jag byggde sorterar varje namnbyte i tre nivåer. Den genomför ändringen när varje ställe är säkert. Den frågar när vissa ställen är tvetydiga eller bara förekommer som text. Den vägrar när metoden hör till ett interface som definieras utanför repot, eller bär ett namn från standardbiblioteket som Error. Familjer hittas genom att matcha metodnamn och en normaliserad signatur, eftersom Go-interface uppfylls strukturellt.
I gin, cobra, chi och det undanhållna repot mux var genomför-nivån ren på 266 av 266 namnbyten. Den täckte 66–78 % av vanliga namnbyten och 28 % av de tvetydiga. Vid första kontakten med ett okänt repo (chi, innan reglerna justerats) kompilerade 41 av 43 genomförda namnbyten. Det är den ärligare uppskattningen av hur planeraren beter sig på ny kod.
Priset är täckning. Mellan 20 och 32 % av förfrågningarna hamnade i fråga-nivån, och mer än hälften av dem hade kompilerat om de genomförts blint. Jag tycker att den avvägningen är värd att göra. Ett verktyg som agenter förlitar sig på är mer användbart när dess säkra svar går att lita på än när det har rätt lite oftare i genomsnitt.
Grindar som aldrig slog till
Styrning är halva löftet med ctx, och mina benchmarks testade det knappt. Över 36 mänskliga rättningar och 94 kompilerande modellpatchar slog duplicerings- och arkitekturgrindarna till noll gånger. Inget av repona i uppgifterna hade en rules.toml, och små buggfixar lämnar lite utrymme att duplicera något.
Den enda strukturella signal som rörde sig var svag. complexity_delta skilde lösta från olösta patchar med en AUC på 0,62, inte bättre än patchens storlek, och den korrelerade med storleken med ρ ≈ 0,5. Den mäter dessutom något annat än namnet antyder. I ctx är det anropskoppling, två gånger fan-out plus fan-in inom samma fil, så en funktion full av förgreningar men utan anrop får noll.
Billiga deterministiska kontroller gjorde det verkliga jobbet. Vid val mellan sex kandidater per uppgift höjde valet av den första som kompilerade andelen lösta från 26 % till 40 %. Med uppgiftens tester dessutom nåddes 60 %, lika mycket som den bästa kandidaten över huvud taget kunde ge. Återkoppling från indexet hjälpte också vid reparation: när en modell namngav en funktion som inte fanns, och fick se de verkliga symbolerna, räddades 3 av 9 misslyckanden, mot 1 av 20 med kompilatorns felmeddelanden.
Det obekvämaste fyndet gällde ctx självt. I juli rapporterade dess egna pull requests "no rules file", och kodbasens granskning gav 6,8 av 10. Ett styrningsverktyg som inte styr sitt eget repo väcker en uppenbar fråga, och den bör jag besvara innan en läsare ställer den.
Vad uppgifterna inte kunde visa
I efterhand begränsade uppgiftsmängden varje experiment. Varje uppgift började med ett test som fallerade, och testets namn pekade rakt på filen som skulle ändras. Enligt den bundna grafen låg bara en av 36 rättningar tre eller fler anrop bort från sitt test. Att hitta rätt ställe var aldrig det svåra, så ett verktyg byggt för just det hade inget att vinna.
Varje benchmark frågade om testerna gick igenom. Jag byggde ctx för att besvara en annan fråga: skrev modellen bra kod utan att duplicera det som redan finns? Det har jag inte testat än. Det kräver uppgifter där återanvändning är rätt svar, till exempel funktionsönskemål hämtade från commits som anropade en befintlig hjälpfunktion någon annanstans i repot, med själva commiten dold. Måttet är om agenten anropar hjälpfunktionen eller skriver en egen kopia, tillsammans med antal kloner, ökad komplexitet och regelbrott.
Andra begränsningar spelar också roll. De flesta körningar använde en enda liten modell, gpt-oss-20b, med ett fönster på 131k tokens, där kontexten sällan blir trång. Vid körningar hos en extern leverantör varierade resultatet med två till fem lösta uppgifter trots identiska kontexter. Skillnader av den storleken är därför brus. Många rättningar är äldre än modellernas träningsgräns. Nästa omgång måste dimensioneras i förväg, köras på 8k–32k tokens och använda rättningar som slagits ihop efter träningsgränsen.
Valet av modell spelade däremot roll för att generera träningsdata. På 20 nya Go-uppgifter löste GLM-5.3 60 % av episoderna och Nemotron-3 Super 35 %. Det gör GLM-5.3 till min lärarkandidat för senare finjustering, förutsatt att licensvillkoren tillåter det.
Vad jag tror nu
Jag började med att se ctx som en bättre retriever. Beläggen pekar mot en smalare och mer användbar roll. Grafen är exakt men ofullständig, så den fungerar bäst som kontroll av föreslagna ändringar och som ett sätt att avgränsa arbetet, inte som enda källa till ställen att ändra. Planeraren för namnbyten visar mönstret: genomför det som är säkert, fråga om resten och vägra det som inte kan göras säkert.
Mina tekniska prioriteringar följer av det. ctx behöver implements-, embeds- och värdereferenskanter, som SCIP-index redan innehåller. Graffakta hör hemma i läsningar, sökningar och ändringar, där små modeller ser dem utan att fråga. Kompilering och tester bör vara grundkontrollen, och strukturella poäng får vara råd tills en benchmark visar att de förutsäger något.
Tre frågor är fortfarande öppna. Får ctx agenter att återanvända befintlig kod i stället för att duplicera den? Hjälper det mer vid 8k–32k tokens, de fönster det är byggt för? Klarar en modell som får ett anropsställe i taget breda ändringar bättre än en som ska göra allt på en gång? Åtagandelistan visar att varje ställe ryms i ungefär 100 tokens, men ingen modell har körts på den.
Metoden kan vara det mest hållbara resultatet. Varje påstående som överlevde hade en deterministisk kontroll bakom sig: en kompilator, ett undanhållet repo eller en baslinje packad på samma sätt. De påståenden som föll, det tidiga retrieval-försprånget och briefens värde, vilade på uppgifter vars prompter avslöjade svaret.