dimhold.by
← Артыкулы

2 радкі каментара ператварылі 62 знаходкі ў 0

Рэўю кода ў мяне ідзе ў 3 стадыі. N агентаў чытаюць файл і паведамляюць пра дэфекты, K скептыкаў спрабуюць абвергнуць кожнае паведамленне, адзін агент злучае ацалелае ў спіс. Сабраў я гэта так таму, што ў claude CLI, якім я ганяю агентаў, ёсць даследчы воркфлоў, пабудаваны роўна на гэтай форме. Яго стадыі ляжаць у бінарніку простымі радкамі: разаслаць пошукі, выцягнуць абвержныя сцвярджэнні, 3 галасы адверсарыяльнай праверкі на сцвярджэнне пры 2 абвяржэннях на забойства, потым злучыць сэнсавыя дублі. Я перанёс гэта на рэўю кода, не спытаўшы, ці трэба рэўю кода такое. З таго часу я плачу за ўсе 3 стадыі і ні разу не выбіраў N або K паводле замеру.

На гэтым тыдні я гэты замер зрабіў. 191 выклік мадэлі і $11.76, лічачы кантрольны заход, які я выкінуў. Стадыя, якую я бараніў бы найзацяцей, узяла з гэтых грошай $4.57 і ні разу не разышлася сама з сабой. Чаго я не чакаў, дык гэта таго, як часта няправым аказваўся мой уласны вымяральны апарат, а не агенты. Так выйшла 5 разоў. Найгоршы з гэтых 5 ледзь не пайшоў загалоўкам гэтага паста.

Файл

clean.mjs гэта зборшчык метрык таго роду, які тут сапраўды патрэбны. 184 радкі: семафор паверх чаргі FIFO, рэтраі з бэкофам і джытарам, ключ кэша, перцэнтыль паводле бліжэйшага рангу і радок дзённай зводкі. Другі скрыпт садзіць у яго 12 дэфектаў літаральнай заменай радкоў. Файл, які чытаюць агенты, выходзіць на 176 радкоў. Рэестр запісвае, куды лёг кожны дэфект. На кожны дэфект ёсць асобны файл, які нясе толькі яго.

Далей кожны дэфект даказваецца прагонам чыстага файла супраць файла, які нясе толькі яго. Даказваць супраць мутаванага файла гэта відавочны ход і ён дае няправільны адказ. Там адзін дэфект умее схавацца за другім. Адзін з маіх так і робіць.

Кожная стадыя працуе з новай часовай папкі. Ніякага CLAUDE.md на шляху і ніякіх інструментаў. Гэта я праверыў рукамі, а не паверыў справаздачы абвязкі. Канфіг MCP пусты. Аперацыйная інструкцыя гэтага рэпазіторыя гэта 889 радкоў правілаў, якія я напісаў сам сабе. Яна моўчкі ідзе ў промпт, калі рабочая папка гэта рэпазіторый. Node 22.23.1, claude CLI 2.1.235, мадэль claude-opus-5 на 4 ядрах.

Пошук

8 шукальнікаў атрымалі адзін і той самы промпт, па 4 у палёце. Яны вярнулі 108 знаходак за $0.76. Мой лічыльнік залічвае знаходку дэфекту, калі названы радок трапляе ў пасаджаны адрэзак, пашыраны на 2 у кожны бок. Ён сказаў, што сярэдні шукальнік ловіць 10.25 з 12 і што семафор не знайшоў ніхто.

Я прачытаў усе 108 сцвярджэнняў, бо апошняя частка выглядала непраўдападобна. Семафор знайшлі ўсе 8. 6 з іх назвалі радок 25, 2 радок 26. Гэта той адзіны live++, які ў файле застаўся. Другі я выдаліў на радку 18, а ацэньваў іх паводле месца ўласнай праўкі. Акно заадно выдала 2 шукальнікам залік за дэфект 12. Іх сцвярджэнні на гэтых радках пра іншае: цела адказу 200, якое не JSON і папка журнала, якую ніхто не стварае.

Значыць дэфект 12 не знайшоў ніхто і ў гэтым файле да яго не даходзіць ніводзін няўдалы статус. Дэфект 12 прыбірае праверку статусу пасля цыкла рэтраяў. Другі пасаджаны дэфект прыбірае праверку, якая не дае дрэннаму статусу наогул трапіць у рэтрай. Цыкл цяпер альбо вяртае добры адказ, альбо кідае выключэнне. Я прагнаў праз файл 9 няўдалых статусаў і да цела адказу не дайшоў ніводзін. Праз файл, які нясе толькі дэфект 12, даходзяць 6.

колькі з 8 шукальнікаў знайшлі кожны пасаджаны дэфект P1 P2 P3 P4 P5 P6 P7 P8 P9 P10 P11 P12 акно радкоў 0 8 8 8 8 8 8 8 8 8 8 2 пасля чытання 8 8 8 8 8 8 8 8 8 8 8 0 P1 L18 P12 L155
12 пасаджаных дэфектаў, палічаных 2 спосабамі. Акно радкоў залічвае знаходку, калі яе радок трапіў у пасаджаны адрэзак, пашыраны на 2. Чытанне саміх сцвярджэнняў пераводзіць P1 з 0 на 8, а P12 з 2 на 0.

Пасля праўкі чытаннем усе 8 шукальнікаў знайшлі адны і тыя ж 11 дэфектаў. На пасаджаных дэфектах у гэтым заходзе 7 з 8 не купілі мне нічога. Абодва спосабы ліку заканчваюцца на 11 з 12. Дэфекты ўнутры ў іх пры гэтым розныя.

Гэта адзін заход, таму я прагнаў тых самых 8 шукальнікаў па тым самым файле з тым самым промптам яшчэ раз. 108 знаходак, роўна столькі ж. Гэта я лічу супадзеннем. З 26 радкоў, названых хоць у адным заходзе, у абодвух названы толькі 18. Аб’яднанне зноў 11 і тыя самыя 11, але адзін шукальнік вярнуўся з 10. Ён прапусціў палітыку рэтраяў, якая рэтраіць 400. Прапусціў па-сапраўднаму. Ва ўсіх яго 14 знаходках пра рэтраі няма нічога. Промпт дазваляе 15, так што выціснуць іх не было чым.

Файл, у які не пасаджана нічога

Тыя самыя 8 шукальнікаў на зыходным clean.mjs вярнулі 0 знаходак. 8 пустых спісаў з 8. Я запісаў гэта як частату ілжывых спрацоўванняў і пайшоў далей, задаволены сваім кодам.

clean.mjs пачынаецца докблокам. Яго радкі з 4 па 6 кажуць, што гэта чыстая версія. Там жа сказана, што дэфекты ў яе садзіць іншы скрыпт. Радок 4 гэта пусты радок каментара. Астатнія 2 нясуць само сцвярджэнне. Скрыпт мутацыі выкідае роўна гэтыя радкі, калі піша файл, які чытаюць шукальнікі. Значыць у сваім кантролі я паведаміў 8 рэўюерам, першым жа, што трапіла ім на вочы, што дэфекты жывуць у нейкім іншым файле.

Я выразаў гэтыя радкі, прагнаў тых самых 8 шукальнікаў і атрымаў 74 знаходкі. Ніводнага пустога спіса. Гэта выглядала як адказ, гэта ледзь не стала загалоўкам паста і чыстым усё роўна не было. Выразанне 3 радкоў падымае кожны радок ніжэй іх на 3. А промпт падае мадэлі лістынг з прастаўленымі нумарамі. Шукальнікі ўбачылі 2 змены там, дзе я хацеў паказаць ім адну.

Тады я напісаў трэці файл. Тыя 2 радкі, якія нясуць сцвярджэнне, заменены на месцы 2 радкамі пра тое, што код робіць. Лічыльнікі бяруцца па разе на пост і кэшуюцца ў журнале. Дзённая зводка чытае гэта назад. 184 радкі як у clean.mjs, кожны нумар радка на сваім месцы, ніводзін байт кода не крануты. diff аддае гэтыя 2 радкі і больш нічога. 62 знаходкі ад тых самых 8 шукальнікаў. 1 з 8 усё роўна вярнуўся пустым. Быў яшчэ больш ранні заход па тым самым файле, які я выкінуў праз тое, што 2 шукальнікі ўпалі на 429. Тыя 6, што адказалі, прынеслі на ўсіх 59 знаходак і ніводнага пустога спіса.

тыя самыя 5 радкоў кода, прачытаныя 4 разы докблок знаходкі пазначылі неабаронены JSON.parse subject.mjs пасаджана дэфектаў: 12 радкоў: 176 108 7 / 8 clean.mjs не пасаджана нічога радкоў: 184 кажа, што файл чысты 0 0 / 8 clean-noheader.mjs не пасаджана нічога радкоў: 181 74 8 / 8 clean-filler.mjs не пасаджана нічога радкоў: 184 62 7 / 8
Той самы зборшчык метрык, прачытаны 4 разы. У другім файле не пасаджана нічога і докблок пра гэта кажа: толькі ён з 4 вярнуўся пустым. Трэці гэтыя радкі выразае, чацвёрты замяняе іх на месцы, таму ў чацвёртым зноў 184 радкі і JSON.parse стаіць на сваім зыходным радку. Апошні слупок гэта адна функцыя ў 5 радкоў, пабайтава аднолькавая ва ўсіх 4 файлах.

Ці добрыя гэтыя знаходкі, я магу вырашыць толькі прагонам самога файла. Я звёў 74 да 16 розных сцвярджэнняў і вырашыў кожнае скрыптом. Усе 16 пацвердзіліся і пакрываюць усе 74. Адно з 16 вырашана прагонам толькі напалову. Да яго я вярнуся ў канцы. Вось 5 з іх. Столь 0 вешае семафор намёртва. Таймер адмены здымаецца да чытання цела, таму пры таймаўце ў 40 мілісекунд чытанне цела даўжынёй 120 мілісекунд даходзіць да канца. cacheKey("a", "b c", ["views"]) і cacheKey("a b", "c", ["views"]) абодва вяртаюць 0f7d1e1c5f119f46. Адзін няўдалы пост прымушае Promise.all выкінуць усе ўжо сабраныя радкі. А summaryLine будуе прэфікс даты з імя файла, таму выдае чалавеку радок 2026-09-05.md: 12 posts, 400 views, p90 90. Мой чысты файл чыстым не быў.

Акно радкоў кажа, што 60 з 62 знаходак трэцяга файла ляжаць у межах 2 радкоў ад таго, што гэтыя 16 сцвярджэнняў ужо вырашылі. На гэты раз я яму не паверыў. Пры чытанні аказваецца, што 4 знаходкі кажуць пра запыты ў палёце, якія ніхто не дэдуплікуе. Акно кладзе іх пад сцвярджэнне пра тое, што трапленне ў журнал любога ўзросту вяртаецца без запыту. Вырашае ж іх іншае сцвярджэнне, што 2 пасты на адным ключы абодва міма кэша і абодва ідуць у сетку. Яшчэ 4 знаходкі кажуць пра fields, які не даязджае да запыту. Акно раскідвае іх па 2 іншых сцвярджэннях, тады як вырашае іх тое, дзе сказана, што fields мяняе ключ кэша і больш нічога. 16 сцвярджэнняў сапраўды вырашаюць усе 8 гэтых знаходак. Акно памыляецца ў тым, якое сцвярджэнне вырашае якую знаходку.

Да чаго я вяртаюся зноў і зноў, дык гэта да readJournal. 5 радкоў, неабаронены JSON.parse, пабайтава аднолькавы ва ўсіх 4 файлах. У мутаваным файле яго пазначылі 7 шукальнікаў з 8 і зноў 7 з 8 у паўторным заходзе. А ў clean.mjs, дзе ён стаіць на радку 83, яго не пазначыў ніхто. Затое ў трэцім файле, дзе ён стаіць на тым самым радку 83, яго пазначылі 7 з 8. На першую пару лікаў у мяне было гатовае тлумачэнне, штосьці пра тое, як астатні файл мяняе тое, што выглядае падазроным. Тую пару яно перажывае, бо мутаваны файл і праўда нясе 12 дэфектаў. З апошнімі 2 з іх яно не робіць нічога. Тыя самыя 184 радкі, той самы радок 83, розніца ў 2 радкі каментара. Адказ зрушыла фраза, якую я напісаў пра ўласны код.

Праверка

Для другой стадыі мне спатрэбіліся ілжывыя сцвярджэнні, бо амаль усё сказанае шукальнікамі было праўдай. Я напісаў 12 штук у іх жа рэгістры і кожнае абверг прагонам кода. Чарга працуе як LIFO, перцэнтыль сартуе на месцы і яшчэ 10 такіх. Супраць іх стаяць 11 сапраўдных сцвярджэнняў словамі саміх шукальнікаў, без праўкі. Далей па 3 незалежныя верыфікатары на сцвярджэнне. І так двойчы. Адверсарыяльны промпт загадвае абвяргаць і пры сумневе лічыць абвергнутым. Нейтральны толькі пытаецца, ці сцвярджэнне праўдзівае.

Выйшла 138 выклікаў і $4.57.

у што абышлася кожная стадыя рэўю і што яна змяніла пошук 8 выклікаў $0.76 знойдзена 11 з 12 праверка 138 выклікаў $4.57 зрушана 0 з 23 вердыктаў сінтэз 3 выклікі $0.68 16 запісаў
149 выклікаў мадэлі на 3 стадыі аднаго рэўю. Слупок гэта тое, у што абышлася стадыя, радок пад ім гэта тое, што выйшла. Часу на дыяграме няма: праверка заняла 219.7 секунды пры 5 у палёце супраць 70.4 у пошуку пры 4.

Усе 3 верыфікатары сышліся на ўсіх 23 сцвярджэннях у абодвух рэжымах. 2 промпты далі аднолькавыя вердыкты на кожным сцвярджэнні. Галасаванне не купіла нічога. Адверсарыяльная фармулёўка таксама.

Вынік гэты вузкі. Такім яго зрабіў мой жа спосаб пабудаваць праверку. Мае 12 ілжывых сцвярджэнняў абвяргаюцца чытаннем 3-5 радкоў файла. Сцвярджэнне, для якога трэба трымаць у галаве адразу 2 функцыі, гэта якраз тое месца, дзе другое меркаванне павінна акупляцца. Вось яго я і не памерыў. Самі 11 праўдзівых сцвярджэнняў сабраны па адным на пасаджаны дэфект, таму 18 знаходак, што леглі міма пасаджаных радкоў, да верыфікатара не дайшлі наогул. Якраз яны і былі найбольш верагоднымі кандыдатамі ў ілжывыя. А тое, што амаль усё сказанае шукальнікамі аказалася праўдай, гэта маё ўражанне ад чытання, а не лік. Лік у мяне ёсць толькі па кантрольным файле.

Сінтэз

108 знаходак на 23 розных радках, 6 з іх бачыў толькі адзін шукальнік. Агент сінтэзу атрымлівае справаздачы. Сам файл ён не бачыць ніколі. 3 прагоны далі па 16 запісаў, па $0.227 за прагон. Пасля таго як я расказаў лічыльніку пра 2 злучэнні ніжэй, аказалася, што прагоны нічога не згубілі і нічога не выдумалі. Промпт наўпрост кажа агенту сінтэзу не выкідаць дэфект праз тое, што яго бачыў толькі адзін рэўюер, так што гэтай уласцівасці я папрасіў, а не выявіў.

радкоў названа 23, запісаў пасля сінтэзу 16 названа 23 злучана 16 158 82 160 152 радкі з 25 па 160 бачыў адзін шукальнік, 6
Дзе 8 шукальнікаў нешта знайшлі і куды гэта паклаў сінтэз. 2 крывыя гэта злучэнні, якіх лік паводле адлегласці паміж радкамі ўбачыць не можа.

Ён яшчэ і злучыў 2 знаходкі, разнесеныя на 76 радкоў: неатамарны запіс журнала і адсутную папку. На злучаным запісе ён назваў абодвух рэўюераў. Мой лічыльнік налічыў тут 2 няправільныя аўтарствы. Другое такое ж злучэнне ён палічыў згубленай знаходкай. Абодва вердыкты трымаліся, пакуль я не прачытаў запісы і не расказаў яму пра іх. Лічыльнік параўноўвае толькі нумары радкоў. Адно злучэнне сказана ў запісе наўпрост, другое сказана толькі як “на абодвух шляхах”.

Вось і пяты. Акно радкоў памылілася ў 2 дэфектах у розныя бакі. Яно ж памылілася другі раз, у тым, якое сцвярджэнне пакрывае якую знаходку кантролю. Докблок у маім кантролі аказаўся няправым наконт усяго кантролю. Мая праўка гэтага докблока зрушыла 178 нумароў радкоў, якія я рухаць не збіраўся. Лічыльнік сінтэзу памыліўся наконт абодвух злучэнняў. Усе 5 гэта промахі майго апарата. Кожны раз, калі мой апарат разыходзіўся з агентам, няправым аказваўся апарат.

Чаго я не правяраў

Адзін файл, дэфекты пасаджаны заменай ад 1 да 6 радкоў кожны. Сапраўдная праца рэўю гэта дыф па некалькіх файлах і дэфект, які жыве ва ўзаемадзеянні. Стадыя пошуку прагнана двойчы, сінтэз 3 разы. Праверка прагнана па адным разе ў кожным з 2 рэжымаў промпта. Кожны варыянт докблока атрымаў адзін заход, таму 62 супраць 0 гэта адна пара заходаў, а не размеркаванне. Ілжывыя сцвярджэнні для верыфікатараў мае і яны лёгкія. З 16 кантрольных сцвярджэнняў адно вырашана прагонам толькі напалову. Што запіс журнала не стварае сваю папку, я паказаў. Адсутнасць перайменавання я ўзяў чытаннем. І якраз пра перайменаванне казалі ўсе 8 знаходак на тым радку. У трэцім кантрольным заходзе 1 шукальнік з 8 вярнуў пусты спіс. Чаму менавіта ён, я не высветліў. Тыя 2 радкі, якія я паставіў замест таго радка, які ўсё выдаваў, гэта не пустое месца. Яны апісваюць журнал і кэш, а там жыве немалая частка з 62 знаходак. Значыць параўнаў я аўтара, які кажа, што файл чысты, з аўтарам, які апісвае кэшаванне. Той радок я параўнаў не з пустым месцам. І прыбраныя 2 радкі неслі адразу 2 сігналы, адбой і паказальнік на іншы файл. Прыбраў я іх разам. Які з 2 спрацаваў, я не падзяляў. Цана выкліку шукальніка ў $0.094, на якой стаіць $2.07, узятая з мутаванага файла. Чысты файл выцягваў адказы даўжэйшыя і даходзіў да $0.29 за выклік, так што $2.07 гэта танны край таго, што я бачыў.

З 191 выкліку 138 гэта верыфікатары і 48 шукальнікі за 6 заходаў. 3 пайшлі на сінтэз, а апошнія 2 гэта паўторы пасля 429. $4.57 супраць $0.76 выглядае як цана ў 6 разоў вышэйшая за стадыю, якая нічога не змяніла. Большая частка гэтага разрыву гэта другі рэжым промпта і 12 сцвярджэнняў, якія я напісаў сам. Рэўю не робіць ні таго, ні другога. У параўнальнай цане адно рэўю гэтага файла каштуе $2.07. Пошук гэта $0.76 з іх, сінтэз $0.23, а праверка 11 сцвярджэнняў, што перажылі дэдуплікацыю, $1.09. Гэтыя 3 лікі ў суме даюць на цэнт больш за вынік, бо кожны акруглены. На 2 шукальніках і 1 верыфікатары тое ж рэўю каштуе $0.78. Я пераходжу на гэта, але другога шукальніка пакідаю праз той заход, які вярнуўся з 10.

152 з 190 файлаў на JavaScript і TypeScript у гэтым рэпазіторыі пачынаюцца докблокам з расказам, для чаго файл. Большую частку пісаў рукамі я. Памерыць далей я хачу дэфект, які жыве адразу ў 2 файлах.