Разбор пайплайна Codenrock Vibe: планировщик покрытия, факт-чекер, слепой калибратор сложности, симулятор кандидата — и грабли, на которых всё это выросло.
Мы делаем платформу оценки кандидатов и сотрудников. Тестовый вопрос для нас — не контент, а измерительный инструмент: если он кривой, компания наняла не того человека. Проблема в том, что наивная генерация — «вставил компетенцию в ChatGPT, скопировал десять вопросов» — даёт результат, который выглядит прилично и не работает.
Вот типичный вопрос из той эпохи (пример стилизованный, но дефект настоящий):
Как называется паттерн, при котором объект создаётся единственный раз, а все обращения идут к одному общему экземпляру?
A) Singleton B) Factory Method C) Observer D) Builder
Вы только что ответили правильно, даже если никогда не программировали: «единственный раз», «один экземпляр» — Singleton. Это утечка ответа в формулировку: вопрос измеряет не знание паттернов, а умение читать. Таких дефектов у наивной генерации — системный набор, и мы прошли через него сами. Ниже — аудит, который нас отрезвил, и конвейер, который из него вырос: до десятка ролей на один вопрос — причём не все роли нейросетевые, ключевые решения принимает детерминированный код — и два источника знаний: фронтирные модели и документы самой компании-клиента.
- Точка отсчёта: аудит, который нас отрезвил
- Главный принцип: генератор никогда не проверяет сам себя
- До генерации: чистим вход и планируем покрытие
- Генерация: не «напиши вопрос», а техзадание с обвесом
- ОТК: панель валидаторов
- Что происходит с браком: ретраи с памятью и ремонт утечки
- Дубликаты и умение остановиться
- Режим второй: вопросы по вашим регламентам, а не по «интернету вообще»
- Человек в контуре
- Музей граблей
- Экономика качества и честные статусы
- Что мы вам не рассказали
Точка отсчёта: аудит, который нас отрезвил
Первая версия была ровно тем, что мы теперь критикуем: один вызов LLM, один промпт, ноль проверок. Потом эксперт вручную разобрал сгенерированный банк из 59 вопросов — и нашёл 12 системных типов брака. 14 из 19 вопросов по Python оказались про одну и ту же подтему (генераторы и ленивая обработка файлов), включая шесть открытых вопросов с одинаковым описанием подряд. Верные варианты были помечены неверными. В коде «с багом» бага не существовало. Тривиальные задачи гордо носили метку «сложно». Запреты из описания компетенции игнорировались. Отклонений — ноль, потому что проверки не существовало.
Отдельного упоминания заслуживает reasoning-режим, на котором всё это работало: генератор запускался с минимальным reasoning-усилием — модель буквально не думала. Одно только включение полноценного reasoning подняло долю прохождения валидации с 52% до 68%.
Главный вывод аудита мы зафиксировали во внутренней документации почти дословно: LLM не может надёжно проверить сама себя, а инструкции в промпте деградируют при масштабной генерации. Каждый из 12 найденных дефектов позже стал машинным кодом проблемы и отдельной проверкой в пайплайне.
Главный принцип: генератор никогда не проверяет сам себя
Ключевое архитектурное решение — adversarial-разделение. Один AI пишет вопрос, независимые reasoning-модели других вендоров его атакуют, а финальное решение «принять / отклонить / переделать» принимает вообще не модель, а детерминированный код. У разных семейств моделей разные слепые зоны, и модель склонна прощать собственные ошибки — поэтому кросс-вендорная проверка зафиксирована в коде как инвариант, а не как пожелание.
Конвейер одного вопроса выглядит так: препроцессор компетенции → планировщик покрытия → генератор → структурный валидатор → факт-чекер → калибратор сложности → симулятор кандидата → детерминированный агрегатор вердиктов → инкрементальная дедупликация, плюс отдельный агент точечного ремонта утечек. Кто из них нейросеть, а кто код — принципиально, поэтому вот карта конвейера:

«Семеро проверяют» из заголовка — буквально: критик разнообразия, структурный валидатор, факт-чекер, калибратор сложности, симулятор кандидата, дедупликатор и агрегатор вердиктов. Пишет — один генератор. Двое из семи проверяющих — не нейросети, а детерминированный код, и это не случайность, а позиция. Вердикты типизированы: более двух десятков машинных кодов проблем.
До генерации: чистим вход и планируем покрытие
Препроцессор компетенции. Описание компетенции — свободный текст, и пользователи пишут туда далеко не только суть навыка (самый дорогой инцидент на эту тему — в «Музее граблей» ниже). Показательный случай: почти все вопросы Python-разработчику оказались про телеметрию роботов — в описание компетенции был вшит контекст компании, и генератор исправно тащил его в каждый вопрос.
Теперь описание предварительно разделяется на «что тестируем» (уходит генератору) и «инструкции и запреты» (уходят проверяющему на контроль соблюдения). Заодно это защита от prompt-injection: эвристики ловят и русские, и английские паттерны инъекций.
Планировщик покрытия. Разнообразие не оставляется на волю случая. До генерации компетенция декомпозируется на аспекты в два прохода: сначала генерация аспектов «по граням», затем отдельный LLM-критик разнообразия находит близнецов — один навык разными словами — и заменяет их аспектами из непокрытых граней. Любопытная деталь: от эмбеддингов здесь мы отказались — классические метрики близости на наших данных разделяли соседние аспекты хуже, чем LLM-критик. Единица разнообразия — пара «аспект × тип вопроса»: один аспект другим типом — это глубина, а не дубль. Если различимых граней меньше плана, система возвращает меньше, а не выдумывает натянутые темы. И отдельный дешёвый вызов решает, уместен ли вообще код: для «коммуникации» и «лидерства» кодовые слоты автоматически конвертируются в открытые вопросы — кодовых задач про переговоры не рождается.
Генерация: не «напиши вопрос», а техзадание с обвесом
Генератор получает не голую тему, а пакет: предметную часть компетенции, назначенный аспект, тип и сложность, правила качества, собранные слоями — универсальные, по типу вопроса, по компетенции, — список уже существующих вопросов, снимок «собратьев по батчу» и — в режиме базы знаний — фрагменты документов клиента. Правила качества — это код, а не пожелание «пиши хорошо». Несколько примеров категорий:
- «вопрос-ЗАДАЧА, а не вопрос-ОПРЕДЕЛЕНИЕ»: если на вопрос можно ответить, перечитав его и не зная предмета, — это утечка;
- психометрическая деталь: дистрактор не должен повторять редкое слово из формулировки — это статистическая подсказка;
- компетенция-зависимые правила: для «Тестирования» кодовые задания обязаны требовать написания тестов, для «ООП» — классы, плюс «антименеджерский фильтр» для технических компетенций — не спрашивать про спринты там, где надо писать код;
- детерминизм кода: запрет timing-зависимого asyncio и random без seed.
Параллельные генераторы «видят собратьев»: назначения перемешаны round-robin по компетенциям, и каждому подкладываются краткие описания уже созданных в батче вопросов — иначе «слепой залп» рожает близнецов. У блоков правил есть бюджет по символам, контролируемый тестами, — промпт не разбухает. Всего в системе около 58 runtime-промптов, каталогизированных с метаданными и уровнем риска редактирования; JSON-контракт ответа не может изменить даже владелец проекта.
Снаружи вся эта машинерия выглядит как один аккуратный диалог:

ОТК: панель валидаторов
Нулевой этаж — структурный валидатор, ноль LLM-вызовов. Детерминированный ранний гейт: полнота вариантов и правильного ответа, длина вариантов, «вопрос спрятан в комментариях кода», «консольный вывод склеен в одну строку», блок-лист нерелевантной доменной терминологии — тот самый кейс с телеметрией роботов из раздела про препроцессор, закреплённый теперь ещё и детерминированной проверкой. Сработал — дорогие агенты даже не запускаются.
Факт-чекер — до шести независимых осей проверки; набор зависит от типа вопроса: от трассировки кода — существует ли заявленный баг вообще — до корректности каждого варианта по отдельности («дистрактор тоже верен» — тоже брак).
Слепой калибратор сложности. Ему намеренно не показывают заявленную сложность — он независимо оценивает уровень и прогнозирует поведение кандидатов на вопросе. Существенное расхождение с заявкой — отклонение с конкретной подсказкой «сделай значительно сложнее/проще». Ловит классику: тривиальный вопрос с меткой «сложный».
Симулятор кандидата (максимальный режим качества) — самый наглядный агент. Модель проходит вопрос как сильный кандидат, не видя разметки. Если она уверенно выбирает вариант, помеченный неверным, и внятно это обосновывает — вопрос неоднозначен, брак. Для задач «найди баг» симулятор иногда отвечает: «код и так корректен» — значит, баг фейковый. Стилизованный пример вердикта:
Задание: «Найдите баг в функции пагинации». Симулятор: «Функция корректна для всех граничных случаев, включая пустой список и последнюю неполную страницу. Заявленного бага не существует». → код проблемы FAKE_BUG, реджект.
Агрегатор вердиктов — принципиально без LLM. Решение принимает детерминированный код: воспроизводимость важнее «интеллектуальности». Схематично — без реальных кодов, порогов и весов, они часть продукта:
problems = collect_all(validators) # один проход, все проблемы сразу
problems -= { p | p.code == TOOL_FAILURE } # сбой инструмента не влияет на вердикт
if exists p: p.fatal and p.confident -> REJECT(reasons = problems)
if exists p: not p.confident -> RETRY(hints = problems) # сомнение — второй шанс, а не расстрел
-> PASS
Строка про TOOL_FAILURE — выстраданный инвариант: сбой самого инструмента проверки не может забраковать вопрос — поломка линейки не делает деталь бракованной.
Выборка кодов проблем (из более чем двух десятков): ANSWER_LEAKED, FAKE_BUG, QUESTION_IN_CODE, NON_DETERMINISTIC_CODE, SEMANTIC_DUPLICATE, WRONG_ANSWER_MARKED.
Что происходит с браком: ретраи с памятью и ремонт утечки
Отклонённый вопрос не выбрасывается молча. Подсказки всех валидаторов («найдено два верных варианта вместо одного», «вопрос проще заявленного») вклеиваются в следующую попытку как обязательные исправления. На последней попытке система меняет сам аспект на резервный из пула планировщика — возможно, тема неудачна в принципе.
Самая частая причина брака — утечка ответа — лечится отдельным механизмом. Если утечка — единственная причина реджекта, вопрос не перегенерируется с нуля: полная регенерация обычно «течёт» снова. Вместо этого дешёвый агент точечно ремонтирует формулировку под жёсткими инвариантами неизменности, после чего вопрос проходит укороченную ре-валидацию. Результат: выход банка вырос с ~72% до ~86%, при этом принятый вопрос не стал дороже.
И контринтуитивный A/B-эксперимент: глубина рассуждений утечку НЕ вылечила — pass rate 0.27 против 0.26, на нашей выборке разница не выходит за пределы шума. Утечка лечится правилами формулировки, а не «думанием». Полезный тезис против карго-культа reasoning.
Дубликаты и умение остановиться
Дедупликация — семантическая, двухуровневая: быстрый фильтр на эмбеддингах отбирает подозрительные пары, затем LLM подтверждает «тот же навык, другой словесный камуфляж» (при недоступности эмбеддингов — fallback на Jaccard). Ключевая идея — таксономия когнитивных уровней: два вопроса про одну тему НЕ дубликаты, если тестируют разные уровни — знание, понимание, применение, анализ. Дубль — совпадение и темы, и уровня.
Поверх работает механизм «насыщения»: если доля дублей по компетенции превысила порог, генерация останавливается с явным статусом — «создано N из M, тема исчерпана» — вместо оплаты заведомого брака. В старой версии был анти-паттерн: при переполненном банке система сама просила модель «сделать вариацию того же навыка» — и сама же удаляла результат как дубликат. Мы платили за мусор, который сами и заказали.
Режим второй: вопросы по вашим регламентам, а не по «интернету вообще»
У пайплайна два равноправных источника знаний. Режим первый — знания фронтирных моделей: компетенции общего рынка вроде Python, ООП, коммуникации. Режим второй — база знаний клиента: внутренние регламенты, процессы, продукты, по которым ни одна публичная модель ничего не знает.

Ингест. Загружаются PDF, DOCX, XLSX, TXT и веб-страницы (версионирование по SHA-256, защита от SSRF); обработка — асинхронный конвейер из пяти стадий: скачивание → парсинг → чанкинг → эмбеддинги → сохранение. Парсер восстанавливает структуру даже из плоского текста четырьмя эвристиками заголовков, включая кириллический капс; таблицы выделяются в отдельные «ассеты». Чанкинг структурно-осознанный, с перехлёстом между соседними чанками — мысль не рвётся на границе.

Grounding. Поиск двухступенчатый: сначала по документам, явно привязанным к компетенции (с приоритетом и флагом «обязательный источник»). Если привязанных источников нет — fallback на все базы проекта. А вот если источники привязаны, но поиск по ним ничего не нашёл, — генерация идёт без grounding’а, с зафиксированным режимом и предупреждением, а не притворяется обоснованной.

Каждый найденный фрагмент попадает в промпт с атрибуцией, а ключевая инструкция генератору — опираться на факты компании, но не копировать дословно: вопрос проверяет понимание, а не запоминание. У готового вопроса сохраняется provenance до конкретных чанков — можно показать, из какого абзаца регламента он «вырос». Разница на стилизованном примере:
Без базы знаний: «В какой срок по закону потребитель может вернуть товар надлежащего качества?» — вопрос про интернет вообще.
С базой знаний: «Клиент требует возврат на 20-й день, ссылаясь на акцию. Как по внутреннему регламенту возвратов обрабатывается такой кейс и кто согласует исключение?» — источник: «Регламент возвратов, раздел 4».
Деградация без падения. Если векторное расширение БД недоступно — молчаливый пересчёт косинусов на Node.js; если упали эмбеддинги — чанки остаются доступными текстовому поиску. Пайплайн нигде не падает целиком из-за отказа одной подсистемы.
Человек в контуре

AI-проверки — не замена эксперту, а фильтр перед ним. Эксперт ставит APPROVE/REJECT с одной из 13 категорий («Ответ виден в коде», «Спорный правильный ответ»…) и комментарием. Дальше фидбэк замыкается в обучающий контур: топ категорий отклонений по компетенции автоматически подмешивается в промпты всех будущих генераций — «ранее отклонено экспертом, не повторяй эти ошибки». Перегенерация идёт через тот же конвейер проверок — без планировщика, тема уже задана старым вопросом, — с контекстом старого («НЕ ПОВТОРЯЙ, сделай другой на ту же тему»); новый вопрос связывается со старым — у вопросов есть родословная. State machine статусов плюс единый фильтр выдачи, вшитый во все пути показа и подсчёта готовности, гарантируют, что забракованный вопрос никогда не попадёт кандидату.
Качество тестируется как код: 12 регрессионных фикстур — по одной на каждую реальную проблему, найденную экспертом; 20 вручную размеченных фикстур утечки (10 «текущих», 10 чистых) — детектор утечек проверяется как модель, по precision/recall. Даже у промпта ремонта есть юнит-тест на бюджет длины: промпт — артефакт с контрактом.
Музей граблей
- «Промпт в описании профессии». Поле описания компетенции изначально никак не валидировалось — и пользователи закономерно стали писать туда собственные ТЗ: «Ты — senior разработчик…», «сгенерируй 5 вопросов», требования к количеству и типам вопросов прямо в тексте. Генератор послушно исполнял чужие инструкции, а проверяющие так же послушно браковали результат: на «грязных» описаниях доля прохождения факт-чекера падала до 3,1%, и система сжигала до 100 LLM-вызовов на один принятый вопрос. Из этого инцидента вырос препроцессор из раздела про подготовку входа; заодно эвристики выучили английские паттерны инъекций — до инцидента ловили только русские.
- «Хвост банка без валидации». При исчерпании бюджета деградация управляемая: отключается только дорогой симулятор кандидата — ниже уровня STANDARD, то есть факт-чекера, калибратора и структурных проверок, система не опускается никогда.
- «Дешёвая модель дешёвой модели рознь». Разброс pass rate валидации между economy-моделями — 0.71–0.84 против 0.28–0.29. Выбор дешёвой модели под валидацию — отдельное исследование, а не «возьмём что подешевле». Конкретные модели мы сознательно не называем — карта «модель → задача» часть ноу-хау, об этом в конце, — но сам факт четырёхкратного разброса переносим на любой стек. Бонус из той же серии: 83% completion-токенов одной дешёвой модели уходило в невидимый reasoning из-за унаследованной настройки.
- «Почти-JSON». Модели возвращают markdown-фенсы, reasoning-преамбулы, неэкранированные бэкслеши (привет, PHP-неймспейсы) и сырые переводы строк внутри строковых литералов. Наш парсер применяет пять стратегий по нарастающей плюс авторемонт, и покрыт примерно сорока юнит-тестами на реальные форматы. У разных вендоров свои сюрпризы: где-то нет structured output, где-то фиксированная температура, где-то служебные reasoning-токены незаметно съедают лимит ответа — без спец-обработки приходит пустой ответ.
Экономика качества и честные статусы
Качество стоит денег, и мы это не прячем: порядка десятка LLM-вызовов на принятый вопрос, банк собирается десятки минут (около получаса на банк из двух десятков вопросов), примерно половина первых генераций отклоняется факт-чекером и перегенерируется. Эта цифра не противоречит ~86% из раздела про ремонт утечки: половина — брак первого прохода, а ~86% — итоговый выход банка после ретраев и точечного ремонта. Есть три уровня качества — FAST (структурные проверки и дедуп), STANDARD (плюс факт-чекер и калибратор), FULL (плюс симулятор кандидата). Квота вопросов тарифа при этом списывается только за вопросы, прошедшие все проверки, — черновики и ретраи в неё не попадают.

Прогресс стримится в UI по стадиям — «Проверка корректности…», «Ремонт утечки…», «Попытка 2 из 3: причина…» — вместо вечного спиннера, а по завершении приходит отчёт с раскладкой недостачи: не прошли проверку качества, дубликаты, темы исчерпаны, технические ошибки.

Статусы готовности не прячутся: если по компетенции пробел — это видно фильтром, а не выясняется на оценке. Сравнение на идентичных описаниях компетенций: старый пайплайн — 59 вопросов и ноль проверок, почти все — с утёкшим в тест доменом компании, а 14 из 19 Python-вопросов — про одну подтему; новый — 5–7 уникальных подтем на компетенцию и ноль дубликатов.
Что мы вам не рассказали
Честно: тексты промптов, численные пороги, формулы агрегации и карту «модель → задача» мы сознательно не публикуем. Это и есть продукт, а публикация точных эвристик упрощает их обход. Зато принципы переносимы:
- Покрытие планируется заранее и защищается критиком разнообразия, а не надеется на случайность.
- Генератор никогда не проверяет сам себя: независимые модели других вендоров, до шести осей проверки, включая адверсариальные.
- Сложность оценивается вслепую.
- Дубликаты ловятся семантически, с учётом когнитивных уровней.
- Брак не выбрасывается втупую: точечный ремонт, ретраи с памятью, смена темы.
- Система умеет остановиться и явно сообщить о неудаче.
- Каждый инцидент закреплён в коде инвариантом и регрессионной фикстурой.
- Вопросы могут расти из ваших собственных регламентов — с provenance до конкретного абзаца.
Если хотите потрогать — генерация банков вопросов по компетенциям и по вашим документам доступна на vibe.codenrock.com. Если хотите поспорить об архитектуре — велком в комментарии. Нам особенно интересно: а как бы вы валидировали сам валидатор? Мы пришли к размеченным фикстурам и precision/recall для детектора утечек — но подозреваем, что это не единственный путь. И если тема зайдёт, отдельных статей просят парсер «почти-JSON», регрессионное тестирование промптов и RAG-ингест документов.
Частые вопросы
Чем генерация тестовых вопросов в Codenrock Vibe отличается от ChatGPT?
ChatGPT выдаёт вопросы одним вызовом без проверок. В Codenrock Vibe вопрос проходит конвейер: планировщик покрытия, генератор, структурный валидатор, факт-чекер на моделях других вендоров, калибратор сложности и симулятор кандидата. Вопросы с ошибками автоматически перегенерируются.
Можно ли генерировать тестовые вопросы по внутренним документам компании?
Да. В базу знаний загружаются PDF, DOCX, XLSX, TXT или веб-страницы, документы привязываются к компетенциям, и вопросы генерируются по фрагментам этих документов. У каждого вопроса сохраняется ссылка на источник вплоть до конкретного абзаца регламента.
Как AI проверяет качество сгенерированных вопросов?
Каждый вопрос проверяет независимый AI-рецензент другого вендора: факты в вариантах ответа, корректность эталонных решений, соответствие заявленной сложности. Отдельный агент проходит вопрос как кандидат и ловит неоднозначности. Решение принять или отклонить принимает детерминированный код, а не модель.
Что происходит с вопросами, которые не прошли проверку качества?
Подсказки валидаторов вклеиваются в следующую попытку генерации как обязательные исправления. При утечке ответа вопрос точечно ремонтируется вместо полной перегенерации, а если тема исчерпана — система останавливается и честно сообщает об этом вместо выдачи дублей.






