TL;DR
Breakpoint Debugging (BPD) — техника автоматической починки кода, которая при ошибке проверки результата (AssertionError) возвращает модели её собственный код с встроенными построчными комментариями значений переменных только для первого упавшего теста. Авторы также вывели строгие правила: в каких задачах подавать трассировку в цикл исправления, а когда выгоднее сбросить диалог и сгенерировать код с нуля.
После нескольких попыток обычной отладки почти все оставшиеся поломки кода — это ошибки расхождения ответа (AssertionError), а не падения интерпретатора. Обычное сообщение «ожидалось 8, получено 6» говорит модели лишь о том, что ответ в конце неверный, но не показывает, в какой именно строке состояние программы пошло не так. Пытаясь прокрутить 50 шагов выполнения «в голове», модель сбивается, застревает и в четверти случаев присылает обратно абсолютно тот же самый сломанный код.
Метод BPD решает проблему в два шага: сначала отфильтровывает тип ошибки и задачи, а затем для прикладных функций с открытым тестом прогоняет код через построчный трассировщик и подписывает реальные значения переменных прямо в конец каждой выполненной строки для одного (первого) упавшего теста. Если же тест скрыт или задача чисто алгоритмическая, цикл отладки останавливается и заменяется чистой перегенерацией с нуля в новом запросе.
Схема метода
Процесс выполняется в цикле итеративной отладки (между шагами запускаются тесты):
ШАГ 1 (Первичный цикл): Генерация кода → Запуск тестов → Если обычное исключение (Crash), вернуть стандартный стек вызова (Traceback)
ШАГ 2 (Развилка при AssertionError):
├── Если тест скрыт ИЛИ задача алгоритмическая (олимпиадная) → СБРОС: генерировать решение с нуля (Resampling) без истории ошибок
└── Если вход упавшего теста виден и ошибка в прикладной логике → Запустить BPD
ШАГ 3 (Разметка BPD): Прогнать ТОЛЬКО ПЕРВЫЙ упавший тест через построчный трассировщик → Дописать значения переменных, итерации циклов и срабатывания условий в комментарии # в конце каждой строки кода
ШАГ 4 (Запрос на исправление): Отправить размеченный код + Вход + Ожидаемый и Фактический вывод → Получить исправленный код
Пример применения
Задача: Вы пишете с помощью LLM функцию расчёта прогрессивного кешбэка и удержания комиссии для селлеров на российском маркетплейсе (Ozon / Яндекс Маркет) с учётом возвратов и порога бесплатной логистики. Код не падает с синтаксической ошибкой, но на тестовом заказе вместо 4250 рублей к выплате возвращает 4850 (AssertionError), а после двух просьб «исправь ошибку» модель раз за разом присылает один и тот же код.
Промпт:
Твоя функция расчёта выплаты селлеру не прошла проверку (AssertionError).
Ниже приведён твой код с построчной трассировкой выполнения на первом упавшем тесте. В комментариях # справа от каждой строки записаны реальные значения переменных, последовательность значений в цикле и срабатывания условий (T — истина, F — ложь, iter — номер итерации).
```python
def calc_seller_payout(orders, commission_rate=0.15): # orders = [{'price': 3000, 'returned': False}, {'price': 2000, 'returned': True}, {'price': 3000, 'returned': False}], commission_rate = 0.15
gross = 0 # gross = 0
return_fee = 0 # return_fee = 0
for item in orders: # item = {'price': 3000, ...}, {'price': 2000, ...}, {'price': 3000, ...}
if item['returned']: # F, T, F
return_fee += 150 # return_fee = 150 (iter 2)
gross += item['price'] # gross = 3000, 5000, 8000
net = gross * (1 - commission_rate) # net = 6800.0
if gross > 5000: # T
logistics_bonus = 200 # logistics_bonus = 200
else:
logistics_bonus = -400
payout = int(net - return_fee - 2000 + logistics_bonus) # payout = 4850
return payout # returns 4850
Input: orders=[{'price': 3000, 'returned': False}, {'price': 2000, 'returned': True}, {'price': 3000, 'returned': False}], commission_rate=0.15 Expected: 4250 Actual: 4850
Найди строку, где фактическое состояние переменных расходится с логикой задачи (обрати внимание на то, как изменился gross на итерации 2 при возвращённом товаре и как это повлияло на порог gross > 5000), и верни исправленную функцию.
**Результат:**
Модель не будет угадывать причину расхождения вслепую и не пришлёт повторно старый код. По комментарию `# gross = 3000, 5000, 8000` на строке сложения она сразу увидит, что после ветки `if item['returned']:` пропущен `continue` (или `else`), из-за чего возвращённый товар за 2000 ₽ прибавился к выручке `gross` и вдобавок ложно активировал бонус `gross > 5000`. В ответе придёт точечно исправленная функция.
---
### Почему это работает
Без трассировки модель вынуждена **симулировать выполнение программы «в голове»** токен за токеном. Даже если вероятность правильно предсказать состояние переменных на одном шаге равна 98%, на дистанции в 50 шагов шанс безошибочно просчитать всю цепочку падает до **36%**. А сам факт несовпадения ответа в `assert` несёт **не более 1 бита информации** о том, какая именно строка из десятков кандидатов испортила промежуточное состояние: ошибка возникает в начале функции, заражает переменные и всплывает только на финальном `return`.
В то же время LLM отлично справляются с **локальной проверкой правдоподобия**, когда промежуточные значения уже выписаны перед глазами. Построчная разметка **BPD** заменяет мысленную симуляцию простым **чтением готовых значений**: модели больше не нужно вычислять, чему равен счётчик на третьем витке цикла — она сразу видит комментарий `# total = 2, 6` рядом с операцией и сопоставляет его с условием задачи.
При этом срабатывает жёсткий **порог полезной информации**:
- **Подавать только 1 упавший тест, а не все сразу:** когда в промпт сваливают трассировки всех упавших тестов, контекст раздувается в 1,5–1,7 раза, внимание модели рассеивается, и она реже чинит баг, чем по **одному первому тесту**.
- **Признак алгоритмического тупика:** если задача олимпиадная или алгоритмическая, трассировка бесполезна даже на открытых тестах — она лишь честно записывает, как **неверный алгоритм** штатно выполняет заложенную в него ошибочную идею.
- **Парадокс крупных моделей:** чем сильнее модель (например, 72B против 7B), тем **выгоднее для неё трассировка по сравнению с перезапуском с нуля**. Сильная модель без новой обратной связи при повторной генерации с высокой вероятностью воспроизводит ту же самую ошибку, тогда как построчный трейс даёт ей недостающий сигнал для исправления.
---
### Шаблон промпта
Ниже приведена программа, которая завершилась ошибкой проверки результата (AssertionError) на тесте, с встроенной построчной трассировкой первого упавшего теста.
Правила чтения встроенной трассировки в комментариях #: - Присваивания: # переменная = значение (или последовательность значений через запятую) - Заголовки циклов: # переменная_цикла = первые 4 и последние 2 значения - Условия (if/elif): # последовательность T (истина) и F (ложь), например Fx2, T - Переходы (break/continue/return): # iter N (номер итерации) или # returns значение
Размеченный код:
{размеченный_код_с_трассировкой_первого_теста}
Input: {входные_данные_первого_упавшего_теста} Expected: {ожидаемый_вывод} Actual: {фактический_вывод}
Проанализируй построчные значения переменных в трассировке, найди первую строку, где состояние программы отклоняется от требуемой логики задачи {описание_задачи}, и напиши исправленную версию кода.
**Что подставлять в плейсхолдеры:**
- `{размеченный_код_с_трассировкой_первого_теста}` — код функции, где в конце каждой выполненной строки добавлен комментарий `#` со значениями переменных на первом упавшем тесте (списки длиннее 6 элементов сокращаются до первых 4 и последних 2 через `...`, строки длиннее 40 символов обрезаются, общий лимит трассировки — до 6 000 символов).
- `{входные_данные_первого_упавшего_теста}` — аргументы, на которых тест упал.
- `{ожидаемый_вывод}` и `{фактический_вывод}` — эталонное и полученное значения из `AssertionError`.
- `{описание_задачи}` — исходное ТЗ или докстринг функции.
---
### Ограничения
> ⚠️ **Бесполезно и вредно на алгоритмических задачах:** Если модель выбрала неверный алгоритм или неправильно поняла математическое условие задачи (типично для олимпиадного программирования вроде LiveCodeBench), трассировка не даёт выигрыша даже на открытых тестах и проигрывает простой генерации решения с нуля.
> ⚠️ **Не работает при скрытых тестах:** Когда тестирующая система сообщает только факт падения скрытого теста («hidden test failed») без входных данных, информативность обратной связи падает до ≤ 1 бита — продолжать итеративную починку бессмысленно, бюджет нужно тратить на новые независимые генерации.
> ⚠️ **Все трейсы сразу ухудшают результат и раздувают расход токенов:** Передача трассировок всех упавших тестов вместо одного первого снижает число починенных задач на всех бенчмарках и увеличивает общий расход токенов на 19–49%.
---
### Как исследовали
Исследователи из **Университета Канберры** сначала проверили, какие именно ошибки остаются после 5 итераций стандартной самоотладки (в стиле PyCapsule со структурированными сообщениями об ошибках) у четырёх моделей семейства **Qwen2.5 Instruct (7B, 14B, 32B и 72B)** на трёх бенчмарках: прикладном **BigCodeBench** (1 140 задач), базовом **HumanEval** (164 задачи) и алгоритмическом **LiveCodeBench** (175 задач). Оказалось, что обычные падения (crashes) модели чинят легко, а среди оставшихся нерешённых задач **от 79% до 100% составляют ошибки `AssertionError`** (79,0% на BigCodeBench, 100% на HumanEval и 93,2% на LiveCodeBench).
Затем авторы собрали все такие «застрявшие» задачи в выборку **BPD set** (1 014 задач на BigCodeBench, 40 на HumanEval и 449 на LiveCodeBench) и при равном бюджете сравнили три стратегии: **чистую перегенерацию с нуля без истории (Fresh start — 6 независимых вызовов)**, **BPD с трассировкой только первого упавшего теста (One trace)** и **BPD с трассировкой всех упавших тестов (All traces)**. На прикладном BigCodeBench режим **One trace** починил **29,5% (299 из 1 014)** безнадёжных задач против **22,4% (227)** у перегенерации с нуля и **25,8% (261)** у всех трейсов сразу, подняв общую точность с 71,9% до 78,4%. При этом на самой крупной модели **72B** отрыв трассировки от перегенерации оказался максимальным: перегенерация с нуля спасла лишь 15,9% задач (34 из 214), а один трейс — **27,1% (58 из 214)**.
Самое яркое изменение произошло в поведении моделей при отладке: доля **повторной отправки идентичного кода** упала более чем втрое — с **28% до 8%** на BigCodeBench и с **23% до 6%** на HumanEval. При этом передача ровно одного трейса не только победила по точности, но и **снизила суммарный расход токенов** на **3,9%** на BigCodeBench и на **16,0%** на HumanEval за счёт более быстрого закрытия задач (авторы вывели точную формулу окупаемости: если трейс удлиняет один запрос в $\rho$ раз, он экономит токены, когда сокращает число попыток минимум на $1 - 1/\rho$). Напротив, на алгоритмическом **LiveCodeBench** чистая перегенерация с нуля обыграла трассировку (6,5% против 4,9%), доказав, что неверную алгоритмическую идею нужно переписывать с чистого листа, а не лечить построчной отладкой.
---
### Оригинал из исследования
1 def sum_of_evens(nums): # nums = [2, 4, 3, 2] 2 total = 0 # total = 0 3 for n in nums: # n = 2, 4, 3 4 if n % 2 == 1: # Fx2, T 5 break # iter 3 6 total += n # total = 2, 6 7 return total # returns 6 Input: [2, 4, 3, 2] Expected: 8 Actual: 6
**Контекст:** Дословный фрагмент промпта Breakpoint Debugging (Fig. 1 из работы), который формируется через `sys.settrace` при падении `AssertionError` и показывает модели, что на 3-й итерации цикла (`n = 3`) сработало условие `break` вместо `continue`, из-за чего последняя двойка в списке `[2, 4, 3, 2]` не вошла в сумму.
---
### Адаптации и экстраполяции
> 💡 **Адаптация для агентных сред разработки (`CLAUDE.md` / `AGENTS.md` для Claude Code и Cursor):**
>
> Впишите четыре проверенных правила из статьи прямо в системную инструкцию вашего кодинг-агента, чтобы он не сжигал токены в бесконечных циклах самоотладки:
>
Правила отладки тестов (Breakpoint Debugging Policy): 1. Если тест упал с исключением (TypeError, KeyError, IndexError и др.) — исправляй код сразу по стандартному стек-трейсу (Traceback). 2. Если тест упал по AssertionError на открытых входных данных и первая попытка исправления не помогла — НЕ гадай вслепую и НЕ отправляй тот же код повторно. Выведи построчные значения локальных переменных ТОЛЬКО для первого упавшего теста (не выводи дампы всех упавших тестов сразу). 3. Если код уже проходит открытые тесты, но валится на скрытых проверках без известных входных данных, либо ошибка кроется в самом выборе алгоритма — прекрати править текущий вариант после 2 неудачных попыток и напиши решение с чистого листа по другому алгоритму. ```
Ресурсы
- Работа: On the Limits of Execution Traces for Repairing Assertion Failures in LLM-Generated Code (arXiv:2611.09212, ноябрь 2026)
- Авторы и институты: Muntasir Adnan, Carlos C. N. Kuhn (Open Source Institute, Faculty of Science and Technology, University of Canberra, Australia)
- Связанные методы и бенчмарки: PyCapsule, LDB (Large Language Model Debugger), NExT, BigCodeBench, HumanEval, LiveCodeBench
