Field Log · Entry

Retrieval Evaluation: Recall@k·MRR·nDCG와 실패 분석 (10/10)

질문과 source span gold evidence가 ingestion, retrieval, ANN, fusion, rerank, context, answer 단계 metric과 failure slice로 연결되는 RAG 평가 harness

오늘의 결론

  • 평가 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 위에 얹는 방법입니다.

RAG 검색 pipeline을 단계별 metric과 failure slice로 평가하는 harness

그림 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 예시

QueryParseChunkBM25@50Dense@50UnionRerank@10ContextAnswer
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
R0parsed fixed chunks + BM25parse coverage, BM25 Recall
R1structure-aware chunkstable/section slice Recall
R2dense exactparaphrase Recall
R3dense ANNIndex Recall, p95
R4BM25+dense RRFunion Recall, nDCG
R5Cross-Encodercandidate Recall 고정, MRR
R6routed query transformadded Recall, drift
R7MMR context selectorcoverage, 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로 결과를 재현할 수 있다.

스스로 확인하기

  1. chunk ID 대신 source evidence span을 gold로 두면 어떤 실험이 쉬워지는가?
  2. gold가 여러 개인 질문에서 Hit@k와 Recall@k는 어떻게 다를 수 있는가?
  3. candidate Recall@50은 그대로인데 nDCG@5가 오르면 어느 단계가 개선된 것인가?
  4. answer가 틀렸을 때 first failure stage를 찾으면 왜 model 교체 실험을 줄일 수 있는가?
  5. 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을 순서대로 다룹니다.

참고자료