7 760 статей
arXiv:2611.09212 82 6 нояб. 2026 г. FREE

Breakpoint Debugging (BPD): построчная трассировка первого упавшего теста и правила выбора между отладкой и перегенерацией кода

КЛЮЧЕВАЯ СУТЬ
На сложных багах проверки ответа (AssertionError) модель сбивается при счёте в уме и в 25% случаев возвращает тот же самый сломанный код. Метод Breakpoint Debugging (BPD) позволяет чинить логические ошибки в коде с первой попытки и вовремя сбрасывать тупиковые ветки отладки. Фишка метода: в конец каждой строки кода подписываются реальные значения переменных только для первого упавшего теста, а на скрытых тестах и олимпиадных алгоритмах отладка сразу меняется на чистую генерацию с нуля.
Адаптировать под запрос
⚡

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

📋 Дайджест исследования

Ключевая суть

На сложных багах проверки ответа (AssertionError) модель сбивается при счёте в уме и в 25% случаев возвращает тот же самый сломанный код. Метод Breakpoint Debugging (BPD) позволяет чинить логические ошибки в коде с первой попытки и вовремя сбрасывать тупиковые ветки отладки. Фишка метода: в конец каждой строки кода подписываются реальные значения переменных только для первого упавшего теста, а на скрытых тестах и олимпиадных алгоритмах отладка сразу меняется на чистую генерацию с нуля.

Принцип работы

Модель как программист без отладчика — пытается прокрутить 50 шагов цикла в голове и теряет нить. Не заставляй её гадать по финальной ошибке «ожидалось 8, получено 6». Преврати слепую симуляцию в простое чтение готовых значений прямо внутри кода. Прогони только один первый упавший тест через построчный трассировщик и допиши состояния переменных в комментарии # справа от каждой строки. А если задача алгоритмическая или вход теста скрыт — не корми модель старыми ошибками, запускай новый диалог с нуля.

Почему работает

Даже при точности 98% на один шаг шанс безошибочно просчитать в уме 50 шагов программы падает до 36%. Сам факт падения проверки даёт не более 1 бита информации о том, какая строка испортила расчёт. Готовые значения переменных в комментариях снимают нагрузку на счёт в уме, и сильная модель сразу видит точку расхождения с логикой задачи. Прикол в том, что подача трассировок всех упавших тестов разом раздувает контекст в 1,5–1,7 раза, сжигает на 19–49% больше токенов и чинит меньше багов, чем один первый тест.

Когда применять

Разработка и автопочинка кода с помощью ИИ → конкретно для прикладных функций с открытыми тестами, особенно когда код не падает с ошибкой синтаксиса, но выдаёт неверный ответ (AssertionError), а модель застряла и шлёт старый вариант. НЕ подходит для олимпиадных алгоритмических задач и скрытых тестов без видимых входных данных — там выгоднее сгенерировать код с нуля.

Мини-рецепт

1. Проверь тип ошибки: если код упал со стандартным сбоем, отдай обычный стек вызова, а для ошибки проверки ответа (AssertionError) оцени задачу.
2. Отсеки тупики: на алгоритмической задаче или скрытом тесте сбрось историю и запроси новое решение с нуля.
3. Сними трассировку одного теста: прогони только первый упавший тест и подпиши в комментарии # справа от каждой строки значения переменных, витки цикла и срабатывания условий (T или F).
4. Сожми длинные данные: списки длиннее 6 элементов сократи до первых 4 и последних 2 через троеточие, а общий объём удержи в пределах 6 000 символов.
5. Отправь на починку: передай размеченный код, входные данные, ожидаемый и фактический ответ с просьбой найти первую строку расхождения.

Примеры

[ПЛОХО] : Твоя функция вернула 4850 вместо 4250 на тесте. Исправь ошибку в коде.
[ХОРОШО] : Функция упала с AssertionError (ожидалось 4250, получено 4850). Вот твой код с построчной трассировкой первого упавшего теста в комментариях # справа от строк: [код с комментариями # gross = 3000, 5000, 8000 и # F, T, F]. Найди первую строку, где значения переменных расходятся с логикой задачи, и верни исправленную функцию.
Источник: On the Limits of Execution Traces for Repairing Assertion Failures in LLM-Generated Code
ArXiv ID: 2611.09212 | Сгенерировано: 2026-11-09 18:06

Проблемы LLM

ПроблемаСутьКак обойти
Модель застревает и возвращает тот же сломанный код при ошибке проверки ответаКод не падает с синтаксической ошибкой, но возвращает неверный результат на тесте (AssertionError). Ты отправляешь модели сообщение «ожидалось X, получено Y» и просишь исправить. Модель пытается прокрутить десятки шагов цикла «в голове», сбивается на промежуточных состояниях переменных и в четверти случаев присылает обратно абсолютно тот же кодДля прикладных задач с открытым тестом подай в промпт код с встроенной построчной трассировкой значений переменных # var = value только для первого упавшего теста. Если же тест скрыт или задача чисто алгоритмическая — не отлаживай код, а сбрось диалог и сгенерируй решение с нуля

Методы

МетодСуть
Построчная трассировка первого упавшего теста в комментариях кодаПри ошибке совпадения ответа (AssertionError) на открытом тесте прогони функцию через трассировщик только на ПЕРВОМ упавшем тесте. Допиши реальные значения переменных, витки циклов и срабатывания условий (T/F) прямо в комментарии # в конце каждой выполненной строки кода. Отправь размеченный код вместе со входом, ожидаемым и фактическим выводом и попроси найти первую строку, где состояние разошлось с логикой задачи. Почему работает: Голый факт неверного ответа в конце функции не говорит, какая строка испортила данные. Готовые числа справа от каждой строки заменяют ненадёжный устный счёт в голове модели простой сверкой значений. Когда применять: Прикладная бизнес-логика, парсеры и расчёты, где известен вход упавшего теста, особенно на крупных моделях. Когда не работает: Олимпиадные и сложные алгоритмические задачи (там неверен сам замысел алгоритма) и закрытые тесты без входных данных

Тезисы

ТезисКомментарий
Трассировка одного первого упавшего теста чинит код лучше, чем подача всех упавших тестов сразуКогда в запрос сваливают построчные логи сразу нескольких проваленных тестов, контекст раздувается в полтора раза и внимание модели рассеивается. Одного первого упавшего теста достаточно, чтобы увидеть конкретную строчку сбоя без информационного шума и перерасхода токенов. Применяй: При автоматической отладке всегда передавай в промпт разметку выполнения только для одного первого упавшего теста
На алгоритмических задачах и скрытых тестах перегенерация с нуля выгоднее пошаговой отладкиЕсли модель выбрала неверный алгоритм, построчная трассировка лишь честно покажет, как ошибочная идея штатно выполняет свои шаги. А при падении скрытого теста без входных данных модель получает слишком мало информации для точечной правки и просто портит код. Применяй: Если ошибка возникла в сложной алгоритмической задаче или на скрытом тесте, сразу сбрасывай контекст и запускай независимую генерацию решения с нуля
Крупные модели получают больше пользы от построчной трассировки, чем малыеСильная модель при простой перегенерации без новой обратной связи склонна уверенно воспроизводить одно и то же решение с тем же самым багом. Но когда ей показывают построчные значения переменных, она гораздо точнее слабых моделей находит место расхождения и исправляет код. Применяй: Используй построчную разметку переменных в первую очередь в пайплайнах с флагманскими и крупными кодовыми моделями
📖 Простыми словами

On the Limits of Execution Traces for Repairing Assertion Failures inLLM-Generated Code

arXiv: 2611.09212

Когда LLM пишет код, который выдает неверный результат, она оказывается в ловушке собственной логики. Проблема в том, что модель не «выполняет» код, а предсказывает его текст, пытаясь угадать значения переменных в уме. Это фундаментальный баг: даже если нейронка ошибается всего в 2% случаев на каждом шаге, через пятьдесят строк кода шанс на правильный итог падает до 36%. Обычное сообщение об ошибке вроде AssertionError дает модели ровно 1 бит информации — «ты не права», но не объясняет, где именно в цепочке рассуждений все пошло не так.

Это как если бы ты собирал шкаф из Икеи по памяти, а в конце у тебя осталась лишняя деталь. Ты понимаешь, что облажался, но не знаешь, на каком шаге: когда прикручивал дно или когда ставил полку в середине. Метод Breakpoint Debugging (BPD) — это когда тебе дают ту же инструкцию, но на каждой странице подписано: «на этом этапе у тебя должно быть вкручено 4 болта». Вместо того чтобы мучительно вспоминать процесс, ты просто сравниваешь реальность с картинкой и сразу видишь косяк.

Суть BPD проста: мы берем первый упавший тест и возвращаем модели ее же код, где в комментариях к каждой строке прописаны реальные значения переменных. Модели больше не нужно гадать, чему равен счетчик на третьем витке цикла — она видит готовые числа и просто сличает их с условием задачи. Выяснилось, что заваливать нейронку трейсами всех упавших тестов — плохая затея: контекст раздувается, внимание рассеивается, и результат становится хуже. Один четкий пример провала работает лучше, чем пачка логов.

Этот принцип универсален для любой сложной отладки, будь то расчет кешбэка или логика распределения заказов. Но есть нюанс: если модель изначально выбрала неверный алгоритм, никакая трассировка не поможет — она просто задокументирует, как код идеально выполняет глупость. Исследование показало, что для мощных моделей типа 72B трассировка жизненно важна, потому что без новой инфы они склонны упрямо повторять одну и ту же ошибку. А вот для мелких моделей иногда проще сбросить диалог и начать с чистого листа.

Короче: если код выдает чушь, не проси AI «просто исправить». Дай ей построчный трейс первого упавшего теста, чтобы превратить гадание на кофейной гуще в проверку фактов. Либо ты даешь модели конкретные цифры из памяти, либо она продолжает уверенно врать, глядя на свой же сломанный код. В мире, где SEO умирает, а GEO рождается, умение кормить AI правильным контекстом — единственный способ получить рабочий продукт, а не набор красивых, но бесполезных символов.

Работа с исследованием

Адаптируйте исследование под ваши задачи или создайте готовый промпт на основе техник из исследования.

0 / 2000
~0.5-2 N-токенов ~10-30с
~0.3-1 N-токенов ~5-15с