Мой блог
Completion gate для локальных моделей: как отделить завершённый ответ от оценки качества
Зачем нужен Completion Gate при тестировании локальных нейросетей
Во время одного из экспериментов по тестированию локальных языковых моделей я зафиксировал показатели, которые идеально смотрелись бы в итоговом отчёте: сразу три тестовых маршрута успешно справились с десятью сценариями из десяти. Быстрый маршрут продемонстрировал медианное время выполнения в 28,9 секунды, более тяжёлой конфигурации потребовалось 99,1 секунды, а комбинированный вариант показал результат 71,0 секунды.
Если делать выводы только по колонке принятых задач, все три подхода выглядят абсолютно одинаковыми. Если подключать к анализу время генерации, то явным лидером кажется наиболее быстрый вариант. Однако если взглянуть на количество автоматически исправленных ответов, картина снова меняется: быстрому маршруту понадобилось три авторемонта, тяжёлому — два, а смешанному — всего один.
Но даже с учётом этих данных выбирать модель рано. Подобная сводка показывает только одно: прошла ли связка из нейросети и технической обвязки текущий набор проверок. Она никак не отражает, насколько полезен полученный документ и смогут ли специалисты реально использовать его в работе.
Проблема заключалась в некорректном выборе единицы измерения. Я объединил в единый показатель факт завершения генерации, валидность JSON-структуры, наличие всех требуемых документов, прохождение автоматических тестов и качественную человеческую оценку. В итоге сформировался высокий процент успеха (pass rate), за которым невозможно было разглядеть реальные причины ошибок и сбоев.
Чтобы решить эту проблему, я внедрил специальный шлюз завершённости — completion gate. Его задача заключается не в оценке логики или стиля ответа, а в ответе на базовый вопрос: создан ли полный и цельный артефакт, который в принципе допустимо передавать на этап проверки качества.
Что именно проверялось в рамках теста
Вместо произвольного текста локальная модель должна была сгенерировать единый JSON-объект, содержащий четыре отдельных документа в формате Markdown:
{
"artifacts": {
"source_map": "# Source Map\n...",
"system_context": "# System Context\n...",
"review_findings": "# Review Findings\n...",
"task_pack": "# Task Pack\n..."
}
}
В этих документах описывались первоисточники, системный контекст, обнаруженные противоречия в требованиях и готовые задачи для разработки. Ключевую роль играли не сами названия блоков, а конкретные проверяемые свойства:
- Каждое утверждение должно опираться на указанный источник.
- Отсутствующие сведения запрещено додумывать или гипотетически достраивать.
- Несогласованные между собой требования должны оставаться явным конфликтом.
- Каждая задача на разработку должна содержать ссылку на решение и чёткие критерии приёмки.
- Итоговый объект должен включать готовые документы, а не обещать сформировать их в будущем.
Такой жесткий формат дает возможность выполнять автоматические проверки, но одновременно создает опасную иллюзию: валидный с точки зрения структуры JSON легко ошибочно принять за готовый к работе результат.
Иллюзия валидного JSON и классические ошибки генерации
Рассмотрим пример объекта, который успешно проходит первичную проверку типов данных:
{
"artifacts": {
"source_map": "results/source-map.md",
"system_context": "results/context.md",
"review_findings": "results/findings.md",
"task_pack": "results/tasks.md"
}
}
Все четыре поля являются строками, схема формально удовлетворена. Однако вместо полноценных документов пользователь получает лишь пути к несуществующим файлам.
Второй вариант внешней имитации ответа выглядит более объёмным:
{
"artifacts": {
"source_map": "# Source Map\nИсточники перечислены в проекте",
"system_context": "# System Context\nСистема описана в документации",
"review_findings": "# Review Findings\nПротиворечий не обнаружено",
"task_pack": "# Task Pack\nТребования необходимо реализовать"
}
}
Снова формальная проверка завершается успехом: ключи на месте, строки не пустые, заголовки присутствуют. Но для реальной работы такой ответ абсолютно бесполезен, так как состоит из бессмысленных заглушек.
Существует и еще одна частая проблема: модель корректно генерирует три документа, но на середине четвёртого среда исполнения прерывает работу из-за тайм-аута, нехватки оперативной памяти или лимита контекста. Оценивать такой оборванный ответ по критериям качества бессмысленно — сам объект оценки просто не успел сформироваться.
Почему одного показателя Pass Rate недостаточно
Утверждение pass rate = 80% не дает никакого представления о том, что именно находится в знаменателе этой дроби. В эти 80% могли попасть как ответы, идеально соответствовавшие схеме с первого раза, так и результат после исправления формата, логически завершенные документы с содержательными ошибками или даже варианты, забракованные человеком при финальной приемке.
Каждая из этих ситуаций требует совершенно разных действий по исправлению:
- Превышение времени (тайм-аут): требует настройки среды исполнения, снижения параллелизма или оптимизации длины генерации.
- Сбой структуры JSON: указывает на проблемы в промпте, валидаторе или контракте.
- Упущенное требование: требует доработки контекста, системного запроса или замены модели на более внимательную к деталям.
- Неверный критерий приёмки: говорит о необходимости пересмотра самого тестового сценария.
Сведение всего к одной цифре уничтожает диагностическую ценность бенчмарка. По этой причине я разделил процесс обработки на четкие последовательные этапы:
started → endpoint_completed | timeout | crash | out_of_memory → output_present → parseable → schema_valid_first_pass ↘ deterministic_repair → schema_valid_after_repair → artifacts_present → artifacts_non_placeholder → quality_eligible → quality_passed | quality_failed → accepted_by_human | rejected_by_human
Все шаги до статуса quality_eligible определяют исключительно завершённость результата. Только после пересечения этого рубежа начинается проверка содержимого, а окончательный вердикт выносит человек.
Метрики и их знаменатели
| Метрика | Знаменатель | Что показывает |
|---|---|---|
| Endpoint completion | Все запуски | Вернула ли среда исполнения хоть какой-то ответ без аварии. |
| Parse rate | Завершённые ответы | Способен ли парсер успешно распарсить результат как JSON. |
| First-pass schema rate | Разобранные ответы | Соблюла ли модель требуемую структуру с первой попытки без ремонта. |
| Artifact completion | Ответы по схеме | Присутствуют ли все обязательные документы в ответе. |
| Quality pass rate | Только quality_eligible | Прошли ли полноценные документы все сценарные проверки. |
| Human acceptance | Проверенные документы | Готов ли эксперт взять данный результат в реальную работу. |
Функционал и работа Completion Gate
Шлюз завершённости не оценивает точность выводов, стиль написания или глубокую логику ответа. Его задача — убедиться, что базовые условия для качественной оценки выполнены.
В моей реализации шлюз проверяет следующий набор критериев:
- Генерация завершена без ошибок timeout, crash или OOM.
- Исходный ответ зафиксирован и сохранен в базе.
- Сформированный объект успешно парсится в JSON.
- Структура полностью соответствует заданной версии JSON Schema.
- Все четыре требуемых документа присутствуют внутри объекта.
- Содержимое каждого документа содержит реальный текст, а не пустые заглушки или ссылки на пути.
- Факт и детали применения авторемонта зафиксированы отдельно.
Фильтрация заглушек требует особого внимания. Простой запрет отдельных стоп-слов не работает: модель быстро начинает формулировать фразы-заменители. Я применил комбинацию жестких признаков: проверка минимальной длины чистого текста после удаления заголовков, поиск обязательных структурных элементов, блокировка строк, похожих на путь к файлу, отслеживание обещаний вроде «описание будет добавлено позже», а также сохранение конкретной причины отклонения вместо общего флага false.
Как перепроверка изменила исторический знаменатель
В сохраненном датасете прошлых запусков по одному из сценариев содержалась 21 запись. После интеграции completion gate я прогнал эти данные через новый фильтр. Результат оказался показательным: только 4 записи из 21 содержали действительно законченный результат, пригодный для смыслового анализа. Остальные 17 прогонов были либо оборваны, либо содержали повреждения и заглушки.
Это не означает, что модель стала работать лучше или хуже — я не пересчитывал содержательные оценки и не перезапускал генерацию. Изменился состав объектов, допустимых к участию в анализе качества.
До внедрения шлюза оборванный JSON получал ноль баллов за анализ требований. Теперь он получает статус incomplete. Это гораздо точнее отражает реальность: обрыв строки не говорит о «неумении» модели анализировать контекст, он свидетельствует о сбое на этапе генерации.
Разделение работает и в противоположную сторону. Наличие валидной JSON-схемы больше не является эквивалентом успеха. Если все поля заполнены формальными фейковыми фразами, запись просто не доходит до стадии quality_eligible.
Анализ результата «10 из 10» и скрытые подводные камни
В следующем эксперименте использовались десять различных ролевых сценариев: составление черновика, поиск упущенных условий, выявление конфликтующих требований, формирование пакета задач и обработка длительного документа.
Все три тестируемых маршрута смогли пройти контракт во всех 10 сценариях:
| Маршрут | Принято | Медиана времени | Число ремонтов |
|---|---|---|---|
| Qwen-only | 10 из 10 | 28,9 с | 3 |
| Bonsai 8B-only | 10 из 10 | 99,1 с | 2 |
| Смешанный | 10 из 10 | 71,0 с | 1 |
Эти цифры позволяют сделать лишь один вывод: каждая из трех конфигураций в состоянии выполнить данный набор задач, но цена по времени и количеству автоисправлений различается. Объявлять победителя по такой таблице нельзя по четырем ключевым причинам:
- Автоматический ремонт: показатель «принято» включает результаты, исправленные детерминированным парсером. Без него три ответа Qwen, два ответа Bonsai и один результат смешанного маршрута не прошли бы валидацию. Мы оценивали связку модель + промпт + схема + рантайм + ремонт, а не чистую нейросеть.
- Отсутствие состязательных тестов: набор не содержал сложных проверок на подмену инструкций, утечку приватных данных и специально заложенных логических ловушек. Успех на базовых сценариях нельзя экстраполировать на безопасность.
- Ограниченность превью: в логах сохранялись лишь фрагменты ответов (
outputPreview). По ним был рассчитан семантический разрыв (preview_based_semantic_diff: 0,82, 0,87 и 0,88), что помогает выбрать вектор дальнейших тестов, но не доказывает превосходство какой-либо модели. - Единичные прогоны: данные показывают принципиальную возможность пройти тест, но не гарантируют стабильность на длинной дистанции. Для подтверждения воспроизводимости необходима серия прогонов на зафиксированном окружении.
Оценка не просто модели, а всего маршрута (Pipeline)
Время получения первого токена или первичного ответа редко отражает реальное время до получения готового результата. После генерации системе может потребоваться разбор JSON, исправление ошибок, повторные запросы и автоматические проверки.
Единицей сравнения должен выступать полностью зафиксированный маршрут:
модель + квантование + сборка рантайма + параметры генерации + версия промпта + JSON Schema + политика ремонта + профиль оборудования
Изменение хотя бы одного компонента создает новую конфигурацию. Успех Qwen с одним чат-шаблоном нельзя автоматически переносить на другой шаблон. Точно так же обновление парсера не делает модель «умнее» — оно лишь делает более надежным конкретный маршрут.
Разные задачи требуют разных ресурсов. Для быстрых черновиков критична минимальная задержка. Для поиска логических противоречий необходима способность удерживать сложный контекст. А для проверки валидности структуры нейросеть вовсе не нужна — с этим эффективнее и надежнее справляется обычный детерминированный код.
На основе этого формируется гипотеза ролевого маршрутизатора:
- Быстрый черновик → легкий и быстрый маршрут.
- Поиск глубоких пробелов и противоречий → тяжелая модель.
- Проверка структуры и ссылок → детерминированный код.
- Исправление мелких ошибок формата → ограниченный авторемонт.
- Финальное решение о приемке → эксперт-человек.
Что сохранять для воспроизводимости экспериментов
Одно лишь название модели и итоговый балл не позволяют воспроизвести бенчмарк. Для каждого запуска я сохраняю или планирую сохранять в логах следующий набор параметров:
run_id, идентификаторы тестового набора и сценария;- Точный артефакт модели, степень квантования и хэш версии;
- Сборку среды исполнения и используемый шаблон чата;
- Параметры генерации (temperature, top_p и др.);
- Версии промпта и структуры JSON Schema;
- Хэш исходных входных данных;
- Полный сырой ответ модели и разобранный объект;
- Количество входных и выходных токенов;
- Время выполнения и пиковый расход памяти;
- Детализированные статусы ошибок (timeout, crash, OOM);
- Тип примененного авторемонта и количество попыток;
- Статус прохождения completion gate;
- Результаты сценарных проверок и итоговый вердикт человека с причиной отклонения.
Любой повторный запуск должен получать новый run_id. Перезапись неудачных попыток ради красивой итоговой таблицы уничтожает историю эксперимента и скрывает реальную нестабильность системы.
Три уровня проверки: Контракт, Сценарий и Человек
Детерминированный код убирает фактор субъективности, когда нейросеть оценивает сам факт своей работы, но он не гарантирует абсолютную истинность проверок. Программа может проверить наличие четырёх заголовков, но пропустить пустой текст под ними. Чтобы исключить подобные слепые зоны, я разделяю проверку на три уровня:
- Контрактная проверка: гарантирует правильность формы. Ответ существует, парсится без ошибок, обязательные поля заполнены и не содержат бессмысленных заглушек.
- Сценарийная проверка: проверяет соблюдение логических условий. Если источник один, система не должна выдумывать несуществующий конфликт. При несовместимых версиях API она обязана зафиксировать расхождение.
- Человеческая приёмка: определяет фактическую пригодность документа. Эксперт оценивает обоснованность выводов, понятность рисков и применимость решений. Автоматический тест не имеет права принимать финальное решение только потому, что код выполнился без ошибок.
План следующего эксперимента и чек-лист перед запуском
Текущая методика уже защищает от выбора моделей по ложным показателям, но для полноценного сравнения маршрутов её нужно расширить. В следующем тесте я планирую закрыть пять ключевых задач:
- Запустить все маршруты через одинаковые версии сценариев, промптов и схем.
- Обеспечить сохранение полных ответов, а не только их превью-фрагментов.
- Провести многократные повторные запуски каждого сценария для оценки воспроизводимости.
- Вернуть в тестовый набор состязательные кейсы (конфликты данных, попытки подмены инструкций, численные неувязки).
- Учитывать суммарное время, затраты на авторемонт и вердикты человеческой приёмки.
Короткий протокол перед запуском сравнения моделей
Перед началом нового тестирования я фиксирую семь обязательных условий:
- Зафиксирована точная конфигурация модели, рантайма, промпта, схемы и железа.
- Проверка завершённости отсечена от оценки качества и человеческой приёмки.
- Система сохраняет все сырые ответы и истории исправлений.
- Для каждой метрики определен собственный честный знаменатель.
- Все маршруты тестируются на абсолютно идентичных сценариях.
- В тестовый набор включены состязательные и стрессовые проверки.
- Назначен ответственный специалист, принимающий итоговый результат.
Completion gate не делает локальную модель умнее и не повышает качество её ответов. Он создаёт чёткую границу, до достижения которой любые метрики качества не имеют никакого практического смысла. Сначала среда исполнения должна выдать целостный объект, затем внешние валидаторы проверяют контракт и сценарные условия, и только после этого человек принимает решение о возможности использования полученного документа.
Источник: habr.com
