Pytanie o człowieka w pętli pada w każdej rozmowie o wdrożeniu agenta i prawie zawsze brzmi tak samo: „a czy da się dodać zatwierdzanie przed wykonaniem?”.
Da się. Problem polega na tym, że odpowiedź „tak” zamyka temat, a powinna go otwierać. Bo zatwierdzanie, które wygląda na wdrożone, a nie zatrzymuje niczego naprawdę, jest gorsze od jego braku. Bez bramki wiesz, że system działa bez nadzoru. Z pozorną bramką masz spokój sumienia i tę samą sytuację.
Zanim o mechanice, rozróżnienie, którego brakuje w większości dyskusji. „Człowiek w pętli” oznacza trzy różne rzeczy o zupełnie różnej sile.
Człowiek przed. Zatwierdza plan, zanim agent cokolwiek wykona. Najsilniejsza bramka i najbardziej kosztowna, bo blokuje każde uruchomienie. Sensowna tam, gdzie działanie jest nieodwracalne.
Człowiek w środku. Agent działa, ale przy określonych operacjach zatrzymuje się i pyta. Wymaga rozstrzygnięcia, które operacje są tymi określonymi, i to jest cała trudność.
Człowiek po. Agent wykonuje wszystko, człowiek przegląda wynik. To nie jest bramka, tylko audyt. Nazywanie tego „human in the loop” w rozmowie o zgodności jest naciąganiem, bo nic nie powstrzymuje.
Większość systemów, które widziałem opisane jako „z akceptacją człowieka”, realizuje wariant trzeci. Warto to nazwać po imieniu przed rozmową z bezpieczeństwem, a nie po niej.
Nie w każdym przypadku. Bramki, które tylko spowalniają, lepiej sobie oszczędzić.
| Sytuacja | Bez zatrzymania | Z zatrzymaniem |
|---|
| Wdrożenie na produkcję | Agent wdraża sam | Człowiek zatwierdza przed wypchnięciem |
| Test bezpieczeństwa na żywym systemie | Ryzyko uszkodzenia środowiska | Wymagana jawna zgoda |
| Usunięcie danych osobowych | Kasowanie bez śladu decyzji | Zgodność wymaga zatwierdzenia z podpisem |
| Operacja finansowa | Transakcja bez potwierdzenia | Potwierdzenie po stronie człowieka |
| Model o niskiej pewności | Zgaduje i działa dalej | Eskaluje zamiast zgadywać |
| Odpowiedź na pytanie o dokumentację | Odpowiada | Odpowiada, bo nie ma czego zatrzymywać |
Ostatni wiersz jest tam celowo. Bramka przy operacji, która nic nie zmienia w świecie, to koszt bez korzyści, a przy okazji uczy ludzi klikać „zatwierdź” odruchowo. Wtedy przestaje działać także tam, gdzie miała działać.
Najczęstsza implementacja wygląda tak, że w instrukcji agenta pojawia się zdanie: „przed wykonaniem operacji nieodwracalnej poproś użytkownika o potwierdzenie”.
To nie jest bramka. To jest prośba.
Model wykona ją w większości przypadków, bo jest posłuszny. Ale decyzja o zatrzymaniu należy wtedy do niego, a on jest probabilistyczny. W tym jednym przebiegu na sto, w którym uzna, że kontekst wskazuje na zgodę wyrażoną wcześniej, wykona operację bez pytania. Nie znajdziesz tego w testach, bo testy przechodzą.
Zatrzymanie musi być w kodzie, w węźle, przez który przepływ musi przejść. Nie w tekście instrukcji. To samo rozróżnienie co przy walidacji danych wejściowych: sprawdzasz je w kodzie, a nie prosisz użytkownika w interfejsie, żeby wpisywał poprawne.
W praktyce oznacza to trzy elementy w grafie:
- Węzeł zatrzymania, który wystawia żądanie decyzji i czeka.
- Zapis decyzji w stanie sesji, na przykład pod kluczem
human_decision.
- Węzeł routujący, który nie jest modelem i kieruje przepływ na podstawie tej wartości.
Trzeci punkt jest równie ważny co pierwszy. Jeśli o kierunku po zatwierdzeniu decyduje kolejny agent na podstawie instrukcji, wracasz do punktu wyjścia. Routing po ludzkiej decyzji ma być zwykłym rozgałęzieniem w kodzie.
Test na pozorną bramkę
Zadaj sobie jedno pytanie: czy da się dojść do wykonania operacji, jeśli model postanowi zignorować instrukcję? Jeśli tak, nie masz bramki, tylko konwencję. Prawdziwa bramka jest miejscem w grafie, którego nie da się ominąć, niezależnie od tego, co model uzna za sensowne.
Jeżeli budujesz to w ADK 2.0, jest jedna pułapka, o której lepiej wiedzieć, zanim spędzisz nad nią pół dnia.
Przerwanie węzła, który czeka na człowieka, framework sygnalizuje wyjątkiem NodeInterruptedError. Ta klasa dziedziczy po BaseException, a nie po Exception, właśnie po to, żeby zwykłe except Exception jej nie połknęło. Jeśli jednak w narzędziu albo w węźle masz except BaseException, przechwycisz ją razem z resztą i pauza przestanie działać. Kod się uruchomi, testy przejdą, a zatrzymania nie będzie.
Oficjalna dokumentacja migracyjna wymienia to wprost i jest to jedna z czterech rzeczy, które psują się po cichu po przejściu na 2.0. Opisałem wszystkie w tekście o migracji.
Praktycznie: except BaseException w kodzie agenta jest prawie zawsze błędem. Jeśli musisz coś posprzątać, użyj finally albo przechwyć konkretny typ wyjątku.
Bramka, która działa, ale nie zostawia śladu, załatwia bezpieczeństwo i nie załatwia zgodności. Przy operacjach objętych regulacjami to jest różnica między „mamy proces” a „potrafimy go wykazać”.
Minimalny zestaw, który warto zapisywać razem z decyzją:
- Kto zatwierdził. Tożsamość, nie nazwa roli.
- Co dokładnie zatwierdził. Treść planu w wersji, którą widział, a nie odnośnik do zasobu, który mógł się od tego czasu zmienić.
- Kiedy. Znacznik czasu po stronie serwera, nie klienta.
- Na jakiej podstawie. Wynik poprzedniego etapu, który człowiek miał przed oczami.
- Decyzja odrzucająca też. Odrzucenia bywają ciekawsze od zatwierdzeń, bo pokazują, gdzie agent regularnie proponuje coś nieakceptowalnego.
Ten ostatni punkt ma wartość operacyjną, nie tylko formalną. Jeśli w logu widzisz, że ludzie odrzucają co trzecią propozycję z tego samego etapu, masz konkretne miejsce do poprawy w instrukcji albo w narzędziach.
Największy zarzut wobec bramek jest praktyczny: spowalniają. To prawda i trzeba to zaprojektować, a nie odkryć po wdrożeniu.
Zatrzymuj na kategorii operacji, nie na każdym kroku. Jedno zatwierdzenie planu obejmujące pięć działań jest lepsze niż pięć osobnych pytań, bo człowiek widzi całość i podejmuje jedną świadomą decyzję zamiast pięciu odruchowych.
Ustaw domyślne zachowanie na odmowę przy przekroczeniu czasu. Jeśli nikt nie odpowie w rozsądnym oknie, przepływ ma się zakończyć odrzuceniem, nie przejściem dalej. To jedno ustawienie odróżnia bramkę od opóźnienia.
Rozdziel zatwierdzanie od wykonania w czasie. Nie każde zatrzymanie musi być synchroniczne. Zapisanie żądania decyzji i podjęcie jej po godzinie działa równie dobrze, o ile przepływ potrafi wznowić się z zapisanego stanu.
Mierz, jak długo ludzie czekają. Jeśli mediana czasu decyzji rośnie, bramka zaraz zacznie być omijana nieformalnie, przez zatwierdzanie bez czytania. To sygnał, żeby zawęzić zakres bramki, a nie żeby ją usunąć.
- Wypisz operacje nieodwracalne, które agent może wykonać. Nie wszystkie działania, tylko te, których nie da się cofnąć w minutę. Zwykle jest ich mniej, niż się wydaje.
- Sprawdź, czy któraś z nich jest zabezpieczona wyłącznie zdaniem w instrukcji. Jeśli tak, to jest Twoja luka i nie zamyka jej lepsze sformułowanie.
- Postaw jeden węzeł zatrzymania na najgroźniejszej z nich. Jedna działająca bramka jest warta więcej niż pięć zaplanowanych.
- Sprawdź, czy w kodzie nie ma
except BaseException. Grep zajmuje minutę, a na ADK 2.0 potrafi uratować całą bramkę.
- Dopiero potem rozszerzaj. Kolejne bramki dokładaj wtedy, gdy log odrzuceń pokaże, że są potrzebne.
Kolejne części serii dotyczą testowania agentów oraz migracji na graf. Całość zaczyna się od drogi od czatu do systemu agentowego.