1. Где кончается реализация и начинается отладка
На практике отладка редко начинается торжественно. Обычно вы только что честно выполняли шаг approved plan, запустили targeted check и получили красный вывод, stack trace и очень человеческое желание написать Claude «ну почини уже как-нибудь». В этот момент важнее всего не ускоряться, а притормозить.
Момент переключения задают ваши stop rules. Один упавший targeted check — ещё не драма. Но если Claude сделал одну-две правки, ошибка не исчезла, а diff начал расти — implementation loop закончился. В нём вы меняете код; в debugging сначала объясняете себе, почему код ломается.
Представьте ситуацию из Commerce OS. Вы шлёте checkout-запрос с пустой корзиной и вместо валидационного ответа получаете серверную ошибку:
curl -i -X POST http://localhost:8080/api/orders \
-H "Content-Type: application/json" \
-d '{ "items": [] }'
# HTTP/1.1 500 Internal Server Error
Если в этот момент сразу попросить «добавь проверку пустой корзины» — вы, возможно, получите менее шумную ошибку, но совсем не факт, что поймёте причину. Баг, который починили без понимания, любит возвращаться позже в другом месте, иногда даже с друзьями.
Полезно держать в голове простое правило: как только проверка падает, следующая задача — не фикс, а диагностическая запись: что воспроизводится, при каком входе, какой фактический результат, какой ожидался, что менялось в последних diff'ах.
2. Симптом, гипотеза и корневая причина — три разные вещи
Новички в отладке часто смешивают три разные вещи в одну: что видно снаружи, что вы предполагаете внутри и что доказано. Отсюда иллюзия, будто «ошибка на 41-й строке» и есть root cause. Но строка падения — место, где система вскрикнула, а не где родилась проблема.
Удобно различать три уровня вот так:
| Уровень | Главный вопрос | Пример в Commerce OS |
|---|---|---|
| Симптом | Что мы наблюдаем? | POST /api/orders возвращает 500 для { "items": [] } |
| Гипотеза | Что могло это вызвать? | валидация пустой корзины не сработала; OrderController пропускает пустой список дальше; сервис ожидает хотя бы один товар |
| Корневая причина | Что подтверждено доказательствами? | OrderController пропускает пустой список товаров в CheckoutService, и дальше checkout-поток ломается на расчёте, который предполагает непустую корзину |
Это различие может показаться занудным, но оно экономит часы: гипотеза — пусть убедительная — всё ещё не факт, а корневая причина подтверждена кодом, логом, reproduction step или точечной проверкой.
Возьмём маленький фрагмент из checkout-сервиса:
import java.math.BigDecimal;
import java.util.List;
public OrderResult checkout(List<OrderItem> items) {
BigDecimal total = pricingService.calculate(items.get(0), items);
paymentService.reserve(total);
return OrderResult.created(total);
}
Если items пустой, падение произойдёт на items.get(0). Но root cause — не IndexOutOfBoundsException на этой строке: это симптом внутри кода. Причина глубже и полезнее — checkout-поток допускает расчёт цены без ранней проверки, что в корзине вообще есть товары.
Это очень похоже на медицину, и аналогия здесь уместна. Температура — симптом, подозрение на инфекцию — гипотеза, подтверждённый анализ — корневая причина. Таблетка «от температуры» не всегда лечит то, из-за чего температура поднялась. С багами та же история — только без белого халата и с большим количеством логов.
3. Соберите улики до первого вопроса к Claude
Самая частая ошибка в отладке с AI звучит так: «Вот у меня что-то не работает, разберись». Без хороших улик Claude строит догадки из воздуха. А воздух в разработке — очень дорогой ресурс, особенно когда Claude уже редактирует файлы.
Поэтому до первого диагностического запроса соберите компактный пакет доказательств: reproduction steps, фактический результат, ожидаемый результат, релевантный фрагмент stack trace, последний diff, список подозрительных файлов и одна-две команды, стабильно воспроизводящие проблему.
Посмотрите на пример разумного фрагмента лога:
ERROR c.a.orders.OrderController - Failed to create order
java.lang.IndexOutOfBoundsException: Index 0 out of bounds for length 0
at com.acme.orders.CheckoutService.checkout(CheckoutService.java:41)
at com.acme.orders.OrderController.checkout(OrderController.java:27)
Для начинающего разработчика stack trace иногда выглядит как древний свиток проклятий, но читать его можно очень практично. Сначала ищите первую строку со своим кодом, а не с внутренностями фреймворка — здесь CheckoutService.java:41. Потом смотрите, кто её вызвал — OrderController.java:27. Виден путь ошибки: запрос попал в контроллер, тот пропустил его дальше, сервис не справился.
Очень полезно сразу зафиксировать эти улики в коротком EVIDENCE_LOG.md:
## Симптом
`POST /api/orders` с `{"items":[]}` возвращает HTTP 500.
## Фактический результат
`IndexOutOfBoundsException` в `CheckoutService.java:41`.
## Ожидаемый результат
Клиент должен получить валидационную ошибку, а не серверный 500.
## Последний diff
Изменения были в `OrderController` вокруг checkout-обработки.
Обратите внимание: про фикс здесь ни слова. И это хорошо — пока вы собираете улики, вы как инженер работаете лучше всего. Начнёте лечить раньше диагноза — вырастет шанс закрасить лампочку вместо того, чтобы устранить причину пожара.
4. Просите диагноз, а не «магический почини»
Очень многое в отладке с AI решает первая формулировка. Напишете «почини checkout» — Claude почти наверняка примет это за разрешение редактировать код. Если вам нужна диагностика, а не правка, это нужно сказать явно и достаточно жёстко.
Хороший debugging-запрос почти всегда содержит четыре вещи: запрет на правки, список входных доказательств, просьбу вернуть гипотезы по вероятности и требование предложить минимальную подтверждающую проверку. Нет пункта — Claude фантазирует или слишком быстро пишет код.
Вот рабочий формат запроса:
Разберите падение checkout.
Код пока не меняйте.
На основе stack trace, reproduction step и последнего diff верните:
1) гипотезы по вероятности;
2) доказательства по каждой гипотезе;
3) минимальную проверку для гипотезы №1;
4) что пока остаётся неизвестным.
В этой формулировке важно всё. «Код пока не меняйте» снимает соблазн утащить вас в patch loop; «что остаётся неизвестным» дисциплинирует и вас. Хороший debugging — честная работа с неопределённостью, а не иллюзия полной уверенности.
Ниже — схема здорового цикла диагностики:
flowchart TD
A[Проверка упала] --> B[Собрали пакет доказательств]
B --> C[Запросили Claude без edits]
C --> D[Получили гипотезы и доказательства]
D --> E[Выбрали гипотезу №1]
E --> F[Сделали минимальную подтверждающую проверку]
F -->|Да, гипотеза подтверждена| G[Записали корневую причину]
F -->|Нет, не подтверждена| D
Заметьте: шага «сразу изменить три файла и посмотреть, стало ли лучше» здесь нет. И это не случайность — диагностическая сессия скучнее сессии реализации, и скука здесь ваш друг: не даёт улететь в красивую, но недоказанную историю.
5. Минимальная подтверждающая проверка
Когда Claude выдал вам список гипотез, следующая задача — не запускать весь проект, все тесты и лунный календарь разом. Нужна smallest confirming check — минимальная проверка, которая подтверждает главную гипотезу либо отправляет ко второй.
Допустим, главная гипотеза такая: OrderController пропускает checkout даже с пустым списком. Минимальная проверка — не прогон всех orders-тестов, а одна точечная репродукция плюс короткая сверка с кодовым путём:
curl -i -X POST http://localhost:8080/api/orders \
-H "Content-Type: application/json" \
-d '{ "items": [] }'
# HTTP/1.1 500 Internal Server Error
А теперь смотрим, куда этот запрос уходит до фикса:
@PostMapping
public OrderResponse checkout(@RequestBody CheckoutRequest request) {
return checkoutService.checkout(request.items()); // пустой список уходит дальше без guard
}
Да, проверка почти смешно маленькая — но именно такие работают лучше всего. Один запрос стабильно воспроизводит 500, а в контроллере видно: пустой список без валидации уходит в сервис. Два факта сошлись — главная гипотеза стала очень сильной. Прогон всего test suite наоборот маскирует картину: много зелёного шума и один красный сигнал.
Здесь есть важный педагогический момент: минимальная подтверждающая проверка — ещё не regression test для PR. Баг на будущее вы не закрываете — вы подтверждаете, что правильно понимаете, откуда он берётся.
6. Узнать patch loop и вернуть сессию
Patch loop — это тот неприятный режим, когда внешне вы всё ещё «исправляете баг», а по факту сыплете новые правки без роста понимания. С AI он особенно коварен: Claude умеет очень убедительно предлагать локально правдоподобные изменения. Каждая отдельная правка звучит разумно — а общая траектория при этом едет в канаву.
Полезно сравнить здоровую диагностику и patch loop напрямую:
| Здоровая отладка | Patch loop |
|---|---|
| одна ведущая гипотеза | много случайных исправлений подряд |
| diff почти не растёт | diff расползается по соседним файлам |
| есть воспроизведение | «вроде должно помочь» |
| есть минимальная проверка | проверяется всё подряд или почти ничего |
| можно сформулировать причину | можно только сказать «теперь ошибка другая» |
На практике patch loop обычно узнаётся по ощущению: «чиним уже минут двадцать, а причину одной фразой всё ещё не сформулируем». Это очень надёжный сигнал остановиться: вернуть последние спекулятивные правки, перечитать diff и открыть свежую сессию с компактным пакетом доказательств. Вы уже видели: перегруженный контекст редко умнеет после ещё двух сообщений.
Если нужно, можно сделать очень короткую техническую паузу:
git diff --stat
git restore src/orders/OrderController.java
Смысл здесь не в самом git restore, а в дисциплине: вы отказываетесь наращивать хаос поверх хаоса. Это очень взрослая привычка, хоть и не выглядит героически, — зато потом не приходится разгребать diff, похожий на следы борьбы медведя с роутером.
Хорошая фраза для себя и для Claude в такой момент звучит примерно так: «Стоп. Мы в patch loop. Вернёмся к симптому, уликам и одной ведущей гипотезе». Простая, а сессию отрезвляет прекрасно.
7. Root cause statement как рабочий артефакт
Когда вы подтвердили ведущую гипотезу, сразу хочется чинить. Но один маленький шаг невероятно повышает качество дальнейшей работы: записать root cause statement явно, одним коротким абзацем — не в стиле романа о страданиях сервиса, а чётко и проверяемо.
Хороший root cause statement отвечает на три вопроса: что ломается, почему и где это доказано. Для нашего примера он может выглядеть так:
## Корневая причина
`OrderController` пропускает checkout-запрос с пустым списком товаров дальше в `CheckoutService`.
Ниже по цепочке сервис расчёта цены работает так, будто в корзине есть хотя бы один товар,
из-за чего запрос заканчивается серверной ошибкой вместо клиентского `400`.
## Подтверждение
- баг стабильно воспроизводится через `POST /api/orders` с `{"items":[]}`
- stack trace ведёт по пути `OrderController.java:27 -> CheckoutService.java:41`
- в текущем `OrderController` до фикса нет ранней проверки `request.items() == null || request.items().isEmpty()`
Обратите внимание, насколько здесь мало магии и насколько много конкретики. Такой фрагмент кладётся в EVIDENCE_LOG.md, передаётся другой сессии, показывается коллеге. Отвлеклись на час и вернулись — не вспоминаете заново, «что мы там вроде поняли».
И только после такой записи отладку действительно можно считать завершённой. Не потому, что баг исправлен, а потому, что вы превратили хаос симптомов в инженерное знание. А с ним работать намного приятнее, чем с надеждой на очередной удачный if (x != null).
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ