ADR 0138 — Golden-set RAG начинает работать в CI, по расписанию — и остаётся warn
Контекст
ADR 0132 (Этап 2 программы измеримого RAG) завершился объявленным Последствием:
Подключение к CI — это
TODO(humano), как и у QA. Без секрета API LLM или новой инфраструктуры (настоящего Ollama в CI) golden-set запускается ВРУЧНУЮ —mix golden_set.rag— и никогда в обычномmix test.
А gate rag-acertivo (docs/gates.yml) родился с severidade: warn, с
комментарием, говорящим ровно почему: «нет CI с LLM... block
обещал бы автоматическую блокировку, которой не существует».
Это Этап 3, решённый владельцем продукта в этой сессии: подключить
golden-set RAG к CI. Вопрос, который ещё был открыт, — каждый PR или
по расписанию, — тоже решён в этой сессии: по расписанию, учитывая стоимость
скачивания nomic-embed-text (~274 МБ) и запуска настоящего поиска по курируемому
корпусу (несколько минут) в каждом окне CI. Golden-set QA
(ADR 0123) здесь ничего
не меняет — он остаётся ровно TODO(humano) по причине, которую следующий
раздел делает точной.
Почему RAG решаем, а QA по-прежнему нет
TODO(humano) ADR 0123 перечисляет недостающую альтернативу: «runner с
GPU, шаг pull Ollama». Эта фраза скрывает важную разницу:
golden-set QA вызывает модель чата, выносящую суждение, — дорого и
измеряет нечто по сути недетерминированное (сам ADR 0123 фиксирует реальные
прогоны с qwen2.5-coder:latest, дающие от 1 до 5 попаданий из 6 между
запусками). Golden-set RAG вызывает только модель embedding
(nomic-embed-text, ~137M параметров) — достаточно CPU,
без GPU, а embedding не сэмплирует: один и тот же вход всегда даёт один и тот же вектор.
ubuntu-latest вообще без GPU — достаточная инфраструктура для
этого golden-set — для QA её было бы недостаточно.
Препятствие, которого не было: не нужно генерировать ни одного секрета
apps/api/scripts/seed-golden-set-rag.ts открывает собственный
NestFactory.createApplicationContext(AppModule) — никогда не говорит с api,
уже работающей как сервер. Четыре секрета, которые api отклонила бы с
примерным литералом в production (AUTH_JWT_SECRET, BRABO_SERVICE_TOKEN,
CREDENTIALS_MASTER_KEY, GIT_OAUTH_STATE_SECRET — RN-114,
ADR 0059), проверяются только
когда NODE_ENV === 'production'
(apps/api/src/infrastructure/security/{oauth-state-secret,auth-key-material,service-token}.ts,
envelope-encryption.service.ts). Скрипт никогда не задаёт NODE_ENV — ни
он, ни mix golden_set.rag, который вызывает его через System.cmd, — поэтому
все четыре попадают в ветку разработки и используют литерал dev без
жалоб. В отличие от docker/smoke.sh, который поднимает
docker-compose.prod.yml с NODE_ENV=production НАМЕРЕННО и поэтому
должен генерировать все четыре через openssl rand, этому workflow не нужен
ни один сгенерированный секрет.
Решение
Новый workflow, отдельный от ci.yml. .github/workflows/ci.yml намеренно
только pull_request (комментарий в начале самого файла):
добавление туда schedule затронуло бы каждый существующий job, без фильтра по job.
.github/workflows/golden-set-rag.yml рождается с schedule (ежедневный cron) +
workflow_dispatch (запуск по запросу — полезно для проверки самого этого
изменения и после калибровки весов/порога поиска, ADR 0080, или
курируемого корпуса).
services: postgres + services: ollama, те же закреплённые версии, что в
остальном продукте. pgvector/pgvector:pg16 (как в test-engine из
ci.yml) и ollama/ollama:0.33.1 — ТА ЖЕ закреплённая версия, что в
docker/docker-compose.yml. Паритет версии Ollama — то, что позволяет
test/fixtures/golden_set_rag/floor.json (ключуемому по МОДЕЛИ, а не по
окружению) оставаться действительным при запуске в CI: Ollama, отличный от того, что
сгенерировал локальный нижний порог, мог бы дать слегка другие embeddings и изменить
ранг пограничного случая, без того чтобы это было настоящей регрессией.
Health-check Ollama использует ollama list, а не curl. --health-cmd
выполняется ВНУТРИ сервисного контейнера, а официальный образ не гарантирует наличия
curl — гарантирует лишь наличие собственного бинарника ollama. docker/ollama/pull-models.sh
(эталонная логика, уже проверенная реальной загрузкой модели) использует
ровно этот шаблон ожидания (until ollama list >/dev/null 2>&1);
этот workflow повторно использует ту же команду как health-cmd вместо того, чтобы изобретать
новый механизм ожидания.
Модель скачивается прямым HTTP-вызовом, POST /api/pull, со стороны
RUNNER (а не изнутри контейнера). curl на runner ubuntu-latest
гарантирован (уже используется в нескольких шагах ci.yml для скачивания бинарника
gate). stream:false заставляет вызов блокироваться до завершения и вернуть
итоговый JSON — без цикла опроса. Это ТОТ ЖЕ endpoint, который бинарник
ollama pull вызывает под капотом (docker/ollama/pull-models.sh документирует
это: «CLI ollama использует HTTP против ${OLLAMA_HOST}/api/*»), просто без
обёртки CLI, которая не установлена на runner.
Явная миграция перед seed. main.ts/app.module.ts ничего не
мигрируют сами при boot — подтверждено чтением apps/api/src/db/migrate.ts,
отдельного скрипта. Job выполняет pnpm --filter api db:migrate перед
mix golden_set.rag.
docs/gates.yml: rag-acertivo остаётся severidade: warn.
Намеренно, а не по забывчивости — см. Consequences. evidencia.tipo меняется с
teste на ci (тот же формат, что у gate backmerge), с workflow,
указывающим на новый файл.
Последствия
warn остаётся warn — по решению о частоте, а не из-за технического
ограничения, и именно эту разницу этот ADR меняет относительно 0132.
Gate теперь по-настоящему ЗАПУСКАЕТСЯ в CI; не существует PR, заблокированного
им. Регрессия точности поиска появляется до 24 ч после merge
(или сразу, через ручной workflow_dispatch), никогда в момент PR, —
компромисс, явно принятый владельцем продукта, учитывая стоимость
~3-6 мин на запуск (pull модели + индексация + 17 поисков), которую навязал бы
запуск в каждом PR.
Golden-set QA (ADR 0123) здесь ничуть не меняется. Он остаётся
полностью ручным, TODO(humano) — асимметрия (детерминированный embedding на CPU
vs. дорогое и недетерминированное суждение чата) намеренна и объяснена выше,
а не несогласованность, которую нужно исправить потом.
Пробел курируемого корпуса остаётся открытым. 17 вопросов по-прежнему измеряют retrieval по тем же 22 курируемым файлам ADR 0132, а не по +130 реальным ADR, — этот этап подключает МЕХАНИЗМ к CI, а не расширяет то, что он измеряет. Расширение корпуса остаётся решением о стоимости embedding в ручном раунде, объявленным отдельно.
Новый риск и где он живёт. Бизнес-логика (скрипт seed,
критерий попадания, тест ExUnit) уже была доказана — 17/17, дважды,
согласно ADR 0132. По-настоящему новое здесь — ОРКЕСТРАЦИЯ
workflow (два сервиса, порядок шагов, pull модели прямым HTTP),
которую невозможно протестировать от начала до конца вне настоящего GitHub Actions.
Рекомендовано пользователю: запустить ручной workflow_dispatch, как только
этот PR будет смержен, чтобы подтвердить зелёный job, не дожидаясь первого
окна по расписанию.