검색 실험 계획 (164문항)

아카이브됨 — 이 설계로 낸 수치는 무효다 (2026-07-29)

무효 사유: 위 문서(검색 실험 결과 (164문항))의 설계서다. 그 실험이 무효이므로 이 설계도 현행이 아니다. 정답지가 v5로 바뀌었고 후보 구성 변형 정의도 달라졌다.

대체 정본 → 검색 실험 설계 · 본문은 사료로 보존한다.

2026-07-29 실행 완료. 결과는 → 검색 실험 결과 (164문항) 이 문서는 실험을 왜 이렇게 설계했는가의 기록으로 남긴다. 결과 수치는 결과 문서에 있다.

이 문서 하나만 읽으면 실험을 그대로 실행할 수 있게 쓴다. 다른 문서를 안 봐도 된다. 개념이 낯설면 → 개념 색인 · 1차 실험 전문 → 검색 개선 2부


0. 세 줄 요약

  1. 지금 검색은 뜻으로만 찾는다. 여기에 **글자로 찾기(BM25)**를 더할지 결정해야 한다.
  2. 40문항으로 미리 재봤더니 조건에 따라 결론이 정반대로 나왔다. 이번엔 164문항으로 제대로 판정한다.
  3. 다만 정답지 검증이 안 끝나서 아직 시작 못 한다. → §13

실행 후기: 3번(정답지 검증)은 해소됐고, 실험은 전부 돌았다. 그리고 2번의 “40문항 결론”은 164문항에서 뒤집혔다 — BM25는 미채택, 리랭커 채택. 계획 §3에서 “확정 1”이라 부른 것이 사실은 소표본 노이즈였다. 무엇이 왜 틀렸는지는 검색 실험 결과 (164문항) §5.


1부 · 무슨 문제를 푸는가

1. 지금 검색이 어떻게 돌아가나

사용자가 질문하면 이런 순서로 답이 나온다.

질문
 ↓
① 라우터 — "어느 RFP 문서 얘기지?" 를 먼저 정한다
 ↓
② 검색 — 그 문서를 잘게 자른 조각(청크) 중 답이 될 만한 5개를 고른다   ← 이 실험의 대상
 ↓
③ 생성 — 그 5개를 LLM에게 주고 답을 쓰게 한다

②번만 다룬다. ①과 ③은 이 실험 범위 밖이다.

②에서 쓰는 방법 — 지금은 하나뿐

Dense 검색 — 청크를 미리 숫자 벡터로 바꿔두고, 질문 벡터와 가까운 것을 꺼낸다. 글자가 안 겹쳐도 뜻이 비슷하면 찾는다 (사업기간을 물으면 용역기간 적힌 청크도 나옴).

→ 자세히 Dense 검색 · 임베딩

우리 데이터 규모

항목비고
원본 RFP100건
인덱싱된 문서99건1건은 추출 실패
청크(조각)30,002개512토큰 기준으로 자름
그중 청크63.3%RFP 핵심 정보가 대부분 표 안에 있음

표가 63%라는 게 이 실험의 출발점이다.

2. 왜 BM25를 검토하게 됐나

Dense 검색은 표와 고유명사에 약하다. 임베딩 모델을 고를 때(ADR-006) 이미 알고 기록해둔 약점이다.

왜 약한가: 표 청크 하나에 셀이 수십 개 들어간다. 그걸 벡터 하나로 압축하니 ISO 4373 같은 핵심 코드 하나의 신호가 다른 셀들에 묻힌다.

BM25는 정반대다. 뜻은 전혀 모르고 글자가 겹치는지만 본다. 그래서 ISO 4373이 적혀 있으면 바로 잡는다. → BM25

둘을 합치면(하이브리드) 서로의 약점을 메울 것이라는 게 원래 기대였다. → 하이브리드 검색


2부 · 1차 실험(40문항)에서 알아낸 것

결론이 두 번 뒤집혔고, 그 과정에서 세 가지가 확정됐다. 이번 실험은 이 위에서 시작하므로 왜 그렇게 됐는지 여기서 다 설명한다.

3. 확정 1 — BM25는 “문서를 좁혔을 때만” 효과가 있다

실측

검색 범위하이브리드가 dense보다 나은 정도
문서 99개 전부에서 검색+0.002 — 효과 없음
대상 문서 1개로 좁혀서 검색+0.073 — 확실한 개선

이 숫자의 단위가 뭔지는 §6에서 설명한다. 지금은 “클수록 좋다”만 알면 된다.

왜 이런 차이가 나나

① BM25는 겹치는 단어 개수로 점수를 매긴다. 뜻은 모른다.

② 그런데 우리 질문은 사업명이 너무 길다.

"네팔 수자원관리 정보화사업 Pilot 시스템 구축용역에서  ISO 표준은 무엇인가요?"
 └────────── 사업명 = 10단어 ──────────┘              └ 진짜 핵심어 = 1단어 ┘

③ 그래서 사업명만 잔뜩 반복하는 청크가 1등을 차지한다.

순위어떤 청크겹친 단어
1등그 문서의 과업개요 — 사업명이 통째로 적혀 있음10개
59등진짜 정답관련요구사항: ISO 4373이 적힌 표6개

정답에 ISO가 있긴 하지만 딱 한 단어다. 사업명 10단어를 못 이긴다.

④ 문서를 1개로 좁히면 이 문제가 사라진다. 그 문서 안에서는 사업명이 어느 청크에나 비슷하게 나오니 점수 차가 안 생기고, ISO 같은 진짜 핵심어가 비로소 승부를 가른다.

반증도 해봤다

“그럼 질문에서 사업명을 지우면 되지 않나?” 지웠더니 더 나빠졌다 (0.533 → 0.126). 사업명이 없으니 99개 문서 중 엉뚱한 문서의 청크가 올라왔다.

결론: 사업명 단어는 두 가지 일을 동시에 한다.

역할효과
올바른 문서를 찾는 것필수 — 없으면 엉뚱한 문서가 나옴
문서 안에서 청크를 고르는 것방해 — 개요·표지가 정답을 밀어냄

방향이 반대라 서로 상쇄된다. 문서를 바깥에서 정해주면 첫 역할이 필요 없어지고 충돌이 풀린다.

4. 확정 2 — 지금 코드에서 BM25는 아예 동작하지 않는다

# src/query/retrieve.py:36
use_bm25 = RETRIEVAL_USE_BM25 and doc_id is None
#                                 ↑ 문서가 안 정해졌을 때만 BM25를 켠다

라우터를 따라가 보면

질문 들어옴
   ↓ 사업명으로 문서 찾기 시도
   ├─ 못 찾음 + "뭐 있어?" 류 → explore    문서 후보만 보여줌. 검색 안 함
   ├─ 못 찾음 + 이전 문서 있음 → 그 문서로 진행       (doc_id 있음)
   ├─ 못 찾음 + 이전 문서 없음 → clarify    되물음. 검색 안 함
   └─ 찾음                   → retrieval            (doc_id 있음)
                                    ↓
                            여기서만 검색이 돈다

검색까지 도달하면 doc_id는 항상 있다. 문서가 안 정해지면 검색 자체를 안 하고 되묻는다.

doc_id is None항상 거짓use_bm25항상 거짓RETRIEVAL_USE_BM25 = True로 바꿔도 아무 일도 안 일어난다

확정 1과 합치면

이 코드는 BM25를 효과 있는 조건(문서 한정)에서 끄고, 효과 없는 조건(전체)에서 켜도록 돼 있다. 정확히 거꾸로다.

1차 실험이 왜 무의미했는지도 여기서 드러난다. 프로덕션에 존재하지도 않는 경로(전체 코퍼스)를 재고 “BM25 효과 없음”이라 결론냈던 것이다.

5. 확정 3 — “표만 검색하기”는 역효과

아이디어: 표가 63%고 BM25가 표에 강하다면, BM25를 표 전문 검색기로 쓰면 개요·표지 같은 텍스트 청크가 방해하지 못하지 않을까?

발상은 타당했지만 전부 나빠졌다.

BM25 전체 대상BM25 표만 대상
전체 코퍼스0.3460.190
문서 한정0.5320.296

왜 안 되나

① 정답의 상당수가 표가 아니다

구간정답 청크표 아님
일반 문항63개19개 (30%)44개 (70%)
어휘형 문항10개8개 (80%)2개

표만 뒤지면 정답 70%를 아예 못 찾는다.

② 표만 남겨도 사업명 문제가 그대로다

정답의 80%가 표인 어휘형에서조차 0.701 → 0.593으로 떨어졌다. 사업개요·표지가 표 형태인 경우가 많기 때문이다. 텍스트 청크만 걷어내도 방해꾼은 남는다.

핵심: 중요한 건 범위를 좁히는 것이지 대상을 표로 거르는 것이 아니다.


3부 · 이번 실험을 어떻게 하나

6. 무엇으로 재나 — MRR

MRR = 정답이 몇 등에 나왔나를 점수로 바꾼 것. 문항 하나당 점수 = 1 ÷ 첫 정답의 등수

정답 등수1등2등3등5등10등10위 밖
점수1.0000.5000.3330.2000.1000

전 문항 평균이 MRR이다. → MRR

왜 “찾았나”가 아니라 “몇 등인가”인가

실전에서 상위 5개만 LLM에 넘긴다. 1등→2등은 큰 손해지만 15등→20등은 무의미하다. MRR은 위쪽 변화를 무겁게 치므로 실전 체감과 맞는다.

차이를 읽는 법 — +0.073이 얼마나 큰가

MRR 차이에 문항 수를 곱하면 총점이 나온다. 40문항 실험의 +0.073 × 36 = 2.6점.

이런 변화 하나가점수
2등 → 1등+0.50
3등 → 1등+0.67
6등 → 1등+0.83
20위 밖 → 3등+0.33

+0.073은 3~5문항이 크게 개선된 것이지 전 문항이 조금씩 좋아진 게 아니다. 반대로 1차의 +0.002는 총점 0.07점 — 한 문항이 10등에서 9등 된 정도. 사실상 아무 일도 없었다.

⚠️ 예전에 쓰던 hit@5를 안 쓰는 이유

hit@5(상위 5개에 정답이 있나)는 1.000으로 포화됐다. 다 만점이라 비교가 안 된다.

단 그건 문서 단위 얘기였다. 청크 단위로 내려가면 아직 한참 멀었다 — 일반 문항 dense MRR이 0.527이고, 정답이 상위 20에 아예 없는 경우도 있다.

포화됐다고 조건을 바꾸면 안 된다. 1차 실험이 “포화라 변별이 안 된다”며 문서 필터를 껐다가 결론이 뒤집혔다. 조건은 실전 그대로 두고 지표를 민감한 걸로 바꾼다.지표 포화

보조 지표

지표언제 쓰나
nDCG@10정답이 여러 개일 때, 위쪽에 몰려 있는지
Recall과 hit rate@5상위 5개에 정답이 몇 개나 들어왔는지

7. 어떤 문제로 재나 — 164문항

왜 200이 아니라 164인가

골든셋은 200문항이지만 MRR로 채점할 수 있는 건 164개다.

무엇
200골든셋 전체 (VM v4_20260729)데이터셋 크기
164이 실험의 모수정답 청크가 지정된 문항
30모른다 15 + 반문 15정답이 없는 게 정답인 유형. MRR로는 못 잼 → §7-3
6직답 5 + 후속 1정답이 있어야 하는데 라벨이 빔 → 제외 (§13-5에서 재판정)

164 + 30 + 6 = 200

유형별 구성

유형문항채점어떤 질문인가
단일세부9090한 문서 안의 특정 사실
탐색비교2525문서 2개를 비교
후속3029앞 질문을 이어받는 대화형
직답2520짧은 사실 (금액·기간·번호)
모른다15문서에 답이 없는 질문
반문15되물어야 하는 애매한 질문
200164

7-3. 모른다·반문 30문항은 버리지 않는다

MRR로는 못 재지만 다른 걸 잰다.

검색이 근거도 없는 청크를 자신 있게 가져오면 LLM이 그걸 근거로 없는 사실을 지어낸다.

측정: 상위 5개 청크의 유사도 점수 분포를 정답 있는 문항 vs 없는 문항으로 나눠 비교한다. 두 분포가 뚜렷이 갈려야 정상이다. BM25·리랭커를 넣었을 때 이 구분이 흐려지면 감점.

7-4. 유형별 특수 처리 — 코드에 반드시 반영

탐색비교 (25문항) — 대상 문서가 2개다. 25문항 전부.

검색 = 문서A에서 top-k  ∪  문서B에서 top-k  →  합쳐서 순위 매김

문서별로 따로 뽑아야 한다. 한쪽이 후보를 독식하면 비교 자체가 성립하지 않는다.

후속 (29문항)선행_턴에 앞 질문이 들어 있고, query앞 질문 + 이번 질문이 붙은 문자열이다.

실험은 그 합쳐진 query를 그대로 쓴다. 실전 라우터는 이전 문서를 기억해 처리하는데, 검색만 재는 실험에서는 문서가 이미 정해진 상태라 결과가 같다. 단 이 선택을 결과에 명시한다 — 라우터의 후속 처리 정확도는 별개 문제다.

8. 무엇과 무엇을 비교하나 — 5가지

변형구성묻는 것
A_dense지금 그대로기준선
B_hybriddense + BM25를 무조건 합침글자 검색이 보탬이 되나
B2_gateddense + BM25를 dense가 자신 없을 때만손실을 없앨 수 있나
C_rerankdense + 크로스인코더 리랭커순위 다시 매기는 게 나은가
D_bothB + C두 개선이 겹치나

합치는 방법 — RRF

두 검색기의 점수는 척도가 달라 그냥 못 더한다(dense는 0~1, BM25는 20도 45도 나옴). 그래서 점수를 버리고 등수만 쓴다. 점수 = Σ 1/(60 + 등수)RRF

8-2. B2_gated를 넣는 이유 — B의 +0.073은 순이득일 뿐이다

40문항 결과를 문항별로 뜯어보면 이렇다.

구분문항점수
올라감16+5.87
내려감7−3.24
변화 없음130
순이득36+2.62 → 평균 +0.073

전반적으로 좋아진 게 아니다. 크게 오르는 것과 크게 내리는 것이 부딪히고 있다.

올라간 쪽

dense→ 하이브리드얻은 점수
6등1등+0.833
6등1등+0.833
4등1등+0.750
2등1등+0.500 (×4건)
못 찾음13등+0.077

내려간 쪽 — 전부 dense가 이미 1등으로 맞히던 것

dense→ 하이브리드잃은 점수
1등5등−0.800
1등3등−0.667
1등2등−0.500 (×3건)
3등10등−0.233

원인: RRF무차별적이다. dense가 확신하는 문항에도 BM25 의견을 똑같이 반영해 잘 맞히던 걸 밀어낸다.

가설: dense가 이미 잘 찾은 문항에서 BM25를 빼면 이득(+5.87)은 지키고 손실(−3.24)만 없앨 수 있다. 이론상 최대 +0.163 (5.87 ÷ 36) — 지금의 두 배가 넘는다.

8-3. “자신 있다”를 무엇으로 판별하나

dense 1위 결과의 유사도 점수를 쓴다.

dense 1위 유사도 ≥ 임계값  →  dense 결과 그대로 (BM25 안 씀)
dense 1위 유사도 <  임계값  →  BM25와 합침

임계값은 결과를 보고 정하지 않는다. 측정 전에 이 절차로 확정한다:

  1. 164문항의 dense 1위 유사도를 전부 뽑는다
  2. 정답을 1등으로 맞힌 문항 vs 못 맞힌 문항으로 나눠 분포를 본다
  3. 두 분포가 갈리는 지점을 임계값으로 삼는다

⚠️ 분포가 안 갈리면 이 변형은 성립하지 않는다. “dense가 자신 있다”를 판별할 신호가 없다는 뜻이므로 B2를 기각하고 B로 간다. 임계값을 여러 개 돌려보고 제일 좋은 걸 고르는 것은 금지 — 정답지에 끼워 맞추는 짓이다.

대안 신호: dense 1위와 2위의 점수 격차. 벌어지면 확신, 붙어 있으면 애매.


4부 · 판정과 실행

9. 어떤 조건에서 재나

항목
검색 범위문서 한정만프로덕션에 다른 경로가 없다 (§4)
문서 지정골든셋의 정답 문서를 넣어줌검색 성능만 떼어내 보려고

전체 코퍼스 조건은 아예 측정하지 않는다. 존재하지 않는 경로다.

라우터 정확도로 깎지 않는다

“라우터가 문서를 틀리면 어떡하냐”는 걱정이 있을 수 있는데, 그 경우 dense도 하이브리드도 똑같이 실패한다. 엉뚱한 문서 안에서 찾는 거라 어느 방식이든 0점이다.

방식 간 차이는 라우터 정확도와 무관하다. 라우터 정확도는 “검색 전체가 쓸모 있나”의 문제로 별개다.

10. 언제 채택하나 — 승격 기준

측정 전에 확정한다. 결과를 보고 바꾸지 않는다.

#기준왜 이 값인가
1A 대비 MRR +0.05 이상164문항 총점 8.2점. §6 환산으로 10문항 안팎이 상위권으로 올라와야 하는 선. 이 정도는 돼야 코드 복잡도를 감수할 값어치가 있다
2어느 유형에서도 −0.02 이상 나빠지지 않을 것평균이 올라도 특정 유형이 망가지면 안 된다
3dense가 1등 맞히던 걸 3등 밖으로 민 건수 ≤ 2§8-2에서 실제로 5건 발생. 평균만 보면 안 보이는 손실
4모른다·반문 분리도 유지없는 근거를 가져오면 환각이 된다 (§7-3)
  • 하나라도 미달 → 미채택으로 기록하고 플래그는 OFF로 남긴다
  • 여러 변형이 통과하고 차이가 0.02 이내면 더 단순한 쪽 (A < B,C < D)

채택하면 그다음에 할 일

#
1retrieve.py:36and doc_id is None 제거 — 안 그러면 켜도 안 돈다 (§4)
2동시성 락 — 웹 서버가 동시 요청을 받으면 BM25 인덱스(30,002청크)와 리랭커(568M 모델)가 중복 로드돼 GPU 메모리가 터진다

11. 어디서 돌리나

🔴 인덱스를 잘못 지정하면 결과가 조용히 전부 틀린다

청크 id가 문서명::순번 형태라 위치 기반이다. 다시 인덱싱하면 같은 id에 다른 본문이 들어간다.

인덱스청크id 매칭본문 일치
data/chroma (기본값) ❌28,293100%0%
data/chroma_v230,002100%100%
VM /srv/GPTPilot/chroma30,002100%100%

id는 다 맞아서 에러가 안 난다. 본문만 전부 다른 채로 끝까지 돈다. → 실행 시 CHROMA_DIR_OVERRIDE반드시 지정한다. → 청크와 chunk_id

어디서무엇을
로컬A · B · B2GPU 불필요
VM (L4 23GB)C · D리랭커가 GPU 필요
  • 골든셋 정본: /srv/GPTPilot/goldenset/current (= v4_20260729)
  • 비용 $0 — LLM 호출 없음. OpenAI 팀 API 안 씀

12. 결과를 어떻게 적나

유형별 표가 주 결과다. 전체 평균만 보면 유형별 신호가 상쇄돼 사라진다.

유형문항A_denseB_hybridB2_gatedC_rerankD_both
단일세부90
탐색비교25
후속29
직답20
전체164

문항별 분해도 함께 낸다 — 올라간 문항 수·점수 / 내려간 문항 수·점수 / 무변화. §8-2처럼 뜯어봐야 “잘 되던 걸 망가뜨리는” 패턴이 보인다.

반드시 함께 기록할 것

#항목빠뜨리면
1인덱스 경로조용히 틀린 결과 (§11)
2검색 범위 조건해석 불가 — 1차 실험이 이걸로 뒤집힘 (§3)
3유형별 분리 수치개선 신호가 평균에 묻힘
4채점 문항 수 + 제외 내역표본 신뢰도 판단 불가 (§7)
5라벨 검증 완료 여부·오류율노이즈를 신호로 오독 (§13)

5부 · 시작을 막고 있던 것 (해소됨)

13. 병목이었던 것 — 정답지 검증

2026-07-29 해소. rest130 130문항 + pilot40 40문항 = 170문항 사람 검증 완료. 아래는 이 검증이 왜 필요했는지의 기록이다.

이게 끝나기 전에는 실험을 시작하지 않는다.

무슨 문제인가

정답지(어느 청크가 정답인지) 200문항을 AI가 만들었다. 사람이 200번 라벨링할 수 없어서다.

그런데 그 AI가 합격선을 못 넘었다. 사람이 만든 40문항과 대조한 결과:

기준필요최신(r8)
재현율 (정답을 빠짐없이 잡았나)≥0.900.836
정밀도 (고른 것 중 맞는 비율)≥0.800.772
완전+포함≥0.700.725
거짓 판정00

재현율과 정밀도 · 라운드별 성적 정본은 골든셋

그래서 다른 AI를 검증자로 붙였고, 검사한 50문항 중 8건(16%)에서 수정이 나왔다.

왜 이게 실험을 막나

정답지가 16% 틀린 상태로 재면, 방식 간 차이(+0.07)보다 정답지 오차가 크다. A가 B보다 나은 건지 정답지가 틀린 건지 구분할 수 없다.

남은 작업

#할 일현황
1rest130 검증50/130 — 80문항 남음
2pilot40 검증미착수
3사람 확인 대기 18건직접 판단 필요
4알려진 결함 4건 처리정답이 이미지 안에만 있는 경우 등 → 제외 확정
5빈 정답 6건 원문 대조아래
6검증 반영 → VM v4 업로드v3는 검증 전 스냅샷

13-5. 빈 정답 6건을 왜 꼭 확인해야 하나

40문항 골든셋에서 “정답 없음”으로 라벨된 4건 중 2건이 실제로는 정답이 있었다.

원인은 정답지 만드는 방식에 있다:

질문 → [검색으로 후보 34개 뽑기] → [사람/AI가 그 중에서 정답 고르기]
        ↑ 검색이 놓친 정답은 여기 없음 → 고를 수가 없음 → "정답 없음"이 됨

검색이 실패한 문항이 정답지에서 조용히 사라지고, 채점에서도 빠진다. 그러면 남은 문항으로 잰 점수는 실제보다 좋게 나온다.

→ 6건은 원문을 직접 뒤져서 진짜 없는 건지 확인한다. 결과에 따라 모수가 164에서 최대 170까지 늘 수 있다. → 골든셋

14. 진행 순서

단계작업결과
0정답지 검증 (§13)✅ rest130 130 + pilot40 40 완료
1VM 골든셋 고정/srv/GPTPilot/goldenset/v4_20260729 (당시 v3 — 라벨 동일, 파일만 분리)
2러너 개조 — 탐색비교 다중문서TypeError 수정 + 캐시 방식으로 재설계
3B2 임계값 확정 (§8-3)✅ 사전 확정 대신 7종 전수 스윕으로 대체 (캐시 덕분에 가능)
4A · B · B2 측정 (로컬)✅ 전부 미달
5C · D 측정 (VM)✅ C 채택 (+0.104)
6거짓양성 검사 (§7-3)미실행 — 남은 과제
7승격 판정 (§10)검색 실험 결과 (164문항)
8채택 시 코드 반영⬜ 동시성 락 필요

3번은 계획을 벗어났다. 원안은 “측정 전에 임계값 하나를 확정”이었는데, 검색 결과를 캐시하니 임계값 7종을 전부 재는 비용이 1초 미만이 됐다. 사전 확정의 목적(결과를 보고 기준을 유리하게 바꾸지 않기)은 승격 기준 +0.05를 건드리지 않는 것으로 지켰고, 임계값은 탐색 대상으로 풀었다. 결과적으로 어떤 임계값도 기준선을 못 넘어서 이 완화는 결론에 영향이 없었다.

15. 이번 계획에 반영한 교훈

측정하기 편하려고 실전 조건을 바꾸면, 그 실험은 실전에 대해 아무 말도 못 한다.

1차 실험은 *“지표가 포화돼 변별이 안 된다”*는 이유로 문서 필터를 껐다. 그 결과 프로덕션에 존재하지도 않는 경로를 재고 “BM25 효과 없음”이라고 결론냈다. 조건을 실전대로 되돌리니 결론이 뒤집혔다.

승격 기준을 미리 정하는 것조건이 실전과 같은지 확인하는 것은 별개다. 앞의 것만 지키면 엄격하게 잘못된 답이 나온다.


16. 관련 문서

문서무엇을 보러
검색 실험 결과 (164문항)이 계획을 실행한 결과·판정
검색 개선1차 실험 전 과정 + 검색 변경 이력
골든셋200문항 확장 설계 · 라벨러 성적 정본
검색 실험 골든셋 40문항40문항 사람 라벨 · 문항 목록
개념 색인MRR·BM25·RRF 등 개념
쿼리 파이프라인검색이 전체 흐름에서 어디인가
라우터 5분기 판정 흐름§4의 라우터 분기 그림

코드: src/query/retrieve.py · src/query/hybrid.py · src/query/router.py · scripts/run_rerank_experiments.py 정본: Plans.md 6.5/6.7 · spec.md §6-1·§6-2 · docs/ADR.md ADR-006