Instrukcje dla AI - dlaczego rozjeżdżają się z projektem i jak to wyłapać

Plik z zasadami dla AI piszesz raz, a agent pracuje według niego miesiącami - także wtedy, gdy połowa zasad przestała być prawdą. W badaniu METR z początku 2025 doświadczeni programiści z AI byli wolniejsi o 19%, choć czuli się szybsi o 20%. W tym poście pokazuję, jak to u siebie rozwiązałem. Instrukcje trafiły pod tę samą kontrolę co kod, dostały własną bramkę, a bramka dostała test, który ją sprawdza.
Któregoś popołudnia agent zameldował mi, że naprawa działa. Miał dowody. Testy przeszły, a na zrzucie ekranu było dokładnie to, co miało się pojawić. Otworzyłem tę samą stronę u siebie - pusto. Odesłałem mu swój zrzut, on odesłał swój, i tak trzy rundy, każdy z nas coraz bardziej pewny swojej wersji. W końcu wyszło na jaw, że nikt tu nikogo nie okłamywał. Strona, nad którą pracowaliśmy, pokazywała różnym osobom różne wersje interfejsu. On patrzył na jedną, ja na drugą.
I teraz najciekawsze. W moich instrukcjach dla agenta nigdzie nie było zdania „zanim porównasz wyniki, upewnij się, że patrzycie na to samo”. To przeoczenie siedziało w instrukcjach od tygodni i nie robiło nic złego, dopóki nie trafiła się sytuacja, w której zaczęło mieć znaczenie. Żadne narzędzie nie miało jak go wykryć. Kod testujemy, konfigurację sprawdzamy, a instrukcje dla AI… instrukcje po prostu piszemy. Raz. A potem wierzymy, że działają.
#Plik, który pracuje, kiedy ty nie patrzysz
Agentem nazywam AI, które dostaje od ciebie zadania w edytorze albo terminalu. Potem sam edytuje pliki i uruchamia polecenia. Jeśli pracujesz z takim agentem, to gdzieś obok twojego kodu leży plik z zasadami. Może nazywa się CLAUDE.md, może AGENTS.md, może to katalog reguł twojego edytora. W takim pliku zwykle opisuje się, jak budować projekt, gdzie leżą testy, czego nie wolno ruszać i jak nazywać commity.
Ten plik ma jedną cechę, która odróżnia go od każdej innej notatki w repozytorium - on się wykonuje. Agent czyta go przed każdym zadaniem i traktuje jak zasadę, której nie może zmienić. Napisałeś tam kiedyś, że aplikacja wstaje na porcie 3001? Agent będzie sprawdzał port 3001 także pół roku po tym, jak ktoś przeniósł ją na 3002. Nie dlatego, że jest głupi. Dlatego, że jest posłuszny.
Skalę tego problemu ktoś już zmierzył. Badanie METR z początku 2025 to rzadki przypadek prawdziwego eksperymentu w naszej branży: doświadczeni programiści, ich własne dojrzałe projekty, zadania losowo przydzielane do „z AI” albo „bez AI”. Wynik zaskoczył wszystkich, łącznie z badanymi. Z AI kończyli zadania średnio o 19% wolniej - a po wszystkim byli przekonani, że AI przyspieszyło ich o jakieś 20%. Odczucie i zmierzony czas wskazywały w przeciwne strony.
Sam METR oznaczył potem tamte wyniki jako nieaktualne i powtórzył eksperyment. Nowy pomiar wyszedł tak niepewnie, że autorzy nazywają go niewiarygodnym sygnałem - między innymi dlatego, że część uczestników odmówiła robienia zadań bez AI. I znowu to samo - liczba, którą wszyscy cytują, po cichu przestała być aktualna.
Raport DORA 2025 dołożył drugą połowę obrazu na poziomie zespołów: tam, gdzie AI jest więcej, wdrożenia są szybsze i zarazem mniej stabilne. Ten sam raport wskazuje, co tę nierównowagę domyka. Im mniejsza porcja zmian, tym mniej ma prawo się w niej zepsuć. Im więcej kodu pokrywają testy automatyczne, tym szybciej widać, że jednak się zepsuło. A zabezpieczenia wbudowane w platformę działają niezależnie od tego, czy zespół o nich pamięta. Czyli dokładnie to, o czym jest reszta tego tekstu.
Długo nie umiałem pogodzić tych wyników z własnym doświadczeniem, bo mam poczucie, że mnie praca z agentem przyspiesza. Tyle że dokładnie takie samo poczucie mieli uczestnicy badania, a pomiar go u nich nie potwierdził. Przestałem więc traktować własne wrażenie jako argument w tej sprawie.
Zamiast tego zadałem sobie inne pytanie. Co musiałoby być prawdą, żeby praca z agentem naprawdę się opłacała? Odpowiedź nie leży ani w modelu, ani w sprycie promptów. Leży w tym, czy wokół agenta stoi coś, co sprawdza jego pracę. I czy ta kontrola sama została kiedykolwiek sprawdzona.
#Instrukcje psują się bezgłośnie
Kiedy psuje się kod, dowiadujesz się o tym od razu. Program przerywa działanie i wypisuje błąd, test przestaje przechodzić, kompilacja się nie udaje. Instrukcje nie dają żadnego z tych sygnałów. One po prostu z każdym tygodniem odrobinę mniej pasują do rzeczywistości.
Utrzymuję zestaw plików, które prowadzą agenta przez cały cykl pracy, od ticketu przez plan i implementację po code review i pull request. Uzbierało się tego kilka tysięcy linii. Niedawno przejrzałem je tak, jak przegląda się kod. W jeden dzień znalazłem między innymi:
- zapisany w instrukcji adres, pod którym startuje aplikacja -
localhost:3001, choć od miesięcy startowała na3002. Agent posłusznie zaglądał pod martwy adres i meldował, że strona nie działa. - zakaz używania jednego z narzędzi, uzasadniony błędem, który to narzędzie kiedyś miało. Sęk w tym, że naprawiono go dwa dni wcześniej. Moja reguła zabraniała czegoś, co już działało poprawnie.
- liczba wpisana na sztywno, czyli ile jest w projekcie skryptów diagnostycznych. Kiedy je policzyłem, prawdziwa wartość była wielokrotnie wyższa.
- gotowe zapytanie do zewnętrznego API, w którym warunek ograniczający wyniki był dopisany jako osobny parametr zamiast wejść do treści samego zapytania - czyli
?status=OPEN&q=...zamiast?q=... AND status=OPEN. API nie rozpoznawało takiego parametru i po prostu go pomijało, bez błędu i bez ostrzeżenia. Zapytanie zwracało więc wszystko, a nie tylko to, o co prosiłem. Na tych danych pracował już kolejny krok, beze mnie.
Żadna z tych usterek nie spowodowała awarii. Każda mogła zostać niezauważona miesiącami i każda po trochu psuła pracę agenta w sposób, który z zewnątrz wygląda jak „no cóż, AI się myli”. Tyle że model się nie mylił. To moje instrukcje kłamały, a on je sumiennie wykonywał.
Z tych czterech znalezisk wyszła jedna zasada, która potem oszczędziła mi najwięcej pracy. W pliku, który się wykonuje, treść dzieli się na dwa rodzaje.
Pierwszy to fakty - numer portu, liczba plików, nazwa katalogu, numer wersji. Fakt zgadza się w dniu, w którym go zapisujesz. Potem projekt idzie dalej, a zapis zostaje taki, jaki był, i w pewnym momencie przestaje być prawdziwy. Nikt tego nie zauważy, bo nikt nie wraca do starych zdań w instrukcji.
Drugi rodzaj to polecenia, które ten sam fakt ustalają na miejscu. Zamiast wpisywać liczbę, wpisujesz komendę, która ją policzy. Agent dostaje wtedy aktualną odpowiedź za każdym razem, niezależnie od tego, jak dawno powstała ta linijka.
Przechodząc przez własne instrukcje, przy każdej linijce warto zadać jedno pytanie. To jest fakt czy polecenie? Faktów ma tam być jak najmniej.
Zresztą znasz to zjawisko, tylko z innych miejsc. README opisujące flagi, których dawno nie ma. Komentarz nad funkcją, który od dawna nie opisuje tego, co ta funkcja robi. Konfiguracja wskazująca serwer sprzed migracji. Dokumentacja rozjeżdża się z rzeczywistością od zawsze - nowe jest tylko to, że ta konkretna dokumentacja sama się wykonuje. Wykonuje się wielokrotnie, nikt tego nie nadzoruje, a brzmi przy tym pewnie.
#Skoro to kod, niech przechodzi przez bramkę
Wniosek nasunął się sam. Jeśli plik zachowuje się jak program, trzeba go traktować jak program. Mój sposób to zwykły skrypt. Przechodzi po wszystkich plikach instrukcji i sprawdza to, co psuje się najczęściej:
- czy ścieżki prowadzą do plików, które istnieją;
- czy polecenia, do których odsyłam agenta, nadal są w
package.json; - czy liczby wpisane na sztywno nie rozjechały się ze stanem projektu;
- czy w gotowych zapytaniach nie ma wzorca, o którym wiem, że cicho zawodzi.
$ ./check.sh
✅ ścieżki do repozytorium
✅ polecenia pnpm w package.json
✅ liczby wpisane na sztywno
✅ warunek, który API po cichu pomija
...
Czysto.Kilkanaście kontroli, komplet zielonych. Wygląda solidnie, prawda?
No właśnie. Przez dłuższą chwilę też tak myślałem - aż zadałem sobie pytanie, które zmieniło mi sposób patrzenia na całą tę układankę: a skąd właściwie wiem, że te kontrole cokolwiek łapią?
Zielone światło wygląda identycznie wtedy, gdy wszystko jest w porządku, i wtedy, gdy kontrola w ogóle się nie wykonała. Bramka ma bowiem nie dwa stany, tylko trzy - przeszła, nie przeszła i nie uruchomiła się. Trzeci jest nie do odróżnienia od pierwszego i to jest cała pułapka. Dotyczy nie tylko moich skryptów, ale każdego kroku w CI, który kończy się sukcesem dlatego, że polecenie w środku po cichu nie wystartowało.
Bramka, której nigdy nie widziałeś na czerwono, jest pod tym względem gorsza niż jej brak. Kiedy bramki nie ma, wiesz, że nikt niczego nie sprawdza, i sprawdzasz sam. Kiedy jest i świeci na zielono, przestajesz sprawdzać.
Dlatego dopisałem drugi skrypt, który testuje pierwszy. Bierze kopię plików, wprowadza do niej po jednym błędzie na każdą kontrolę - martwą ścieżkę, zepsutą liczbę, nieistniejące polecenie - i sprawdza, czy kontrola podniesie alarm.
Kontrola w tym skrypcie wygląda dziś tak:
{
id: "martwe-sciezki",
describe: "ścieżka cytowana w instrukcji istnieje w repozytorium",
run(file, ctx) {
return file.lines.flatMap((line, index) => {
const paths = line.match(/docs\/[\w-]+\.md/g) ?? [];
const dead = paths.filter((path) => !ctx.fileExists(path));
return dead.map((path) => ({
line: index + 1,
message: `nie ma pliku ${path}`,
}));
});
},
fixture: { inject: "patrz docs/nie-ma-takiego.md" },
}Cały pomysł mieści się w ostatnim polu. fixture to celowo zepsuty fragment, na którym ta kontrola ma się zapalić, i jest obowiązkowy - kontrola bez niego nie rejestruje się wcale, narzędzie rzuca błędem przy starcie. Kto pisze kontrolę, pisze też dowód, że umie ona zaświecić na czerwono. Nie ma tu kroku, który dałoby się pominąć. Bez tego pola kontrola po prostu nie istnieje.
To zresztą nie jest mój wynalazek, tylko stara technika w nowym miejscu. W testowaniu mutacyjnym psuje się kod celowo i sprawdza, czy testy to zauważą. Jeśli po wprowadzeniu błędu wszystkie testy nadal przechodzą, to znaczy, że nie sprawdzały tego, co miały sprawdzać. Z bramką dla instrukcji jest dokładnie tak samo.
Pierwsze uruchomienie tego testu wykryło dwie kontrole, które nigdy w życiu nie zapaliłyby się na czerwono. Powód okazał się banalny do bólu. Ich wzorce używały składni, której systemowy grep na macOS nie rozumie. Narzędzie zgłaszało błąd, ale kanał błędów był wyciszony, więc kontrola kończyła się „sukcesem”. Świeciła na zielono od pierwszego dnia. Bo nigdy nie działała.
Zamiast wierzyć mi na słowo, przeżyj to samo w miniaturze. Poniżej jest mały plik instrukcji i bramka z czterema kontrolami. Jest też przycisk, którego normalnie nikt ci nie daje - taki, który psuje jedną kontrolę tak, żeby nikt tego nie zauważył.
- 1
# Zasady dla agenta - 2
Setup opisany w docs/setup-old.md - przeczytaj przed startem. - 3
Testy trzymamy obok kodu. - 4
Przed commitem odpal pnpm build:fast i popraw błędy. - 5
Otwarte PR-y pobieraj tak: - 6
curl ".../items?status=OPEN&q=owner=me" - 7
Nigdy nie commituj sekretów.
- pliki z odsyłaczy istnieją
- polecenia pnpm są w package.json
- warunek pomijany przez API
- poprawne kodowanie znaków
W tym pliku są trzy błędy. Uruchom bramkę i zobacz, czy je znajdzie.
#Reguły pisze awaria
Pomysł na to, jak pisać same reguły, przyszedł z zupełnie innej dziedziny. Podpatrzyłem go u ludzi, których praca polega na tym, żeby duże serwisy nie przestawały działać - w branży mówi się na nich SRE, od Site Reliability Engineering. Kiedy coś psuje się w środku nocy, to oni odbierają telefon. Obowiązuje u nich zasada, że procedur awaryjnych nie pisze się na zapas - pisze je awaria. Wziąłem ją u siebie w niezmienionej postaci. Żadna reguła w moich plikach nie powstała, bo „tak wypada”. Każda ma w treści datę i zdarzenie, które ją wymusiło.
Jedna z moich ulubionych brzmi niepozornie: jeśli liczysz elementy na stronie, która może w pewnym momencie stać się pusta, i wychodzi zero, najpierw sprawdź, czy jest tam jeszcze co liczyć. Skąd się wzięła? Agent miał policzyć, ile pozycji pokazuje pewna strona, i porównać to z tym, co było przed zmianą. Zmierzył zero i orzekł: kod się zepsuł, selektory do wymiany. Diagnoza brzmiała przekonująco, miał nawet zrzut ekranu. Straciłem godzinę na debugowanie kodu, któremu nic nie dolegało. Prawda leżała gdzie indziej - strona dotyczyła wydarzenia, które zdążyło się już zakończyć, więc nie miała czego wyświetlać. Zero było poprawną odpowiedzią na pytanie zadane stronie, która nie miała już nic do pokazania.
Spróbuj wymyślić taką regułę przy biurku, na sucho. Nie wymyślisz. Nikt nie wpada na taki warunek, dopóki nie zobaczy go na własne oczy. I właśnie dlatego reguły z incydentów są tyle warte. Pokrywają dokładnie te przypadki, których nie obejmie żadna „lista dobrych praktyk”.
Mają też dwie mniej oczywiste zalety. Model traktuje je poważniej - krótkie „dlaczego” w treści reguły działa na niego lepiej niż goły zakaz. A regułę z datą i powodem można po czasie rozliczyć. Raz na miesiąc siadam i sprawdzam, czy powód, dla którego powstała, nadal istnieje. Bo reguły też tracą ważność - obejście słabości narzędzia przestaje być potrzebne z każdą jego nową wersją. Pamiętasz zakaz używania czegoś, co naprawiono dwa dni wcześniej? Gdyby nie ten przegląd, wisiałby u mnie do dziś.
#„Tego nie ma w danych” - mówi agent
Ta część jest najprostsza do opisania i najtrudniejsza do utrzymania, bo wymaga niewygodnej dyscypliny wobec samego siebie.
Model językowy, który czegoś nie znalazł, rzadko przyznaje „nie znalazłem”. Znacznie chętniej ogłasza „tego nie ma” - tonem osoby, która właśnie sprawdziła. Brzmi to jak fakt, a jest hipotezą. Czasami trafną, i to jest w tym wszystkim najgroźniejsze, bo trafne hipotezy uczą nas ufać nieuzasadnionym.
Dlatego umówiłem się z agentem, że każde twierdzenie przychodzi razem z dowodem. Mówisz „tych danych nie ma w bazie”? Pokaż wynik zapytania - wklejony, nie streszczony. Automatyczny test na żywej stronie wyszedł negatywnie? To jest początek diagnozy, nie diagnoza. Strona mogła się nie doładować. Mogła pokazać inną wersję interfejsu - jak w historii, od której zacząłem. Każda z tych rzeczy zdarzyła mi się naprawdę i każda przez chwilę udawała zepsuty kod.
Dokładnie tak samo traktuję zresztą własne odczucia. Badanie METR mówi to jasno. Moje „szło dziś szybko” jest bezwartościowe jako miara. Więc mierzę rzeczy zewnętrzne - tę samą stronę przed zmianą i po niej, liczby z bramek, trend uwag z code review na moich pull requestach. Jeśli system działa, uwag ma być z czasem mniej. A jeśli nie ubywa, to znaczy, że system nie działa, tylko wygląda, jakby działał.
#Od czego zacząć u siebie
Zanim powiem, od czego zacząć, dwa uczciwe zastrzeżenia. Po pierwsze, to się opłaca dopiero od pewnej skali. Przy pliku na sto linijek wystarczy przeczytać go raz na kwartał i bramka będzie kosztowała więcej, niż da. Zwraca się, gdy plików jest kilka, mają po kilkaset linii i korzysta z nich więcej niż jedna osoba. Po drugie, bramka nie sprawdza wszystkiego. Łapie wyłącznie to, co da się porównać ze stanem repozytorium. Reguła, która jest po prostu zła albo szkodliwa, przejdzie przez nią na zielono, bo wszystko, co da się w niej sprawdzić, zgadza się ze stanem projektu.
Nie potrzebujesz moich kilku tysięcy linii - one opisują moją pracę, nie twoją. Mechanika jest krótka:
- Trzymaj instrukcje dla agenta w repozytorium i wersjonuj je jak kod - bo są kodem.
- Napisz skrypt, który sprawdza je pod kątem cichego rozjazdu z projektem: martwe ścieżki, nieaktualne liczby, odwołania do rzeczy, których już nie ma. Bash i grep wystarczą na start.
- Przetestuj ten skrypt na czerwono. Zepsuj po kolei każdą rzecz, którą ma wykrywać, i zobacz na własne oczy, że ją łapie. Jeden wieczór - a bez tego kroku punkt 2 produkuje wyłącznie fałszywy spokój.
- Nowe reguły twórz z incydentów i zapisuj przy nich datę oraz powód. Raz na miesiąc sprawdzaj, które są już niepotrzebne.
- Kiedy agent mówi ci, jak coś wygląda w projekcie, poproś o dowód. Wynik zapytania zamiast zapewnienia, pomiar zamiast wrażenia.
Gdybym jutro stracił wszystkie te pliki, odtworzyłbym je w kilka tygodni i nie byłaby to wielka strata. Wartość nie leży bowiem w samych plikach, tylko w trzech nawykach, które one utrwalają. Każde twierdzenie wymaga dowodu, każdy incydent zostawia po sobie regułę, a każda bramka musi choć raz zapalić się na czerwono, zanim zacznę jej ufać. Pliki tylko pamiętają to wszystko za mnie.
Komentarze
Wczytywanie komentarzy…