- BrainTools - https://www.braintools.ru -

Хочу рассказать про наш проект Exam AI — внутреннюю платформу для аттестации и тренировки сотрудников.
Это не “ещё один тестик на 20 вопросов”, а система, где:
контент живёт в управляемом банке вопросов;
вопросы можно генерировать из нормативных документов через LLM;
экзамен идёт как stateful runtime со сложным сценарием;
есть роли, назначения, апелляции, аналитика и multi-tenant модель организаций.
Текст будет не маркетинговый. Больше про инженерные решения, компромиссы и то, что реально ломалось.
Во многих компаниях подготовка экзаменов выглядит примерно так:
Появился новый регламент.
Методист руками пишет вопросы.
Дальше вручную правит формулировки, удаляет дубли и снова согласует.
Проблемы понятны:
долго;
дорого;
плохо масштабируется;
при изменении документов весь цикл почти с нуля.
Наша цель была прагматичной: сократить путь “документ -> экзамен”, но не потерять контроль качества.
То есть не “отдать всё ИИ”, а сделать управляемый конвейер.
Python 3.14
FastAPI
SQLAlchemy 2 (async) + PostgreSQL
pgvector для эмбеддингов
pydantic-ai / Pydantic models
TaskIQ + Redis для фоновых задач
MinIO для медиа
React 19 + TypeScript
Vite
TanStack Query
Zustand
xState 5 (runtime flow)
Orval (генерация API-клиента из OpenAPI)
Tailwind + DaisyUI
OIDC/SSO (Keycloak через внутренний SDK)
Docker / Traefik
CI с quality/security гейтами
На backend мы изначально держали жёсткое разделение:
domain — сущности, value objects, протоколы;
application — use cases;
infrastructure — репозитории, внешние адаптеры, агенты;
presentation — API/схемы.
Это банально звучит, но на длинной дистанции спасает.
Когда у тебя появляется:
новый источник контента;
новый LLM-провайдер;
новый вариант scoring/validation;
ты меняешь адаптеры и оркестрацию, а не переписываешь всё ядро.
Первая ошибка [1], которую мы (как и многие) сделали:
“Дадим модели большой документ и попросим сгенерировать N вопросов”.
Результат:
тематические перекосы;
поверхностные вопросы;
дубли в разных формулировках;
плохая воспроизводимость между запусками.
Поэтому мы перешли к пайплайну с явными стадиями.
Документ режется на фрагменты, строятся эмбеддинги, формируется карта тем.
Планируем генерацию как отдельный шаг:
сколько вопросов на тему;
какой тип контента;
приоритеты;
какие области знаний покрываем.
Генерируем не свободный текст, а строго типизированную структуру (Pydantic-схемы).
Невалидный ответ -> retry с уточнением требований.
Автопроверки кандидатов:
semantic near-duplicate;
валидность ожидаемого ответа;
привязка к knowledge area;
проверка формата/полноты.
Последнее слово у эксперта: approve/edit/reject.
Мы сознательно оставили “человека в контуре” как quality gate.
Один из самых неприятных продовых багов: пользователь просит 2 вопроса по теме, а получает два перефраза одного и того же кейса.
Проблема была архитектурная:
пользователь мыслит “тема = широкая область”;
модель часто мыслит “тема = конкретный сценарий”.
Что сделали:
На этапе scan начали нормализовать check_focus как нумерованный список независимых граней темы.
На этапе generate стали жёстко выбирать одну грань на текущий вопрос.
Добавили отрицательный контекст: последние формулировки + повторяющиеся опорные токены.
В prompt зафиксировали требование менять минимум 2 измерения кейса (контекст, тип ошибки, роль проверяющего, решение, нормативный акцент).
После этого “два вопроса = два аспекта” стало воспроизводиться намного стабильнее.
Экзамен — это не просто POST /answer.
Есть:
текущий шаг;
таймеры;
переходы между фазами;
ограничения по действиям;
восстановление сессии.
Мы вынесли логику [2] на фронте в xState, а на бэке поддержали событийную модель для сессии.
Профит: исчезает класс “невозможных UI-состояний”, когда кнопка активна, но по бизнес-логике действие уже запрещено.
У нас есть изоляция по организациям + режим platform-admin (god-view с org-switcher).
Самая частая ошибка в такой схеме:
фильтр по organization_id добавили в один эндпоинт;
забыли в другом;
в UI кэш не инвалидировали при переключении org.
Один из реальных дефектов: при переключении организации на экране пользователей продолжали отображаться данные не той org.
Что исправляли:
backend: org-context обязателен на user-list эндпоинте, фильтрация через связь пользователь -> департамент -> организация;
frontend: централизованное прокидывание organization_id и инвалидация query-кэша при смене активной организации;
тесты: регресс-guard на org-isolation в ключевых сценариях.
Вывод: multi-tenant лучше проектировать как “системный инвариант”, а не как “пару where в SQL”.
Мы генерируем API-хуки из OpenAPI (Orval), поэтому:
контракт backend/frontend синхронизируется автоматически;
типовые поломки ловятся на type-check, а не от пользователей;
меньше ручного кода вокруг сетевого слоя.
Если у команды много изменяющихся эндпоинтов, это экономит массу времени.
У нас в check-pipeline входят:
форматирование;
линт;
mypy/ts type-check;
unit/integration тесты;
security-аудит зависимостей.
Практически:
уязвимости ловятся рано, до релиза;
обновление библиотек становится регулярной рутиной, а не “пожаром раз в полгода”.
Отдельный урок: security-гейты должны быть настроены реалистично (что блокирует релиз, а что — warning с трекингом).
Не генерация как таковая, а пост-валидация и антидублирование.
Пользователь не должен смотреть в “вечный спиннер” во время scan -> plan -> generate -> validate.
Сложно не написать фильтр, а не забыть его во всех read-path.
Когда проект растёт, важнее не “идеальный рефактор за месяц”, а последовательные безопасные изменения с регресс-тестами.
Сразу закладывал бы typed output от модели + строгую валидацию.
Сразу фиксировал бы anti-dup pipeline как часть бизнес-логики, а не как “косметику”.
Сразу проектировал бы multi-tenant контур и test-guards на него.
Сразу ставил бы contract-first между backend и frontend.
Сразу держал бы “человека в контуре” для контента высокого риска.
Мы получили не “демо с LLM”, а производственную платформу, где:
экзамены и тренировки живут в едином контуре;
генерация контента ускоряет подготовку, но остаётся контролируемой;
архитектура выдерживает изменения без постоянного хаоса.
Если интересно, могу в следующем посте разобрать один узкий блок с кодом и метриками:
антидублирование вопросов (от prompt до валидатора);
org-isolation end-to-end (backend + frontend + тесты);
runtime экзамена на xState и восстановление сессии.
Автор: azamat_sandboxer
Источник [4]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/32405
URLs in this post:
[1] ошибка: http://www.braintools.ru/article/4192
[2] логику: http://www.braintools.ru/article/7640
[3] sandboxer.ru: https://sandboxer.ru/projects/exam-ai
[4] Источник: https://habr.com/ru/companies/sandboxer/articles/1053514/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1053514
Нажмите здесь для печати.