왜 유사도 정렬만으로는 부족한가
벡터 검색은 보통 쿼리 임베딩과 문서 임베딩의 코사인 유사도로 상위 k개를 뽑는다. 문제는 이 방식이 "가장 비슷한" 것만 고르기 때문에, 상위 결과가 서로도 비슷하다는 점이다. 예를 들어 같은 공지사항이 세 번 재게시되었거나, 한 문서의 인접 청크가 겹치는 내용을 담고 있으면 상위 5개가 사실상 한 가지 사실만 반복한다.
RAG 파이프라인에서 이 중복은 치명적이다. LLM 컨텍스트 예산은 한정적인데, 같은 정보로 슬롯을 낭비하면 답변에 필요한 다른 근거가 잘려 나간다. 정확도 상위 정렬(relevance-only)은 관련성은 보장하지만 정보의 다양성은 보장하지 않는다.
MMR의 아이디어
Maximal Marginal Relevance는 "쿼리와의 관련성"과 "이미 뽑은 결과와의 비유사성"을 동시에 최적화한다. 후보 문서 하나를 고를 때마다 다음 점수를 최대로 하는 문서를 선택한다.
score(d) = λ · sim(d, query) − (1 − λ) · max sim(d, s) (s ∈ 이미 선택된 집합)
λ가 1이면 순수 관련성 정렬과 같고, 0에 가까울수록 다양성을 강하게 밀어붙인다. 실무에서는 0.5~0.7 사이가 무난하다. 핵심은 "이미 뽑은 것과 겹치면 감점"이라는 페널티 항이다.
파이썬 구현
임베딩만 준비되면 외부 라이브러리 없이 numpy로 충분하다. 검색 엔진에서 넉넉히 후보(fetch_k)를 받아온 뒤, 그 안에서 MMR로 최종 k개를 재선별하는 구조가 표준이다.
import numpy as np
def mmr(query_vec, cand_vecs, k=5, lambda_=0.6):
# cand_vecs: (N, dim), 모두 L2 정규화되었다고 가정
q = query_vec / np.linalg.norm(query_vec)
sim_q = cand_vecs @ q # 쿼리-후보 유사도
sim_dd = cand_vecs @ cand_vecs.T # 후보 간 유사도
selected, candidates = [], list(range(len(cand_vecs)))
while candidates and len(selected) < k:
if not selected:
best = int(np.argmax(sim_q[candidates]))
selected.append(candidates.pop(best))
continue
# 각 후보의 MMR 점수 계산
redundancy = sim_dd[np.ix_(candidates, selected)].max(axis=1)
scores = lambda_ * sim_q[candidates] - (1 - lambda_) * redundancy
best = int(np.argmax(scores))
selected.append(candidates.pop(best))
return selected
후보 간 유사도 행렬은 fetch_k가 크면 메모리를 잡아먹으므로, fetch_k는 20~50 정도로 제한하는 편이 좋다. 최종 k개를 위해 그 4~10배를 후보로 받는다는 감각으로 잡으면 된다.
파라미터 선택 기준
| λ 값 | 동작 | 적합한 상황 |
|---|---|---|
| 0.9~1.0 | 관련성 우선, 다양성 거의 없음 | 정답이 한 문서에 몰려 있는 FAQ |
| 0.6~0.7 | 균형 | 일반 RAG, 문서 요약 근거 수집 |
| 0.3~0.5 | 다양성 강조 | 탐색적 질의, 여러 관점 비교 |
fetch_k는 다양성 폭을 결정한다. 너무 작으면 후보 자체가 비슷해 MMR이 개입할 여지가 없고, 너무 크면 관련성 낮은 문서까지 후보에 섞여 노이즈가 는다.
벡터 DB에 붙이기
대부분의 벡터 스토어는 MMR 검색 모드를 옵션으로 제공한다. 직접 구현 대신 엔진 기능을 쓰면 후보 fetch와 재선별이 한 번에 처리된다.
results = vectorstore.max_marginal_relevance_search(
query="환불 정책 예외 사유",
k=5, # 최종 반환 개수
fetch_k=30, # MMR 후보 개수
lambda_mult=0.6,
)
단, 엔진 내장 MMR은 후보 fetch를 근사 최근접(ANN)으로 하므로, 정확도가 중요한 경우 fetch_k를 늘려 후보 품질을 먼저 확보해야 한다.
주의할 점
- MMR은 중복 제거의 완전 대체가 아니다. 완전 동일 문서(해시 일치)나 URL 중복은 인덱싱 단계에서 걸러라. MMR은 "의미적으로 겹치는" 경우를 완화할 뿐이다.
- 임베딩 정규화 전제를 확인하라. 코사인 유사도를 내적으로 계산하려면 벡터가 L2 정규화되어 있어야 한다. 정규화가 안 되면 페널티 항이 왜곡된다.
- 레이턴시 비용이 있다. fetch_k×fetch_k 유사도 계산이 추가되므로 fetch_k를 무작정 키우면 지연이 늘어난다. 상위 정렬로 충분한 도메인이라면 MMR을 강제할 필요는 없다.
- λ는 평가로 정하라. 감으로 0.5를 넣지 말고, 실제 질의 세트에서 답변 정확도나 근거 커버리지를 측정해 튜닝하는 편이 안전하다.