JavaRush /Курсы /Claude code /Debugging и root cause analysis

Debugging и root cause analysis

Claude code
18 уровень , 1 лекция
Открыта

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).

1
Задача
Claude code, 18 уровень, 1 лекция
Недоступна
Сбор debug input из терминала
Сбор debug input из терминала
1
Задача
Claude code, 18 уровень, 1 лекция
Недоступна
Ranked hypothesis list внутри Claude Code
Ranked hypothesis list внутри Claude Code
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ