В одном из сравнений локальных моделей я получил результат, который приятно показывать в таблице: три маршрута выполнили по десять сценариев из десяти. Медиана быстрого маршрута составила 28,9 секунды. Более тяжёлому потребовалось 99,1 секунды, смешанному — 71,0.
Если смотреть только на колонку «принято», маршруты равны. Если добавить время, победителем выглядит быстрый. Если учесть ремонты результата, картина снова меняется: маршрутам потребовалось соответственно три, два и одно исправление.
Но и после этого выбирать модель рано. Таблица отвечает на вопрос, прошла ли связка из модели и окружающего кода текущий набор проверок. Она ещё не показывает, насколько хорош созданный документ и сможет ли человек использовать его в работе.
Эта ошибка возникла не в модели. Я неправильно определил единицу измерения. В одну метрику попали законченный ответ, корректный JSON, наличие обязательных документов, прохождение автоматических проверок и человеческая оценка содержания. Так появился высокий процент, из которого нельзя было понять причину успеха или сбоя.
Чтобы разобрать результат, я добавил отдельный шлюз завершённости — 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 легко принять за готовый результат.
Например, следующий объект проходит поверхностную проверку типов:
{ "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% ничего не говорит о знаменателе. В восемь успешных случаев могли попасть ответы, прошедшие схему с первой попытки, ответы после ремонта формата, законченные документы с содержательной ошибкой и результаты, отклонённые человеком.
Эти события требуют разных исправлений. Тайм-аут ведёт к настройке рантайма или объёма ответа. Повреждённый 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 | завершённые ответы | можно ли разобрать результат |
First-pass schema rate | разобранные ответы | соблюла ли модель контракт без ремонта |
Artifact completion | ответы по схеме | существуют ли все требуемые документы |
Quality pass rate | только | прошли ли целые документы сценарные проверки |
Human acceptance | проверенные документы | готов ли ответственный человек использовать результат |
Такой отчёт длиннее одной цифры, но сохраняет причину отказа.
Что делает completion gate
Шлюз не оценивает стиль, полноту анализа или правильность решения. Он проверяет предпосылки оценки.
В моём случае минимальный набор выглядел так:
1. Процесс завершился без timeout/crash/OOM. 2. Сырой ответ сохранён. 3. Парсер разбирает ответ как JSON. 4. Объект соответствует версии схемы. 5. Все четыре артефакта присутствуют. 6. Каждый артефакт содержит текст, а не путь или заглушку. 7. Ремонт формата, если он был, записан отдельно.
Проверка заглушек требует осторожности. Запретить несколько фраз недостаточно: модель найдёт другой способ вернуть формально заполненное поле. Я использовал сочетание простых признаков:
минимальная длина содержимого после удаления заголовка;
наличие обязательных структурных элементов для конкретного документа;
запрет значений, похожих только на файловый путь;
поиск известных обещаний вроде «будет добавлено позже»;
проверка ссылок между документами;
сохранение причины отказа вместо общего
false.
Это не защита от всех бессодержательных ответов. Шлюз лишь отсекает случаи, которые заведомо нельзя передавать в качественную оценку. Чем больше смысловых правил попадает в completion gate, тем выше риск незаметно превратить его в ещё одного судью качества.
Повторная проверка изменила знаменатель
В историческом наборе одного сценария сохранилась 21 запись. После добавления шлюза я прогнал их повторно. Только четыре записи содержали целые результаты, пригодные для дальнейшей оценки. Остальные 17 были незавершёнными или повреждёнными.
Из этого нельзя заключить, что качество модели стало выше или ниже. Я не запускал модель заново и не пересчитывал содержательные оценки. Изменился состав объектов, которым вообще разрешено участвовать в такой оценке.
До шлюза оборванный JSON мог получить нулевой балл за анализ требований. После шлюза он получает статус incomplete. Это точнее: ответ ничего не доказывает о содержательном качестве, потому что генерация не завершилась.
Разделение помогает и в обратную сторону. Валидная схема больше не означает успех. Если четыре строки содержат пути или заглушки, запись не достигает quality_eligible.
При повторной проверке я сохранил исходные записи и создал новую производную оценку. Поле afterScored=true получили четыре попытки; 17 остались вне качественного знаменателя. Старые данные не перезаписывались, поэтому можно восстановить как прежний отчёт, так и причину изменения.
Откуда взялись 10 из 10
Следующая версия эксперимента содержала десять ролевых сценариев. Среди них были подготовка черновика, поиск пробелов, обнаружение конфликтующих требований, сборка набора задач и работа с длинным документом.
Три маршрута прошли текущий контракт во всех десяти сценариях:
Маршрут | Принято | Медиана времени | Ремонты среди принятых |
|---|---|---|---|
Qwen-only | 10 из 10 | 28,9 с | 3 |
Bonsai 8B-only | 10 из 10 | 99,1 с | 2 |
Смешанный | 10 из 10 | 71,0 с | 1 |
Медианы пересчитаны по десяти значениям elapsedMs для каждого маршрута. Число ремонтов взято из первичных JSON-записей. Эти данные позволяют сделать ограниченный вывод: каждая из трёх зафиксированных конфигураций смогла пройти данный набор сценариев, а нагрузка ремонта и время различались.
Таблица не позволяет назвать победителя по четырём причинам.
Первая: «принято» включает ограниченный детерминированный ремонт. Три результата Qwen, два результата Bonsai и один результат смешанного маршрута не прошли контракт в исходном виде. Мы измеряем связку модель + запрос + схема + рантайм + ремонт, а не изолированную модель.
Вторая: набор не включал часть состязательных проверок из более раннего эксперимента. В нём не было полного покрытия утечки конфиденциальных данных, подмены инструкций и специально подготовленных противоречий. Успех на ролевых сценариях нельзя переносить на отсутствующие классы риска.
Третья: в сохранённых записях доступны outputPreview, а не полные ответы. По этим фрагментам был рассчитан preview_based_semantic_diff: 0,82, 0,87 и 0,88. Такие значения помогают выбрать направление следующего запуска, но не подтверждают превосходство одного маршрута. Семантическое сравнение полных документов по этим файлам невоспроизводимо.
Четвёртая: это единичные сохранённые прогоны. Они показывают возможность пройти контракт, но не стабильность. Для оценки стабильности нужны повторы при неизменных версиях модели, квантования, рантайма, запроса, схемы и входных данных.
Почему маршрут нужно оценивать целиком
Скорость первого ответа редко равна времени до принятого результата. После вызова могут понадобиться разбор, ремонт JSON, повторная генерация, автоматические проверки и ручная проверка.
Поэтому единицей сравнения я выбрал зафиксированный маршрут:
model artifact + quantization + runtime build + generation parameters + prompt version + schema version + deterministic repair policy + hardware profile
Изменение любого элемента создаёт новую конфигурацию. Результат Qwen с одной версией шаблона чата нельзя автоматически переносить на другую. Так же нельзя считать, что обновление парсера улучшило модель: оно улучшило маршрут.
Разные стадии работы предъявляют разные требования. Для черновика важна задержка. Для поиска конфликтов — способность удерживать несколько версий решения и ссылаться на основания. Для проверки структуры модель вообще не обязательна: типы, обязательные поля и известные инварианты надёжнее проверяет обычный код.
Отсюда возникает гипотеза ролевого роутинга:
быстрый черновик → быстрый маршрут поиск пробелов и конфликтов → более тяжёлый маршрут контракт и обязательные ссылки → детерминированный код допустимый ремонт формата → ограниченный repair step решение о пригодности → человек
Текущие данные эту гипотезу не подтверждают и не опровергают. Смешанный маршрут потребовал меньше ремонтов в одном наборе, но работал медленнее Qwen-only. Для решения нужны стоимость и задержка на принятый результат, серия повторов и полные выходные данные.
Что сохранять для воспроизводимости
Название модели и итоговый балл не позволяют повторить эксперимент. Для каждого запуска я сохраняю или планирую сохранять:
run_id, идентификаторы набора и сценария;точный артефакт модели, квантование и хэш версии;
сборку рантайма и шаблон чата;
параметры генерации;
версии запроса и JSON Schema;
хэш входных данных;
полный сырой ответ и разобранный объект;
число входных и выходных токенов;
время выполнения и наблюдаемую память;
отдельные статусы timeout, crash и out-of-memory;
ошибки разбора и схемы;
тип ремонта и число попыток;
результат completion gate;
результаты сценарных проверок;
решение человека и причину отклонения.
Полные входы и ответы могут содержать рабочие данные. Публиковать их автоматически нельзя. В открытый отчёт можно вынести агрегаты, хэши, версии и обезличенные примеры, сохранив исходные артефакты в защищённом хранилище.
Повторный запуск должен получать новый run_id. Перезапись неудачной попытки удобна для красивой таблицы, но уничтожает историю эксперимента. Новая схема или новый запрос также требуют новой версии профиля.
Почему внешний тест не равен истине
Детерминированный код убирает ситуацию, в которой модель сама оценивает собственный ответ. Он не гарантирует правильность критерия.
Тест может требовать четыре заголовка и пропустить пустой текст под ними. Может искать слово OAuth и не распознать эквивалентное описание. Может проверить наличие ссылки на источник, не проверив, подтверждает ли источник конкретный вывод.
Поэтому я разделяю три типа проверки.
Контрактная проверка подтверждает форму: ответ существует, парсер читает его, нужные поля заполнены и не состоят из очевидных заглушек.
Сценарная проверка подтверждает наблюдаемое требование. При одном источнике система не должна выдумывать конфликт. При двух несовместимых версиях API она должна показать расхождение. Неизвестное значение не должно появляться из правдоподобного шаблона.
Человеческая приёмка определяет, пригоден ли документ для исходной задачи. Ответственный человек проверяет основания, понятность риска и последствия решения. Автоматический тест не получает права принять результат только потому, что он исполняемый.
Если критерий оказался неверным, задача может пройти все проверки и вернуться позже. Такой случай полезно хранить отдельно от обычного нового запроса: связать новую запись с закрытой и указать причиной возврата промах критерия приёмки. Тогда можно измерять долю результатов, которые прошли тест, но позднее вернулись из-за неверного определения done.
Следующий эксперимент
Текущая сводка уже помогает не выбирать модель по ложному знаменателю. Для сравнения маршрутов её недостаточно. Следующий запуск должен закрыть пять пробелов.
Все маршруты проходят одни и те же версии сценариев, входа, запроса и схемы.
Система сохраняет полные ответы, а не только предварительные фрагменты.
Я запускаю каждый сценарий несколько раз для оценки стабильности.
Я возвращаю в набор состязательные случаи: конфликт источников, подмену инструкции, утечку данных, числовую несогласованность и молчаливый пробел.
Отчёт считает суммарное время, ремонты, повторные вызовы и человеческую приёмку.
Отдельно нужно проверить предел локального оборудования. По текущим данным нельзя утверждать, что более тяжёлые сценарии уже проверены в частном облаке: такого эксперимента не было.
Короткий протокол перед сравнением моделей
Перед новым запуском я фиксирую семь условий:
Определена точная конфигурация модели, рантайма, запроса, схемы и оборудования.
Завершённость отделена от качества и человеческой приёмки.
Система сохраняет полные исходные ответы и все ремонты.
Для каждой метрики указан собственный знаменатель.
Маршруты получают одинаковые сценарии.
Набор содержит обычные и состязательные случаи.
Назван человек, который принимает результат и отвечает за его использование.
Completion gate не делает модель умнее и не повышает качество ответов. Он проводит границу, до которой качественная метрика ещё не имеет смысла.
Сначала рантайм должен вернуть целый объект. Затем парсер и внешний код проверяют контракт. Сценарные тесты проверяют согласованные свойства документа. После этого человек решает, можно ли использовать результат.
В моём эксперименте именно это разделение превратило красивое «10 из 10» из ответа в начало диагностики.

