Перейти к основному содержимому

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, не дожидаясь первого окна по расписанию.