Кодинг-модели: как честно проверить их на реальном баге
Функция из четырёх строк должна возвращать первые n символов строки. На «rustacean» она работает. На «café» падает с паникой. В этой панике спрятано всё, чего не показывают лидерборды кодинг-моделей.
Разработчик под псевдонимом Athreya aka Maneshwar превратил баг в воспроизводимую методику сравнения моделей (оригинал на dev.to): один баг, один промпт, три модели и вердикт по выводу компилятора вместо уверенности объяснений. Ценность здесь не в трёх конкретных моделях, а в ловушках. Их легко не заметить, и каждая превращает сравнение в фикцию.
Баг, который ломается только на Unicode
Вот проблемная функция:
pub fn prefix(input: &str, n: usize) -> &str {
if n >= input.len() {
return input;
}
&input[..n]
}
Замысел: взять первые n символов. Реальность: input.len() считает байты, а не символы. В UTF-8 буква «é» занимает два байта, поэтому в строке «café» четыре символа и пять байт. Срез &input[..4] разрезает «é» пополам, а Rust такое запрещает: срез &str обязан заканчиваться на границе символа. Паника на валидной строке.
Тестов в статье три. ascii проверяет, что «rustacean» с n=4 даёт «rust». unicode проверяет, что «café» с n=4 возвращает «café», и на сломанном коде падает. beyond_length проверяет «é» с n=10. Последний тест на сломанном коде проходит, и это не мелочь: в разделе о ловушках станет понятно почему.
Как тест-кейс баг хорош по трём причинам. Исправление обязано сохранить тип &str, то есть обойтись без String и лишних аллокаций. Нужно аккуратно обойти границы символов. И нельзя сломать уже работающие случаи. Одна строка кода, а требований на три абзаца.
Почему лидерборды кодинг-моделей этого не видят
Публичные рейтинги измеряют усреднённые проценты на чужих задачах. Модель может писать красивые объяснения и при этом выдавать код, который не компилируется. Тезис статьи: патчи надо судить по выводу компилятора, а не по уверенности объяснений.
Это не отменяет рейтинги полностью, но расставляет приоритеты. Мы уже разбирали, почему глобальные рейтинги моделей почти случайны: 72% голосов взаимно отменяются, а вероятность победы первой модели над пятидесятой всего 53%. И у нас есть большой честный бенчмарк кодинг-агентов на реальном проекте, где восемь комбинаций инструментов и моделей писали код для продакшена. Логика одна: чем ближе тест к вашему коду, тем меньше в нём шума.
Как устроен честный тест
Промпт для всех моделей одинаковый. В переводе он звучит так: «Исправь функцию. Она должна возвращать первые n Unicode-скаляров без паники. Сохрани тип возврата &str. Кратко объясни баг, верни полный src/lib.rs и добавь регрессионный тест для эмодзи и комбинирующего символа. Не добавляй внешних зависимостей». Исходный файл прикладывается целиком, и ни одна модель не узнаёт о провалах других.
Условия тоже одинаковые: temperature 0, одни и те же системные инструкции, одинаковый лимит вывода. Температура 0 снижает случайность, но не гарантирует идентичные ответы при повторных вызовах, поэтому сохранять нужно всё: сырой ответ, время, число входных и выходных токенов и точный идентификатор модели из API.
Эталонное решение, которое в статье называют оракулом, использует char_indices:
pub fn prefix(input: &str, n: usize) -> &str {
let end = input
.char_indices()
.nth(n)
.map(|(index, _)| index)
.unwrap_or(input.len());
&input[..end]
}
char_indices() идёт по границам символов, nth(n) даёт смещение после n символов, а unwrap_or закрывает случай, когда n больше длины строки. Оракул держат вне промптов: его задача проверять чужие патчи, а не подсказывать их.
Проверочный набор шире, чем в статье: ASCII, многобайтовые символы, пустая строка, n=0, n больше длины, эмодзи, разложенные диакритики. Вердикт выносит cargo test, а не судья-человек и не сама модель.
Три модели, один промпт
Кандидаты: DeepSeek V4 Pro, Claude Fable 5.1 и Grok Build 0.1. Точные идентификаторы сверяют с каталогом API до запуска, а каждую первую попытку сохраняют без правок.
Что смотреть в патчах. У DeepSeek: итерацию по границам символов вместо произвольного байтового индекса. У Fable: сохранён ли &str и как обработан комбинирующий символ. Тонкость: запрос одного скаляра от «e с акцентом» может вернуть просто «e», и по условиям этой задачи такое поведение правильно, потому что скаляры и графемные кластеры это разные единицы. У Grok: не поменялся ли API, не появились ли зависимости, не превратился ли &str в String. Такие правки важны, даже если видимый результат выглядит корректно.
Столбцы бланка отвечают каждый за своё. «Первая попытка» показывает, нужен ли модели второй шанс, и это главный практический показатель, потому что в работе вы тоже не всегда готовы объяснять задачу заново. «Число попыток» отделяет модель, которая чинит код сразу, от той, которая чинит его после подсказки. «Время» и «токены» переводят качество в деньги: два одинаково верных патча могут отличаться по стоимости в разы.
Таблицы с победителем в статье нет, и это осознанный выбор автора. Пустой бланк со столбцами «первая попытка», «число попыток», «время», «токены» это рабочий лист для повторения, а не рейтинг. Воспроизводимый артефакт здесь код и логи, а не картинка с местами.
Три ловушки, которые ломают даже такой тест
Поднимает их комментатор под ником Omega, по собственному признанию AI-агент, который проверяет утверждения по первоисточникам. Лучший разбор методики сравнения моделей написал агент, и в этом есть ирония.
Первая: тесты неравноценны, и модель их читает. beyond_length проходит на сломанном коде, потому что ранний return срабатывает раньше среза. Значит, сигнал несёт только тест unicode, а он открыт модели. Дальше хуже: промпт просит модель добавить свои регрессионные тесты, и если вердикт выносят тесты, которые модель видела или писала сама, исполнитель сам себе ставит оценку. Различающие кейсы надо держать скрытыми и считать только их.
Вторая: форму Unicode выбирает тест, а не модель. Собранная «é» (U+00E9) это четыре скаляра: c, a, f, é. Разложенная (U+0065 U+0301) это пять скаляров: c, a, f, e и комбинирующий акут. Строки выглядят одинаково, но «первые четыре скаляра» для них дают разные ответы. Тест, привязанный к одному представлению, завалит патч, который на другом представлении абсолютно корректен. Два файла выглядят идентично, вердикты разные, код не менялся.
Третья: общий шлюз это общая переменная. Если все три модели вызываются через один gateway с одинаковой системной обёрткой, дефолтами и обрезкой, этот слой влияет на все ответы сразу, и «три модели» перестают быть независимыми наблюдениями.
От себя добавлю четвёртую: один баг ничего не доказывает. Микробенчмарк показывает поведение в одном конкретном failure mode и не отвечает на вопрос, какая модель лучше вообще. Выбор модели это серия таких тестов из вашего домена, а не один красивый прогон.
Как повторить это на своём коде
Процедура короткая. Создаём библиотеку командой cargo new unicode-prefix --lib, заменяем src/lib.rs на сломанную версию, убеждаемся, что cargo test воспроизводит панику. Отправляем одинаковый промпт каждой модели и сохраняем сырой JSON, таймстампы, токены и версию модели. Каждую попытку извлекаем в чистое рабочее дерево и прогоняем cargo fmt --check, cargo test и cargo clippy. Первую попытку не трогаем: провалы часть результата.
Структура репозитория из статьи:
baseline/src/lib.rs
oracle/src/lib.rs
prompts/task.txt
runs/<model>/response.json
runs/<model>/cargo-test.txt
Главный совет: не копировать именно этот баг, а взять свой. Любой баг из трекера, который вы уже починили, это готовый тест-кейс: у него есть верное решение, воспроизведение и критерий приёмки. Десяток таких кейсов из вашей кодовой базы даст более полезную картину, чем публичный рейтинг, потому что измерит ровно то, что ломается у вас.
Если трекер пустой, кейсы берутся из истории коммитов: каждый багфикс это готовый тест. Откатите фикс, оставьте тест и посмотрите, восстановит ли модель поведение. Лучшие кандидаты это всё, что связано с границами: срезы строк, округления, таймзоны, конкурентный доступ. Модели особенно уверенно ошибаются там, где код выглядит простым, а условие спрятано в деталях.
Микробенчмарк и большой бенчмарк: зачем оба
Сравнение на одном баге не заменяет большие бенчмарки, а дополняет их. Большой тест отвечает на вопрос, как модели ведут себя в среднем на широком классе задач, и работает фильтром: если модель проваливает базовые вещи, до ваших задач она не дойдёт. Микробенчмарк отвечает на другой вопрос: как эта модель ведёт себя на том, что ломается именно у вас. Первый даёт контекст, второй даёт решение.
Работают они вместе. Сначала большой бенчмарк отсеивает явных аутсайдеров, потом серия микробенчмарков из ваших багов выбирает между финалистами. Публичный рейтинг остаётся справочной информацией, а не аргументом: расхождение между рейтингом и вашими прогонами это не ошибка измерения, а сигнал, что ваши задачи не похожи на те, на которых считали рейтинг.
Часто задаваемые вопросы
Почему нельзя просто доверять лидербордам кодинг-моделей?
Рейтинги усредняют по чужим задачам и легко загрязняются: тесты попадают в обучающие данные, а формулировки подгоняются под метрику. Высокий процент на публичном наборе говорит о совпадении с этим набором, а не о том, как модель справится с багом в вашем проекте.
Что такое Unicode-скаляр и почему тесты на него ломаются?
Unicode-скаляр это кодовая точка, одна единица в терминах Rust. Но буква с диакритикой бывает одним скаляром (é) или двумя (e плюс комбинирующий акут), а эмодзи собираются из нескольких скаляров. Тест обязан явно фиксировать, что он считает символом: скаляр, графемный кластер или байт.
Сколько задач нужно, чтобы вывод о модели был надёжным?
Один баг показывает только один failure mode. Для практического выбора нужна серия из 10–20 задач из вашего домена: типовые фичи, сложные баги, рефакторинги. Смотрите на долю патчей, прошедших с первой попытки, и на характер правок, а не на средний балл.
Итог
Методика умещается в один абзац: возьмите реальный баг, зафиксируйте один промпт, держите различающие тесты скрытыми, судите патчи по выводу компилятора и не верьте объяснениям. Три модели из примера, DeepSeek V4 Pro, Claude Fable 5.1 и Grok Build 0.1, здесь второстепенны: важна дисциплина сравнения, которую они иллюстрируют.
Следующий шаг простой. Откройте свой трекер, найдите баг, который вы недавно починили, и прогоните его через две-три кодинг-модели, которыми пользуетесь каждый день. Час работы, и у вас появится метрика, которой нет ни у одного лидерборда: как эти модели работают именно на вашем коде.