ACToR: 5% токенов решают, будет ли сгенерированный код рабочим
Когда языковая модель генерирует код для реального репозитория, она уверенно пишет 95% токенов. Проблема в оставшихся пяти: одно неверное имя метода, одна неправильная индексация, и весь последующий код уходит по ложному семантическому следу. Функция компилируется, выглядит правдоподобно и не проходит ни одного юнит-теста.
Именно на этом наблюдении построен ACToR (Adaptive Critical Token-Aware Retrieval), новый фреймворк от команды DeepSoftwareAnalytics, опубликованный на arXiv (2609.01601). Идея: не пытаться «улучшить retrieval в целом», а находить те самые решающие позиции прямо в процессе генерации и подавать контекст репозитория точечно, именно туда. Результат: прирост до 8,4% на RepoExec и до 15,4% на CoderEval относительно сильнейших baseline-методов, причём с меньшими вычислительными затратами, чем у итеративных подходов.
Что такое критические токены
Критический токен — это позиция в генерируемой последовательности, ошибка в которой меняет саму логику дальнейшей реализации и приводит к функциональному провалу всего кода. По данным авторов, таких позиций всего 5–11% от общего числа сгенерированных токенов, но именно на них сосредоточена бо́льшая часть ошибок и неопределённости модели.
Авторы приводят показательный пример из бенчмарка CoderEval. Функция popitem должна удалить из кэша самый ранний вставленный элемент, опираясь на репозиторную структуру self._order. Правильная реализация начинается с next(iter(self._order)). Но в решающей позиции модель генерирует self вместо next, и всё ломается: вместо итератора по структуре порядка вставки появляется вызов вида self.buffer.popitem(...), который игнорирует логику репозитория. Дальнейший код генерируется вокруг этой ошибочной операции и полностью расходится с задуманным поведением. При этом ошибка в «обычном» токене той же функции затрагивает лишь локальную деталь типизации и не рушит поток управления.
Второй пример показывает синтаксическое разнообразие проблемы. В функции normalize_cmd критическими оказались токены трёх разных типов: слово в комментарии (модель «выдумала» несуществующий параметр env), имя API (вместо репозиторного parse_filename сгенерирована наивная конкатенация строк) и индексное выражение (в normexe передан весь объект cmd вместо одного элемента). Фиксированным синтаксическим правилом такое разнообразие не поймать, а одноразовый retrieval перед генерацией не подстрахует все эти точки.
Почему классический RAG для кода не справляется
Существующие подходы к repository-level генерации, от RepoCoder до RLCoder, работают с контекстом на уровне задачи: извлекли релевантные фрагменты, положили в промпт, сгенерировали ответ. Даже итеративный RepoCoder, который уточняет запрос по мере генерации, делает это раундами, а не в момент самой ошибки. Неявное допущение такой схемы: один раз подобранный контекст обслуживает всю генерацию от начала до конца.
Но авторегрессионная генерация сильно зависит от пути. Разные позиции требуют разного контекста, и ошибка в решающей точке необратимо уводит траекторию в сторону. Получается разрыв: retrieval работает на уровне задачи, а ошибки рождаются на уровне отдельных токенов. ACToR закрывает именно этот разрыв.
Как устроен ACToR
Фреймворк состоит из двух фаз: офлайн-обучения детектора и онлайн-инференса с точечным retrieval.
Офлайн: учимся распознавать критические токены
Тренировочные данные строятся из RepoST-Train: после фильтрации (реальные проекты с более чем десятью звёздами на GitHub, не менее пятнадцати функций на репозиторий, без пересечения с тестовыми бенчмарками) остаётся 102 репозитория и 1203 функции с полными сигнатурами и docstring.
Каждый сгенерированный токен оценивается по трём критериям. Первый — token mismatch: top-1 предсказание модели расходится с эталонным токеном. Второй — неопределённость, измеренная энтропией распределения на этой позиции. Третий — последующее влияние через внимание: берётся среднее по top-5 весам self-attention от следующих токенов к текущему. Большой вес означает, что последующие позиции «опираются» на этот токен, и ошибка в нём распространится дальше. Top-5 вместо среднего по всем позициям выбран не случайно: усреднение размывает концентрированные зависимости, а единичный максимум чувствителен к шуму.
Чтобы ошибка самой модели не портила оценку последующих токенов, применяется teacher forcing: после оценки позиции предсказание заменяется эталонным токеном, и анализ продолжается на скорректированном контексте. Отдельная тонкость — дисбаланс классов: некритических токенов подавляющее большинство. Для баланса авторы отбирают «сложные негативы» по селф-информации токена, оставляя среди негативов те позиции, где модель менее уверена. Это улучшает качество обучающего сигнала.
Сам классификатор смехотворно мал по меркам LLM: ансамбль трёхслойных MLP на 4–10 млн параметров, который принимает скрытое состояние последнего слоя основной модели и выдаёт бинарную вероятность «критический / некритический». Для сравнения, backbone-модели в экспериментах — от 1,3 до 13 млрд параметров.
Онлайн: retrieval по требованию
Во время генерации ансамбль проверяет скрытое состояние каждого нового токена. Пока позиции некритичны, генерация идёт как обычно, без лишних затрат. Как только классификатор срабатывает, из текущего префикса строится новый запрос, выполняется свежий retrieval по репозиторию, контекст обновляется, и токен перегенерируется уже с точным контекстом. Поправка вносится ровно там, где она нужна, а не постфактум.
Второй компонент — позиционно-взвешенный retriever. Обычные dense-модели усредняют эмбеддинги токенов при пулинге, теряя информацию о том, где в фрагменте что находится. ACToR взвешивает токены по сумме двух гауссиан, сосредоточенных на концах фрагмента: начало (сигнатура функции) и конец получают повышенный вес. Простая идея, но, как показывает абляция, отказ от неё стабильно снижает качество на обоих бенчмарках.
Результаты: цифры
Эксперименты проведены на двух бенчмарках: RepoExec (355 задач на Python с кросс-файловыми зависимостями) и CoderEval (230 задач из реальных опенсорс-проектов, запуск в Docker). Генераторы — DeepSeekCoder 1.3B и 6.7B, CodeLlama 7B и 13B. Базовые линии: RawPrompt без retrieval, ванильный RawRAG, итеративный RepoCoder и RLCoder с обучением retriever через RL.
ACToR выигрывает почти везде. Наиболее впечатляющие результаты: CodeLlama-7B с ACToR показывает Pass@5 выше baseline на 8,4% на RepoExec и на 15,4% на CoderEval, а CodeLlama-13B достигает 39,57% Pass@5 на CoderEval — лучший результат среди всех конфигураций. Любопытная деталь: на DSCoder-6.7B фреймворк чуть проигрывает в Pass@1 (16,45% против 16,68% у RLCoder), зато выигрывает в Pass@3 и Pass@5. Авторы трактуют это так: точечная коррекция повышает разнообразие жизнеспособных решений, что важнее при нескольких попытках.
Отдельно стоит таблица эффективности. Средняя латентность на токен выросла лишь на 3,3 мс (22,6 мс против ~19), потому что пересчёт KV-кэша происходит только в редких критических позициях. Зато end-to-end время на сэмпл у ACToR 2,66 секунды против 4,50 у итеративного RepoCoder, а средняя длина сгенерированного кода — минимальная среди всех методов (94,6 токена). Точечный retrieval оказался не только точнее, но и дешевле многораундовой генерации.
Абляции подтверждают, что все компоненты работают. Удаление критерия token mismatch роняет Pass@1 на CoderEval на 37,9%, удаление критерия внимания — на 29,6%. Отключение динамического инференса с точечным retrieval даёт падение до 15,8%, а замена взвешенного пулинга на обычное усреднение — до 9,1%. Любопытно, что важность критериев зависит от бенчмарка: на CoderEval, где ошибки локальны, больнее всего терять mismatch, а на RepoExec с его длинными кросс-файловыми зависимостями критичнее сигнал внимания. При этом анализ чувствительности показывает устойчивость: на сетке из 25 комбинаций порогов дисперсия Pass@1 не превышает 2,83, так что метод не требует тонкой настройки.
Есть и честное ограничение. Пороги срабатывания детектора подобраны эмпирически (неопределённость 0,8, влияние внимания 0,05), обучение классификатора привязано к конкретному backbone, а все эксперименты — на Python и моделях до 13B. Как детектор поведёт себя на современных моделях в 70B+ или на других языках, авторы не проверяли.
Что интересного в самих критических токенах
Анализ состава критических токенов дал несколько нетривиальных выводов. Во-первых, пересечение между токенами с высоким вниманием и токенами с ошибками мало: модели концентрируют внимание на том, в чём уверены, а не на том, где ошибаются. Во-вторых, по синтаксису ошибки и неопределённость концентрируются в ключевых словах языка: они перепредставлены в категории «mismatch + uncertainty» в 7,64 раза относительно своей обычной частоты, и в 6,05 раза в категории «только mismatch». Идентификаторы встречаются среди ошибок чаще по абсолютному числу, но ключевые слова непропорционально опасны. А вот внимание модели направлено на структурные элементы: операторы перепредставлены в категории «только внимание» в 1,86 раза. Иными словами, модель смотрит на скелет кода, а спотыкается о ключевые слова.
С ростом масштаба картина тоже показательна. У DeepSeekCoder переход с 1.3B на 6.7B заметно снижает долю mismatch (с 3,29% до 2,43%) и неопределённости, у CodeLlama эти метрики почти не меняются между 7B и 13B, зато в обеих сериях растёт точность фокусировки внимания на важных токенах. Похоже, что масштаб улучшает генерацию кода не столько через «меньше сырых ошибок», сколько через более избирательное внимание.
Почему это важно
Работа меняет рамку разговора о RAG для кода. Вместо вопроса «какой контекст извлечь для задачи» ставится вопрос «в какие моменты генерации модели реально нужна помощь». Это сдвиг от статического пайплайна к динамическому, от контекста-на-всю-задачу к контексту-по-требованию. И это дёшево: детектор на несколько миллионов параметров и редкие точечные запросы вместо итеративных раундов генерации.
Код и данные открыты: github.com/DeepSoftwareAnalytics/ACToR. Для практиков, которые строят кодовые ассистенты поверх open-source моделей, это готовый паттерн: лёгкий классификатор поверх скрытых состояний плюс триггер retrieval по требованию — без переобучения основной модели.
Часто задаваемые вопросы
Чем ACToR отличается от обычного RAG для генерации кода?
Обычный RAG извлекает контекст один раз перед генерацией и использует его для всей задачи. ACToR отслеживает каждый сгенерированный токен, распознаёт критические позиции (всего 5–11% от всех) и выполняет свежий точечный retrieval только там, где ошибка необратимо уведёт код по неверному пути.
Насколько дорого обходится проверка каждого токена?
Недорого. Детектор — ансамбль MLP на 4–10 млн параметров против миллиардов у основной модели. Латентность на токен растёт всего на 3,3 мс, а общее время генерации ниже, чем у итеративного RepoCoder (2,66 с против 4,50 с на сэмпл), потому что коррекция запускается редко.
На каких моделях проверяли ACToR?
На DeepSeekCoder 1.3B и 6.7B и на CodeLlama 7B и 13B, с UniXcoder в роли retriever. Прирост воспроизводится на всех четырёх моделях и обоих бенчмарках, что говорит о переносимости подхода между архитектурами и масштабами.
Итог
Ошибки при генерации кода не распределены равномерно: они сосредоточены в малой доле критических токенов, которые определяют судьбу всей функции. ACToR первым систематически измерил этот феномен и построил вокруг него рабочий фреймворк: лёгкий детектор критичности на скрытых состояниях, точечный retrieval по требованию и позиционно-взвешенный пулинг. Прирост до 15,4% на CoderEval при меньшем времени генерации, чем у итеративных методов, делает подход практически привлекательным. Если вы строите кодовые ассистенты, идея «retrieval в момент ошибки, а не до него» определённо заслуживает эксперимента на вашем стеке.