Błąd w kodzie napisanym przez AI debugujesz tak samo jak każdy inny: najpierw go odtwarzasz w teście, potem sprawdzasz w debuggerze, jakie wartości faktycznie mają zmienne, i dopiero wtedy poprawiasz. Zasada, którą AI łatwo łamie: poprawiasz przyczynę, nie objaw, więc kolejnych łatek od modelu nie przyjmujesz bez zrozumienia, dlaczego działają. Gdy po dwóch próbach błąd wciąż wraca, odłóż czat i otwórz debugger.
Kod od AI zwykle działa za pierwszym razem na przykładzie z promptu. Problem zaczyna się, gdy dostaje inne dane. Wtedy wklejasz błąd z powrotem do czatu, dostajesz poprawkę, a po chwili wyskakuje następny błąd w tym samym miejscu. Ten artykuł pokazuje, jak z tej pętli wyjść: na prostej funkcji w Pythonie, z uruchomionymi testami i debuggerem.
Czym debugowanie kodu z AI różni się od zwykłego debugowania?
Sama metoda jest ta sama. Różni się to, co sprawia, że w pętli „wklej błąd, dostań poprawkę” tak łatwo utknąć:
| Zwykły kod | Kod od AI |
|---|---|
| Znasz intencję, bo sam ją zapisałeś | Intencja jest w prompcie, a kod ją tylko przybliża |
| Błąd zwykle jest w miejscu, które ostatnio zmieniałeś | Błąd może siedzieć w części, której nie czytałeś |
| Poprawka wynika z Twojego zrozumienia | Poprawka wynika z tego, co model uzna za prawdopodobne |
| Piszesz test, bo chcesz | Kod „działa”, więc testu nie ma |
Ostatni wiersz jest najważniejszy. Bez testu nie masz niczego, co powie, czy poprawka rzeczywiście coś naprawiła, więc oceniasz ją na oko. Tak działa pętla, w której model kolejno „naprawia” własne łatki.
Od czego zacząć debugowanie, gdy kod od AI nie działa?
Od odtworzenia błędu, a nie od poprawki. Zapisz najmniejszy przypadek, który go wywołuje (po angielsku minimal reproducible example), jako test. To, co masz w głowie, nie wystarczy: test jest jedyną rzeczą, która po poprawce powie Ci prawdę.
Przykład. Poprosiłeś model o funkcję liczącą średnią ocenę produktu i dostałeś ją w tej postaci:
def average_rating(reviews):
ratings = [review["rating"] for review in reviews]
total = sum(rating for rating in ratings if rating is not None)
return round(total / len(ratings), 2)
Wygląda rozsądnie, a nawet radzi sobie z recenzjami bez oceny (None). Zanim cokolwiek poprawisz, zapisz oczekiwane zachowanie w trzech testach. Test, który opisuje zachowanie przed implementacją, to ta sama idea co w TDD i BDD na przykładzie w pytest:
from ratings import average_rating
def test_average_of_rated_reviews():
reviews = [{"rating": 5}, {"rating": 3}]
assert average_rating(reviews) == 4.0
def test_unrated_reviews_are_ignored():
reviews = [{"rating": 5}, {"rating": None}, {"rating": 3}]
assert average_rating(reviews) == 4.0
def test_no_reviews_returns_none():
assert average_rating([]) is None
Uruchamiasz pytest i widzisz dwa różne błędy:
FAILED test_ratings.py::test_unrated_reviews_are_ignored - AssertionError: assert 2.67 == 4.0
FAILED test_ratings.py::test_no_reviews_returns_none - ZeroDivisionError: division by zero
2 failed, 1 passed
Jeden jest głośny (wyjątek), drugi cichy (zły wynik bez żadnego komunikatu). Od tego rozróżnienia zależy dalsza praca.
Jak czytać traceback, żeby wiedzieć, gdzie szukać?
Komunikat o błędzie z listą wywołań (po angielsku traceback albo stack trace) czytasz od dołu. Ostatnia linia mówi, co poszło nie tak, a linia nad nią, gdzie:
def average_rating(reviews):
ratings = [review["rating"] for review in reviews]
total = sum(rating for rating in ratings if rating is not None)
> return round(total / len(ratings), 2)
E ZeroDivisionError: division by zero
ratings.py:4: ZeroDivisionError
Tu odpowiedź jest prosta: len(ratings) wynosi zero, bo lista recenzji jest pusta. Zwróć uwagę, że traceback wskazuje miejsce, w którym błąd wyszedł na jaw. Nie zawsze jest to miejsce, w którym powstał.
Dlaczego kolejne poprawki od AI przesuwają błąd zamiast go naprawić?
Bo model dostaje komunikat o błędzie i szuka najprostszej zmiany, która sprawi, że ten komunikat zniknie. Dla ZeroDivisionError poprawka jest oczywista (if not reviews: return None). Dla cichego błędu z wynikiem 2.67 nie ma komunikatu, więc model zgaduje. Typowa odpowiedź wygląda tak:
def average_rating(reviews):
if not reviews:
return None
ratings = [review["rating"] for review in reviews]
total = sum(rating for rating in ratings if rating is not None)
if None in ratings:
return round(total / (len(ratings) - 1), 2)
return round(total / len(ratings), 2)
Test z jedną recenzją bez oceny przechodzi, więc poprawka wygląda na udaną. Dodaj jednak czwarty test z dwiema recenzjami bez oceny:
def test_two_unrated_reviews():
reviews = [{"rating": 5}, {"rating": None}, {"rating": None}, {"rating": 3}]
assert average_rating(reviews) == 4.0
FAILED test_ratings.py::test_two_unrated_reviews - AssertionError: assert 2.67 == 4.0
1 failed, 3 passed
len(ratings) - 1 odejmuje dokładnie jedną recenzję, niezależnie od tego, ile ich jest bez oceny. To jest poprawka na objaw: dopasowana do jednego przypadku testowego, a nie do przyczyny. Przyjęcie jej bez zrozumienia kończy się kolejnym błędem za tydzień, już na produkcji.
Jak sprawdzić w debuggerze, co naprawdę dzieje się w kodzie?
Zamiast zgadywać, zatrzymaj program w podejrzanym miejscu i sprawdź wartości. W Pythonie wystarczy wbudowany debugger pdb. Uruchamiasz z jego pomocą skrypt run_dbg.py, który wywołuje funkcję na przykładowych danych, ustawiasz punkt zatrzymania (breakpoint) w linii z dzieleniem i wypisujesz zmienne:
python -m pdb run_dbg.py
(Pdb) b ratings.py:4
(Pdb) c
(Pdb) p ratings
[5, None, 3]
(Pdb) p total
8
(Pdb) p len(ratings)
3
(Pdb) p [r for r in ratings if r is not None]
[5, 3]
Teraz widać przyczynę. Suma (8) jest policzona tylko z ocen, a dzielnik (3) liczy wszystkie recenzje, także te bez oceny. Licznik i mianownik pochodzą z dwóch różnych zbiorów. Żadna łatka na len(ratings) - 1 tego nie naprawi, bo problem nie jest w liczbie, tylko w tym, co liczymy.
Najczęstsze polecenia pdb, które wystarczą na początek:
| Polecenie | Co robi |
|---|---|
b plik.py:4 | ustawia breakpoint w linii 4 |
c | wykonuje program do następnego breakpointu |
n | wykonuje bieżącą linię i przechodzi do następnej |
s | wchodzi do wywoływanej funkcji |
p zmienna | wypisuje wartość zmiennej lub wyrażenia |
q | kończy debugowanie |
Jeśli wolisz zatrzymywać się dopiero wtedy, gdy test padnie, pytest --pdb otwiera ten sam debugger w miejscu błędu. Wszystkie polecenia pdb, breakpoint() i pytest --pdb opisuje artykuł o debugowaniu w Pythonie.
Jak poprawić błąd u źródła i upewnić się, że wrócił do zera?
Poprawiasz przyczynę: filtrujesz recenzje bez oceny przed liczeniem, żeby suma i liczba pochodziły z tego samego zbioru. Przy okazji obsługujesz przypadek, w którym nie zostaje żadna ocena:
def average_rating(reviews):
ratings = [review["rating"] for review in reviews if review["rating"] is not None]
if not ratings:
return None
return round(sum(ratings) / len(ratings), 2)
4 passed
Zwróć uwagę na dwie rzeczy. Po pierwsze, ta wersja jest krótsza od łatki, bo usunęła przyczynę zamiast dokładać obejście. Po drugie, test z dwiema recenzjami bez oceny zostaje w zestawie na stałe: następnym razem, gdy ktoś (lub model) dotknie tej funkcji, ten błąd wróci do Ciebie od razu, a nie od klienta.
Jak opisać błąd modelowi, żeby dostać użyteczną odpowiedź?
Gdy już wiesz, że chcesz poprosić model o pomoc, nie wklejaj samego komunikatu. Daj mu to, co masz w debuggerze.
Dobre zgłoszenie ma pięć elementów:
- Co miało się stać (jedno zdanie, np. „średnia z ocen, bez recenzji bez oceny”).
- Co się stało (konkretny wynik: „2.67 zamiast 4.0”).
- Najmniejszy przypadek, który to wywołuje (test z danymi, a nie opis słowami).
- Co już sprawdziłeś (np. „w pdb
total= 8,len(ratings)= 3”). - Czego nie wolno zmieniać (np. „nie zmieniaj sygnatury funkcji ani innych testów”).
Przykład gotowego zgłoszenia:
Funkcja average_rating ma zwracać średnią z ocen, pomijając recenzje bez oceny (None).
Test test_unrated_reviews_are_ignored zwraca 2.67 zamiast 4.0 dla [5, None, 3].
W debuggerze: total = 8 (z samych ocen), len(ratings) = 3 (razem z None).
Wygląda, że licznik i mianownik liczą z różnych zbiorów. Wyjaśnij przyczynę
i pokaż minimalną poprawkę. Nie zmieniaj sygnatury ani istniejących testów.
Piąty punkt jest tym, który najczęściej pomija się, a najbardziej chroni: bez niego model chętnie „przy okazji” przepisuje test, który mu przeszkadza. Jak opisywać nie tylko błędy, ale i same zadania, pokazuje artykuł o promptach do kodu.
Kiedy przestać pytać AI i otworzyć debugger samodzielnie?
Odłóż czat i weź się za debugger, gdy zajdzie którakolwiek z trzech rzeczy:
- Po dwóch poprawkach ten sam test nadal pada (albo pada inny w tym samym miejscu). Trzecia próba zwykle jest wariacją pierwszych dwóch.
- Poprawka zmienia więcej niż miejsce błędu. Gdy model dotyka innych funkcji albo testów „przy okazji”, nie wiesz już, co przeglądasz.
- Nie potrafisz własnymi słowami wyjaśnić, dlaczego poprawka działa. Jeśli nie umiesz, nie będziesz wiedzieć, kiedy przestanie.
To samo dotyczy przeglądu kodu przed scaleniem: jak go robić, opisuje code review kodu napisanego przez AI.
Co zrobić, gdy błąd wygląda na nieistniejącą funkcję albo bibliotekę?
To inny przypadek niż w tym artykule. Jeśli kod w ogóle się nie uruchamia, bo model użył metody albo pakietu, które nie istnieją, nie debugujesz logiki, tylko sprawdzasz źródło. Jak to rozpoznać, opisuje artykuł o halucynacjach AI w kodzie. Tutaj chodzi o kod, który się uruchamia i liczy źle, a to trudniejsze, bo nic nie zgłasza błędu.
Podsumowanie: szybka lista kroków
- Odtwórz błąd najmniejszym możliwym testem.
- Odróżnij błąd głośny (wyjątek) od cichego (zły wynik).
- Przeczytaj traceback od dołu, pamiętając, że wskazuje, gdzie błąd wyszedł, a niekoniecznie gdzie powstał.
- Zatrzymaj program w debuggerze i sprawdź wartości, zamiast zgadywać.
- Poprawiaj przyczynę, a nie objaw, i zostaw test, który odtworzył błąd.
- Do modelu idź z konkretami (oczekiwanie, wynik, przypadek, co sprawdzone, czego nie ruszać).
- Po dwóch nieudanych próbach odłóż czat.
Chcesz uczyć się budować backend z AI, z testami od pierwszego dnia i code review swojego kodu? Zobacz ścieżkę AI Engineer.


