Mariusz Tober

Meldunek agenta nie jest dowodem: bramka odbioru dla robotników AI

4 min czytania#agenci#testy#llm#inżynieria

Rozdzielam pracę na kilku agentów kodujących naraz: różne modele, różne narzędzia wiersza poleceń, każdy w swoim drzewie roboczym gita. Ja planuję i odbieram. Przez pierwsze tygodnie odbiór polegał na czytaniu meldunku. To był błąd, który wyszedł bardzo konkretnie: agenci meldowali zapis plików, których nie było, uruchamiali kolejnych agentów, o które nikt nie prosił, i kończyli zadanie zdaniem „wszystkie testy przechodzą”.

Kontrakt zamiast zaufania

Pierwsza zmiana: kryterium przyjęcia podaje zlecający przy starcie, nie robotnik przy końcu. Kontrakt to komenda testu, lista plików, które mają powstać, ewentualnie artefakt z minimalnym rozmiarem. Odbiór jest kodem, który ten kontrakt powtarza. Kod wyjścia i log robotnika nie mają znaczenia, bo pisze je ta sama strona, która ma interes w zielonym wyniku.

Druga zmiana: robotnik pracuje w klatce bwrap z własną przestrzenią PID. Drugie uruchomienie binarki agenta w tej klatce kończy się odmową i wpisem do dziennika naruszeń. To zamknęło problem agentów, które same rekrutowały pomocników.

Sześć dróg, którymi zła praca przechodziła odbiór

Potem zleciłem przegląd adwersarialny samej bramki: „znajdź sposób, żeby nieprawdziwa praca dostała zielone”. Przegląd znalazł sześć dróg. Każda jest prawdziwa i każdą da się wykonać bez złej woli, przez zwykłe „naprawię, żeby test przeszedł”:

  1. Test z kontraktu biegł w drzewie robotnika, więc mierzył .venv/bin/pytest, który można było podmienić (plik poza gitem).
  2. conftest.py z hakiem zamieniającym FAIL w PASS.
  3. addopts = --collect-only w pyproject.toml: pytest zbiera testy, nie uruchamia ich, kończy kodem 0.
  4. xfail dopisany do testu zamiast naprawy.
  5. Tryb „nowy test ma padać na kodzie bazowym” zaliczał kod 127, czyli command not found. Jeden test-kanarek, który rzeczywiście padał, legalizował wydmuszki obok.
  6. Kontrakt „ma powstać plik” przyjmował plik z jedną literą.

Do tego klatka montowała system plików z zapisem, więc robotnik mógł dosięgnąć katalogu stanu bramki z kontraktem i werdyktem.

Jak wygląda odbiór teraz

Rozważyłem i odrzuciłem dwie prostsze opcje. Biała lista montowań (wszystko tylko do odczytu oprócz worktree) psuje same agenty, bo piszą do katalogu domowego. --noconftest wywraca każdy zestaw z fiksturami. Klatka jest więc listą zakazów, a nie listą pozwoleń, i ta granica jest opisana wprost.

Kryteria przed pomiarem

Mechanizm sprawdziłem na prawdziwym projekcie przez jeden dzień pracy kilku agentów. Kryteria spisałem przed startem, w osobnym pliku, z progami. Pięć kryteriów, w tym: zero zleceń na narzędziach spoza mandatu, zero zleceń bez odbioru, brak obejść, odmowy nie blokują pracy, odrzuconych najwyżej 10%.

Wynik: cztery z pięciu spełnione. Zlecenia bez odbioru spadły z 58 na 76 do zera, zlecenia spoza mandatu z 3 do zera. Piąte kryterium padło: odrzucone 4 z 12 u agentów i 7 z 29 w próbie szerokiej.

Kusiło, żeby uznać 24–33% za „blisko” albo przesunąć próg. Zamiast tego rozebrałem wszystkie 7 odrzuceń. Każde było wadą kontraktu, nie pracy:

Poprawki: katalogi pamięci podręcznej narzędzi dostają w klatce prywatny tmpfs, zlecenia tylko do odczytu mają osobny tryb z obowiązkowym plikiem wyniku (każda inna zmiana w repo to odrzut), a stan „bez zmian” jest osobnym wynikiem, nie odrzuceniem. Raport z próby mówi „nie sprawdziło się w całości” i tak zostaje. Po poprawkach to inna wersja mechanizmu, którą trzeba zmierzyć od nowa.

Czego to nie łapie

Bramka sprawdza, że kod przechodzi test z kontraktu, i nic więcej. Jeśli kontrakt jest słaby, bramka przepuszcza słabą pracę z pieczątką. Dlatego test kontraktu piszę ja albo inny agent niż wykonawca, a oczekiwane wartości biorę z niezależnego źródła. Autor zawsze napisze test, który jego kod przechodzi.

Druga granica: robotnik, który pracuje bezpośrednio w głównym checkoucie zamiast w worktree, nie ma ochrony głównego katalogu. To świadome ograniczenie, zapisane obok decyzji, a nie odkrycie na produkcji.

← Wszystkie wpisy