4편 끝에 이렇게 적었다. 질문 5개를 눈으로 보는 걸로는 정말 좋아졌는지 말하기 어렵다고. 지금 내 서비스도 그렇다. 완료라는 게 없고 계속 고도화해야 하는데, 뭔가 바꿀 때마다 "이게 나아진 건가?"를 확인할 방법이 필요하다.
그래서 시리즈 마지막 편은 검색 품질을 숫자로 재는 법이다. 1~4편의 문서 14개로 시험지를 만들어 키워드, 벡터, 하이브리드, 필터, 재정렬을 채점해 봤다. 그런데 첫 시험은 거의 다 만점이 나왔다. ㅡ,.ㅡ; (1편 · 2편 · 3편 · 4편)
시험지 만들기: 질문과 정답을 미리 정해 둔다
검색 평가는 시험과 같다. 질문을 만들고, 각 질문의 정답 문서를 사람이 미리 정해 둔다. 그다음 검색 방법마다 정답을 몇 위에 찾는지 본다.
# 평가용 질문과 정답 문서 번호 (사람이 미리 정해 둔다)
EVAL = [
("GPU 서버 최대 사용 기간", {7}), # 정답이 하나
("문서를 자동으로 분류해 주는 AI", {2, 13}), # 정답이 둘 이상이어도 된다
("2주 넘게 쓰고 싶으면?", {7}), # 문서와 겹치는 단어가 적은 질문
("0137 문서", {12}), # 사용자는 번호를 다 안 쓴다
# ...
]
NO_ANSWER = ["연차 휴가 신청 방법", "사내 식당 메뉴"] # 문서에 답이 없는 질문
자바 개발자 입장에서는 JUnit 테스트 케이스와 똑같다. 입력(질문)과 기대값(정답 문서)을 적어 두고, 코드를 바꿀 때마다 다시 돌린다. 검색 설정을 하나 바꿨는데 다른 질문이 망가지는 일을 이걸로 잡는다.
채점 기준: Hit@k와 MRR
점수는 두 가지만 봤다.
- Hit@k: 정답 문서가 k위 안에 들어온 질문의 비율. Hit@3이 0.9면 질문 10개 중 9개는 3위 안에 정답이 있다는 뜻이다. RAG는 보통 상위 3~5개를 생성형 AI에게 넘기니 Hit@3, Hit@5가 실무에서 중요하다.
- MRR(Mean Reciprocal Rank): 정답이 처음 나온 순위의 역수를 평균 낸 값. 1위면 1점, 2위면 0.5점, 3위면 0.33점, 못 찾으면 0점이다. 1에 가까울수록 정답이 위에 있다.
def first_hit(ranks, gold):
"""정답 문서가 처음 나온 순위 (못 찾으면 None)"""
for r, doc in enumerate(ranks, 1):
if doc in gold:
return r
return None
def evaluate(search): # search(질문) -> 문서 번호 순위 목록
hits = {1: 0, 3: 0, 5: 0}; mrr = 0
for q, gold in EVAL:
r = first_hit(search(q), gold)
for k in hits:
if r and r <= k: hits[k] += 1 # Hit@k: k위 안에 들었나
mrr += 1 / r if r else 0 # MRR: 1위면 1, 2위면 0.5, 3위면 0.33
n = len(EVAL)
return {f"Hit@{k}": v / n for k, v in hits.items()} | {"MRR": mrr / n}
for name, search in [("키워드", kw_rank), ("벡터", vec_rank), ("RRF", hybrid)]:
print(name, evaluate(search))
정답 문서가 여러 개일 때 그중 몇 개를 찾았는지 보는 Recall@k, 순위별로 점수를 깎는 nDCG 같은 지표도 있다. 처음에는 Hit@k와 MRR 두 개면 충분했다.
첫 번째 시험: 다 만점이었다
질문 20개로 첫 시험을 쳤다. 질문은 "GPU 서버 최대 사용 기간", "영수증은 어떻게 제출해?"처럼 만들었다.
| 방법 | Hit@1 | Hit@3 | MRR | 질문당 시간 |
|---|---|---|---|---|
| 키워드 | 0.85 | 0.90 | 0.875 | 0 ms |
| 벡터 | 0.95 | 1.00 | 0.975 | 14 ms |
| 하이브리드(RRF) | 0.95 | 1.00 | 0.975 | 13 ms |
| RRF + 필터 | 1.00 | 1.00 | 1.000 | 14 ms |
| + 재정렬 | 0.95 | 1.00 | 0.975 | 381 ms |
벡터 이후로는 Hit@3이 전부 1.00이다. 차이가 안 보인다. 오히려 재정렬은 "PyTorch 까는 법" 하나를 1위에서 2위로 내렸고, 시간은 27배 더 걸렸다. 이 결과만 보면 "재정렬은 돈만 들고 쓸모없다"는 결론이 나온다.
원인은 시험지였다. 첫 시험지의 질문은 문서 제목을 보면서 만든 것이라, 질문과 문서가 단어를 많이 공유했다. 시험이 쉬우면 누가 잘하는지 가려지지 않는다.
두 번째 시험: 어려운 질문과 답이 없는 질문
그래서 실제 사용자가 물을 법한 말투로 12개를 다시 만들었다. 문서와 겹치는 단어가 적은 질문이다.
- "2주 넘게 쓰고 싶으면?" (정답: 제3장 사용 시간)
- "서버 다 쓰고 나서 해야 할 일" (정답: 제5장 반납)
- "0137 문서" (정답: TD-2024-0137. 사용자는 번호를 다 안 쓴다)
- "운영 매뉴얼 옛날 버전 말고 지금 거" (정답: R2)
- "텐서플로 설치" (정답: 딥러닝 프레임워크 설치 가이드)
| 방법 | 쉬운 시험 MRR | 어려운 시험 MRR | 어려운 시험 Hit@1 | 어려운 시험 Hit@5 |
|---|---|---|---|---|
| 키워드 | 0.875 | 0.639 | 0.58 | 0.75 |
| 벡터 | 0.975 | 0.844 | 0.75 | 0.92 |
| 하이브리드(RRF) | 0.975 | 0.725 | 0.58 | 1.00 |
| RRF + 필터 | 1.000 | 0.729 | 0.58 | 1.00 |
| + 재정렬 | 0.975 | 0.958 | 0.92 | 1.00 |
시험이 어려워지자 순위가 완전히 바뀌었다. 쉬운 시험에서 쓸모없어 보이던 재정렬이 어려운 시험에서는 혼자 0.958이다. 키워드 검색은 '택시비', '머신러닝 공부', '0137 문서'를 아예 못 찾았다. '0137 문서'는 형태소 분석기가 문서 쪽 번호를 '2024-0137' 한 덩어리로 다루기 때문에 '0137'만으로는 맞지 않았다. 4편에서 만든 문서번호 필터도 'TD-0000-0000' 모양만 알아봐서 움직이지 않았다.
제일 흥미로운 줄은 하이브리드다. Hit@1은 0.58로 벡터(0.75)보다 낮다. 4편에서 본 것처럼 키워드 쪽 잡음이 섞이면서 1위를 자주 놓친다. 그런데 Hit@5는 1.00이다. 정답을 1위로 올리지는 못해도 5위 안에는 빠짐없이 넣는다. 그걸 재정렬이 다시 줄 세우니 0.92가 된다.
정리하면 하이브리드는 "후보를 놓치지 않는 역할", 재정렬은 "후보 중에 정답을 위로 올리는 역할"이다. 4편에서 정리한 순서(하이브리드 → 재정렬)가 왜 그 순서인지 숫자로 확인한 셈이다.
답이 없는 질문: '관련 문서 없음'은 생각보다 어렵다
마지막으로 문서 14개 어디에도 답이 없는 질문 3개를 던졌다. RAG에서는 이때 "모르겠다"고 해야 한다. 엉뚱한 문서를 근거로 그럴듯한 답을 만들면 안 된다.
| 답이 없는 질문 | 벡터 1위 (점수) | 재정렬 1위 점수 |
|---|---|---|
| 연차 휴가 신청 방법 | 출장비 정산 규정 (0.840) | 0.004 |
| 사내 식당 메뉴 | 출장비 정산 규정 (0.839) | 0.000 |
| 법인카드 한도 올리는 법 | 출장비 정산 규정 (0.815) | 0.000 |
벡터 검색은 셋 다 출장비 정산 규정에 0.8 넘는 점수를 줬다. 3편에서 본 것처럼 벡터 점수로는 "관련 없음"을 거를 수 없다. 재정렬은 0.004 이하로 확실히 낮다.
그런데 4편에서 "재정렬 점수가 낮으면 관련 문서 없음으로 볼 수 있다"고 쓴 건 반만 맞았다. 답이 있는 질문 12개의 재정렬 1위 점수도 최저 0.000, 중간값 0.017, 최고 0.597이었다. 이번 문서들이 대부분 제목 한두 줄짜리라, 재정렬 모델 눈에는 "답이 될 만큼 내용이 충분한 문서"가 거의 없었던 거다. 이 상태로 기준점을 0.01에 두면 답이 있는 질문의 절반을 "모르겠다"로 버리게 된다.
결국 기준점은 실제 문서와 실제 질문으로 재서 정해야 한다. 이것도 평가셋이 있어야 할 수 있는 일이다.
평가셋을 만들 때 내가 얻은 교훈
- 문서를 보면서 질문을 만들지 않는다: 쉬운 시험이 된다. 실제 사용자 질문 기록이 있으면 거기서 뽑는 게 제일 좋다.
- 어려운 질문을 일부러 넣는다: 같은 뜻 다른 말, 번호 일부만 쓴 질문, "옛날 거 말고" 같은 조건 질문.
- 답이 없는 질문도 넣는다: "모르겠다"고 할 수 있는지 재야 한다.
- 숫자 하나에 매달리지 않는다: 질문 12개면 한 문제가 0.08이다. 틀린 질문 목록을 같이 봐야 왜 틀렸는지 안다.
- 바꿀 때마다 같은 시험지로 다시 돌린다: 테스트 코드처럼.
이걸로 RAG 검색 이해하기 시리즈를 마친다. 키워드 검색에서 시작해 벡터, 하이브리드, 재정렬, 평가까지 왔다. 직접 돌려 보니 "어느 방법이 제일 좋다"는 정답은 없었다. 질문이 어떻게 들어오느냐에 따라 순위가 바뀌었다. 그래서 마지막에 남는 건 우리 질문으로 만든 시험지다.
이상 검색 품질 평가 끝!


댓글
댓글 쓰기