Strona główna / Artykuły

Jak agent sztucznej inteligencji naprawiającej dokumenty oszukał samego siebie: fałszywie pozytywne wyniki w środowisku Docker Sandbox

Agent naprawczy LangGraph wysłał rozwiązanie bez żadnych efektów, a następnie spowodował problemy w pakietach src-layout. Przyczyną była błąd w środowisku Docker sandbox, a prawdziwą naprawą okazała się ta znaleziona w komponencie click.

Wydawca: REACTAPP.TOP7 min czytania1666 słów

Źródła: **I built an AI Doc-Fixing Agent. Here’s every way it fooled itself first.**

Agent sztucznej inteligencji stworzony w celu naprawy uszkodzonych przykładów kodu w dokumentacji dał dziwny wynik w swoim pierwszym prawdziwym repozytorium. „Naprawił” błąd, którego w rzeczywistości nie istniało – wziął doskonale poprawny wiersz, niczego nie zmieniając, i opublikował identyczny tekst jako potwierdzoną poprawkę.

- print(tqdm.__version__, sys.version, sys.platform)
+ print(tqdm.__version__, sys.version, sys.platform)

Oba wersje są zgodne. Agent przekonał samego siebie, że musi naprawić coś, co nigdy nie było błędne, a to było dopiero pierwszym z trzech sposobów, w jakie oszukiwał samego siebie, zanim pojawił się prawdziwy winowajca.

Co robi DocuPatch

DocuPatch to agent napędzany LangGraph. Przechodzi przez pliki Markdown w repozytorium GitHub, wyodrębnia bloki kodu Python i uruchamia je w izolowanym kontenerze Docker. Przykłady, które rzeczywiście zawodzą, są wysyłane do Gemini w celu naprawy; naprawa jest ponownie sprawdzana w tej samej izolacji, a tylko potwierdzony wynik staje się gałęzią oraz prośbą o połączenie.

Struktura repozytorium:

  • agent/ingest.py klonuje repozytorium i znajduje pliki Markdown.
  • agent/graph.py uruchamia pipeline LangGraph dla każdego pliku.
  • agent/parser.py wyodrębnia bloki kodu Python.
  • agent/tester.py uruchamia każdy fragment kodu wewnątrz kontenera Docker.
  • agent/context_collector.py zbiera odpowiednie pliki i kod w języku Python potrzebne do utworzenia fragmentu kodu.
  • agent/fixer.py tworzy naprawiony fragment kodu.
  • agent/github_client.py tworzy fork, zapisuje poprawkę i otwiera prośby o zmianę.
  • tests/ zawiera testy dla każdego elementu.
  • Zasada jest prosta: kod dokumentacyjny ulega degradacji. API się zmieniają, funkcje są przemieniane w nazwy, przykłady przestają działać, a nikt tego nie zauważa, dopóki ktoś nie wklei niepracującego fragmentu z pliku README. Agent automatyzuje ten proces wykrywania problemów. Znajduje podejrzany fragment, sprawdza, czy jest uszkodzony, wykonując go, prosi sztuczną inteligencję o poprawkę, sprawdza, czy poprawka działa, ponownie ją uruchamiając, i dopiero wtedy otwiera prośbę o zmianę.

    Proces składa się z etapu klonowania i analizy, który dostarcza dane do pętli weryfikacyjnej, a poprawka zostaje przyjęta dopiero po dwóch potwierdzeniach: oryginał nie zadziałał, natomiast zamiennik tak.

    Zasadą przewodnią jest evidence-gating. Nic nie trafia do LLM, chyba że test w środowisku sandbox pokaże, że występuje usterka, a nic nie jest publikowane, chyba że ponowny test w tym środowisku potwierdzi, że naprawa działa. Ta zasada przetrwała. Samo środowisko sandbox przez długi czas już nie istniało.

    Pierwszy test: tqdm

    Pierwotny test w rzeczywistych warunkach dotyczył 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
    

    Dwa pliki zostały „naprawione” i opublikowane, co na papierze wyglądało jak sukces. Następnie sprawdzono rzeczywisty rozbieżność:

    - print(tqdm.__version__, sys.version, sys.platform)
    + print(tqdm.__version__, sys.version, sys.platform)
    

    Rzeczywiście, obie linie są identyczne. Gemini nic nie zmieniło i zregenerowało kod bajt po bajcie. System przetestował fragment kodu, uznał go za uszkodzony, „naprawił” go, zwracając ten sam kod, przetestował ponownie, zobaczył sukces i wysłał komit bez żadnych zmian na forku.

    To nie był błąd naprawczy. Był to fałszywy pozytyw na poziomie testowania, ponieważ oryginalny fragment kodu nigdy nie był uszkodzony. Linia importująca tqdm i sys oraz wyświetlająca informacje o wersji to coś zupełnie normalnego w przypadku użycia tqdm, więc dlaczego sandbox zgłosił błąd? Sprawa została zaklasyfikowana jako podejrzana, a następnie sprawdzono inny repozytorium, aby uzyskać drugi punkt danych przed wyciąganiem wniosków.

    Drugi test: pallets/click

    W projekcie pallets/click wzorzec był nie do przeoczenia:

    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'
    

    Za każdym razem ten sam błąd. Sześć plików przeszło przez po trzy pełne pętle refleksji, a każda awaria była taka sama: ModuleNotFoundError: No module named 'click'.

    To powtarzanie się jest wskaźnikiem. Gdy sześć plików i sześć oddzielnych prób z użyciem LLM zawodzi z powodu tego samego błędu importu, wspólnym czynnikiem nie jest kod, lecz środowisko, w którym działają. Przepisanie fragmentu kodu nie naprawi braku modułu, jeśli sandbox nie może dostrzec tego pakietu, bez względu na to, co zostanie napisane.

    Otwarcie pliku tester.py ujawniło mechanizm:

    command.extend(["-v", f"{project_dir}:/workspace", "-w", "/workspace", "-e", "PYTHONPATH=/workspace"])
    

    Wewnątrz kontenera sklonowany repozytorium jest zamontowany, a zmienna PYTHONPATH ustawiona jest na najwyższy poziom tego repozytorium. To działa poprawnie, gdy katalog pakietu znajduje się dokładnie tam, ale przestaje działać, gdy pakiet znajduje się o poziom niżej. Projekt Click jest zagnieżdżony: jego plik pyproject.toml przechowuje rzeczywisty kod w katalogu src/click/, zamiast w katalogu click/ znajdującym się na korzeniu, co jest standardem dla wielu nowoczesnych projektów. Przy użyciu PYTHONPATH=/workspace interpreter szukał w katalogu, w którym w ogóle nie było katalogu click.

    Ręczna weryfikacja poza procesem przetwarzania potwierdziła to:

    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
    

    Drugi wynik dał najwięcej informacji. Wskazanie PYTHONPATH na właściwą folder naprawiło problem z importem, jednak click.__version__ nadal wywoływał błędy, ponieważ obecne wersje click generują __version__ za pomocą importlib.metadata, który czyta metadane zainstalowanych pakietów, a nie pliki źródłowe. Takie metadane powstają dopiero po rzeczywistym pip install, a nigdy po prostym manipulowaniu ścieżką.

    Zatem pojawiły się dwa błędy. PYTHONPATH wskazywał na niewłaściwą folder dla repozytoriów typu src-layout. A nawet jeśli wskazywał poprawnie, zwykły trik z PYTHONPATH nie zapewnia prawidłowej instalacji pakietu, podczas gdy coraz więcej pakietów oczekuje pełnej instalacji, a nie tylko możliwości importu.

    Jeden objazd zasługuje na wzmiankę, ponieważ był najbardziej pouczającym odcinkiem. Sporo czasu poświęcono przypuszczeniom dotyczącym dziwactwa montowania Dockera dostępnego tylko w systemie Windows, przeprowadzając eksperymenty z użyciem cudzysłowów, różnych styli ścieżek oraz list argumentów w porównaniu z ciągami znaków w shellu. Prawdziwe wyjaśnienie okazało się banalne: folder testowy był pusty, ponieważ wcześniejsze polecenie git clone bezgłośnie nie udało się go wypełnić, a nikt tego nie sprawdził. Pusty wynik polecenia dir C:\temp\click_test nie oznaczał „jeszcze brak odpowiedzi”, lecz był samą odpowiedzią, a został zignorowany trzy razy, zanim ktoś zatrzymał się, by go przeczytać.

    Rozwiązanie: zainstaluj, nie zgaduj

    Prawdziwym rozwiązaniem nie jest trafniejsze przewidywanie ścieżki, lecz całkowite unikanie domysłów. pip install -e . czyta własny plik pyproject.toml lub setup.cfg projektu, określa właściwą lokalizację pakietu niezależnie od jego struktury i tworzy prawdziwe metadane instalacji, dzięki czemu wywołania importlib.metadata zachowują się tak samo jak u każdego rzeczywistego użytkownika biblioteki.

    Nowa logika pliku tester.py działa w następującej kolejności:

    • Jeśli istnieje plik pyproject.toml, setup.py lub setup.cfg, środowisko sandbox tworzy obraz Dockera i uruchamia w nim pip install -e .. To tzw. „tryb instalacji”, w którym błąd importu może być uznany za autentyczny.
    • Jeśli brakuje metadanych pakietowania, ale istnieje plik requirements.txt, stosuje się wcześniejszy sposób oparty na PYTHONPATH, czyli „tryb pythonpath”.
  • W trybie pythonpath, jeśli fragment kodu zawiedzie z powodu nierozwiązanej importu własnego pakietu projektu, należy oznaczyć to jako ENV_ERROR, czyli szczere „nie można stwierdzić”, zamiast FAIL.
  • Po tej zmianie istniejący zestaw testów pokazał 26 z 27 przetestowanych przypadków jako udanych. Jedyna nieudana próba była właściwa, ponieważ sprawdzała stary podpis funkcji, który został celowo zmieniony. Oczekiwania zostały zaktualizowane, a trzy nowe testy objęły ten przypadek krawędziowy, dzięki czemu całkowita liczba udanych testów wyniosła 74.

    Prawdziwy błąd w dokumentacji click

    Dzięki temu, że sandbox instalował projekt i już nie zgadywał, click został uruchomiony ponownie. Sześć plików, które wcześniej zawodziły w identyczny sposób, teraz miało szansę, a jeden z nich ujawnił rzeczywisty, dotąd nieodkryty błąd we własnej dokumentacji click.

    docs/commands.md opisuje łańcuchowe procesy w interfejsie CLI, a jedna z funkcji przykładowych zawiera następujący tekst:

    @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)
    

    Funkcja faktycznie otrzymuje fin, czyli identyfikator pliku dostarczony przez funkcję click. Dokumentacja natomiast przechodzi przez pętlę nad input, co oznacza użycie wbudowanej funkcji Pythona input(), a nie pliku. Przechodzenie przez pętlę nad nią jest nieprawidłowe i powoduje błąd już przy uruchomieniu kodu, ale czytelnik tego nie zauważy, ponieważ nazwa wydaje się prawdopodobna.

    DocuPatch wykrył ten problem, potwierdził rzeczywisty błąd w środowisku sandbox, stworzył poprawkę, sprawdził, że poprawiona wersja działa poprawnie od początku do końca, i ją opublikował. Był to pierwszy wynik w całym dochodzeniu, który stanowił zarówno prawdziwy błąd w dokumentacji, jak i prawdziwą poprawkę, bez żadnych zakłóceń ze strony środowiska sandbox ani fałszywych wyników. W tym właśnie celu istnieje ten projekt.

    Lekcje z debugowania

    • Sprawdź wynik przed zaufaniem mu. Etykiety takie jak „Przejdź” lub „Opublikuj” są tak samo niezawodne, jak test, który znajduje się poniżej nich. Problem z tqdm pojawił się wyłącznie dlatego, że otwarto plik z różnicami i nie przyjęto za pewnik zielonego znaku zatwierdzenia.
    • Identyczne błędy w wielu przypadkach wskazują na wspólną przyczynę. Sześć plików, sześć niezależnych prób z użyciem LLM oraz jedna wiadomość o błędzie stanowią sygnał na poziomie systemu.
    • Obserwuj, zanim będziesz formułować teorie. Minęły godziny na spekulacje dotyczące ścieżek w Dockerze i Windows, zanim ktoś sprawdził, czy folder z testami rzeczywiście zawiera jakiekolwiek pliki.
    • Błądujący test może jedynie potwierdzić coś, a nie uszkodzić system. Gdy zmiana sygnatury funkcji run_snippet spowodowała błąd w jednym teście, właściwym pytaniem było to, co ten test faktycznie sprawdzał, a nie to, co zostało uszkodzone.

    Teraz błąd w środowisku testowym został naprawiony, objęty testami i potwierdzony w repozytorium, z którym wcześniej miał problemy. Dokumentacja Click zawierała prawdziwy defekt – nazwę zmiennej, która powoduje błąd po uruchomieniu przykładu przez kogokolwiek – a agent ją zlokalizował, poprawnie ją naprawił i sprawdził naprawę przed publikacją. To właśnie jest centralna idea projektu, udowodniona na rzeczywistym przypadku, a nie syntetycznym.