Field Log · Entry
Retrieval Evaluation: Recall@k·MRR·nDCG와 실패 분석 (10/10)
오늘의 결론
- 평가 dataset의 gold는 특정 chunk ID보다 원문 source와 evidence span에 가깝게 정의합니다.
- Hit/Recall은 찾았는지, MRR/nDCG는 얼마나 위에 배치했는지 측정합니다.
- ingestion coverage, exact retrieval, ANN, fusion, rerank, context, answer를 단계별로 분리합니다.
- 전체 평균 외에 질문 유형·언어·문서 형식·filter·freshness slice를 봅니다.
- 모든 run은 corpus·dataset·model·index·prompt·code version을 가진 immutable artifact여야 합니다.
앞 글까지 RAG 검색 pipeline의 부품을 만들었습니다.
parse → chunk → BM25 / dense ANN → hybrid fusion
→ rerank → context select → generate
이제 “embedding을 바꾸니 좋아진 것 같다”가 아니라 어떤 질문에서 어느 단계가 얼마나 개선됐는지 증명해야 합니다. 검색 시리즈의 마지막 글은 평가를 pipeline 위에 얹는 방법입니다.
그림 1. 하나의 최종 점수로 숨기지 않고 ingestion부터 answer까지 각 단계의 입력·출력·상한을 측정한다. run manifest가 모든 version을 고정해 비교를 재현한다.
먼저 평가 질문을 구체적으로 쓴다
“검색 성능을 평가한다”는 너무 넓습니다. 다음처럼 실험 질문으로 바꿉니다.
H1. structure-aware 256-token chunk가 fixed 512-token보다
table 질문 Recall@20을 높이는가?
H2. BM25+dense RRF가 exact identifier bucket의 성능을 유지하면서
paraphrase bucket nDCG@10을 높이는가?
H3. Cross-Encoder가 candidate Recall@50을 바꾸지 않고
gold evidence의 MRR과 context Precision@5를 높이는가?
H4. MMR selector가 evidence coverage를 유지하면서
context duplicate token ratio를 낮추는가?
한 run에서 바꾸는 핵심 변수를 하나로 제한하면 결과를 해석하기 쉽습니다.
Evaluation dataset의 최소 계약
질문 text와 정답 string만 있으면 retrieval 평가가 어렵습니다. 다음 정보를 둡니다.
{
"question_id": "q-retention-017",
"question": "Pro 해지 후 backup은 며칠 보존되나요?",
"answerable": true,
"reference_answer": "365일",
"evidence": [
{
"source_id": "policy:retention",
"revision_id": "2026-06-01",
"page": 8,
"start_char": 1510,
"end_char": 1588,
"relevance": 3
}
],
"required_aspects": ["retention_period", "after_cancellation"],
"slices": ["table", "numeric", "policy", "temporal"],
"created_from": "support-log-redacted",
"label_version": "v3"
}
왜 chunk ID를 gold로 쓰지 않을까
chunk size나 overlap을 바꾸면 ID와 경계가 달라집니다. chunk-42를 gold로 두면 chunker 실험마다 label을 다시 만들어야 합니다.
원문 evidence span을 gold로 두고 candidate chunk span과 겹치는지 계산하면 같은 dataset으로 여러 chunker를 비교할 수 있습니다.
gold span : 1510–1588
candidate span: 1400–1700
→ gold를 포함하는 relevant chunk
표, image, multi-page evidence는 cell 좌표·bbox·element ID처럼 더 적합한 locator를 사용할 수 있습니다.
Relevance label은 binary보다 넓을 수 있다
3 = 질문을 직접 완전히 답하는 핵심 근거
2 = 답에 필요한 일부 조건 또는 보조 근거
1 = 주제는 관련 있지만 답을 지지하기엔 부족
0 = 관련 없음
binary Recall에는 relevance ≥ 2를 relevant로 보는 식의 threshold가 필요합니다. nDCG는 graded relevance를 그대로 사용할 수 있습니다.
label guideline과 edge case 예시를 문서화합니다.
- 같은 사실을 paraphrase한 문서
- 최신과 폐기 revision
- 정답 일부만 포함한 chunk
- 질문과 주제는 같지만 조건이 다른 hard negative
- 답은 맞지만 권한 밖 source
권한 밖 문서는 relevance가 높아도 반환 가능한 gold가 아닙니다.
Dataset을 어디서 만들까
실제 query log
장점:
- production 분포와 표현을 반영
- 오타·짧은 질문·대화 의존성을 포함
주의:
- 개인정보 redaction과 접근 통제
- 기존 system이 보여 준 문서에 label이 편향
- click이 relevance와 동일하지 않음
- 자주 묻지 않지만 위험한 질문이 적음
Domain expert 작성
장점:
- 중요한 policy와 edge case를 포함
- authoritative evidence label 가능
주의:
- 실제 사용자보다 문장이 잘 정리돼 있을 수 있음
- 작성자가 문서 vocabulary를 그대로 사용해 BM25에 유리
Synthetic question
문서에서 LLM으로 질문을 생성할 수 있습니다. coverage를 빠르게 늘리지만 다음 편향이 있습니다.
- 질문이 source 문장 표현을 복사
- 쉬운 answerable 질문 중심
- 생성 model의 스타일에 고정
- 잘못된 evidence와 answer
synthetic set은 별도 slice로 표시하고 human validation 없이 production 대표 점수로 쓰지 않습니다.
권장 구성
real logs + expert critical cases + validated synthetic coverage
source를 섞되 각 origin별 metric을 따로 봅니다.
Train, dev, test를 나누는 방법
query를 무작위로만 나누면 같은 문서의 비슷한 질문이 양쪽에 들어갈 수 있습니다.
가능한 split:
- source document 단위
- time-based split
- customer/tenant 단위
- question template family 단위
- entity 단위
train: parameter·model 학습
dev : threshold·k·weight 선택
test : 최종 한 번의 비교
RRF constant, chunk size, reranker 후보 수를 test에 맞추면 test가 dev가 됩니다. 변경할 때마다 test를 들여다보는 leakage를 피합니다.
Hit@k와 Recall@k
Hit@k
top-k에 relevant item이 하나라도 있으면 1입니다.
Hit@k(q) = 1 if top-k ∩ Relevant(q) ≠ ∅ else 0
gold 하나만 필요한 lookup 질문에 직관적입니다. dataset 평균은 Hit Rate@k라고 부르기도 합니다.
Recall@k
여러 relevant evidence 중 몇 개를 찾았는지 봅니다.
Recall@k(q) = |top-k ∩ Relevant(q)| / |Relevant(q)|
비교·multi-hop 질문에서 gold 3개 중 1개만 찾으면 Hit@k는 1이지만 Recall@k는 1/3입니다.
metric 이름을 report에 명확히 씁니다. 단일 gold dataset에서 Hit@k와 Recall@k가 같아 보일 수 있어 혼용되기 쉽습니다.
Precision@k
top-k 중 relevant가 얼마나 되는지 봅니다.
Precision@k(q) = |top-k ∩ Relevant(q)| / k
retrieval candidate 단계에서는 recall을 위해 precision을 양보할 수 있습니다. LLM context에 넣는 작은 k에서는 noise가 answer를 방해하므로 precision이 중요합니다.
결과 수가 k보다 적을 때 denominator를 k로 둘지 반환 결과 수로 둘지 정의합니다. 표준 비교와 application 의미가 다를 수 있으므로 report에 명시합니다.
MRR: 첫 relevant item의 위치
RR(q) = 1 / rank of first relevant item
MRR = mean(RR(q))
첫 gold가 1위면 1, 2위면 0.5, 10위면 0.1입니다. relevant item이 없으면 0입니다.
MRR은 첫 정답을 빠르게 보여 주는 lookup에 적합하지만 두 번째 이후 relevant evidence의 순서는 반영하지 않습니다.
DCG와 nDCG: graded relevance와 순위 discount
Discounted Cumulative Gain의 한 형태는 다음과 같습니다.
DCG@k = Σ (2^relᵢ - 1) / log₂(i + 1)
i=1..k
rank가 내려갈수록 log discount를 받고, 높은 relevance grade는 더 큰 gain을 갖습니다.
Ideal DCG는 같은 relevance label을 가장 좋은 순서로 배치한 점수입니다.
nDCG@k = DCG@k / IDCG@k
0에서 1 사이이며 1이면 가능한 최적 순서입니다. relevant item이 전혀 없는 query의 IDCG=0 처리 방식을 명시해야 합니다.
하나의 ranking으로 수치를 계산한다
gold relevant = {B, D}
ranking = [A, B, C, D, E]
top-3에서:
Hit@3 = 1
Recall@3 = 1/2 = 0.5
Precision@3 = 1/3 ≈ 0.333
RR = 1/2 = 0.5
binary relevance의 DCG@5는 rank 2와 4의 gain을 더합니다.
DCG@5 = 1/log₂(3) + 1/log₂(5)
≈ 0.631 + 0.431 = 1.062
IDCG@5 = 1/log₂(2) + 1/log₂(3)
≈ 1.000 + 0.631 = 1.631
nDCG@5 ≈ 1.062 / 1.631 = 0.651
Metric을 Python으로 구현한다
from collections.abc import Sequence
import math
def validate_ranking(ranking: Sequence[str]) -> None:
if len(set(ranking)) != len(ranking):
raise ValueError("ranking contains duplicate IDs")
def hit_at_k(ranking: Sequence[str], relevant: set[str], k: int) -> float:
validate_ranking(ranking)
if not relevant:
raise ValueError("Hit@k is undefined without relevant items")
return float(any(item in relevant for item in ranking[:k]))
def recall_at_k(ranking: Sequence[str], relevant: set[str], k: int) -> float:
validate_ranking(ranking)
if not relevant:
raise ValueError("Recall@k is undefined without relevant items")
found = len(set(ranking[:k]) & relevant)
return found / len(relevant)
def precision_at_k(ranking: Sequence[str], relevant: set[str], k: int) -> float:
validate_ranking(ranking)
if k <= 0:
raise ValueError("k must be positive")
found = len(set(ranking[:k]) & relevant)
return found / k
def reciprocal_rank(ranking: Sequence[str], relevant: set[str]) -> float:
validate_ranking(ranking)
for rank, item in enumerate(ranking, start=1):
if item in relevant:
return 1.0 / rank
return 0.0
def dcg_at_k(relevances: Sequence[float], k: int) -> float:
return sum(
(2.0 ** relevance - 1.0) / math.log2(rank + 1)
for rank, relevance in enumerate(relevances[:k], start=1)
)
def ndcg_at_k(relevances: Sequence[float], k: int) -> float:
actual = dcg_at_k(relevances, k)
ideal = dcg_at_k(sorted(relevances, reverse=True), k)
return actual / ideal if ideal > 0 else 0.0
ranking = ["A", "B", "C", "D", "E"]
relevant = {"B", "D"}
grades = [0, 1, 0, 1, 0]
print(hit_at_k(ranking, relevant, 3)) # 1.0
print(recall_at_k(ranking, relevant, 3)) # 0.5
print(precision_at_k(ranking, relevant, 3)) # 0.333...
print(reciprocal_rank(ranking, relevant)) # 0.5
print(ndcg_at_k(grades, 5)) # 약 0.651
production evaluator에서는 source span을 candidate ID relevance로 mapping하는 단계가 먼저 필요합니다.
Chunk와 evidence span을 mapping한다
from dataclasses import dataclass
@dataclass(frozen=True)
class EvidenceSpan:
revision_id: str
start_char: int
end_char: int
grade: int
@dataclass(frozen=True)
class RetrievedChunk:
chunk_id: str
revision_id: str
start_char: int
end_char: int
def overlap_fraction_of_gold(
chunk: RetrievedChunk,
gold: EvidenceSpan,
) -> float:
if chunk.revision_id != gold.revision_id:
return 0.0
overlap = max(
0,
min(chunk.end_char, gold.end_char)
- max(chunk.start_char, gold.start_char),
)
gold_length = gold.end_char - gold.start_char
return overlap / gold_length if gold_length > 0 else 0.0
relevant 판정 예:
gold coverage ≥ 0.8 → full relevant
0.2 ≤ coverage < 0.8 → partial relevant
coverage < 0.2 → not relevant
threshold는 task에 맞게 정하고 label guideline에 포함합니다. 짧은 숫자 span만 겹친다고 관련 있다고 판정하지 않도록 heading/source 조건을 더할 수 있습니다.
Multi-hop은 Any와 All을 나눈다
gold evidence가 여러 개 필요한 질문에서 하나만 찾는 것과 모두 찾는 것을 구분합니다.
Evidence groups:
G1 = {A의 설립일을 지지하는 passages}
G2 = {CEO 취임일을 지지하는 passages}
Any-group Hit@k = G1 또는 G2 중 하나 발견
All-groups Hit@k = G1과 G2 모두 발견
같은 사실을 지지하는 대체 passage는 한 group으로 묶고, 서로 다른 reasoning step은 다른 group으로 둡니다.
{
"evidence_groups": [
{"group_id": "founding_date", "alternatives": ["span-A", "span-B"]},
{"group_id": "ceo_start", "alternatives": ["span-C"]}
]
}
단순 document Recall만으로 multi-hop completeness를 과대평가하지 않습니다.
Unanswerable query는 별도 task다
corpus에 답이 없는 질문은 relevant set이 비어 있어 Recall@k가 정의되지 않습니다.
이 query를 evaluator에서 삭제하면 system이 항상 무언가를 답하는 문제를 측정하지 못합니다.
별도 metric을 둡니다.
- no-evidence detection accuracy
- abstention precision/recall
- unsupported answer rate
- irrelevant context injection rate
- false positive retrieval score distribution
answerable query → 필요한 evidence를 찾는가?
unanswerable query → evidence가 없음을 감지하고 abstain하는가?
similarity threshold 하나로 answerability를 결정하려면 model·query bucket별 calibration이 필요합니다.
Stage별 상한을 측정한다
최종 answer score 하나는 원인을 숨깁니다.
Stage 0 · Ingestion coverage
gold source가 corpus snapshot에 있는가?
gold span이 parsed element에 남아 있는가?
Stage 1 · Chunk coverage
gold span을 충분히 포함하는 chunk가 생성됐는가?
Stage 2 · Dense exact / BM25
각 retriever top-N에 relevant chunk가 있는가?
Stage 3 · ANN
exact top-k를 ANN이 재현했는가? (Index Recall)
gold Task Recall은 유지됐는가?
Stage 4 · Union / Fusion
union에 gold가 있는가?
fusion이 gold를 top-M으로 올렸는가?
Stage 5 · Rerank
candidate recall은 고정
MRR/nDCG/Precision@k가 올랐는가?
Stage 6 · Context selection
gold aspect coverage를 유지했는가?
중복과 token은 줄었는가?
Stage 7 · Generation
answer가 맞는가?
각 claim이 selected evidence에 의해 지지되는가?
citation이 올바른 source span을 가리키는가?
이 단계 trace가 retrieval harness의 핵심입니다.
단계 matrix 예시
| Query | Parse | Chunk | BM25@50 | Dense@50 | Union | Rerank@10 | Context | Answer |
|---|---|---|---|---|---|---|---|---|
| q1 | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ | ✓ | ✓ |
| q2 | ✓ | ✓ | ✗ | ✓ | ✓ | ✗ | ✗ | ✗ |
| q3 | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
q3는 embedding을 바꿀 문제가 아니라 parsing 문제입니다. q2는 candidate에는 gold가 있었지만 rerank/context에서 떨어졌습니다.
Failure taxonomy
각 실패 query에 첫 번째 실패 stage와 reason code를 붙입니다.
INGESTION_MISSING_SOURCE
PARSE_GOLD_TEXT_LOST
CHUNK_GOLD_SPLIT
SPARSE_VOCAB_MISMATCH
DENSE_SEMANTIC_MISS
ANN_NEIGHBOR_MISS
FILTER_FALSE_NEGATIVE
FUSION_DEMOTED_GOLD
RERANK_DEMOTED_GOLD
CONTEXT_BUDGET_DROPPED_GOLD
CONTEXT_ASPECT_UNCOVERED
GENERATION_UNSUPPORTED_CLAIM
CITATION_INVALID
첫 실패 stage를 고정하면 downstream의 연쇄 실패를 중복 집계하지 않습니다.
Slice 없이는 평균이 거짓말한다
전체 Recall@20이 0.82 → 0.84로 올랐어도 다음과 같을 수 있습니다.
paraphrase : 0.70 → 0.84
exact code : 0.96 → 0.72 ← 심각한 regression
추천 slice:
- exact identifier / paraphrase / numeric / negation
- single-hop / multi-hop / comparison
- answerable / unanswerable
- Korean / English / mixed-language
- PDF paragraph / table / image OCR / HTML / code
- short / medium / long evidence
- recent revision / old revision / deleted source
- filter selectivity와 tenant size
- conversational / standalone
- real / expert / synthetic origin
slice sample 수와 confidence interval도 표시합니다. query 3개의 100%는 안정적인 100%가 아닙니다.
Latency와 cost도 quality metric 옆에 둔다
{
"quality": {
"recall@20": 0.91,
"mrr": 0.74,
"ndcg@10": 0.78,
"context_precision@5": 0.72
},
"latency_ms": {
"transform_p95": 42,
"sparse_p95": 21,
"dense_p95": 29,
"fusion_p95": 2,
"rerank_p95": 58,
"selection_p95": 4,
"end_to_end_p95": 410
},
"cost": {
"embedding_tokens": 18,
"reranked_pairs": 50,
"context_tokens": 2810,
"llm_input_tokens": 3420
}
}
quality를 0.5% 올리기 위해 p95와 비용이 3배가 됐다면 production 목표에 맞는지 판단해야 합니다.
Paired comparison을 사용한다
두 run을 서로 다른 query sample 평균으로 비교하면 query 난이도 차이가 섞입니다. 같은 query에서 metric 차이를 계산합니다.
delta_i = metric_new(query_i) - metric_base(query_i)
mean_delta = mean(delta_i)
paired bootstrap으로 confidence interval을 추정할 수 있습니다.
import numpy as np
def paired_bootstrap_ci(
baseline: np.ndarray,
candidate: np.ndarray,
samples: int = 10_000,
seed: int = 42,
) -> tuple[float, float, float]:
baseline = np.asarray(baseline, dtype=np.float64)
candidate = np.asarray(candidate, dtype=np.float64)
if baseline.shape != candidate.shape:
raise ValueError("paired arrays must have the same shape")
deltas = candidate - baseline
rng = np.random.default_rng(seed)
boot_means = np.empty(samples, dtype=np.float64)
for i in range(samples):
indices = rng.integers(0, len(deltas), size=len(deltas))
boot_means[i] = deltas[indices].mean()
low, high = np.percentile(boot_means, [2.5, 97.5])
return float(deltas.mean()), float(low), float(high)
query가 서로 독립이 아니고 같은 template/source cluster에 묶여 있다면 cluster bootstrap을 고려합니다.
confidence interval이 0을 포함하지 않는 것만으로 business significance가 보장되지는 않습니다. 최소 개선 폭과 regression gate를 함께 둡니다.
Regression gate 예시
quality_gates:
overall_recall_at_20:
min: 0.90
max_drop_vs_main: 0.005
exact_identifier_recall_at_20:
min: 0.96
max_drop_vs_main: 0.0
unanswerable_abstention_recall:
min: 0.85
citation_validity:
min: 0.99
performance_gates:
retrieval_p95_ms:
max: 120
end_to_end_p95_ms:
max: 600
mean_context_tokens:
max: 3500
전체 평균 개선으로 security·exact code·unanswerable regression을 덮지 못하게 합니다.
LLM-based RAG evaluation은 보조 측정이다
RAGAS, RAGChecker 같은 framework는 context relevance, faithfulness, answer relevance 등 RAG의 여러 측면을 진단하려 합니다.
유용한 질문:
- retrieved context가 질문과 관련 있는가?
- answer의 claim이 context에 의해 지지되는가?
- 필요한 gold context가 빠졌는가?
- answer가 질문에 직접 답하는가?
하지만 LLM judge score를 ground truth처럼 쓰면 안 됩니다.
- judge model과 prompt에 민감
- position·verbosity bias
- domain 사실을 잘못 판단
- 같은 model family answer에 유리할 수 있음
- version update로 score drift
human-labeled calibration set에서 judge-human agreement를 확인하고, judge model·prompt·temperature를 manifest에 pin합니다.
Claim-level faithfulness와 citation
answer를 atomic claim으로 나눕니다.
Answer:
"Pro backup은 해지 후 365일 보존되며 [E1],
사용자가 즉시 삭제를 요청할 수 있습니다 [E2]."
Claim 1 → E1이 entail하는가?
Claim 2 → E2가 entail하는가?
Citation E1/E2 → 실제 selected evidence ID인가?
평가 축:
- citation validity: 존재하는 evidence ID인가?
- citation correctness: cited evidence가 claim을 지지하는가?
- citation completeness: 지지가 필요한 claim 모두 인용됐는가?
- faithfulness: 모든 factual claim이 evidence에 근거하는가?
검색 Recall이 높아도 generator가 다른 evidence ID를 인용하면 citation correctness는 낮습니다.
Run manifest가 재현성을 만든다
{
"run_id": "retrieval-2026-07-16T034500Z-9f31",
"code_commit": "git-sha",
"dataset_version": "rag-eval-v12",
"corpus_snapshot": "knowledge-2026-07-15",
"parser": "docling-adapter-v4",
"chunker": "structure-token-256-o32-v3",
"sparse_index": "bm25-ko-v8",
"embedding_model": "model-id@revision",
"dense_index": "hnsw-M32-efS64-v7",
"query_transform": "routed-v2",
"fusion": "rrf-k60-w50-v2",
"reranker": "cross-encoder-domain-v4",
"selector": "coverage-mmr-lambda0.7-v3",
"generator": "model@version",
"prompt": "grounded-answer-v6",
"random_seed": 42,
"hardware": "cpu/gpu instance description",
"started_at": "2026-07-16T03:45:00Z"
}
run directory 예시:
runs/{run_id}/
├── manifest.json
├── per_query.jsonl
├── aggregate.json
├── slices.json
├── failures.jsonl
├── latency.json
└── comparison-to-main.json
per_query.jsonl이 있어야 평균이 변한 query를 다시 볼 수 있습니다.
Per-query stage trace
{
"question_id": "q-retention-017",
"parse_coverage": true,
"chunk_coverage": true,
"sparse": {"gold_rank": 8, "top_k": 50},
"dense_exact": {"gold_rank": 3, "top_k": 50},
"dense_ann": {"gold_rank": 4, "index_recall_at_10": 0.9},
"union_contains_gold": true,
"fusion_gold_rank": 2,
"rerank_gold_rank": 1,
"context_contains_all_aspects": true,
"answer_correct": true,
"citation_valid": true,
"first_failure": null,
"latency_ms": 381
}
새 run과 baseline을 query ID로 join해 rank movement와 failure transition을 봅니다.
q-104: DENSE_SEMANTIC_MISS → fixed by query expansion
q-207: success → RERANK_DEMOTED_GOLD regression
Evaluation harness의 실행 순서
1. immutable corpus snapshot 준비
2. versioned dataset load·schema validation
3. ingestion coverage audit
4. pipeline run + full stage trace
5. deterministic metric 계산
6. slice·failure aggregate
7. paired baseline comparison + CI
8. quality/performance gate
9. artifact 저장
10. 사람이 regression query 검토
LLM judge처럼 비결정적인 단계는 raw input/output과 retry를 기록하고 deterministic metric과 분리합니다.
Offline과 online evaluation
offline test는 빠르고 재현 가능하지만 실제 사용자 행동을 완전히 대신하지 않습니다.
online signal:
- answer accept/reject
- citation click
- follow-up reformulation
- task completion
- escalation to human
- latency·timeout·abandonment
click은 position bias와 UI 영향을 받습니다. A/B test, interleaving, guardrail metric과 함께 해석합니다.
online feedback를 그대로 gold label로 쓰면 기존 ranking이 보여 준 문서만 선택되는 feedback loop가 생깁니다. exploration과 human adjudication이 필요할 수 있습니다.
Freshness와 drift 평가
문서와 query 분포는 바뀝니다.
new product names
new error codes
policy revision
new tenant vocabulary
model/index upgrade
평가 운영:
- 최근 query를 정기 sampling
- 새로운 failure cluster 발견
- source revision별 freshness slice
- deleted document retrieval rate
- no-longer-valid citation rate
- dataset quarterly/monthly version update
같은 frozen test set은 regression에 필요하고, rolling recent set은 drift 탐지에 필요합니다. 둘 다 유지합니다.
오늘까지의 검색 실험 표
두 번째 10편의 흐름을 하나의 ablation table로 정리할 수 있습니다.
| Run | 변경 | 먼저 볼 metric |
|---|---|---|
| R0 | parsed fixed chunks + BM25 | parse coverage, BM25 Recall |
| R1 | structure-aware chunks | table/section slice Recall |
| R2 | dense exact | paraphrase Recall |
| R3 | dense ANN | Index Recall, p95 |
| R4 | BM25+dense RRF | union Recall, nDCG |
| R5 | Cross-Encoder | candidate Recall 고정, MRR |
| R6 | routed query transform | added Recall, drift |
| R7 | MMR context selector | coverage, redundancy, faithfulness |
각 row는 이전 row와 한 가지 핵심 차이만 갖게 설계합니다.
Production 체크리스트
- gold evidence가 source span 또는 안정적인 원문 좌표로 정의된다.
- relevance guideline과 label version이 있다.
- train/dev/test leakage를 source·time·template 관점에서 점검했다.
- Hit, Recall, Precision, MRR, nDCG의 정의를 report에 명시한다.
- multi-hop Any/All coverage와 unanswerable task를 분리한다.
- ingestion부터 answer까지 stage별 상한을 측정한다.
- 전체 평균과 critical slice regression gate가 있다.
- quality, p95 latency, cost를 한 report에서 본다.
- paired comparison과 confidence interval을 사용한다.
- run manifest와 per-query artifact로 결과를 재현할 수 있다.
스스로 확인하기
- chunk ID 대신 source evidence span을 gold로 두면 어떤 실험이 쉬워지는가?
- gold가 여러 개인 질문에서 Hit@k와 Recall@k는 어떻게 다를 수 있는가?
- candidate Recall@50은 그대로인데 nDCG@5가 오르면 어느 단계가 개선된 것인가?
- answer가 틀렸을 때 first failure stage를 찾으면 왜 model 교체 실험을 줄일 수 있는가?
- overall metric이 올라도 exact identifier slice gate가 실패하면 배포를 막아야 하는 이유는 무엇인가?
두 번째 10편으로 문서가 검색 자산이 되는 순간부터 candidate 생성, fusion, reranking, query 변환, context 선택, 평가까지 전체 retrieval layer를 연결했습니다.
다음 글부터 이 고정 pipeline 위에 agent loop와 harness를 올립니다. query router, evidence sufficiency, 재검색 state machine, tool schema, budget·timeout, memory, tracing, evaluation-driven guardrail을 순서대로 다룹니다.
참고자료
- BEIR: A Heterogeneous Benchmark for Zero-shot Evaluation of Information Retrieval Models
- MTEB: Massive Text Embedding Benchmark
- RAGAS: Automated Evaluation of Retrieval Augmented Generation
- RAGCHECKER: A Fine-grained Framework for Diagnosing Retrieval-Augmented Generation
- ARES: An Automated Evaluation Framework for Retrieval-Augmented Generation Systems
- Evaluation of Retrieval-Augmented Generation: A Survey
- NIST TREC Evaluation Resources