Prompt engineering dla programistów: debugowanie, refaktor i testy z AI

0
53
Rate this post

Nawigacja:

Jak programista powinien myśleć o AI: partner, nie magik

AI jako szybki współprogramista, który widzi wzorce w tekście

Dla programisty AI to przede wszystkim bardzo szybki czytnik i analityk tekstu. Model językowy nie uruchamia kodu, nie ma dostępu do twojego środowiska ani do logów w czasie rzeczywistym. Operuje jedynie na tym, co mu podasz w promptach: fragmencie kodu, komunikacie błędu, opisie kontekstu. To ograniczenie, ale jednocześnie ogromna zaleta – potrafi błyskawicznie zauważyć wzorce, niespójności i powtórzenia, które człowiek przeoczy po kilku godzinach patrzenia w ten sam plik.

Stąd kluczowe jest ustawienie oczekiwań: AI nie „wie”, czy kod działa. Nie odpali testów, nie sprawdzi timeoutów w produkcji, nie podepnie się do bazy. Za to jest w stanie:

  • streścić skomplikowaną funkcję w kilku zdaniach,
  • przepisać fragment w innym stylu (np. async/await zamiast callbacków),
  • pokazać inne podejście do rozwiązania tego samego problemu,
  • zaproponować hipotezy, skąd bierze się dany błąd.

Największa siła AI w rękach programisty to zdolność „odbicia myśli”: opisujesz problem, pokazujesz kod, dostajesz alternatywny sposób spojrzenia, który pobudza twoją własną analizę. Jeśli traktujesz model jako kompilator czy środowisko wykonawcze, pojawia się frustracja. Jeśli widzisz w nim partnera od tekstu, zyskujesz realne przyspieszenie.

„Napisz za mnie” kontra „pomóż mi dojść do rozwiązania”

Różnica między proszeniem AI o gotowca („napisz za mnie moduł X”) a prowadzeniem dialogu problemowego jest ogromna. W pierwszym wariancie otrzymujesz kod oderwany od reszty projektu, często niepasujący do twojej architektury, namingów, założeń biznesowych. Taka generatka bywa bardziej obciążeniem niż pomocą – trzeba ją przeglądnąć, dopasować, przetestować, naprawić.

Druga strategia: używasz AI, żeby szybciej dojść do rozwiązania, ale sam trzymasz kierownicę. Zamiast prośby „napisz serwis do obsługi płatności”, tworzysz ciąg promptów:

  • „Streść logikę tego istniejącego modułu płatności”
  • „Wskaż miejsca o największej złożoności i ryzyku błędu”
  • „Zaproponuj, jak podzielić ten moduł na mniejsze klasy z jasną odpowiedzialnością”
  • „Na podstawie tego planu zaproponuj implementację tej konkretnej klasy, bez dotykania reszty”

Z takim podejściem używasz AI do analizy, projektowania i refaktoru, ale decyzje zostają po twojej stronie. Łatwiej też weryfikujesz otrzymane propozycje – bo powstały na bazie kroków, które zrozumiałeś i świadomie zleciłeś.

Ograniczenia modeli: halucynacje i brak kontekstu projektu

Modele generatywne potrafią „halucynować”: wymyślają klasy, metody, parametry, które wyglądają wiarygodnie, ale nie istnieją. Dzieje się tak zwłaszcza wtedy, gdy prosisz o coś zbyt ogólnie („napisz serwis do integracji ze Stripe w Javie”) albo nie podajesz dość kontekstu (framework, wersja SDK, istniejące interfejsy). Model wtedy zgaduje na podstawie ogólnej wiedzy o danej technologii.

Drugi problem to brak kontekstu projektu. Model nie zna twoich decyzji architektonicznych, nie widzi historii z Gita, nie widzi dokumentacji wewnętrznej. Jeśli chcesz, by zachował założenia, musisz je dosłownie włożyć w prompt: wzorce nazewnictwa, warstwy systemu, konwencje wyjątków, preferowane biblioteki.

Bez tego AI będzie proponowało „książkowe” lub przypadkowe rozwiązania, często niepasujące do reszty systemu. Dlatego przy pracy nad kodem istotniejsze niż przy innych zastosowaniach są:

  • precyzyjny kontekst (fragmenty kodu, opis warstw, ograniczenia),
  • jasno określony zakres („dotknij tylko tej funkcji/klasy”),
  • żądanie uzasadnień („wyjaśnij, dlaczego proponujesz takie zmiany”).

Kiedy AI przyspiesza, a kiedy tylko dodaje chaos

AI świetnie sprawdza się w zadaniach, gdzie trzeba szybko „mielić” tekstowy materiał: duży plik z kodem, długi stack trace, mnogość wariantów testów. Sprawdza się zwłaszcza w:

  • debugowaniu złożonych błędów, gdy pokazujesz logi, stack trace i kod,
  • lokalnym refaktorze (jedna klasa, funkcja, moduł),
  • generowaniu testów jednostkowych i integracyjnych na bazie istniejącej logiki,
  • analizie przepływu danych między kilkoma punktami kodu.

Znacznie gorzej radzi sobie w sytuacjach typu „napisz od zera cały moduł”, gdy nie masz spisanej specyfikacji. Wtedy każda odpowiedź modelu jest tylko interpretacją niejasnego opisu. Efekt: kod na piasku, bez fundamentu. Lepsze podejście – wspólnie z AI doprecyzować specyfikację (przykłady wejść/wyjść, edge case’y, kontrakty), a dopiero później przejść do implementacji krok po kroku.

Podstawy prompt engineering w kontekście kodu

Struktura dobrego promptu dla programisty

Dla pracy z kodem sprawdza się powtarzalna struktura promptu. Zamiast „wrzucam kod i liczę na cud” warto stosować ramę:

  • Rola: jakiego typu pomocnika potrzebujesz („doświadczony backend developer w .NET”, „mentor od testów w JUnit”).
  • Kontekst: krótki opis projektu, fragment architektury, informacje o frameworkach i wersjach.
  • Cel: debug, refaktoryzacja, napisanie testów, opis przepływu, porównanie fragmentów.
  • Ograniczenia: czego nie wolno modelowi zmienić (interfejsy publiczne, nazwy endpointów, kontrakty API).
  • Format odpowiedzi: kod + komentarze, tylko pseudokod, lista kroków, diff, wypunktowane propozycje.

Taki schemat można zamienić w osobiste szablony, które powielasz i modyfikujesz. Im bardziej konsekwentnie stosujesz strukturę, tym łatwiej wykrywasz, który element promptu zawodzi (np. brak kontekstu frameworka, za ogólny cel, brak ograniczeń).

Jak przekazywać fragmenty kodu przy ograniczonym context window

Modele mają ograniczone context window, czyli ilość tekstu, który mogą jednocześnie uwzględnić. Przy większych projektach trzeba selekcjonować treść, a nie wrzucać na oślep całe repozytorium. Dobrze działają następujące strategie:

  • Wycinanie fragmentu problematycznego – tylko funkcja/klasa bez reszty, jeśli błąd jest lokalny.
  • Streszczanie modułu – prosisz AI, żeby najpierw streściło większy plik, a potem pracujesz na streszczeniu plus krótszych fragmentach kodu.
  • Selekcja kluczowych plików – przekazujesz tylko pliki związane z konkretną funkcjonalnością (np. kontroler, serwis, repozytorium).
  • Iteracja po częściach – omawiasz fragmenty sekwencyjnie, prosząc model, żeby „zapamiętał” istotne założenia w konwersacji.

Praktycznie: gdy masz np. 800‑linijkowy plik, rzadko trzeba wklejać go w całości. Często wystarczy: nagłówki klas, sygnatury metod publicznych, newralgiczny fragment implementacji i krótki opis reszty. Resztę można streścić razem z AI, żeby odciążyć context window.

Konkrety zamiast „napraw” – formułowanie celów

Polecenie „napraw ten kod” jest dla modelu za słabe. Po pierwsze, nie ma kompilatora. Po drugie, nie wie, czy „naprawa” ma dotyczyć bugów, wydajności, stylistyki czy czytelności. Lepsze są polecenia operujące na konkretnym błędzie lub kryterium technicznym:

  • „Znajdź przyczynę błędu NullPointerException w tym stack trace i wskaż dwóch potencjalnych winowajców w kodzie.”
  • „Zaproponuj dwie alternatywne wersje tej funkcji: jedną pod czytelność, drugą pod minimalną liczbę alokacji.”
  • „Przejrzyj kod pod kątem ryzyka deadlocków w tej sekwencji wywołań.”
  • „Wskaż miejsca, w których wyjątkowość domenowa jest łapana zbyt ogólnie (catch Exception).”

Taki rodzaj pytań kieruje uwagę modelu tam, gdzie jej potrzebujesz. Jednocześnie dostajesz hipotezy, a nie „magicznie naprawiony kod”. Potem możesz poprosić o konkretną poprawkę, jeśli hipoteza wydaje się sensowna.

Ustalanie stylu odpowiedzi: pseudokod, gotowy kod, komentarze

W zależności od etapu pracy potrzebujesz innego typu odpowiedzi. Warto jawnie o to prosić:

  • Na etapie analizy: „Odpowiedz tylko w formie listy kroków, bez generowania kodu”.
  • Na etapie projektowania: „Zaproponuj pseudokod lub prosty szkic API bez implementacji szczegółowej”.
  • Na etapie implementacji: „Wygeneruj gotowy kod w C#, tylko zawartość metody, bez klasy i usingów”.
  • Na etapie review: „Oceń ten kod pod kątem czytelności i utrzymania, wypunktuj uwagi, ale niczego nie zmieniaj”.

Ścisłe kontrolowanie formatu odpowiedzi (np. „zwróć tylko blok kodu między trzema backtickami, bez komentarza tekstowego”) sprzyja automatyzacji i integracji z narzędziami (np. gdy kopiujesz wygenerowany kod prosto do IDE). Dobrze jest mieć kilka ulubionych formuł typu „Odpowiadaj tylko…”, które dodajesz automatycznie do promptów.

Debugowanie z AI: od błędu do hipotezy, nie do gotowca

Jak przygotować prompt debugowy: błąd, kod, co już sprawdzono

Efektywny prompt do debugowania ma podobną strukturę jak dobrze zadane pytanie seniorowi w zespole. Im więcej zbędnych informacji usuniesz, a im lepiej opiszesz sedno, tym szybciej trafisz do przyczyny. W praktyce przydaje się prosty szkielet:

  • Opis problemu własnymi słowami – co się dzieje, w jakich okolicznościach, jaki jest oczekiwany efekt.
  • Stack trace lub komunikat błędu – skrócony, ale pełny w kluczowym fragmencie.
  • Fragment kodu, który najprawdopodobniej jest powiązany z błędem.
  • Lista tego, co już sprawdziłeś – by model nie proponował oczywistości.
  • Prośba o strukturę odpowiedzi – opis działania, hipotezy, propozycje poprawek.

Przykład celu w debugowym promptcie: „Potrzebuję hipotez przyczyny i propozycji, jakie eksperymenty lokalnie wykonać, żeby potwierdzić/wykluczyć daną hipotezę”. Teraz AI staje się generatorem scenariuszy debugowania, a nie generatorem przypadkowych zmian w kodzie.

Od opisu kodu do lokalizacji błędu

Skuteczna technika to zmuszenie modelu, by najpierw zrozumiał, co kod robi, zanim cokolwiek w nim zmieni. Można użyć prostego schematu trzystopniowego:

  1. „Najpierw opisz krok po kroku, co robi ten kod.”
  2. „Następnie wskaż 2–3 miejsca szczególnie podatne na błąd z uwzględnieniem stack trace.”
  3. „Na końcu zaproponuj konkretną poprawkę dla najbardziej prawdopodobnej przyczyny błędu.”

Dzięki temu widzisz sposób rozumowania modelu. Możesz w każdej chwili przerwać i skorygować interpretację („tu się mylisz, ta funkcja nie może zwracać null, bo…”) zamiast ślepo stosować wygenerowaną łatkę. To przypomina rozmowę mentoringową: najpierw upewnienie się, że obie strony rozumieją tę samą logikę, potem dopiero ingerencja.

Przykład: debugowanie null pointera krok po kroku

Prosty scenariusz: aplikacja w Javie rzuca NullPointerException przy próbie zapisu zamówienia. Masz stack trace i fragment serwisu. Zamiast „napraw ten błąd” możesz przeprowadzić model przez sekwencję kroków:

  1. Wklej stack trace i część kodu z serwisu, poproś: „Wskaż miejsce w kodzie, które odpowiada za ten wyjątek. Nie zmieniaj jeszcze kodu, tylko zlokalizuj potencjalną przyczynę”.
  2. Po otrzymaniu odpowiedzi: „Załóżmy, że obiekt customer nigdy nie powinien być null na tym etapie. Zaproponuj 3 hipotezy, skąd może się pojawić null i jakie logi dodać, by to potwierdzić”.
  3. Szablon rozmowy z AI przy złożonym błędzie

    Przy trudniejszych problemach (race condition, memory leak, dziwny błąd w produkcji) pojedynczy prompt zwykle nie wystarcza. Sprawdza się krótkie „drzewko rozmowy”, które kontrolujesz krok po kroku. Przykładowy schemat:

  1. Diagnoza obserwacji
    „Opiszę objawy błędu i środowisko wykonania. Twoje zadanie: wypisz 5–7 potencjalnych klas problemu (konfiguracja, wątkowość, I/O, dane wejściowe, cache, GC, timeouts, inne). Nie proponuj jeszcze zmian w kodzie.”
  2. Wejście w konkretną klasę problemu
    „Skupmy się na podejrzeniu X (np. race condition). Wymień typowe wzorce błędów tego typu, które mogą pasować do mojego przypadku, i wskaż, jakie dodatkowe dane/logi będą potrzebne, żeby je potwierdzić.”
  3. Praca na kodzie i logach
    „Na podstawie tego kodu i fragmentu logów wskaż najbardziej prawdopodobny wzorzec błędu z poprzedniej listy. Podaj dwie hipotezy i test/eksperyment dla każdej.”
  4. Projekt poprawki
    „Dla hipotezy A zaproponuj zmianę w kodzie minimalną pod względem zakresu, która może usunąć problem. Pokaż diff i wyjaśnij skutki uboczne.”

Taki przebieg rozmowy przypomina sesję z doświadczonym inżynierem: najpierw zawężenie klasy problemu, potem hipotezy, dopiero na końcu kod.

Kolorowy kod na ciemnym ekranie symbolizujący debugowanie i testy z AI
Źródło: Pexels | Autor: Markus Spiske

Wzorce promptów do debugowania (szablony gotowe do użycia)

Minimalny szablon: „Szybka analiza błędu”

Gdy trzeba tylko szybkiej analizy, bez głębokiego śledztwa, sprawdza się krótki szablon, który łatwo wklejać w IDE lub narzędzie typu chat w edytorze:

Rola: Jesteś doświadczonym programistą <stack/technologia>.

Kontekst:
- Środowisko: <dev / stage / prod>
- Technologia: <np. Spring Boot 3, Java 21>
- Objaw błędu: <krótki opis>

Dane:
- Komunikat/stack trace:
<wklej>

- Fragment kodu powiązany z błędem:
<wklej>

Twoje zadanie:
1. Zlokalizuj najbardziej prawdopodobne miejsce powstania błędu.
2. Wypisz 3 potencjalne przyczyny, z krótkim uzasadnieniem.
3. Zaproponuj 3 proste eksperymenty / modyfikacje logowania,
   które pomogą odróżnić, która przyczyna jest prawdopodobna.

Odpowiadaj w formie wypunktowanej, bez generowania pełnego kodu.

Szablon: „Proszę o hipotezy, nie gotowe łatki”

Gdy masz wrażenie, że model „pcha się” w autokorektę kodu, można go mocno przyhamować poleceniem nastawionym na hipotezy:

Rola: Senior developer w <technologia>, skupiony na analizie, nie na przepisywaniu kodu.

Cel:
Potrzebuję hipotez przyczyny błędu i planu dalszych kroków,
nie gotowego patcha.

Dane wejściowe:
- Opis: <co się dzieje, co miało się dziać>
- Stack trace (skrócony do sedna):
<wklej>
- Fragmenty kodu:
<wklej>

Ograniczenia:
- Nie proponuj zmian w kodzie na tym etapie.
- Skup się na analizie możliwych ścieżek wykonania.

Format odpowiedzi:
1. Krótki opis tego, jak interpretujesz problem.
2. 3–5 hipotez przyczyny, każda:
   - opis,
   - na jakich założeniach się opiera,
   - jakie dane potrzebne do jej potwierdzenia.
3. Lista konkretnych kroków, które mam wykonać lokalnie
   (logi, breakpointy, testy) do weryfikacji hipotez.

Szablon: „Debugowanie na poziomie architektury”

Przy problemach rozproszonych (kilka usług, kolejki, cache) pojedynczy stack trace często nie wystarcza. Tu pomaga ustrukturyzowany opis przepływu:

Rola: Architekt systemów rozproszonych <np. w ekosystemie JVM/Kubernetes>.

Kontekst:
- Architektura: <krótki opis serwisów, kolejki, bazy>
- Problem: <objawy – np. sporadyczne timeouty, utrata wiadomości>

Dane:
- Schemat przepływu (tekstowo):
  <serwis A> --> <kolejka> --> <serwis B> --> <baza>
- Kluczowe logi (wycinki z timestampami):
  <wklej>

Cel:
1. Zidentyfikuj 3 najbardziej ryzykowne miejsca w przepływie.
2. Zaproponuj, jakie dodatkowe logi/metryki dodać w tych miejscach.
3. Zaproponuj 2–3 scenariusze testów obciążeniowych / chaos engineering,
   które pomogą odtworzyć problem.

Nie generuj kodu, skup się na planie.

Szablon: „Porównanie dwóch hipotez naprawy”

Czasem masz już dwie różne koncepcje fixa (np. swoją i AI) i chcesz, by model je porównał. Można to ubrać w prosty wzorzec:

Rola: Doświadczony reviewer w <technologia>.

Kontekst:
Mamy błąd <krótki opis>. Poniżej są dwie propozycje naprawy.

Kod oryginalny:
<wklej>

Propozycja A (moja):
<wklej>

Propozycja B (np. z poprzedniej rozmowy z AI):
<wklej>

Twoje zadanie:
1. Porównaj A i B w kontekście:
   - bezpieczeństwa (np. concurrency, walidacja wejścia),
   - wydajności,
   - czytelności,
   - zgodności z zasadami <np. DDD, Clean Architecture>.
2. Wskaż konkretne ryzyka w każdej wersji.
3. Zaproponuj ewentualne połączenie zalet A i B w jedną wersję,
   ale pokaż tylko pseudokod, nie pełny kod.

Refaktoryzacja z AI: czytelność, podział odpowiedzialności, usuwanie duplikacji

Jak poprosić o refaktoryzację, a nie o „przepisanie na nowo”

Bez wskazania zakresu model ma tendencję do totalnego przemeblowania kodu. W wielu projektach jest to nieakceptowalne (kontrakty API, ograniczenia wersji, dogadane style). Dobrze działa jasne określenie granic:

  • Zakres zmian – „Możesz zmieniać tylko tę klasę”, „Nie ruszaj publicznych interfejsów ani DTO”.
  • Cel refaktoru – „Zwiększenie czytelności”, „Zmniejszenie duplikacji”, „Uproszczenie logiki warunków”.
  • Poziom agresywności – „Drobny refaktor”, „Możesz rozbić na kilka klas, ale bez zmiany warstwy API”.

Prosty przykład promptu:

Rola: Senior developer w <technologia>, skupiony na czytelności i utrzymaniu.

Cel:
Drobna refaktoryzacja dla większej czytelności.
Nie zmieniaj zachowania biznesowego.

Dane:
<wklej klasę / funkcję>

Ograniczenia:
- Nie zmieniaj sygnatur metod publicznych.
- Nie dodawaj nowych zależności zewnętrznych.
- Nie zmieniaj logowania (tylko możesz dodać, nie usuwać).

Zadania:
1. Wypisz problemy z czytelnością i nadmierną złożonością.
2. Zaproponuj konkretne kroki refaktoryzacji (bez kodu).
3. Następnie pokaż zrefaktoryzowaną wersję kodu.

Odpowiedź:
Najpierw lista problemów, potem lista kroków, na końcu blok kodu.

Refaktoryzacja krokowa: od diagnozy do finalnego kodu

Zamiast brać „finalny kod” na wiarę, lepiej prowadzić AI krok po kroku. Przykładowy scenariusz dla sporej klasy serwisu:

  1. Diagnoza: „Przejrzyj klasę i wypisz maksymalnie 10 punktów z największymi problemami (duplikacje, za długa metoda, zbyt wiele odpowiedzialności). Nie zmieniaj kodu.”
  2. Plan: „Na podstawie listy problemów zaproponuj plan refaktoringu podzielony na małe kroki, które można wdrażać osobno (krok 1, krok 2…).”
  3. Implementacja pojedynczego kroku: „Zrealizuj tylko krok 1 w kodzie i pokaż diff względem oryginału.”
  4. Review: „Wyjaśnij, jak krok 1 wpływa na zachowanie i jakie testy uruchomić, by upewnić się, że nic nie zostało złamane.”

Tak prowadzony refaktor można spokojnie rozłożyć na kilka commitów i przeplatać z normalną pracą, a AI ma jasno zdefiniowane granice każdego kroku.

Usuwanie duplikacji z pomocą AI

Modele dobrze wykrywają powtarzające się wzorce, nawet gdy nie są identyczne tekstowo. Można to wykorzystać, jeśli zadasz odpowiednio precyzyjne pytanie. Jeden z użytecznych wzorców:

Rola: Pomocnik do refaktoryzacji, skupiony na powtarzalnych fragmentach logiki.

Dane:
Poniżej masz kilka metod/klas z różnych plików:
<wklej kilka fragmentów, z krótkim opisem, skąd pochodzą>

Zadanie:
1. Wskaż grupy fragmentów, które realizują tę samą lub bardzo podobną logikę.
2. Dla każdej grupy zaproponuj wspólną abstrakcję (np. wspólną metodę,
   klasę pomocniczą, strategię) – opisowo, bez kodu.
3. Dla 1–2 najciekawszych grup pokaż proponowaną implementację wspólnej
   abstrakcji w kodzie.

Ograniczenia:
Nie zmieniaj publicznych API kontrolerów i interfejsów repozytoriów.

W praktyce dobrze jest wcześniej samodzielnie wybrać „podejrzane” fragmenty zamiast wrzucać cały moduł – wtedy odpowiedź jest konkretniejsza i mniej hałaśliwa.

Podział odpowiedzialności w zbyt dużej klasie

Klasy typu „God object” można rozbijać z AI stopniowo. Dobry wzorzec polega na tym, że model najpierw tylko grupuje metody logicznie:

Rola: Mentor clean code.

Kontekst:
Poniższa klasa jest zbyt duża.
Chcę ją rozbić na kilka mniejszych, bardziej spójnych.

Dane:
<wklej klasę>

Zadania:
1. Pogrupuj metody w logiczne pakiety odpowiedzialności (np. walidacja,
   dostęp do bazy, logika biznesowa X).
2. Zaproponuj docelowy podział na klasy/moduły (bez kodu):
   - nazwa klasy,
   - odpowiedzialność w jednym zdaniu,
   - które metody tam przenieść.
3. Dla jednego z pakietów odpowiedzialności wygeneruj kod nowej klasy
   z tymi metodami, pokazując jak minimalnie zmodyfikować oryginalną klasę,
   żeby z niej korzystała.

Takie podejście ułatwia dyskusję w zespole – najpierw recenzujecie sam podział odpowiedzialności, dopiero potem akceptujecie wygenerowany kod.

Wzorce promptów do refaktoryzacji oraz pracy na większym kodzie

Praca na „mapie modułu” zamiast na pojedynczym pliku

Gdy trzeba ogarnąć większy fragment systemu, pojedynczy plik to za mało. Zamiast wrzucać od razu cały katalog, lepiej najpierw zbudować z AI coś w rodzaju mapy modułu:

Rola: System designer w <technologia>.

Kontekst:
Chcę zrozumieć strukturę modułu <nazwa> i znaleźć miejsca,
które najbardziej skorzystają na refaktoryzacji.

Dane:
Poniżej nagłówki klas i sygnatury metod publicznych z modułu:
<wklej>

Zadania:
1. Zbuduj opis struktury modułu:
   - główne odpowiedzialności,
   - powiązania między klasami.
2. Wypisz 5–10 potencjalnych „hotspotów” do refaktoryzacji
   (za duża klasa, zbyt wiele zależności, chaotyczne nazewnictwo…).
3. Zaproponuj kolejność, w jakiej warto się nimi zająć,
   uzasadniając wybór (np. wpływ na czytelność, ryzyko regresji).

Dalej można wejść z AI w każdy hotspot osobno, korzystając z wcześniejszych szablonów refaktoryzacji krokowej.

Iteracyjna praca na częściach pliku

Przy naprawdę dużych plikach (np. złożone kontrolery, duże agregaty DDD) dobrym nawykiem jest „sesja iteracyjna”. Przykładowy przebieg:

  1. „Pokażę ci tylko nagłówek klasy i listę metod publicznych. Na tej podstawie spróbuj zgadnąć, czy klasa ma zbyt szeroką odpowiedzialność. Wypisz swoje podejrzenia.”
  2. „Teraz wklejam implementację metody processOrder. Oceń tylko tę metodę: czy jest zbyt długa, co można z niej wydzielić, jakie są zależności.”
  3. „Na podstawie analizy processOrder zaproponuj wyłącznie nazwy potencjalnych metod/klas pomocniczych (bez implementacji).”
  4. „Wygeneruj implementację tylko jednej z tych metod pomocniczych, zakładając minimalną zmianę w oryginalnej metodzie.”

Taka rozmowa wymusza na modelu skupienie się na jednym problemie naraz i ogranicza ryzyko, że „przemieli” wszystko na nowo.

Refaktoryzacja pod testowalność

Wiele legacy-kodu trudno przetestować, bo ma twarde zależności (statyczne wywołania, new w środku, singletony). AI może pomóc zaprojektować „most” do świata bardziej testowalnego:

Most od legacy do kodu testowalnego

Zamiast przepisywać legacy od zera, lepiej „otoczyć” go warstwą, którą da się sensownie testować. AI dobrze sprawdza się jako konsultant przy projektowaniu takiego mostu. Chodzi o dwa kroki: po pierwsze – zidentyfikować, co blokuje testy, po drugie – zaproponować minimalne zmiany, które da się wprowadzać partiami.

Rola: Architekt nastawiony na testowalność i minimalne zmiany w legacy.

Kontekst:
Mam klasę, którą trudno testować jednostkowo.
Chcę zaprojektować małe kroki, które poprawią testowalność,
ale bez przepisywania całości.

Dane:
<wklej klasę / fragmenty>

Zadania:
1. Wypisz konkretne powody, dla których ta klasa jest trudna do testowania
   (twarde zależności, statyczne wywołania, logika w konstruktorze, itp.).
2. Zaproponuj 3–5 małych, niezależnych kroków refaktoryzacji
   (np. wprowadzenie interfejsu, wyciągnięcie fabryki), opisowo – bez kodu.
3. Wygeneruj kod tylko dla pierwszego kroku, pokazując diff względem oryginału.
4. Wypisz przykładowe testy jednostkowe, które staną się możliwe po kroku 1.

Taki schemat zmusza model do myślenia etapami i zwykle kończy się sensownym planem migracji, zamiast fantazji o „nowej, idealnej architekturze”. W praktyce świetnie działa np. w serwisach, które robią wszystko: od parsowania HTTP po SQL.

Wzorzec „wyciągnij zależność”

Bardzo typowy ból: klasa sama sobie tworzy zależności przez new, albo używa statycznych helperów. Da się to oswajać małymi krokami, prosząc AI wprost o wprowadzenie punktu wstrzykiwania:

Rola: Senior dev, skupiony na wprowadzeniu wstrzykiwania zależności.

Kontekst:
Poniższa klasa sama tworzy instancje kilku zależności, co utrudnia testy.

Dane:
<wklej klasę>

Zadania:
1. Wskaż wszystkie miejsca, gdzie klasa tworzy bezpośrednio zależności
   ("new" w środku, statyczne metody "X.getInstance", itp.).
2. Zaproponuj, które zależności warto wstrzyknąć przez konstruktor,
   a które można tymczasowo zostawić.
3. Zaproponuj nową sygnaturę konstruktora i pola prywatne (bez implementacji).
4. Następnie pokaż kompletną zrefaktoryzowaną klasę,
   ograniczając zmiany tylko do:
   - dodania pól,
   - zmiany konstruktora,
   - podmiany "new" na użycie wstrzykniętych zależności.

Dalej można poprosić o listę miejsc w kodzie, które wywołują ten konstruktor, oraz o sugestię, jak je zaktualizować. To często oszczędza czas przy większych przepięciach pod DI.

Testy jako pierwszy obywatel refaktoru

Przy refaktoryzacji opłaca się traktować testy jako integralny element tasku, nie „kiedyś tam później”. Dobry wzorzec to poproszenie AI, by każdemu krokowi refaktoru towarzyszyły testy, które go „chronią”:

Rola: Mentor TDD.

Kontekst:
Chcę zrefaktoryzować tę klasę, ale mam mało testów.

Dane:
Kod produkcyjny:
<wklej>

Dostępne testy (jeśli brak, napisz "brak"):
<wklej>

Zadania:
1. Zaproponuj minimalny zestaw testów, które powinienem dopisać
   PRZED refaktoryzacją, żeby złapać najbardziej ryzykowne regresje.
2. Napisz szkielety tych testów (bez pełnych danych wejściowych,
   ale z jasnym opisem scenariusza).
3. Na końcu zaproponuj plan refaktoryzacji w krokach, gdzie każdy krok
   ma przypisane testy, które powinny pozostać zielone.

Dzięki temu rozmowa z AI zaczyna się od testów, a nie od „pięknego” kodu. Dla wielu legacy-modułów to jedyny sensowny sposób, żeby w ogóle zacząć sprzątać.

Generowanie testów z AI: jednostkowe, integracyjne, scenariusze brzegowe

Jakiej pomocy przy testach realnie oczekiwać

Modele świetnie radzą sobie z trzema rzeczami: wymyślaniem scenariuszy, projektowaniem struktury testów i produkcją szablonów. Znacznie słabiej – z dopasowaniem się do lokalnych konwencji i frameworkowych niuansów. Lepiej więc prosić o:

  • listę przypadków testowych z krótkimi opisami,
  • szablony testów w preferowanym frameworku,
  • propozycje danych brzegowych i nietypowych,
  • pomoc w dopisaniu testów regresyjnych pod konkretny błąd.

Reszta – nazwy helperów, mocków, konkretne asercje – zwykle i tak wymaga ręcznej korekty, więc lepiej traktować odpowiedź jak scaffolding niż gotowy kod do merge.

Wzorzec: „opisz, a potem napisz” dla testów jednostkowych

Zamiast prosić od razu o kod testów, dobrze jest wymusić najpierw opis scenariuszy. Daje to możliwość szybkiego review, zanim powstanie „mięsko”:

Rola: Specjalista od testów jednostkowych w <technologia> (<framework testowy>).

Kontekst:
Chcę dopisać testy jednostkowe do poniższej klasy.

Dane:
<wklej klasę / metody>

Zadania:
1. Wypisz listę scenariuszy testowych w formacie:
   - nazwa przypadku,
   - cel,
   - przykładowe dane wejściowe,
   - oczekiwany efekt (opisowo).
2. Podziel scenariusze na:
   - pozytywne (happy path),
   - błędne wejścia,
   - scenariusze brzegowe (granice zakresu, null, pusty string, itp.).
3. Po mojej akceptacji scenariuszy (w następnej wiadomości)
   wygeneruj kod testów w <framework>.

Po akceptacji scenariuszy można zawęzić zakres, np. „wygeneruj kod tylko dla happy pathów” albo „zacznij od scenariuszy brzegowych dla metody calculatePrice”.

Generowanie testów regresyjnych pod konkretny błąd

Przy realnych incydentach często brakuje czasu na wymyślanie pełnego pokrycia. AI może pomóc dorywczo – najpierw tymczasowy test, który odtwarza bug, potem stopniowe rozszerzanie.

Rola: Inżynier jakości.

Kontekst:
Mieliśmy błąd produkcyjny opisany poniżej.
Chcę test regresyjny, który go łapie.

Opis błędu:
<wklej z ticketu / logów>

Fragment kodu odpowiedzialny za błąd:
<wklej>

Zadania:
1. Zaproponuj, jak odtworzyć błąd w teście jednostkowym
   (dane wejściowe, konfiguracja, mockowane zależności).
2. Napisz kod testu regresyjnego w <framework>,
   z krótkim komentarzem w teście, co dokładnie sprawdza.
3. Wypisz 2–3 dodatkowe scenariusze, które są "obok" tego błędu
   (np. inne wartości graniczne), ale nie generuj jeszcze dla nich kodu.

W praktyce przyspiesza to zamknięcie incydentu: masz od razu test, który potwierdza naprawę, nawet jeśli reszta pokrycia jest jeszcze uboga.

Testy integracyjne z AI: skupienie na kontraktach

Przy testach integracyjnych liczy się kontrakt między komponentami: dane wejściowe/wyjściowe, formaty, ewentualne side-effecty. Model łatwo „odpływa” w stronę ogólników, więc dobrze ustalić konkret:

Rola: Inżynier testów integracyjnych.

Kontekst:
Chcę napisać testy integracyjne dla endpointu / procesora
korzystającego z bazy/kolejki.

Dane:
- Specyfikacja endpointu / komunikatu:
  <wklej OpenAPI / opis payloadu>
- Fragment kodu kontrolera / handlera:
  <wklej>
- Informacja o środowisku testowym (np. in-memory DB, testcontainers):
  <opisz>

Zadania:
1. Zidentyfikuj główne ścieżki przepływu (happy path, błąd walidacji,
   błąd warstwy zewnętrznej, np. DB/HTTP).
2. Dla każdej ścieżki zaproponuj strukturę testu integracyjnego:
   - setup (dane w bazie, konfiguracja),
   - wywołanie (request / komunikat),
   - asercje (status, body, stan bazy / kolejki).
3. Dla jednego wybranego scenariusza wygeneruj kompletny kod testu
   w <framework>.

Taki schemat dobrze się sprawdza np. przy endpointach z wieloetapową walidacją albo asynchronicznym przetwarzaniem. AI często podpowie scenariusze, o których zespół nie pomyślał (np. „duplikat idempotentnego requestu”).

Testy brzegowe i „złe dane” z pomocą AI

Programiści mają naturalną tendencję do myślenia w kategoriach happy path. Model, który patrzy na kod „z boku”, potrafi dość kreatywnie wymyślać złośliwe kombinacje wejść.

Rola: "Złośliwy" tester, który szuka dziur w walidacji.

Kontekst:
Potrzebuję listy złośliwych i brzegowych danych testowych
dla poniższej funkcji / endpointu.

Dane:
- Sygnatura i opis funkcji / endpointu:
  <wklej>
- Aktualna implementacja walidacji (jeśli jest):
  <wklej>

Zadania:
1. Wypisz listę danych testowych w kategoriach:
   - wartości minimalne / maksymalne,
   - wartości tuż obok granic,
   - wartości nietypowe (np. bardzo długie stringi, unicode, puste),
   - wartości ewidentnie niepoprawne.
2. Dla każdej pozycji napisz krótko:
   - co chcemy sprawdzić,
   - jaki powinien być oczekiwany rezultat (opisowo).
3. Na końcu zaproponuj, jak te dane można wykorzystać
   w testach parametryzowanych w <framework>.

Z takiej listy można potem ręcznie wybrać najbardziej sensowne przypadki, dopasować je do potrzeb systemu (np. limitów z bazy) i dopiero wtedy prosić model o generację gotowych testów.

Testy dla legacy bez dokumentacji

Częsty scenariusz: jest duża klasa, brak dokumentacji, testów prawie nie ma, a biznes boi się zmian. AI można wtedy potraktować jak detektywa do odtwarzania „ukrytej” specyfikacji z kodu.

Rola: Detektyw specyfikacji w legacy kodzie.

Kontekst:
Mamy legacy-klasę bez dokumentacji i testów.
Chcę zrozumieć jej zachowanie i zaprojektować minimalny zestaw testów
regresyjnych, zanim ruszę z refaktoryzacją.

Dane:
<wklej klasę / kluczowe metody>

Zadania:
1. Na podstawie kodu opisz w punktach, co ta klasa robi z perspektywy biznesu
   (maks. 10 punktów, prostym językiem).
2. Wypisz potencjalne "kontrakty" tej klasy:
   - jakie dane przyjmuje,
   - czego wymaga,
   - co gwarantuje w wyniku.
3. Na tej podstawie zaproponuj 5–10 testów regresyjnych, które:
   - nie zakładają zmiany implementacji,
   - tylko utrwalają obecne zachowanie,
   - obejmują też przypadki "dziwne", ale obecnie dozwolone.
4. Dla 2–3 z nich wygeneruj szkielety testów w <framework>.

Takie testy nie są „idealne” z perspektywy projektowania domeny, za to bardzo dobrze zabezpieczają przed niechcianą zmianą zachowania przy pierwszym większym refaktorze.

Łączenie debugowania, refaktoru i testów w jednym cyklu

W pracy dziennej rzadko robisz tylko jedno: najczęściej najpierw szukasz błędu, potem go łatasz, a przy okazji sprzątasz kod i dopisujesz test. Z AI można ułożyć na to powtarzalny mini-proces.

Rola: Partner do cyklu "debug - fix - refactor - test".

Kontekst:
Mam błąd w module <nazwa>. Chcę przejść cały cykl z twoją pomocą,
ale w małych krokach.

Dane:
- Opis błędu:
  <wklej>
- Fragmenty kodu, które podejrzewam:
  <wklej>

Zadania (etapowe):
Etap 1 – diagnoza:
1. Wskaż najbardziej prawdopodobne miejsca źródła błędu
   i zaproponuj hipotezy (bez zmian w kodzie).

Etap 2 – test regresyjny:
2. Zaproponuj test, który odtwarza błąd (opis + szkic w <framework>).

Etap 3 – poprawka:
3. Zaproponuj jedną małą poprawkę kodu, minimalnie ingerującą w strukturę.
   Pokaż diff i wyjaśnij wpływ na zachowanie.

Etap 4 – lokalny refaktor:
4. Zaproponuj 2–3 drobne usprawnienia czytelności/testowalności
   w okolicy naprawionego miejsca, nie zmieniając zachowania.

Etap 5 – rozszerzenie testów:
5. Zaproponuj dodatkowe testy, które zabezpieczą refaktoryzowane fragmenty.

Taki scenariusz można powtarzać przy każdym większym błędzie. Z czasem powstaje z tego styl pracy, w którym AI jest asystentem procesu, a nie generatorem losowego kodu.

Najczęściej zadawane pytania (FAQ)

Jak programista powinien myśleć o AI przy pracy z kodem?

Najbezpieczniej traktować AI jako bardzo szybkiego współprogramistę od analizy tekstu, a nie jako kompilator czy środowisko wykonawcze. Model nie widzi twojego runtime’u, logów ani bazy – operuje wyłącznie na tym, co mu wyślesz w promptach.

AI świetnie sprawdza się jako „lustro do myśli”: streszcza funkcje, pokazuje inne podejścia, wskazuje niespójności. Ty nadal podejmujesz decyzje architektoniczne i weryfikujesz propozycje, ale dostajesz drugą parę oczu, która się nie męczy po kilku godzinach patrzenia w ten sam plik.

Jak pisać prompty do debugowania błędów w kodzie?

Zamiast „napraw ten kod”, lepiej formułować zadania wprost pod konkretny problem. Do promptu wrzuć: krótki opis kontekstu, fragment kodu, pełen stack trace oraz to, czego dokładnie oczekujesz (np. dwie hipotezy przyczyny błędu).

Przykładowy schemat: najpierw rola („działasz jako senior Java dev”), potem kontekst (framework, wersja, gdzie występuje błąd), dalej konkretne dane (kod + logi) i jasny cel („wskaż potencjalne miejsca NullPointerException i zaproponuj, jak zawęzić diagnozę”). Dzięki temu dostajesz sensowne hipotezy, a nie losowe poprawki.

Jak używać AI do refaktoryzacji, żeby nie wprowadzić chaosu w projekcie?

Przy refaktorze kluczowy jest mały, jasno określony zakres. Zlecaj AI lokalne zmiany: jedna klasa, jedna funkcja, jeden moduł, zamiast prosić o „przepisanie całej warstwy serwisów”. W promptach zaznacz, czego model nie może ruszać (publiczne API, kontrakty, nazwy endpointów).

Dobrze działa podejście etapowe: najpierw „streść logikę tej klasy”, później „wskaż miejsca o największej złożoności”, a dopiero na końcu „zaproponuj podział na mniejsze klasy i pokaż implementację tylko tej jednej”. Ty pilnujesz zgodności z architekturą, AI pomaga uporządkować myśli i zaproponować warianty.

Jak uniknąć halucynacji AI przy generowaniu kodu?

Halucynacje najczęściej biorą się z ogólnych, mało precyzyjnych poleceń i braku kontekstu. Zamiast „napisz integrację ze Stripe w Javie”, doprecyzuj framework, wersję SDK, istniejące interfejsy i oczekiwane kontrakty wejście/wyjście. Poproś też o krótkie uzasadnienie proponowanych rozwiązań.

Dobry nawyk to weryfikacja: porównuj wygenerowany kod z dokumentacją biblioteki oraz z twoimi istniejącymi wzorcami projektowymi. Jeżeli model wymyśla klasy lub metody, których nie rozpoznajesz, poproś go wprost: „wymień źródła, na podstawie których twierdzisz, że ta metoda istnieje” albo „dostosuj rozwiązanie do tego interfejsu, bez tworzenia nowych zależności”.

Jak przekazywać duże pliki kodu do AI przy ograniczonym context window?

Zamiast wklejać cały plik, wybierz tylko to, co wpływa na problem. Najczęściej wystarczą: sygnatury metod publicznych, newralgiczny fragment implementacji i krótki opis reszty. Resztę możesz wspólnie z AI streścić w jednym z pierwszych promptów.

Przy większych modułach sprawdza się podejście krokowe: najpierw prosisz o streszczenie pliku, potem pracujesz na konkretnych fragmentach (np. jedna metoda), dalej selekcjonujesz powiązane klasy (kontroler, serwis, repozytorium). Dzięki temu nie przepełniasz context window, a model mimo wszystko widzi najważniejsze zależności.

Kiedy AI realnie przyspieszy pracę programisty, a kiedy lepiej z niego nie korzystać?

AI mocno pomaga przy „mieleniu tekstu”: długie stack trace, duże pliki, przegląd potencjalnych edge case’ów, generowanie szkiców testów jednostkowych lub integracyjnych. Sprawdza się też przy lokalnym refaktorze i analizie przepływu danych między kilkoma punktami w kodzie.

Znacznie gorzej radzi sobie z „napisz od zera cały moduł bez specyfikacji”. Wtedy każda odpowiedź jest tylko interpretacją twojego nieprecyzyjnego opisu. W takich sytuacjach lepiej najpierw użyć AI do doprecyzowania wymagań (przykładowe wejścia/wyjścia, kontrakty, edge case’y), a dopiero później przejść do implementacji, prowadzonej krok po kroku.

Czy AI może za mnie pisać testy jednostkowe i integracyjne?

AI dobrze sprawdza się jako generator pierwszej wersji testów na bazie istniejącej logiki. Potrafi szybko zaproponować zestaw przypadków wejściowych i szkice testów pod popularne frameworki (JUnit, pytest, NUnit itd.), ale nie zna twoich wewnętrznych konwencji, helperów ani narzędzi.

Praktyczny schemat: najpierw poproś o „wypunktowanie scenariuszy testowych dla tej metody wraz z edge case’ami”, potem „wygenerowanie testów w danym frameworku pod tę listę scenariuszy”. Na końcu sam dopasuj nazwy, asercje i setup do standardów projektu. Dzięki temu AI robi brudną robotę, a ty pilnujesz jakości.

Poprzedni artykułIPv6 bez bólu: konfiguracja i testy w sieci domowej
Następny artykułBudowanie społeczności: jak prowadzić projekt open source na Discordzie i GitHubie
Marta Rutkowski
Marta Rutkowski zajmuje się tematami AI/ML i analizą danych, łącząc podejście inżynierskie z krytycznym spojrzeniem na trendy. W tutorialach prowadzi od przygotowania danych po wdrożenie, pokazując, jak mierzyć jakość modeli, unikać przecieków danych i kontrolować koszty obliczeń. Zamiast obietnic „magii AI” stawia na powtarzalne eksperymenty, czytelne metryki i rzetelne źródła, w tym publikacje oraz dokumentację narzędzi. Interesują ją także kwestie etyki, prywatności i bezpieczeństwa w systemach uczących się, dlatego wnioski zawsze osadza w realnych ograniczeniach.