Wersja skrócona
- Agenci kodujący AI są dobrzy w dodawaniu kodu, a słabi w zauważaniu, że kod już istnieje. Pozostawieni sami sobie dają repozytorium drugą funkcję pomocniczą na każdą pierwszą, a każdy czytelnik musi sprawdzić obie, by zaufać którejkolwiek.
- Lekarstwem są dwa nawyki zapisane tak, by agent ich przestrzegał. Grain: znajdź najbliższy istniejący przykład tego samego rodzaju rzeczy i odzwierciedl jego kształt, miejsce i nazwy. Ponytail: wchodź po drabinie od „czy to w ogóle musi istnieć” do „minimum, które działa” i zatrzymaj się na pierwszym szczeblu, który się utrzyma.
- Oba są teraz publiczną umiejętnością na licencji MIT: grain-skill na GitHubie . Zawiera reguły pisania, przebieg recenzji ze stałymi tagami i kształt zamykającej linii, która zmusza agenta do nazwania tego, co odzwierciedlił.
- W naszym głównym repozytorium od sierpnia: 386 zgłoszeń i pull requestów ma komentarz z planem wskazujący wzorzec do odzwierciedlenia, w 1 333 commitach do drzewa liczącego około 18 000 plików. 35 miejsc w kodzie ma komentarz
ponytail:wskazujący celowe uproszczenie i ścieżkę jego rozbudowy. - Niewygodne odkrycie: agent wywołał tę umiejętność jako umiejętność zero razy. Reguły zadziałały, bo umieściłem je także w CLAUDE.md projektu, w definicjach agentów-workerów i w liście kontrolnej recenzji. Umieść regułę tam, gdzie agent i tak już czyta.
Pisałem o tym już raz i nie spodobał mi się rezultat, bo opisywał zasadę, a nie mechanizm. To jest wersja z mechanizmem.
Błąd, któremu to ma zapobiegać
Trzy przykłady z naszego backendu, wszystkie prawdziwe, wszystkie z pull requestów napisanych przez agenta, które przeszły testy.
Nowa trasa REST, która pobierała serwis aplikacyjny bezpośrednio z fabryki. Każda pokrewna trasa w repozytorium idzie trasą do kontrolera, do serwisu, do repozytorium, przy czym każdy krok jest wstrzykiwany. Ta jedna spłaszczyła łańcuch do trasy i serwisu. Działała. Oznaczała też, że miejsce, w którym dla tej funkcji żyje walidacja żądań i kształtowanie odpowiedzi, przestało istnieć, a następny agent, który dotknie tej funkcji, ma dwa kształty do wyboru.
Zbyt duży serwis rozbity na _payloads.py, _protocol.py i _tokens.py, każdy będący modułem gołych async def-ów, przy czym serwis wywołuje protocol.discover(...) przez import. Sprawdzenie długości pliku przeszło na zielono. Wróciło dokładnie to, czemu ma zapobiegać wstrzykiwanie zależności: współpracownik wybierany w momencie importu, niepodmienialny bez patchowania modułu, nieobecny w konstruktorze, który dokumentuje, od czego serwis zależy.
Drugi normalizator. Jeden już istniał. Agent go nie znalazł, napisał kolejne osiem linii robiących prawie to samo i teraz dwie funkcje o różnym zachowaniu na przypadkach brzegowych twierdzą, że kanonizują to samo pole.
Żaden z nich nie jest błędem w takim sensie, w jakim wyłapuje go test. Każdy to agent wybierający kod, który mógł wygenerować, zamiast kodu, który istniał. Zrób tak w kilkuset pull requestach, a repozytorium przestanie mieć jakikolwiek kształt.
Grain
Nowy kod powinien być nieodróżnialny od kodu wokół niego. Ta umiejętność zamienia to zdanie w pięć kroków, które agent wykonuje przed pisaniem.
Najpierw znajdź wzorzec. Dla endpointu, serwisu, repozytorium, hooka zapytań, slice’a store’a lub komponentu zlokalizuj najbliższy istniejący i skopiuj jego kształt, umiejscowienie pliku i nazewnictwo. Nie wymyślaj struktury, gdy wzorzec istnieje. W naszym repozytorium slice branding jest nazwanym wzorcem dla backendu, bo jest mały, czysty i obejmuje każdą warstwę, a tabela w umiejętności mówi dla każdego rodzaju artefaktu, który plik otworzyć i co odzwierciedlić.
Szanuj warstwy. Prezentacja wywołuje aplikację, aplikacja wywołuje domenę, a infrastruktura implementuje domenę. Nowy import przechodzący przez te granice oblewa sprawdzenie architektury. Przeczytaj dokument o warstwach, nie wyprowadzaj go od nowa.
Lokalny grain kontra grain repozytorium. Dopasuj się do swoich sąsiadów. Ale jeśli sami sąsiedzi odbiegają od kanonicznego łańcucha, podążaj za kanonicznym łańcuchem i odnotuj rozbieżność, zamiast kopiować dryf. „Pasuje do pozostałych pięciu tras w tym pliku” to za mało, jeśli wszystkie sześć pomija warstwę, której używa kanoniczny slice. Porównywałeś zbyt lokalnie.
Użyj ponownie kanonicznej funkcji pomocniczej. Jeśli funkcja pomocnicza, typ lub wzorzec już istnieje w repozytorium, użyj go. Dwa normalizatory zmuszają każdego czytelnika do porównania obu.
Nazwy niosą znaczenie. Używaj ponownie słownictwa, które już jest w kodzie. Nowa nazwa dla istniejącego pojęcia to błąd czekający na czytelnika.
Test na końcu to ten, który daję recenzentom: jeśli potrafisz rozpoznać, które linie napisał agent, wyłącznie po ich kształcie, to walczył ze słojem.
Ponytail
Leniwe znaczy efektywne, nie niedbałe. Najlepszy kod to kod, którego nigdy nie napisano. Najpierw zrozum problem w pełni — każdy plik, którego dotknie zmiana, prawdziwy przepływ end-to-end — a potem wchodź po tej drabinie i zatrzymaj się na pierwszym szczeblu, który się utrzyma.
Z drabiną jadą jeszcze dwie reguły.
Poprawka błędu to przyczyna źródłowa, nie objaw. Zgłoszenie nazywa objaw. Przeszukaj grepem każdego wywołującego funkcję, której zamierzasz dotknąć. Jeden warunek w funkcji współdzielonej to mniejszy diff niż warunek u każdego wywołującego, a załatanie tylko ścieżki wskazanej w zgłoszeniu zostawia pokrewnych wywołujących zepsutych.
Utrzymuj diff w minimalnym rozmiarze. Diff to to, co czyta recenzja. Żadnych refaktoryzacji przy okazji, żadnych zmian nazw, żadnego przeformatowywania, żadnych ulepszeń „skoro już tu jestem”, żadnego dotykania plików, których zadanie nie wymagało. Każde z nich zakopuje prawdziwą zmianę. Jeśli diff jest większy niż problem, przytnij go. Ulepszenia, które zauważyłeś, to osobne zgłoszenie, a nie dodatkowe commity w tej gałęzi.
I jeszcze granice, bo „leniwe” to słowo, które najłatwiej zrozumieć opacznie. Nigdy nie upraszczaj walidacji danych wejściowych na granicy zaufania, obsługi błędów zapobiegającej utracie danych, bezpieczeństwa, podstaw dostępności ani niczego, o co poprosił użytkownik. Nigdy nie bądź leniwy w rozumieniu: drabina skraca rozwiązanie, nigdy lekturę. Mały diff w złym miejscu to drugi błąd. Niebanalna logika zostawia po sobie jedno uruchamialne sprawdzenie — najmniejszą rzecz, która zawodzi, jeśli logika się psuje.
Gdy agent świadomie wybiera jakiś szczebel i zna jego pułap, mówi o tym w kodzie:
# ponytail: global lock, per-account locks if throughput matters
Mamy teraz w repozytorium 35 takich komentarzy. Każdy to decyzja, którą przyszły czytelnik musiałby inaczej odtworzyć, i znacznik mówiący, gdzie zajrzeć, gdy pułap zostanie osiągnięty.
Jak sprawić, by agent rzeczywiście to robił
Zasada zapisana w dokumencie to nie zachowanie. Trzy mechanizmy zamieniły te dwa w zachowania, i uszeregowałbym je właśnie w tej kolejności.
Brief niesie wzorzec. W wzorcu architekta główny model pisze brief dla workera, a pole Design w tym briefie wskazuje najbliższy istniejący wzorzec do odzwierciedlenia i minimalną zmianę naprawiającą przyczynę źródłową. Worker nie musi szukać słoja. Dostaje go podanego. W naszym repozytorium tym właśnie jest 491 komentarzy z planem przy 386 zgłoszeniach i pull requestach: krok planowania, który nazywa to, co ma być odzwierciedlone, zanim ktokolwiek zacznie pisać.
Agent kończy tym, co odzwierciedlił. Każdy raport implementującego kończy się jedną linią, mirrored: <path>, albo nazywa nowy wzorzec i wyjaśnia, dlaczego żaden sąsiad nie pasował. To jest tanie i to najlepsza pojedyncza pomoc recenzencka, jaką dodałem. Linia mirrored: wskazująca plik złego rodzaju mówi ci, że diff jest zły, jeszcze zanim go otworzysz. Brak takiej linii mówi ci, że agent nie patrzył.
Recenzja egzekwuje ten sam próg, w ustalonej kolejności. Nasza umiejętność recenzencka ma dwa przebiegi ze stałymi tagami, i to tagi sprawiają, że ustalenia są porównywalne między recenzentami i między agentami.
Najpierw struktura. Czy to odbiega od najbliższego wzorca, duplikuje istniejącą funkcję pomocniczą, przechodzi przez warstwę, leży w złym miejscu, sięga do wnętrzności innego modułu, używa niespójnych nazw, przejmuje własność czegoś, co powinno być wstrzyknięte, albo łamie kanoniczny łańcuch wywołań? Potem cięcia. Co można usunąć, zastąpić biblioteką standardową, zastąpić natywną funkcją, wyrzucić jako spekulatywne albo zmniejszyć?
Kolejność jest regułą, nie preferencją. Spójność bije cięcia. Cięcie, po którym kod odbiega od swoich sąsiadów, nie jest wygraną, bo czytelnik musi teraz nauczyć się dwóch kształtów, żeby zaoszczędzić osiem linii. Uruchom najpierw strukturę, a przebieg cięć dotknie tylko kodu, który już pasuje.
Co się stało w repozytorium, łącznie z tą częścią, która nie zadziałała
Od 1 sierpnia nasze główne repozytorium przyjęło 1 333 commity, z których większość napisali agenci pod recenzją. 386 zgłoszeń i pull requestów z tego okresu ma komentarz z planem wskazujący wzorzec do odzwierciedlenia — w sumie 491 takich komentarzy. Kanoniczny łańcuch to domyślny kształt nowego endpointu, a dwie starsze trasy pomijające kontroler są wypisane w dokumencie jako rozbieżność wycofywana, a nie drugi styl.
35 komentarzy ponytail: to mniejsza liczba, niż się spodziewałem, i myślę, że tak jest dobrze. Większość uproszczeń nie zasługuje na komentarz. Zasługują te, które mają pułap.
A teraz część, o której wolałbym nie donosić. Wysłałem grain i ponytail jako umiejętność Claude Code: opis w indeksie umiejętności, treść ładowaną na żądanie i linię w CLAUDE.md mówiącą, żeby wywołać ją przed pisaniem kodu. W ciągu pięciu tygodni transkrypcji agent wywołał ją po nazwie zero razy. Zamykająca linia mirrored: pojawiła się w siedmiu odpowiedziach.
Reguły mimo to zadziałały. Zadziałały, bo ten sam tekst był też w trzech miejscach, które agent czyta bez decydowania się na to: w CLAUDE.md projektu, który jest w kontekście od pierwszej tury; w definicjach agentów-workerów, które mówią „odzwierciedlaj kształt, umiejscowienie i nazewnictwo najbliższego istniejącego przykładu” oraz „sięgaj po najleniwsze rozwiązanie, które rzeczywiście działa” jako reguły, a nie jako umiejętność do rozważenia; i w liście kontrolnej recenzji, której tagi oblewają pull request, który je ignoruje. Treść umiejętności stała się kanonicznym odniesieniem, na które te trzy miejsca wskazują. Jako wyzwalacz nie zrobiła nic.
Więc lekcja, którą przekazałbym każdemu, kto pakuje konwencje dla agenta, jest taka. Umiejętność to dobre miejsce na długą wersję reguły. To złe miejsce na samą regułę, bo agent musi zdecydować, że ją załaduje, a przeważnie tego nie zrobi. Umieść wersję jednoakapitową w pliku, który zawsze jest w kontekście, umieść wersję rozkazującą w definicji agenta, który pisze kod, i umieść sprawdzenie w recenzji, która blokuje merge. Potem skieruj te trzy miejsca do umiejętności po szczegóły.
Publiczna wersja pod github.com/LivinTribunal/grain-skill jest ułożona w ten sposób: krótki SKILL.md z regułami, które agent może trzymać w głowie, pełny dokument z drabiną i antywzorcami, tagi recenzji oraz README, które mówi, który akapit trafia do twojego CLAUDE.md, a który do definicji twojego implementującego. Zastąp przykład własnym kanonicznym wzorcem i gotowe.

