Як агент AI для практыкавання Docs-Fixing абмануў сябе: ложныя позытыўныя рэзультаты ў Docker Sandbox
Агент для практыкавання документацыі LangGraph выдаў фікс, які нічога не змяніў, а пасля таго спачатка зламаў пакеты src-layout. Баг у Docker-сандбоксе стаў прычыной гэтага, а справжнія фікс быў знайдзен у click.
Крыніцы: **I built an AI Doc-Fixing Agent. Here’s every way it fooled itself first.**
Агент AI, створаны для выправлення зламаных прыемакоў коду ў дасведчэннях, даў страннае рэзультат на сваём першым рэальным рэпазітарыі. Ён „выправіў“ баг, якога не існавало: взяў аблікованы, абсалютна цільны радок, нічога не змяніў і опублікаваў той самы тэкст як падтверджанае выправленне.
- print(tqdm.__version__, sys.version, sys.platform)
+ print(tqdm.__version__, sys.version, sys.platform)
Обе лініі збігаюцца. Агент пераканаў ся, што трэба вылечыць тое, чаго ніколі не было, і гэта быў толькі першы з трох спосабаў, якім ён абмануў ся, прычаму паказваўшыся справжній вінаватык.
Што робіць DocuPatch
DocuPatch — гэта агент, які працюе на базе LangGraph. Ён праходзіць па файлах у фармаце Markdown у рэпазітарыі GitHub, выкарыстоўвае блокі коду на Python і запускае іх у ізольаваным кантэйнерэ Docker. Экзампляры, якія справды не працуюць, надаюцца Gemini для вылечэння; пасля чаго паследнія зноў пераглядаюцца ў там самым ізольаваным сераўерэ, і толькі пасля паўнай перапрацоўкі рэзультат стае часткай новай гілкі чы рэквэсту на з’еднанне.
Структура рэпазітарыя:
agent/ingest.pyклонуе рэпазітарый і знаходзіць файлы у фармаце Markdown.agent/graph.pyзапускае працэс LangGraph для кожнага файла.agent/parser.pyвыкарыстоўваецца для выкарыстоўвання блакоў коду на Python.agent/tester.pyзапускае кожны фрагмент коду ўнутрь кантэйнера Docker.
agent/context_collector.py сабіра неабяжлівыя файлы Python і код для фрагмента.agent/fixer.py стварае вылечаны фрагмент.agent/github_client.py стварае форк, зберагае вылечэнне і ачыяе PR-ы.tests/ мае тэсты для кожнага фрагмента.Сутнасць простая: код дакументацыі выцягваецца. API-ы зменяюцца, функцыі перайменоўваюцца, прыклады перастаюць працаваць, і ніхто не звертае на гэта увагі, пакуль хтось не запішае нефункцыональны фрагмент з README. Агент автаматызуе выяўленне гэтых проблем. Ён знаходзіць падозрэльны фрагмент, пераканваецца, што ён нефункцыональны, запускаючы яго, просіць LLM пра вылечэнне, пераканваецца, што вылечэнне працуе, запускаючы яго знову, і толькі пасля гэтага ачыяе pull request.
У цікле ёсць стадыя клонавання і аналізу, якая падае даныя ў цікл на адпаведнае падтверджэнне, і вылечэнне застаецца толькі пасля двух падтверджэнняў: первісны варыянт не працаваў, а заменіты — працуе.
Каналізуючым прынцыпам являецца працэваханне на адпаведнасці з доказамі. Нічога не прабіваеся да LLM, як толькі тэст у сэндбоксе не паказвае, што ён не працуе, і нічога не публікуецца, як толькі парадны запуск у сэндбоксе не паказвае, што паследнія праблемы усунуты. Шырокі прынцып застаўся. Сам сэндбокс, на дзвяны час, такога не зрабіў.
Першы тэст: tqdm
Пачатковы рэальны запуск быў спрамованы на tqdm/tqdm:
Found 9 Markdown file(s). Starting audit...
Auditing: .github\ISSUE_TEMPLATE\bug.md
Generating repair for snippet #1
Verifying generated repair for snippet #1
Publishing verified repair(s)...
VERIFIED REPAIR: Branch published
Два файлы былі «вылагоджаны» і опублікованы, што на паперы ўзглядае як успех. Але потым была пераглядзена рэальная разніца:
- print(tqdm.__version__, sys.version, sys.platform)
+ print(tqdm.__version__, sys.version, sys.platform)
Знову два радкі кладоцца ідэнтычнае. Gemini нічога не змяніла і перагенеравала код байт за байтом. Сістэма перапрацавіла фрагмент коду, адзначыла яго як несправны, «вылечыла» яго, вернуўшы той самы код, зноў перапрацавіла яго, побачыла, што ён працуе, і адправіла коміт без жадных змян у форк.
Гэта не быў баг, які трэба было вылечыць. Гэта быў хыбны позытыўны рэзультат на роўні тэставання, таму што сам фрагмент коду ніколі не быў несправным. Радок, які імпортуе tqdm і sys і выводзіць інфармацыю пра версію, ёсць абычным прыкладам віднашання к коду tqdm, тады чаму сэндбокс адзначыў неудачу? Гэта пытанне было запісана як падазрэльнае, і быў спробаваны наступны рэпазітарый, каб атрымаць другі даны, перш чым робіць вывары.
Другі тэст: pallets/click
У pallets/click патэрн быў немагчымы не пазначыць:
Auditing: docs\advanced.md
Generating repair for snippet #2
Verifying generated repair for snippet #2
[Reflection Loop 2/3] Revising repair for snippet #2
...
[Reflection Loop 3/3] Revising repair for snippet #3
NOT PUBLISHED: Generated repair failed verification:
ModuleNotFoundError: No module named 'click'
Адзіныя адказы, ўсё час. Шасць файлаў кожны працаваў праз тры цэлыя цыклы адбівання, і кожны адказ быў той самы: ModuleNotFoundError: No module named 'click'.
Гэтая паўтарэнне ёсць параднікам. Калі шасць файлаў і шасць аднародных спробаў LLM усе збягаюць з-за аднаго толькі адказу пра імпорт, спяльны фактар — не код, а асяроддзе, якая яго падтрымляе. Перапісваўшы частаку коду, нельга вылечыць нехватку модуля, калі сандбокс не можа пабачыць пакет, незалежна ад таго, што было напісана.
Ачыненне tester.py адкрыло механізм:
command.extend(["-v", f"{project_dir}:/workspace", "-w", "/workspace", "-e", "PYTHONPATH=/workspace"])
Унутрэ кантэйнера кланаваны рэпазітары ўстановліваецца, а PYTHONPATH настаўляецца на верхні ўрадзік рэпазітарыя. Гэта працуе, калі каталог пакета знаходзіцца працоўна там, але выклікае проблемы, калі пакет знаходзіцца на адны ўрадзік ніжэй. У прыкладзе Click яго pyproject.toml зберагае рэальны код у каталозе src/click/, а не ў каталозе click/ на корані, як гэта прыменяецца ў структуры src у багацо сучасных праектаў. Калі PYTHONPATH=/workspace, інтэрпрэтар шукаў у каталозе, які вовсе не меў каталога click.
Ручная перапрацавка па захадах, якія выконваюцца пахацела, падтвердзіла гэта:
docker run --rm -v "C:\temp\click_test:/workspace" -w /workspace -e PYTHONPATH=/workspace python:3.10-slim python -c "import click"
# ModuleNotFoundError: No module named 'click'
docker run --rm -v "C:\temp\click_test:/workspace" -w /workspace -e PYTHONPATH=/workspace/src python:3.10-slim python -c "import click; print(click.__version__)"
# import works... but:
# importlib.metadata.PackageNotFoundError: No package metadata was found for click
Другі рэзультаты навучылі больш за ўсе. Настаўленне PYTHONPATH на правы папаку вылечыла проблему з імпортом, але click.__version__ яшчэ іноды выклікаў крах, таму што сучасныя версіі click ствараюць __version__ за дапамою importlib.metadata, які чытае метаданы інсталаваных пакетаў, а не ісходныя файлы. Такія метаданы існуюць толькі пасля справжньага запуску pip install, а не пасля простага змены шляху.
Такім чынам супакоўваліся два багі. PYTHONPATH быў настаўлены на неправы папаку для рэпазітарыяў з структурой src-layout. А нават калі ён быў настаўлены правільна, просты трук з PYTHONPATH не дае можлівасці стварыць належна інсталаваны пакет, тады як все больш пакетаў ачакуецца, што яны будуць інсталаваны, а не проста будуць доступныя для імпорту.
Адзін збіг парадоксальных аспектаў заслуговае на адзначэнне, адколькі гэта быў найболей павучальны ўчастак. Больш часу было пасвячана выявленню асоблівасці пры прыўязцы Docker, якая дзейнічыць толькі пад Windows, праз эксперыменты з кавычкамі, стылем пацек і спісамі аргументаў у супрацоўцы з рэчамі. Асалодзіцельныя адказы былі простыя: папка для тэстаў была порожняя, таму што празлеглы каманды git clone тыхо не падаў у заданні, і ніхто гэта не пераканал. Рэзультат каманды dir C:\temp\click_test, які паказаў порожнюю лістынг, не быў „жаданым адказам“, а самым адказам, прычым яго трохі разоў проігноравалі, прычым ніхто не зупініўся, каб яго прачытаць.
Рашэнне: інсталюйце, а не здагадвайцеся
Праўы спосаб не ў тым, каб правяць болей розумныя прыпускі ўзгляду на шлях, а ў тым, каб узагалі не робіць прыпускаў. Команда pip install -e . чытае власны файл pyproject.toml або setup.cfg праекта, вырашае, дзе знаходзіцца правы пакет незалежна ад структуры, і запісвае асалідныя метаданы інсталяцыі, таму вызовы importlib.metadata будуць працаваць так, як і для будзь-каго рэальнага корыстніка бібліятэкі.
Логіка новага файла tester.py выконваліцца за следуючым порадкам:
- Калі існуе файл
pyproject.toml,setup.pyабоsetup.cfg, сандбокс стварае адпаведны образ Docker і запускае там командуpip install -e .. Гэта ў «рэжыме інсталяцыі», дзе можна верыць у помылкі падчас імпорту. - Калі няма метаданаў пакетавання, але ёсць файл
requirements.txt, выкарыстоўваецца падход, заснованы на PYTHONPATH, тэйкам «рэжым pythonpath».
Пасля змены існуючы набор тэстаў паказаў, што 26 з 27 тэстаў працавалі без змян. Одын неудачны тэст быў правым, адколькі пераканваўся ў стары фармат функцыі, які быў намерна зменены. Аспекты, якія ляглі воспрыемліцца, былі адкорэктаваны, і трохі новых тэстаў пакрылі крайнія кейсы, чым загальная колькасць працаваючых тэстаў вырасла да 74.
Праўdziвая прычына проблемы ў дакументацыі click
Завдзяк таму, што сэндбокс цяпер інсталюеў праект і больш не здагадваецца, click запускаўся знову. Шасць файлаў, якія раней не моглі працаваць, цяпер паклікаліся на справядлівую шанс, і адны з іх адкрыў справжню, незнайдзеную багу ў самай дакументацыі click.
docs/commands.md опісвае ланцоўку командной лініі, і адна з прыкладных функцыяў чытае:
@cli.result_callback()
def process_pipeline(processors, fin):
- iterator = (x.rstrip("\r\n") for x in input)
+ iterator = (x.rstrip("\r\n") for x in fin)
Функцыя на самай працо прыме fin — кантроллер файлу, які падае з кліка. У дасэбнах жа здзейснюецца цыкл над input, які насправды апіляецца да вбудованай функцыі Python input(), а не да файлу. Цыкл над імі ёсць некорэктным і выклікае адзіну памылку як толькі код запускаецца, але чытальнік не зможа гэта пазначыць, таму што назва здаецца правдападобной.
DocuPatch знайшоў гэта, падтвердзіў рэальную памылку ў сэндбоксе, створыў выправленне, пераканаўся, што пасправленая версія працуе без проблем, і опублікаваў яе. Гэта быў першы рэзультат у всім розследаванні, які быў як справжней памылкай у дасэбнах, так і справжнім выправленнем, без жадных пахібных сігналаў з сэндбокса чы абманлівых рэзультатаў. Самэ гэтага і служыць праекту.
Урокі з дэбаггінгу
- Пераканаўцеся ў рэшцы пры вынесенні вердыкуту, прычым даваючы доверлівасць рэзультату. Абазаванні на кшталт "Пройшло" або "Опублікована" ўзімаюць у сваёй надзеямасці толькі столькі, сколькі тэст, які знаходзіцца пад імі. Функцыя tqdm не павінна была выканаць нічога, таму што быў адкрыты дыферэнцыял, а зелёная галочка не была спрыймана як абавесць.
- Ідэнтычныя аберанні ў багацькох прыкладах указваюць на адну спяльную прычыну. Шасць файлаў, шасць незалежных спроб выкарыстоўвання LLM і адна паведамленне пра аберанне даўаюць сігнал на рэжыме сістэмы.
- Спачатку спазірайце, а потым стварайце тэорыі. Прашло калькі гадзін на тэорыі, касаючыся Docker і шляхоў у Windows, прычым ніхто не пераканаўся, чы ў папцы з тэстамі ёсць якія-небудзь файлы.
- Аберанне тэста можа толькі падтвердзіць, а не пашкодзіць. Калі змена падпісу
run_snippetспануліла адзін тэст, правым пытаннем было тое, што саме стверджваў той тэст, а не тое, што было спанулена.
Гэтая проблема з «пісочніцай» ўжо выправлена, яна перакрыта тэстамі і падтверджана ў рэпазітарыі, дзе раней былі проблемы. У дасведчэннях Click была справжняя бяга, аднак імя зменнай, якае спрычынялаі проблемы праз кожны раз, калі запускаліся прыклады, было знайдзенае агентам, які правільна яго выправіў і пераканаўся ў правільнасці выправлення перш чым яго публікуваць. Гэта і ёсць цэнтральная ідэя праекту, падтверджаная на рэальным кэсе, а не на сінтэтычным.