Essä · 26 oktober 2026
Bygg din egen benchmark i stället för att lita på andras
Johan Öhman Saldes

Labbens benchmarks svarar på en fråga du inte ställde
Om du har kodat med AI ett tag kör du redan informella benchmarks varje dag. Du byter modell, märker en skillnad och säger till en kollega att den här är bättre. Det är en benchmark utan kontroll, utan protokoll och utan möjlighet att visa att du har fel. De publicerade topplistorna är inte mycket bättre för dina syften. Ett labb har valt uppgifterna, prompten, harnessen och inställningarna, och du kan inte pröva något av det mot din egen kod.
Den här artikeln handlar om alternativet: en liten, tråkig och reproducerbar harness som kör modeller mot uppgifter från dina egna repon, sparar varje rå utdata och bedömer resultatet med något du kan kontrollera. Det är uppställningen som ger värdet. När utdata från de här svarta lådorna ligger i filer som du kan ställa frågor till, kan du besvara frågor som ingen topplista besvarar, och du kan komma på dig själv med att ha fel. Det var precis vad som hände i våra egna experiment: huvudresultatet vi började med höll inte.
Varför publicerade benchmarks inte säger så mycket om din kod
Ett publicerat resultat visar hur en modell klarade någon annans uppgifter under någon annans val. Tre saker gör det till en svag vägledning för dina beslut.
Den första är att uppgifterna inte är dina. Publika testsviter är inte skrivna i ditt språk, med dina konventioner eller med din beroendegraf, och en modell som toppar en lista kan ändå misslyckas med den sortens ändringar som ditt team gör varje vecka. Den andra är att dolda val flyttar resultatet. Prompt, hur kontexten packas, redigeringsformat, samplingsinställningar och omförsök påverkar alla utfallet. I våra körningar vände sättet vi packade kontexten på ett huvudresultat, och redigeringsformatet avgjorde hur stor andel av svaren som ens gick att applicera (52 % mot 79 %). Den tredje är att en enda siffra döljer hur modellen misslyckas. En andel godkända säger ingenting om huruvida modellen fallerar genom att hitta på en funktion, skicka ogiltig JSON eller skriva en patch som aldrig går att applicera, och de felen kräver olika åtgärder.
Till det kommer brus. Med identiska kontexter och temperatur 0 mot en hostad endpoint skilde sig resultaten med 2 till 5 av 36 uppgifter mellan körningar. En skillnad som är mindre än så är inget fynd, och topplistor visar sällan spridningen.
Uppställningen är själva produkten
Harnessen består av en handfull skript, och varje del finns där för att ta bort ett sätt att lura sig själv.
Grunden är en fryst uppsättning uppgifter med svar som går att kontrollera. Vi använde 36 validerade commits med buggfixar (29 från gin, 3 från cobra, 3 från httpx och 1 från flask) och plockade dessutom fram 210 commits med signaturändringar och namnbyten ur historiken, som en andra sorts uppgift.
Sedan behövs ett orakel som inte är en åsikt. För namnbyten avgör Go-kompilatorn (go test -run '^#x27; -gcflags=-e ./...) om hela repot och dess tester fortfarande bygger. För buggfixarna avgör måltesterna. Ingen, inte heller en modell, får bedöma sitt eget arbete.
Verktygen måste vara låsta till en bestämd version. Den släppta binären av verktyget vi testade indexerade nästan inga anropskanter i Go (16 på gin, 0 på cobra), så en benchmark av den hade mätt en bugg och inte en förmåga. Vi byggde i stället från en specifik commit på main och noterade vilken.
Försöksarmarna får bara skilja sig åt i en enda sak. Prompten, inställningarna och modellerna är desamma, och mellan armarna ändras bara redigeringsformatet eller bara urvalet av kontext.
Vi lade också medvetet in enkla baslinjer. Vanlig grep, en regel baserad på filnamn och BM25 står bredvid den smarta metoden. Om den smarta metoden inte slår dem vill du veta det innan du bygger vidare på den.
Slutligen är allt skriptat och reproducerbart. Varje siffra i våra rapporter går att ta fram igen med skript i en enda mapp. Hemligheter skickas via miljön och ligger inte i någon fil.
Spara allt och ställ frågorna efteråt
Modellanropet är den dyra delen och den som inte går att upprepa, så spara det som kommer ut. Varje kandidat, bedömning, reparationsrunda och mänsklig baslinje hamnade hos oss i JSONL-filer. Därefter kostar en ny fråga ett skript i stället för en ny omgång API-anrop, och flera saker kunde vi bara göra för att utdata fanns sparade.
Vi kunde spela upp urvalsstrategier i efterhand. Med sex sparade kandidater per uppgift jämförde vi att välja slumpmässigt (26 % lösta), att ta den första som kompilerar (40 %), att ta den minsta patchen (44 %) och att ta den som kompilerar och klarar måltesterna (60 %, lika bra som det bästa något urval kunde åstadkomma), utan ett enda nytt modellanrop.
Vi kunde också bygga en taxonomi över felen. Av 75 svar från ett enda försök namngav 12 % en funktion som inte finns, 17 % hade en söktext som inte matchade filen och 25 % av search/replace-svaren var ogiltig JSON. Den sammanställningen visade vad vi behövde åtgärda och vad vi kunde strunta i.
Sparade utdata gjorde det dessutom möjligt att pröva en ny design mot gammal data. Vi utvärderade ett föreslaget redigeringskuvert (edit envelope) mot befintliga svar: målet som modellen angav var giltigt i 60 av 69 tolkningsbara svar, och när målet i stället härleddes från funktionshuvudet steg det till 64.
Och vi kunde rätta våra egna misstag. En bugg hos oss, inaktuella symbolintervall efter att indexet byggts om, förstörde den första bedömningen av en av armarna. Eftersom rådata fanns sparad kunde vi rätta harnessen och bedöma om. Den felaktiga filen ligger kvar i repot, märkt som oanvänd, för öppenhetens skull.
Om du börjar logga i dag bör du åtminstone spara modellens identifierare, hela prompten och kontexten, samplingsinställningarna, det råa svaret, vad parsern plockade ut och resultatet från varje bedömare. Lägg till kostnad och latens också. De är billiga att registrera nu och omöjliga att rekonstruera senare.
Vad våra benchmarks lärde oss
Den viktigaste lärdomen var att vårt första positiva resultat var en följd av uppställningen.
Huvudresultatet vände när baslinjen blev rättvis. Vår första studie visade att en grafbaserad kontextväljare slog BM25 med p ≤ 0,004. Men BM25 hade lagt sin budget på testfiler. När testfilerna uteslöts krympte försprånget till p ≈ 0,05 för den bästa varianten, och för de andra var det inte signifikant. En regel baserad på filnamn, helt utan index, fick in rätt fil i kontexten i 29 av 36 uppgifter.
Verktygets styrka låg inte heller där vi väntade oss. I ett kompilatorkontrollerat test av namnbyten på 150 funktioner i gin gav både grafen och vanlig grep 90 % rena namnbyten. Skillnaden låg i precisionen: 14 felaktiga redigeringsställen för grafen mot 56 för grep. Det hade vi missat om vi bara hade frågat vilken som var mest träffsäker.
Felmönstren sa mer än andelen godkända. När vi jämförde hur en modell levererar en redigering syntes var förlusterna uppstod, långt innan andelen lösta uppgifter skilde armarna åt.
Search/replace, exakt text
- Applicerade
- 39 (52 %)
- Kompilerar
- 32
- Lösta
- 14 (18 %)
Search/replace, tolerant mot blanksteg
- Applicerade
- 48 (64 %)
- Kompilerar
- 40
- Lösta
- 18 (24 %)
Ersätt en namngiven symbol
- Applicerade
- 59 (79 %)
- Kompilerar
- 54
- Lösta
- 21 (28 %)
| Redigeringsformat (gpt-oss-20b, 75 kandidater var) | Applicerade | Kompilerar | Lösta |
|---|---|---|---|
| Search/replace, exakt text | 39 (52 %) | 32 | 14 (18 %) |
| Search/replace, tolerant mot blanksteg | 48 (64 %) | 40 | 18 (24 %) |
| Ersätt en namngiven symbol | 59 (79 %) | 54 | 21 (28 %) |
Riktningen är tydlig men urvalet litet (det bästa parade testet gav p = 0,23), så vi redovisar det som en ledtråd och inte som ett resultat.
Billiga, deterministiska kontroller slog de smarta. Att välja den kandidat som kompilerar och klarar måltesterna gav samma resultat som det bästa möjliga valet. Våra strukturella kvalitetssignaler slog aldrig till på 94 kompilerande kandidater, och den bästa av dem skilde bra patchar från dåliga med en AUC på 0,62, ett mått som dessutom var sammanblandat med patchstorleken.
Till sist: en planerare som säger ”det gör jag inte” är mer värd än en som gissar. En planerare för namnbyten som bara utför säkra ändringar, frågar om osäkra och vägrar sådana som är kopplade till gränssnitt applicerade 266 av 266 ändringar utan fel. Priset är täckningen. Ungefär en femtedel till en tredjedel av förfrågningarna hamnar i den nivå där planeraren frågar.
Så lurar du dig själv med din egen benchmark
Det mesta som gick fel för oss låg i uppställningen, inte i modellerna.
Den lömskaste fällan var ett cirkulärt facit. Vi härledde vad som måste ändras genom textmatchning på name(, samma mönster som grep använder, så grep fick en recall på 1,00 redan genom konstruktionen. Fråga alltid om oraklet delar metod med någon av de tävlande. En kompilator eller en testsvit gör det inte.
Nästa var asymmetrisk förbehandling. En baslinje förlorade i tysthet budget till testfiler. Det var rätt att rätta det, men på modellnivå gick den rättade baslinjen sämre för en av modellerna (från 29 till 22 av 36). Redovisa därför baslinjer på båda sätten.
Brus är lätt att ta för signal. Identiska kontexter flyttade sig 2 till 5 uppgifter mellan körningar, så mät spridningen mellan körningar först och jämför armarna sedan. Små urval lockar samtidigt till starka meningar. Våra modellkörningar omfattade 25 uppgifter gånger 3 sampel på en modell. Inget av de parade testerna hade styrka nog att upptäcka små effekter, och vi skrev ”inte signifikant” bredvid dem.
Det är också lätt att trimma mot testmängden. Vår planerare för namnbyten trimmades på tre repon, och bara det fjärde (mux, 40 av 40 rena) användes aldrig för trimning. Den ärliga siffran från första kontakten kommer från ett repo som kontrollerades innan regeln för det lades till: 41 av 43.
Mått betyder inte alltid vad namnet säger. Verktygets ”komplexitet” visade sig vara anropskoppling (två gånger fan-out plus fan-in inom samma fil), inte kontrollflöde. Läs källkoden för ett mått innan du drar slutsatser av det. Versionsdrift ställer till det på samma sätt: en benchmark av en release som saknade en funktion mätte releasen och inget annat. Lås bygget och notera vilket det är.
Läck heller inte hemligheter. Nycklar hör hemma i miljövariabler. Klistra aldrig in dem i skript, delade anteckningar eller projektinstruktioner, och rotera alla som ändå har hamnat där.
Skriv till sist ner vad du inte körde. Våra anteckningar säger att testet på modellnivå av att dela upp en bred ändring ställe för ställe fortfarande återstår, och att experimenten med namnbyten och redigeringar bara omfattade Go.
Börja den här veckan
Du behöver inget forskarteam. Du behöver en mapp, några dussin verkliga uppgifter och vanan att spara all utdata.
Välj 20 till 40 uppgifter ur din egen historik, till exempel commits med buggfixar som har ett test som fallerar. Välj sedan ett orakel du litar på, alltså kompilatorn, testsviten eller båda, och undvik där det går facit som bedöms av en modell eller bygger på textmatchning. Lägg till enkla baslinjer som grep, en regel baserad på filnamn och vanlig nyckelordssökning. Logga varje anrop som en fil eller en rad, med modell-id, prompt och kontext, inställningar, rått svar, tolkningsresultat, bedömarnas utdata, kostnad och latens.
Kör sedan varje arm flera gånger och mät spridningen innan du jämför något. Ändra en variabel per arm, modell, kontext eller redigeringsformat, men aldrig två samtidigt. Analysera från de sparade filerna med parade jämförelser och spela upp urvals- och reparationsstrategier i efterhand. Håll några uppgifter utanför trimningen, och skriv ner vad du inte körde.
Om du har använt AI för att koda ett tag är nästa steg inte en bättre modell. Det är att lära dig mäta den du har, på din kod, och spara vad den säger.