Локальная модель Qwen2.5-Coder 7B нашла 11 дефектов из 13 на моих учебных примерах и ни одного на боевом коде. Один и тот же промпт, один и тот же каталог правил. Дальше - как я к этим числам шёл, где сам чуть не обманулся и почему на боевом коде вышел ноль.
Спор про то, годится ли нейросеть для проверки кода 1С, идёт на площадке постоянно, и идёт он на мнениях: одни говорят "попробовал, ерунда", другие "у меня работает". Мерить нечем - нужен набор кода, про который заранее известно, где там дефект, а где его нет. У меня такой набор оказался под рукой.
Дальше будут числа и четыре способа обмануться при их получении. Способы, кажется, полезнее чисел: свою проверку вы будете делать на своём коде, а обмануться сможете ровно так же.
Что было под рукой
Летом я писал анализатор кода внешних обработок на встроенном языке 1С (BSL): лексер, парсер, таблица типов переменных, 21 правило по тексту модулей и ещё семь по схемам компоновки. Никакого ИИ внутри нет: на одном и том же коде он всегда даёт один и тот же ответ.
Чтобы его откалибровать, пришлось собрать контрольный набор, дальше я зову его золотым. Взяли корпус обработок, прогнали по нему прототип на регулярках, получили 32 срабатывания и отдали каждое на три независимых прохода языковой модели. Задание у проходов было особое: считать вердикт-кандидат ошибочным, пока не доказано обратное. Читали объемлющие процедуры целиком, от заголовка до последней строки. Выходит, линейку для языковой модели частично размечала тоже языковая модель. Одна опора при этом есть: анализатор без всякого ИИ, который на этом наборе калибровался, с разметкой согласен.
Расклад: семь позиций - настоящие дефекты, двадцать - ложные срабатывания, пять - спорные. То есть точность прототипа на регулярках 22 процента: семь настоящих находок на тридцать два срабатывания, спорные в числитель не зачтены. Это, кстати, отдельный ответ тем, кто предлагает "да напиши регулярку и не мучайся".
Вот этот набор я и достал, чтобы померить языковую модель.
Как мерил
Всё локально, без единого запроса наружу. llama.cpp, модели Qwen2.5-Coder в трёх размерах: 1.5B, 3B и 7B, квантование q4_K_M (модель сжата до 4 бит). Машина обычная: ноутбучный i7 2019 года, шесть ядер, 16 гигабайт.
Модели отдавал каталог правил (идентификатор, уровень, заголовок) и процедуру. Просил назвать нарушенные правила либо ответить словом ЧИСТО. Каталог давал весь, из 28 правил вместе с компоновочными, столько было в движке на момент замера, хотя к модулю применяется только 21.
И сразу про решение, без которого весь замер был бы бессмысленным. Я считал два числа порознь: сколько настоящих дефектов модель назвала правильным правилом и сколько правил назвала там, где дефекта нет. Усреднять их нельзя.
Первый прогон: выглядело отлично
Золотой набор лежал на сетевой шаре, до которой ещё надо было добраться, и я начал с синтетики. Написал 20 процедур: двенадцать с ровно одним известным дефектом и восемь ловушек - код, на котором наивный детектор срабатывает зря. Вроде такой:
Функция СформироватьМакетыПоНастройкам(МассивНастроек, СхемаКомпоновки) Результаты = Новый Массив; КомпоновщикМакета = Новый КомпоновщикМакетаКомпоновкиДанных; Для Каждого Настройка Из МассивНастроек Цикл Макет = КомпоновщикМакета.Выполнить(СхемаКомпоновки, Настройка, , , Тип("ГенераторМакетаКомпоновкиДанных")); Результаты.Добавить(Макет); КонецЦикла; Возврат Результаты; КонецФункции
Выполнить() в цикле есть, только это сборка макета в памяти, к СУБД обращения нет. Регулярка тут срабатывает, и это ложняк.
| Модель | Нашла из 13 | Выдумала на 7 чистых | Промолчала на чистом | Сек на вызов |
|---|---|---|---|---|
| 1.5B | 9 | 109 | 3 из 7 | 9,0 |
| 3B | 11 | 194 | 0 из 7 | 24,1 |
| 7B | 11 | 0 | 7 из 7 | 32,8 |
Семёрка: одиннадцать дефектов из тринадцати и ни одной выдумки на чистом коде. Восемьдесят пять процентов полноты при нулевом шуме.
Вот примерно тут я и мог остановиться, написать "локальная модель находит 85 процентов дефектов" и пойти делать продукт.
Четыре способа обмануться, на которых я обманулся
Это главная часть статьи. Числа выше и числа ниже вы получите свои, а вот попасться сможете точно так же.
Способ первый: считать только полноту.
Посмотрите на колонку "выдумала" у младших моделей: сто девять и сто девяносто четыре при семи чистых кейсах. Разгадка простая: получив процедуру, они чаще всего называли каталог почти целиком. 3B назвала двадцать и больше правил в 17 процедурах из 20, в среднем 24,6 правила на процедуру, 1.5B - в 13 из 20 и дала всего шесть разных ответов на двадцать разных процедур. Код они почти не читали и пересказывали промпт.
При таком поведении дефекты "находятся" механически: назови все 28 правил, нужное окажется среди них. Формально 3B показала те же 11 из 13, что и семёрка. Мерил бы я одну полноту - в отчёт уехало бы "трёшка не хуже семёрки, берём её, она вдвое легче".
Проверка, которую стоит завести сразу: считать, сколько раз модель назвала двадцать и больше правил разом. Столько правил в одной процедуре это пересказ каталога, чтения кода за ним нет.
Способ второй: доверять своей разметке.
Двадцать процедур я написал сам и сам же разметил. Потом прогнал их через собственный анализатор - и на одной из ловушек он сработал.
Процедура ЗаписатьНастройкиПачкой(ТаблицаНастроек) НачатьТранзакцию(); Попытка Для Каждого Строка Из ТаблицаНастроек Цикл Запись = РегистрыСведений.НастройкиОбмена.СоздатьМенеджерЗаписи(); Запись.Узел = Строка.Узел; Запись.Значение = Строка.Значение; Запись.Записать(); КонецЦикла; ЗафиксироватьТранзакцию(); Исключение ОтменитьТранзакцию(); ВызватьИсключение; КонецПопытки; КонецПроцедуры
Я писал её как ловушку на правило "транзакция без отката": транзакция есть, откат есть, дефекта нет. И проглядел, что Запись.Записать() стоит внутри цикла. Настоящая проблема производительности, просто другого класса.
Движок был прав, я нет. Кейс переразмечен, цифры в таблице выше уже пересчитаны.
Способ третий: поверить своему инструменту замера.
Ранний прогон показал 147 выдумок на восьми чистых кейсах, около восемнадцати правил на процедуру. Для такой таблицы число обычное: у 3B потом вышло и больше. Подозрительным было другое - в поле "ответ модели" лежало эхо промпта. Консольная утилита сначала печатает баннер, потом повторяет весь запрос, и только затем отвечает. Моя чистка вывода срезала управляющие коды раньше, чем искала границу, и портила единственный надёжный маркер. Разбор потом честно выгребал из эха все 28 идентификаторов.
Позже тот же класс ошибки повторился при восстановлении золотого набора: я искал процедуры по строке кода из разметки, а такая строка встречается в корпусе десятки раз. Для одного кейса подтянулась чужая процедура, для другого - процедура без того самого пустого Исключение, ради которого кейс и заведён.
Общее у обоих случаев: цифры выглядели правдоподобно. Скрипт не падал и не ругался, числа спокойно вставали в таблицу и просились в публикацию.
Способ четвёртый, самый дорогой: мерить на коде, который написал сам.
Про него - весь следующий раздел.
Второй прогон, на настоящем коде
Синтетика хороша тем, что её быстро сделать. Плоха тем, что её делал я. Мои процедуры - от 9 до 25 строк, один подчёркнутый дефект, ничего лишнего. Настоящий код так не выглядит.
До шары я добрался и восстановил шестнадцать позиций золотого набора: пять настоящих дефектов, девять ложняков, две спорных. Процедуры от 13 до 378 строк, медиана 105. Живой код из боевых обработок, со всей его грязью. Та же семёрка, тот же промпт, тот же каталог.
| Показатель | Синтетика | Боевой код |
|---|---|---|
| Настоящих дефектов | 13 | 5 |
| Найдено | 11 | 0 |
| Ловушек, где детектор ошибся | 7 | 9 |
| Промолчала на них | 7 из 7 | 9 из 9 |
На пятнадцати процедурах из шестнадцати ответ был "ЧИСТО". Единственный раз, когда модель решилась что-то назвать - процедура на 24 строки с пустым блоком Исключение, который молча глотает сбой внешней отправки. Модель назвала исполнение кода из переменной и инъекцию во внешний запрос. Ни того, ни другого там нет.
При этом ложные срабатывания она не повторила ни разу: девять из девяти - молчание. На удочку, где регулярки дали 22 процента точности, языковая модель не ловится. Она осторожна. Только вместе с осторожностью не находит вообще ничего.
Это, если подумать, довольно точное описание сотрудника, который на любой вопрос отвечает "вроде нормально".
Почему первый набор льстил
Первое объяснение, которое приходит в голову - длина: на трёхстах строках внимание размазывается. Отбросить его мне нечем. Короткий боевой дефект в наборе был один, процедура на 24 строки с пустым блоком Исключение, и этот класс модель пропускала даже на синтетике. Дефекты тех классов, что она на синтетике находила, в боевом наборе все длиной от 146 строк. Длину и жанр мой набор развести не может.
Похоже, дело ещё и в жанре: учебный пример с одним подчёркнутым дефектом и боевая процедура, которая делает пять дел сразу, - разные задачи. Но это гипотеза, и я обязан назвать, чего не проверял. Не пробовал давать модели примеры боевого стиля перед вопросом. Не менял формулировку задачи под длинный код. Открыт и вопрос, не привычнее ли модели моя манера письма: синтетику писал я сам.
Как проверить это у себя за вечер
Дальше практическая часть. Мой результат вы повторять не обязаны: у вас другой код, и в этом весь смысл. Порядок такой.
Час на стенд. Скачать сборку llama.cpp под Windows: файл вида llama-bXXXXX-bin-win-cpu-x64.zip, это 18 мегабайт. Если есть видеокарта, брать вариант vulkan на 33 мегабайта: для него CUDA (пакет NVIDIA для вычислений на видеокарте) не нужна. Модель - Qwen2.5-Coder-7B-Instruct в формате GGUF (файл модели для llama.cpp), квант q4_K_M, 4,4 гигабайта, лицензия Apache 2.0. Запускается одной командой, в системе после неё ничего не остаётся.
Вечер на набор. Двадцать процедур из своих рабочих обработок, взятых как есть, без причёсывания. Половина с дефектами, которые вы знаете, половина чистых. И вот тут главное: чистые берите такие, где детектор по тексту сработал бы зря - Выполнить() у компоновщика, счётчик, к которому в цикле прибавляют единицу, Записать() у файла на диске. Именно на них видно разницу между разбором и угадыванием.
Полчаса на прогон. Отдать модели каталог своих правил и процедуру, попросить назвать нарушенные либо ответить одним словом. И записывать два числа порознь: сколько дефектов названо правильным правилом и сколько правил названо на чистых процедурах.
Пять минут на проверку самой проверки. Прежде чем верить итогу, посмотреть глазами три сырых ответа целиком. Именно то, что вернула модель, до всякой сводки. Если в ответе видно кусок вашего же вопроса - разбор сломан, и все цифры прогона можно выбрасывать. Если на двух разных процедурах ответ одинаковый - модель не читает код.
Сколько это занимает
Про время обычно пишут "медленно". Вот конкретика с боевых процедур, семёрка на шести ядрах без видеокарты:
| Длина процедуры | Время разбора |
|---|---|
| 250 строк | 370 секунд |
| 219 строк | 258 секунд |
| 146 строк | 141 секунда |
| 79 строк | 89 секунд |
| 24 строки | 38 секунд |
Шесть минут на процедуру в 250 строк. Это один прогон без повторов, так что разброс мне неизвестен - читать как порядок величины.
Резонное возражение: мерил на ноутбуке 2019 года без видеокарты, а у людей машины получше. Проверил и это, но на короткой задаче: семёрка пересказывала синтетическую процедуру в двух предложениях, ответ до 160 токенов. Сборка llama.cpp с Vulkan, часть слоёв на GTX 1650. Медиана вызова упала с 11,4 до 7,0 секунды, минус 39 процентов.
Переносить это на боевую процедуру напрямую нельзя: там основное время уходит на чтение кода, а я мерил короткий вход. При том же соотношении шесть минут стали бы почти четырьмя, но это уже арифметика, замера на длинном коде с видеокартой у меня нет. Упирается всё быстро: модель на 4,4 гигабайта в карту на четыре помещается только частично, дальше память кончается на кэше контекста.
Что я в итоге решил у себя
Поиск дефектов оставил обычному разбору. Лексер, парсер и таблица типов находят пустой блок Исключение и вложенность глубже пяти уровней за доли секунды. И ровно эти два дефекта модель пропустила даже на удобной для неё синтетике: задача структурная, уровни и пустые блоки считаются, понимать смысл для этого не нужно.
Модель оставил на объяснении уже найденного, и это я позже померил отдельно. Та же семёрка на шестнадцати объяснениях назвала 27 имён методов, 26 из них в коде действительно есть. На ста чужих обработках, уже с моделью общего назначения, выдуманных с нуля имён вышло ноль: разбор в статье Как прикрутить локальную модель к 1С, чтобы она не врала. С починкой хуже, поэтому способ починки в моём инструменте печатает движок.
Младшие модели первым делом проверяю на пересказ каталога. С моим промптом 3B и 1.5B каталог именно пересказывали, и переход к 7B оказался ступенькой: 24,6 правила на процедуру у трёшки против почти нуля у семёрки. Оговорка обязательна: промпт был один, вариантов вроде "назови только те правила, в которых уверен" я не пробовал.
Чего этот замер не говорит
Выборка на боевом коде мала. Пять дефектов, из них два - копии одной и той же процедуры на 378 строк. Первую копию модель разобрала подозрительно быстро, за минуту с небольшим, хотя процедура такой длины у неё идёт минут десять, вторую сервер отдал из кэша. Прочитала ли она их целиком, я не уверен, поэтому честный счёт такой: из трёх дефектов, которые модель точно видела целиком, она не нашла ни одного. На таком объёме это наблюдение, из которого никакого "модели не работают" не следует. Следует одно: проверьте на своём коде, прежде чем встраивать.
Восстановить удалось шестнадцать позиций из тридцати двух. Четыре из потерянных - сериализация управляемой формы, там объемлющей процедуры нет физически. Ещё восемь дали по нескольку одинаково подходящих кандидатов: одна и та же обработка встречалась в нескольких местах, и брать наугад я не стал. Оставшиеся четыре не нашлись.
Одно семейство моделей и одна задача. Всё в этой статье относится к поиску нарушений по готовому каталогу. Как та же модель объясняет найденное, я мерил отдельным заходом, числа выше и в статье про приём. Как чинит - в инструмент это вошло движком.
Только локальные модели, которые помещаются на рабочий ноутбук. Про облачные не утверждаю ничего: там другой класс и другой разговор с безопасником.
Гипотеза, которая не подтвердилась. По ходу дела возникла идея подложить модели справку по методам платформы, встреченным в коде, чтобы она не выдумывала несуществующие. Сделал адресную выборку, только по тем именам, что реально есть в процедуре. Стало хуже: со справкой модель нашла 8 дефектов вместо прежних 11, а время выросло с 33 секунд до 65. Модель стала осторожнее и потеряла три настоящих дефекта. Гипотезу не выбрасываю, переношу: справка задумывалась под генерацию исправлений, и мерить её надо там.
И законный вопрос после всего этого: если инструмент врал дважды, почему верить оставшимся цифрам. Отвечаю тем, чем каждая защищена. Ответы моделей идут через локальный сервер и приходят готовым JSON, консольного эха там нет по устройству; на синтетических прогонах отдельная проверка на эхо вопроса тоже молчит. Разметка синтетики сверена с самим анализатором, и одну ошибку он там нашёл. Позиции золотого набора восстановлены по двум независимым признакам сразу, а что не сошлось по обоим, в набор не попало.
Это перечень того, что можно перепроверить, не веря мне на слово.
Кому интересно посмотреть на сам анализатор, которым я всё это сверял, - он лежит отдельной публикацией: Анализ кода внешних обработок 1С. Языковой модели внутри нет, весь разбор на встроенном языке.
Другие наши инструменты для работы с нейросетями:
- ИИ-анализ кода 1С - схема, к которой привёл этот замер, в готовом виде: ищет движок, локальная модель только объясняет его находки, а способ починки печатается из реестра правил.
- Локальная LLM на встроенном языке 1С - бесплатная учебная модель на чистом BSL: видно, почему она отвечает одинаково уверенно и на вопрос, который знает, и на тот, которого не видела.
- Обучение на глазах - учебная модель на двенадцати шагах обучения: на промежуточных шагах она уверенно выдаёт имена методов, которых в платформе нет.
- Выгрузка метаданных для LLM - если модель пишет запросы или код под вашу конфигурацию: даёт ей настоящие имена объектов и таблиц запроса, чтобы она не достраивала регистры и реквизиты по памяти.
Вопрос, на который у меня ответа нет
Меня смущает одна вещь, и этими данными её не решить. Модель молчала на боевом коде, и длину я из причин не исключил: короткий боевой дефект был ровно один. Ложняки её при этом не путали - их она прошла все.
Похоже, разница в том, что мои примеры выглядят как задача: короткая процедура с одним дефектом посередине. А боевой код выглядит как код - в нём двадцать вещей происходит одновременно, и ни одна не подчёркнута.
Если это так, то любой замер на подготовленных примерах будет завышать результат систематически, всегда в одну сторону. И тогда обзоры вида "проверил пять моделей на пяти задачах" меряют не то, что нужно.
А у вас есть опыт, когда модель находила в боевом модуле то, что вы пропустили глазами? Мне интересен именно этот случай: если он у кого-то есть, значит, дело в моём промпте, и мерил неправильно я.
Вступайте в нашу телеграмм-группу Инфостарт