Workspace IndexAlgorithms › Day 98

RAG design — retriever quality metrics and evaluation harnesses (golden sets, property tests, regression) TODO

Algorithms · Day 98 / 100 · G. AI Engineering (Day 97-100)

Concept

RAG retrieves relevant documents from an external knowledge store for a given query, then generates an answer grounded in that evidence; quality has to be evaluated separately for the retrieval stage and the generation stage. Retrieval quality is measured with metrics like recall@k (whether the correct document lands in the top k), MRR (the rank of the first correct hit), and nDCG (which weights results by rank). Generation quality is split into groundedness — whether the answer is actually supported by the retrieved evidence — and relevance to the query; an unsupported claim is a failure of the generation stage, not the retrieval stage. An evaluation harness is built from a golden dataset of fixed queries with expected evidence, property tests that check invariants like "don't answer if there's no evidence," and regression tests that rerun previously passing cases every time chunking, embeddings, or the prompt changes. Without this harness, you can only judge the effect of a parameter change by feel, which makes it impossible to tell improvement from regression.

RAG systems tend to degrade quietly — without a regression harness, you only find out that changing one chunking size broke a whole class of queries after user complaints have piled up.

Code & Formula

# RAG 설계·리트리버 품질 지표 — 토이 문서 집합에서 코사인 유사도 리트리버로
# top-k 검색을 수행하고 recall@k 로 검색 품질을 측정하는 최소 예시.

import math, re
from collections import Counter

docs = [
    "raft leader election uses randomized timeouts",
    "b+tree index supports range queries efficiently",
    "lsm tree favors write throughput over read amplification",
    "vector database uses hnsw for approximate nearest neighbor search",
    "merkle tree enables logarithmic membership proofs",
]

def to_vec(text):
    words = re.findall(r"[a-z]+", text.lower())
    return Counter(words)

def cosine(a: Counter, b: Counter) -> float:
    keys = set(a) | set(b)
    dot = sum(a[k] * b[k] for k in keys)
    na = math.sqrt(sum(v * v for v in a.values()))
    nb = math.sqrt(sum(v * v for v in b.values()))
    return dot / (na * nb) if na and nb else 0.0

doc_vecs = [to_vec(d) for d in docs]

def retrieve(query, k=3):
    qv = to_vec(query)
    scored = [(cosine(qv, dv), i) for i, dv in enumerate(doc_vecs)]
    scored.sort(reverse=True)
    return scored[:k]

query = "how does approximate nearest neighbor search work"
top = retrieve(query, k=3)
print(f"질의: {query!r}")
for score, i in top:
    print(f"  top: score={score:.3f}  doc[{i}]={docs[i]!r}")

# recall@k 평가: 이 질의의 정답 문서는 doc[3] (hnsw ANN) 이라고 가정
gold_idx = 3
eval_set = [("how does approximate nearest neighbor search work", 3),
            ("what helps range queries on sorted keys", 1),
            ("how are membership proofs made small", 4)]

def recall_at_k(eval_set, k):
    hits = 0
    for q, gold in eval_set:
        retrieved_ids = [i for _, i in retrieve(q, k)]
        hits += gold in retrieved_ids
    return hits / len(eval_set)

print(f"\nrecall@1 = {recall_at_k(eval_set, 1):.2f}")
print(f"recall@3 = {recall_at_k(eval_set, 3):.2f}")

Exercise

Build a golden set of thirty queries with expected evidence documents from your own corpus, measure recall@5 across two or three different chunk sizes, and record the results in a table.

Practical Connection

This applies directly to building an internal query tool over codebase docs or market-rules docs, and the habit of setting up metrics and regression tests first is the same engineering discipline used to manage performance regressions in a matching engine or settlement logic.

If you study this on a given day, add a note link and a ✅ to this line in the source curriculum (docs/knowledge/dev-100-curriculum.md) and this spot will lead straight to the note body. You can also write directly on this page — but regenerating overwrites it, so it's safer to keep anything you want to save as markdown under docs/algorithms/.


한국어

RAG 설계·리트리버 품질 지표와 평가 하네스(골든·프로퍼티·회귀) TODO

Algorithms · Day 98 / 100 · G. AI 엔지니어링 (Day 97–100)

개념

RAG는 질의에 대해 외부 지식 저장소에서 관련 문서를 검색한 뒤 그 근거를 붙여 답을 생성하는 구조이며, 품질은 검색 단계와 생성 단계로 분리해 평가해야 한다. 검색 품질은 정답 문서가 상위 k개 안에 들어왔는지를 보는 recall@k, 첫 정답의 순위를 보는 MRR, 순위별 가중치를 반영하는 nDCG 같은 지표로 측정한다. 생성 품질은 답이 제시된 근거에 실제로 뒷받침되는가를 보는 근거성과 질의에 대한 적합성으로 나누어 보며, 근거 없는 진술은 검색이 아니라 생성 단계의 실패다. 평가 하네스는 고정된 질의와 기대 근거를 담은 골든 데이터셋, 근거가 없으면 답을 만들지 않는다는 식의 불변식을 검사하는 프로퍼티 테스트, 그리고 청킹·임베딩·프롬프트를 바꿀 때마다 기존 통과 케이스를 다시 돌리는 회귀 테스트로 구성한다. 이 하네스가 없으면 파라미터 변경의 효과를 체감으로만 판단하게 되어 개선과 퇴행을 구분할 수 없다.

RAG 시스템의 성능 저하는 대개 조용히 일어나서, 회귀 하네스 없이는 청킹 크기 하나 바꾼 것이 특정 질의군을 망가뜨렸다는 사실을 사용자 불만이 쌓인 뒤에야 알게 된다.

코드 · 수식

# RAG 설계·리트리버 품질 지표 — 토이 문서 집합에서 코사인 유사도 리트리버로
# top-k 검색을 수행하고 recall@k 로 검색 품질을 측정하는 최소 예시.

import math, re
from collections import Counter

docs = [
    "raft leader election uses randomized timeouts",
    "b+tree index supports range queries efficiently",
    "lsm tree favors write throughput over read amplification",
    "vector database uses hnsw for approximate nearest neighbor search",
    "merkle tree enables logarithmic membership proofs",
]

def to_vec(text):
    words = re.findall(r"[a-z]+", text.lower())
    return Counter(words)

def cosine(a: Counter, b: Counter) -> float:
    keys = set(a) | set(b)
    dot = sum(a[k] * b[k] for k in keys)
    na = math.sqrt(sum(v * v for v in a.values()))
    nb = math.sqrt(sum(v * v for v in b.values()))
    return dot / (na * nb) if na and nb else 0.0

doc_vecs = [to_vec(d) for d in docs]

def retrieve(query, k=3):
    qv = to_vec(query)
    scored = [(cosine(qv, dv), i) for i, dv in enumerate(doc_vecs)]
    scored.sort(reverse=True)
    return scored[:k]

query = "how does approximate nearest neighbor search work"
top = retrieve(query, k=3)
print(f"질의: {query!r}")
for score, i in top:
    print(f"  top: score={score:.3f}  doc[{i}]={docs[i]!r}")

# recall@k 평가: 이 질의의 정답 문서는 doc[3] (hnsw ANN) 이라고 가정
gold_idx = 3
eval_set = [("how does approximate nearest neighbor search work", 3),
            ("what helps range queries on sorted keys", 1),
            ("how are membership proofs made small", 4)]

def recall_at_k(eval_set, k):
    hits = 0
    for q, gold in eval_set:
        retrieved_ids = [i for _, i in retrieve(q, k)]
        hits += gold in retrieved_ids
    return hits / len(eval_set)

print(f"\nrecall@1 = {recall_at_k(eval_set, 1):.2f}")
print(f"recall@3 = {recall_at_k(eval_set, 3):.2f}")

연습

자신의 문서 모음에서 질의 서른 개와 기대 근거 문서를 골든셋으로 만들고, 청킹 크기를 두세 가지로 바꿔 가며 recall@5를 측정해 결과를 표로 남겨라.

실무 · Verex 연결

코드베이스 문서나 마켓 규칙 문서에 대한 내부 질의 도구를 만들 때 그대로 쓰이며, 지표와 회귀 테스트를 먼저 세우는 태도 자체는 매칭 엔진이나 정산 로직의 성능 회귀 관리와 동일한 엔지니어링 습관이다.

공부한 날 원본 커리큘럼(docs/knowledge/dev-100-curriculum.md)의 이 줄에 노트 링크와 ✅ 를 붙이면, 이 자리는 노트 본문으로 바로 이어집니다. 노트 없이 이 페이지에 바로 적어도 됩니다 — 다만 다시 생성하면 덮어쓰이므로, 남길 글은 docs/algorithms/ 의 마크다운으로 쓰는 편이 안전합니다.

← 97. 트랜스포머 계산 구조·KV 캐시·추론 서빙(연속 배칭·PagedAttention)…99. 에이전트 루프 설계 →