Ломаные решения как источник тестов: как RobustTests чинит RL-обучение кода

16 сентября 202610 просмотров

Когда проверочных примеров мало, модель учится обходить проверку вместо того, чтобы писать корректный код. Фреймворк RobustTests строит тесты на основе «почти правильных» сломанных решений и добавляет пошаговую награду по pass rate — на Qwen3-32B это принесло +3% на LiveCodeBench.

Ломаные решения как источник тестов: как RobustTests чинит RL-обучение кода

Проблема: тестов мало, а награда за них — вся

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

На практике это допущение почти всегда нарушено. Набор тестовых случаев узкий, покрытие дырявое, и модель довольно быстро находит не задачу, а лазейку: подгоняет код под конкретные проверки вместо того, чтобы научиться решать класс похожих задач. Дальше запускается знакомая спираль — reward hacking, за которым идёт деградация политики. Модель теряет в общих навыках ровно настолько, насколько преуспевает в узкой хитрости.

Грубая аналогия: экзамен, состоящий из двух вопросов. Студент, который выучил ответы на эти два вопроса, получает пятёрку, но предмета не знает. И чем дольше длится такое обучение, тем хуже он становится во всём остальном.

Идея RobustTests: ошибки как генератор тестов

Обычно тесты придумывают, отталкиваясь от условия задачи: какие бывают входы, где края диапазонов, что произойдёт при пустом вводе. Логично, но именно этот путь и даёт узкое покрытие — автор тестов и автор решения смотрят на задачу с одной и той же стороны и одинаково слепы к одним и тем же местам.

RobustTests переворачивает процесс. В основе фреймворка — синтез тестовых случаев, управляемый ошибочным кодом (faulty-code-driven test case synthesis). Речь не о случайном сломанном коде, а о «почти корректных» решениях: тех, что отличаются от правильного небольшим изменением логики — перепутанным знаком сравнения, неверной границей цикла, пропущенной веткой. Каждое такое решение почти работает, и именно поэтому оно ценно.

Дальше ищется вход, на котором почти правильный код расходится с эталонным. Найденный вход и становится тестом. Такой тест обладает высокой диагностической силой: он не просто «что-то проверяет», а различает два близких поведения — то, которое мы хотим от модели, и то, которое выглядит правдоподобно, но ошибочно.

Работа описана в препринте arXiv:2608.24135 (Yiwen Zhang и ещё восемь авторов, среди них Xiaodong Yan, Zhenyu Huang, Deng Zhao и другие; v1 — 25 августа 2026, v2 — 27 августа 2026, DOI 10.48550/arXiv.2608.24135, принято к EMNLP 2026). Авторы относят материал сразу к двум разделам — cs.AI и cs.SE, что логично: это и про обучение моделей, и про инженерию тестирования.

Фильтрация: агенты-валидаторы и кластеризация

Любой автоматический синтез тестов легко превращается в генератор мусора. Часть придуманных входов окажется невалидной, часть будет дублировать друг друга, часть — проверять одно и то же поведение под разными углами. Полезного сигнала от такого набора мало, а шума много.

Поэтому в пайплайне предусмотрен второй слой — агенты-валидаторы, которые отсеивают некорректные и лишние тестовые случаи. К ним добавлена кластеризация по поведенческим признакам: тесты группируются по тому, какое именно поведение кода они различают, и дубли внутри группы схлопываются. В итоге остаётся компактный набор, где каждый элемент добавляет новую информацию, а не повторяет соседа.

Плотная награда вместо редкого сигнала

Второй компонент фреймворка касается не тестов, а того, как из них считается награда. Классический бинарный подход — «прошло всё или не прошло ничего» — плохо работает, когда тестов становится много и они разной сложности: модель решает почти всё, спотыкается на одном краевом случае и получает тот же ноль, что и полностью сломанное решение. Сигнал обучения обрывается, и учиться на нём почти нечему.

RobustTests вводит пошаговую плотную функцию вознаграждения (stepwise dense reward), опирающуюся на долю пройденных проверок — pass rate. Модель получает частичный сигнал и понимает направление движения: не «провал», а «осталось два теста из тридцати». Это решает сразу две задачи. Во-первых, снижает число ложных отрицательных срабатываний (false negatives), когда корректное решение забраковывается из-за слишком строгого или просто ошибочного теста. Во-вторых, делает обучение устойчивее: вознаграждение перестаёт быть редким событием и превращается в шкалу.

Отдельно стоит подчеркнуть связку этих двух идей. Плотная награда имеет смысл только тогда, когда тесты действительно различают разные типы ошибок, — иначе вы просто усредняете шум. А синтез тестов от почти правильных решений без плотной награды оставит всё тот же обрыв сигнала. Компоненты работают в паре.

Датасет и результаты

На этом пайплайне авторы собрали расширенную версию датасета CodeContests+ — с заметно более высокой диагностической полезностью: тестовые наборы стали точнее указывать, на каком именно шаге решения модель ошибается.

Главное измерение — не размер датасета, а поведение модели после дообучения. RL-тренировка Qwen3-32B с использованием RobustTests даёт абсолютный прирост 3% на LiveCodeBench. Код и данные авторы выложили в открытый доступ.

Здесь стоит соблюдать трезвость. Три процентных пункта на одном бенчмарке при дообучении одной модели — это не переворот в области, а аккуратное улучшение с понятным механизмом. Ценность работы лежит скорее в методологии: она предлагает воспроизводимый рецепт, как выжать из тестов больше сигнала, не расширяя их количество вручную. Ограничения тоже очевидны — перенос результата на другие модели, языки и типы задач ещё предстоит проверить.

Что из этого стоит забрать в свою практику

Даже если вы не собираете RL-пайплайн и просто оцениваете качество генерации кода, логика переносится почти без изменений:

  • Пишите тесты от ошибок, а не только от условия. Возьмите решение, которое почти работает, и найдите вход, где оно ломается. Такой тест почти всегда информативнее десятка придуманных «в лоб».
  • Считайте долю пройденных проверок, а не факт прохождения. Частичный балл даёт градиент и в обучении, и в аналитике качества — видно, где именно модель проваливается.
  • Фильтруйте синтезированные тесты. Без валидации и дедупликации автоматически сгенерированный набор быстро превращается в свалку похожих друг на друга проверок.
  • Следите за тем, что награда на самом деле поощряет. Плотная шкала уменьшает тягу к обходным путям, но не отменяет её: если тесты не различают поведение, любая награда рано или поздно будет взломана.

Идея, которую несёт RobustTests, по-человечески проста: лучший источник сложных тестов — не фантазия автора, а собственные почти-ошибки системы. То, что модель едва не сделала правильно, — самый честный индикатор того, где проходит граница между «похоже на решение» и «решение».

Часто задаваемые вопросы

Похожие материалы

Все материалы
RobustTests: тесты из ломаных решений для RL-обучения кода