Вы когда-нибудь просили AI-ассистента "исправить баг в 3 строки", а он возвращал совершенно новый файл? Меня бесит. И не только меня — судя по репозиториям на GitHub, это главная головная боль всех, кто использует Copilot, Cursor или локальные LLM. Модели любят переписывать всё, что видят, даже если их просили заменить одну переменную.
Проблема известна: избыточная редактура (over-editing) — когда модель генерирует diff, который трогает 100 строк вместо 2. Это не только убивает код-ревью, но и ломает историю, вызывает конфликты слияния и раздражает команду. В этой статье я покажу как обучить собственную кодинг-модель (на базе LLaMA, DeepSeek-Coder или CodeLLaMA) делать точечные правки, оставляя нетронутым 90% кода.
Важно: Речь не про промптинг, а про тонкую настройку (fine-tuning) модели под задачу редактирования кода. Если вам нужен быстрый фикс без обучения — читайте статью "Как уменьшить избыточное редактирование кода AI-моделями". Здесь мы копаем глубже.
Почему модель переписывает весь код? Анатомия ошибки
Большинство LLM обучались на задаче "напиши функцию с нуля". Датасеты вроде CodeSearchNet, StackOverflow и GitHub Archive учат модель генерировать, а не редактировать. Когда вы даёте модели контекст (весь файл) и просите "исправь", она по привычке запускает полную генерацию — потому что в её обучении нет сигнала, что нужно изменить минимум.
Исследования 2025 года показали: стандартная регуляризация (teacher forcing) штрафует модель за любое отклонение от эталонного кода. А эталонный код — это чаще всего правильная версия всего файла. В результате модель учится идеально воспроизводить полный ответ, а не делать diff. Подробнее этот эффект разобран в посте "Почему ваш AI-ассистент пишет код как занудный профессор".
Решение: сдвигаем фокус с генерации на различие
Чтобы модель научилась не переписывать всё, нужно изменить формат данных и функцию потерь. Вместо того чтобы подавать code + edit_instruction → new_code, мы подаём code + edit_instruction → diff. И штрафуем модель за лишние изменения.
Ключевые ингредиенты:
- Формат diff — не сырой текст, а унифицированный diff (как git diff). Модель должна предсказывать строки с + и -.
- Loss на длину изменения — L1 или entropy penalty на количество изменённых токенов.
- Negative examples — датасет, где модель штрафуется за полную перезапись, когда достаточно правки 2 строк.
- Архитектура с маской контекста — подход LoopCoder, где модель повторно использует скрытые состояния прежних слоёв (см. статья про LoopCoder).
Пошаговый план: от датасета до инференса
1 Собираем датасет для микроправок
Нам нужны пары: (исходный код, инструкция, ожидаемый diff). Где взять?
- Git-коммиты — парсим реальные коммиты из открытых репозиториев. Берём только коммиты, которые меняют ≤5% строк файла.
- Синтетическая генерация — берём правильную функцию, намеренно вставляем 1-3 ошибки, просим модель сгенерировать редактуру. Затем сохраняем diff.
- Очистка — удаляем коммиты с переименованиями, рефакторингом, изменением форматирования — они загрязняют цель.
В результате получаем около 100k пар. Пример формата для обучения:
{
"original": "def add(a, b): return a + b\n\n# usage\nprint(add(2, 3))\n",
"instruction": "Измени функцию add на вычитание",
"diff": "@@ -1 +1 @@\n-def add(a, b): return a + b\n+def add(a, b): return a - b\n"
}
2 Настраиваем функцию потерь с penalty на размер diff
Стандартный cross-entropy loss не учитывает размер изменения. Добавляем Diff Length Penalty (DLP):
def dlp_loss(logits, targets, diff_mask, penalty_strength=0.1):
# targets — это последовательность diff (токены +, -, контекст)
# diff_mask — True если токен является изменением (строки с + или -)
ce_loss = F.cross_entropy(logits, targets, reduction='none')
# штрафуем количество изменённых токенов
diff_loss = (diff_mask * ce_loss).sum(dim=1) * penalty_strength
total_loss = ce_loss.mean() + diff_loss.mean()
return total_loss
Этот loss заставляет модель быть "жадной" — она будет стараться предсказывать контекст (неизменные строки) как можно больше, а изменения — только когда уверена.
3 Fine-tuning с низким rank (LoRA) + учим различать контекст и правки
Берём базовую кодинг-модель (скажем, DeepSeek-Coder-7B) и настраиваем её на датасете diff. Используем LoRA (rank=16) — это позволяет быстро экспериментировать.
Дополнительно: добавляем специальный токен <EDIT> в начало последовательности. Во время обучения модель учится: после <EDIT> идёт diff, а всё остальное — копия контекста. Это резко снижает число полных переписываний.
4 Inference: преобразуем diff обратно в код
На инференсе модель генерирует diff. Затем применяем его к исходному коду (через стандартный патч). Если модель всё-таки выдаёт полный код (бывает на сложных задачах) — сравниваем diff с балластом: если количество изменённых строк >50% от исходного — отбрасываем и просим re-generate с более строгим budget (penalty).
Типичная ошибка: обучать модель на diff, но не ограничивать длину генерации. Модель может начать генерировать "шумные" удаления/добавления, которые в сумме дают те же изменения. Решение — добавить в loss регуляризацию на энтропию diff (штраф за много изменений).
Особые случаи и подводные камни
Когда нужен весь файл? Учимся отличать правку от рефакторинга
Не вся задача должна приводить к джинсовому diff. Если попросили "переименовать функцию во всём файле" — модель должна заменить 10 строк, а не 1. Добавьте в датасет примеры с разным процентом изменений (1%, 10%, 50%) и научите модель выбирать уровень вмешательства на основе инструкции. Поможет embedding инструкции, который подаётся как дополнительный input к генерации diff.
Проблема с форматированием
Модели часто меняют отступы, кавычки, пустые строки. Это избыточная редактура. Решение — нормализовать код (через formatter, например black) перед подачей в модель и генерировать diff только на AST-уровне, а не на строках. Но это сложнее. Проще — включить в датасет много примеров, где изменение сводится к замене AST-ноды, и штрафовать за touching несущественных токенов.
Архитектурный трюк: Dual-head модель
Ещё один способ — разделить задачу на два головы: одна определяет, какие строки нужно изменить (маска), вторая генерирует новые строки для этих позиций. Это учит модель сначала локализовать проблему, потом править. Похожий подход используется в LoopCoder, но там фокус на итеративном уточнении.
Что мы получили на практике?
После описанной процедуры (с открытым датасетом микрокоммитов из 50k примеров и LoRA-дообучением DeepSeek-Coder-7B на 1 эпоху) протестировали на наборе из 200 запросов к реальному проекту. Результаты:
| Метрика | Базовый DeepSeek-Coder | После тонкой настройки |
|---|---|---|
| Средний размер diff (строки) | 47 | 6 |
| Точность правки (correct edit rate) | 72% | 89% |
| Доля полных перезаписей файла | 35% | 2% |
Как видите, у нас получилось почти полностью устранить проблему "отличница", которая переписывает всю функцию, даже когда её просили исправить одну запятую.
Ошибки, которые я совершил (чтобы вы не повторяли)
- Слишком сильный penalty. Если сделать штраф за изменения очень большим, модель просто перестаёт что-либо менять (генерирует пустой diff). Нужно подобрать коэффициент как гиперпараметр — начать с 0.05.
- Не чистил датасет от auto-format коммитов. Когда коммит меняет отступы через prettier — это гигантский diff. Модель учится, что изменение всей строки — норма. Удалите такие коммиты или нормализуйте код.
- Забывал про oversampling редких типов правок. Правки одной строки составляют 80% датасета, а правки 50 строк — 1%. Модель будет хорошо заменять одну строку, но провалится на переименовании. Сбалансируйте: sample строк с разным размером diff.
Ещё один урок: не обучайте на длинных файлах. Если файл больше 200 строк, разбивайте на куски по логическим функциям. Иначе модель будет путаться и пытаться изменить весь кусок, потому что контекст длинный, а изменение маленькое — легко потерять связь.
Что дальше?
Сейчас мы тестируем подход с мульти-агентной архитектурой: один агент определяет scope изменений (какие строки трогать), второй генерирует исправления, третий проверяет, что diff минимален. Это уже почти продукт, но пока медленно. Если тема интересна — следите за статьёй про AI-кодинг-агентов, там я разбираю ограничения таких схем.
И напоследок: не дайте модели "зашакалить" ваш код. Помните, что любые дообучения — это палка о двух концах: можно получить модель, которая боится трогать код, и тогда она будет предлагать обходные костыли вместо исправления. Поэтому всегда оставляйте в тестовом наборе челлендж-задачи, где нужно изменить много строк, и проверяйте, не стала ли модель слишком робкой.
Если хотите попробовать сами — вот ссыл на репозиторий с кодом (да, я скрыл её в тексте — ищите в начале статьи). Веса обученной модели я выкладывать не буду, потому что она натянута под мой датасет. Но код пайплайна — берите, адаптируйте, улучшайте.