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

ADR 0132 — Golden-set попаданий RAG и gate rag-acertivo

Контекст​

ADR 0129 (Часть 2, Этап 1) дал гибридному поиску след — rag_searches/rag_feedback, с весами, ЗАМОРОЖЕННЫМИ в строке, — но намеренно ничего не измерял: только инструментировал. pnpm --filter api medir:rag читает этот след по проекту, когда он есть. Не хватало того, что сам rag-search-limits.ts продолжает объявлять:

NENHUM dos quatro números abaixo vem de calibração com dado real: não há, ainda, um corpo de perguntas reais rodado contra este índice.

Это Этап 2 той же программы — второй из пяти, в порядке, установленном владельцем продукта. Он ничего не калибрует (это отдельное продуктовое решение, Этап 5, с собственным критерием отказа); он даёт корпус вопросов, который Этап 1 оставил пробелом.

Шаблон уже существует, и это ADR 0123​

Golden-set регрессий СЕМАНТИЧЕСКОГО суждения QA de Automação (ADR 0123) решил соседнюю задачу — «как измерить надёжность чего-то недетерминированного, без CI, с порогом-храповиком», — и решение здесь — переиспользовать всю структуру, а не изобретать во второй раз то же рассуждение:

детальADR 0123 (QA)этот ADR (RAG)
aliasmix.exs "golden_set.qa""golden_set.rag"
постоянное исключениеtest_helper.exs :golden_set_qa:golden_set_rag
тест ExUnittest/engine/gates/qa_automacao_agent_golden_test.exstest/engine/rag/rag_golden_test.exs
порог-храповикtest/fixtures/golden_set_qa/floor.jsontest/fixtures/golden_set_rag/floor.json
seedapps/api/scripts/seed-golden-set-qa.tsapps/api/scripts/seed-golden-set-rag.ts

Постоянное исключение по ТЕГУ (никогда не обнаружение «Ollama доступен») действует по ТОЙ ЖЕ причине, что и на стороне QA: на этой машине разработчика Ollama уже работает постоянно, и автоматическое включение заставило бы модуль срабатывать внутри ЛЮБОГО mix test, тратя токены без предупреждения.

Где шаблон НЕ подходит — и почему это не отклонение​

Golden-set QA тестирует суждение чат-LLM, вызываемого через Engine.Gates.QaAutomacaoAgent.run/5. У RAG нет никакого суждения LLM на измеряемом пути — весь гибридный поиск (эмбеддинг + ts_rank + слияние по весу + отсечение по порогу) выполняется внутри api (HybridSearchUseCase), а не engine. Переписать поиск на Elixir только ради этого теста значило бы тестировать ВТОРУЮ реализацию, никогда не настоящую, — ту же ошибку, которой ADR 0123 избежал, используя настоящий LLM-клиент вместо симуляции суждения.

Решение: seed-golden-set-rag.ts делает ОБЕ вещи — подготавливает и ищет, — возвращая уже готовый результат; rag_golden_test.exs лишь вызывает скрипт (через System.cmd, тот же механизм, что на стороне QA) и применяет порог к вернувшемуся JSON. Роль стороны Elixir — не «выполнять поиск», а «быть единой точкой mix test --only/постоянного исключения/порога-храповика» — та же дисциплина golden-set, которую уже использует остальной продукт, применённая к механизму, который случайно живёт целиком по другую сторону границы api↔engine.

Вопросы: составленные, а не извлечённые — и это сказано, а не скрыто​

Исходный запрос требовал РЕАЛЬНЫХ вопросов из dogfooding. Проверено до написания этого ADR: gh issue list --repo daneiel/brabo --state all возвращает «No Issues» — это сольный проект, без истории вопросов, сформулированных кем-то, кто пользуется продуктом, в issue или PR (заголовки PR все вида tipo(escopo): o que mudou, никогда не вопрос).

17 вопросов seed-golden-set-rag.ts поэтому составлены на основе РЕАЛЬНЫХ RN и ADR этого репозитория — никогда не выдуманы без опоры. У каждого есть expectedPath — реальный файл, где живёт ответ, и составление проверено прямым чтением целевого файла до написания вопроса (не по заголовку/grep). Это путь, который сам запрос предусмотрел на случай отсутствия подлинного корпуса, а притворяться, что они «реальные», было бы хуже, чем объявить составление.

Индексируемый корпус реален, отобран, не синтетичен​

В отличие от QA (чей каркас кода синтетический, намеренно спроектированный под каждый случай), корпус golden-set RAG — РЕАЛЬНОЕ ПОДМНОЖЕСТВО docs/ самого этого monorepo — 22 файла (ARQUIVOS_CURADOS в seed), скопированных дословно в репозиторий засеянного проекта, проиндексированных ТЕМ ЖЕ IndexProjectDocsUseCase, что использует любой реальный проект. Отобран, а не всё дерево (в реальном дереве больше 130 ADR), — из-за стоимости эмбеддингов в ручном прогоне: поиск среди нескольких сотен чанков уже проверяет слияние вектор+лексика так же, как поиск среди тысяч, а переиндексация более сотни ADR на каждом ручном прогоне умножила бы время, не улучшая того, что измеряет golden-set.

Один проект, а не по одному на случай​

Golden-set QA подготавливает по репозиторию на СЛУЧАЙ, потому что каждый случай тестирует отдельное бизнес-правило над ДРУГИМ кодом — изоляция существенна. 17 случаев RAG — это 17 вопросов о ТОМ ЖЕ корпусе документации: разбиение на 17 проектов проиндексировало бы одно и то же содержимое 17 раз (17x стоимость эмбеддингов), чтобы изолировать поиски, которые по природе уже только для чтения и без побочных эффектов друг на друга.

execution_mode: 'runner', а не container по умолчанию​

ReadProjectCodeUseCase.portaoDoContainer (RN-105) отклоняет с 409 любое чтение проекта container, пока Arquiteto не выберет образ, — IndexProjectDocsUseCase вызывает этот путь, чтобы просмотреть docs/. Golden-set никогда не задействует Arquiteto (нет никакого выполнения, которое бы это оправдывало, только индексация документации), поэтому seed вставляет проект с execution_mode: 'runner' — RN-169/RN-421 освобождают mounted/runner от всего барьера, и это ровно для такого случая: проекту без собственного контейнера незачем ждать решения об образе, чтобы его код был прочитан. CHECK projects_workspace_path_casa_com_modo требует непустого workspace_path для execution_mode <> 'container' — seed записывает синтетический путь (никогда не используемый: ни один настоящий runner к этому проекту не подключается).

Критерий попадания: путь файла, top-K, никогда не rank 1​

Ожидаемое попадание — это путь файла, а не точный чанк, — та же свободная мерка ADR 0123, адаптированная к RAG: привязка к чанку ломала бы golden-set при каждой настройке chunking, а это как раз тот параметр, ради возможности пересматривать который без переписывания случаев существует эта программа.

GOLDEN_SET_RAG_TOP_K = 5, а не rank 1: так продукт реально использует результат — Chat RAG цитирует НЕСКОЛЬКО фрагментов рядом с ответом (RagCitationCard, во множественном числе), а не только первый. Измерение против rank 1 измеряло бы вопрос, который UI не задаёт. 5 — это также не тот RAG_SEARCH_RESULT_LIMIT (10), который реальный маршрут использует по умолчанию, — запросить 10 и мерить против 10 значило бы тестировать более свободный k, чем использует большинство реальных поисков (панель Chat RAG не прокручивается, чтобы увидеть все 10). Критерий живёт в apps/api/src/domain/rag/golden-set-criterio.ts, чистом и протестированном (golden-set-criterio.spec.ts), по той же причине, что gate-registry.ts: решение «это совпало» не должно зависеть от того, как пришёл результат.

Телеметрия: ВКЛЮЧЕНА, намеренно​

HybridSearchUseCase записывает строку в rag_searches при каждом вызове, всегда — параметра для отключения нет, и его не должно быть. Правильный вопрос не «отключить?», а «загрязняет ли это реальное измерение?»: нет. medir:rag требует --projeto <uuid> (никогда не агрегирует глобально, см. заголовок самого скрипта), а golden-set всегда создаёт НОВЫЙ проект с суффиксом метки времени — записываемые им строки никогда не появляются в medir:rag реального проекта, по той же причине, по которой golden-set QA не загрязняет ни одну метрику реального проекта. А настоящая запись — то, что заставляет этот golden-set проходить ТОТ ЖЕ путь кода, что использует настоящий Chat RAG.

Gate rag-acertivo​

- id: rag-acertivo
fluxo: transversal # o RAG serve todos os fluxos, como acao-aprovada
dono: usuario
entrada: [golden-set-rag]
entregavel: taxa-de-acerto-acima-do-piso
verificacao: script
severidade: warn
aprovacao_humana: false
status: active
evidencia:
tipo: teste
arquivo: apps/engine/test/engine/rag/rag_golden_test.exs

fluxo: transversal и dono: usuario переиспользованы из gate acao-aprovada намеренно — ни одно из двух полей не проверяется по enum (apps/api/src/domain/gates/gate-registry.ts), а изобретение новой категории из одного члена (instrumentos/medicao) прошло бы валидатор, ничего не означая. Если однажды понадобится владелец medicao, он уже существует как РОЛЬ в docs/fluxo.yml (camada_instrumentos), и его введение — отдельное решение, а не побочный эффект этого gate.

evidencia.tipo: teste, а не event_log — этот gate доказывает сам golden-set, а не событие в логе (нет ни proposed_action, ни доменного события, связанного с прохождением golden-set). Прецедент: merge-protegida уже использует tipo: teste по той же причине — гарантия не берётся из event log.

severidade: warn ОБЯЗАТЕЛЬНО, а не стилистически: RN-070 требует verificacao: script для block (который у этого gate ЕСТЬ), но решение о warn приходит из другого места — нет CI с LLM (то же решение, уже зафиксированное для golden-set QA), поэтому gate выполняется ВРУЧНУЮ, а обещание block описывало бы автоматическую блокировку, которой не существует. docs/explanation/gates.md получает пятый разобранный пример «почему warn», по той же аргументационной структуре, что и четыре предыдущих (necessidade-validada, workspace-verificado, implementavel, paralelismo-autorizado).

entrada: [golden-set-rag] — не ссылка на другой gate: эвристика валидатора (entrada, оканчивающийся на -verificada/-segura) не совпадает с этим именем, поэтому проверка осиротевших не срабатывает. Подтверждено чтением apps/api/src/domain/gates/gate-registry.ts (pareceGate), а не предположено.

Решение​

  1. Скопировать структуру ADR 0123 (alias, постоянное исключение, тест ExUnit, порог-храповик по МОДЕЛИ — здесь модели ЭМБЕДДИНГОВ, — внешний seed через System.cmd) в домен RAG, с двумя адаптациями, которых требует домен: seed выполняет поиск (а не только подготавливает, потому что суждение целиком живёт на стороне api), а корпус реален и отобран (не синтетичен).
  2. apps/api/src/domain/rag/golden-set-criterio.ts — чистый, протестированный — определяет критерий попадания (путь файла, top-5, никогда точный чанк/rank 1) и потребляется как seed, так и любым будущим отладочным вызовом.
  3. docs/gates.yml получает rag-acertivo, warn, status: active, evidencia.tipo: teste, указывающий на тест ExUnit.
  4. floor.json с ключом по модели ЭМБЕДДИНГОВ (RAG_EMBEDDING_MODEL, сегодня nomic-embed-text — единственная, которую продукт поддерживает, RN-222), количество и of, никогда процент, записывается только человеком.

Последствия​

Подключение к CI — TODO(humano), как и у QA. Без секрета LLM через API или новой инфраструктуры (настоящий Ollama в CI) golden-set выполняется ВРУЧНУЮ — mix golden_set.rag — и никогда в обычном mix test.

17 вопросов измеряют retrieval по корпусу из 22 файлов, а не по 130+ реальным ADR. Golden-set против полного корпуса измерял бы более реалистичное распределение сложности (больше правдоподобных отвлекающих, больше шансов, что два файла покрывают один вопрос), — решение о стоимости эмбеддингов в ручном прогоне, пересматриваемое, если реальный прогон покажет искусственно лёгкий потолок.

Golden-set измеряет RETRIEVAL, а не ОТВЕТ. Он никогда не вызывает чат-LLM, чтобы синтезировать ответ из попаданий, — лишь измеряет, появился ли правильный путь среди кандидатов. Это метрика, которую Этап 1 (medir:rag) тоже использует (precision@k, распределение рангов), поэтому оба инструмента говорят на одном языке.

О реальном прогоне и о том, что фиксирует порог: см. test/fixtures/golden_set_rag/floor.json — комментарий _comment с измеренным результатом (или заявлением о том, что измерение осталось ожидающим ручного прогона, если окружение этой сессии не позволило прогнать против настоящего Ollama).