RAG Hybrid Search 튜닝 가이드: 감 대신 Decision Tree로 판단하기
Hybrid Search란
RAG에서 검색은 보통 두 갈래입니다.
Dense Search — 임베딩 벡터 기반. 쿼리와 문서의 의미적 유사도로 찾습니다. "고객 불만 처리"를 검색하면 "클레임 대응 프로세스"도 잡아냅니다. 대신 정확한 키워드나 고유명사에는 약합니다.
Sparse Search — BM25 같은 키워드 매칭 기반. "ERR-5012"를 검색하면 정확히 그 문자열이 있는 문서를 찾습니다. 대신 동의어나 문맥은 이해 못 합니다.
BM25 (Best Matching 25): 문서에서 특정 단어가 얼마나 자주, 얼마나 독점적으로 등장하는지를 수식으로 계산하는 고전적 랭킹 알고리즘. TF-IDF의 개량판.
TF-IDF (Term Frequency-Inverse Document Frequency): 문서 내 단어의 중요도를 수치화하는 기법. 특정 문서에서 자주 등장하되, 전체 문서에서는 드문 단어일수록 높은 값을 가집니다.
Hybrid Search는 이 둘을 조합하는 겁니다. Dense가 놓치는 걸 Sparse가 잡고, Sparse가 놓치는 걸 Dense가 잡습니다. 대부분의 프로덕션 RAG가 hybrid를 쓰는 이유입니다.
근데 문제는 — 조합하는 순간 건드릴 수 있는 파라미터가 확 늘어난다는 겁니다. 그래서 감으로 튜닝하게 됩니다. 이 글은 감 대신 decision tree로 판단하는 방법을 정리합니다.
평가 인프라부터 세팅하기
튜닝의 전제는 측정입니다. 이거 없으면 튜닝이 아니라 '찍기'일 뿐입니다.
Golden Test Set
쿼리 30~50개 + 각 쿼리에 대한 정답 문서(gold passage) 매핑. 이건 사람이 직접 만들어야 합니다. LLM한테 초안을 뽑되, 반드시 사람이 검수해야 합니다. 쿼리는 두 그룹으로 나눠야 합니다:
- 키워드 쿼리: 고유명사, ID, 코드명 검색 (예: "ERR-5012", "calculateTax 함수")
- 시맨틱 쿼리: 의미 기반 검색 (예: "고객 환불 절차가 어떻게 되나요")
이 분리가 나중에 alpha 튜닝할 때 핵심이 됩니다.
핵심 지표
| 지표 | 측정 대상 | LLM 호출 | 용도 |
|---|---|---|---|
| Recall@k | 정답 문서가 top-k 안에 있는 비율 | X | 검색 커버리지 |
| Precision@k | top-k 중 관련 문서 비율 | X | 검색 정밀도 |
| MRR (Mean Reciprocal Rank/평균 역순위) | 정답의 평균 순위 역수 | X | 랭킹 품질 |
| NDCG@k (정규화 감쇠 누적 효용) | 관련 문서가 상위에 몰려있는 정도 | X | reranker 평가 |
| SemScore | 생성 답변과 정답의 임베딩 유사도 | X | 답변 품질 |
| RAGAS Faithfulness | 답변이 검색 문서에 근거하는지 | O | hallucination 탐지 |
| RAGAS Context Precision | 관련 청크의 상위 랭킹 여부 | O | 정밀 랭킹 평가 |
NDCG: Normalized Discounted Cumulative Gain. 상위 순위에 관련 문서가 몰려있을수록 높은 점수를 부여하는 랭킹 품질 지표입니다. "관련 문서가 있긴 한데 10번째에 있다"와 "1번째에 있다"를 구분합니다.
SemScore: 생성된 답변과 정답을 각각 임베딩한 뒤 코사인 유사도로 품질을 측정하는 지표. 12개 LLM의 답변을 8개 지표로 평가한 벤치마크에서, GPT-4 기반 평가(G-Eval)만큼 인간 판단과 높은 상관관계를 보였습니다. LLM 호출 없이 임베딩만으로 계산하므로 대량 하이퍼파라미터 스윕(sweep)에 적합합니다.
RAGAS: RAG 전용 평가 프레임워크. Faithfulness(근거 기반 답변인지), Answer Relevancy(질문에 맞는 답인지), Context Precision/Recall(검색 품질)을 LLM 호출로 판정합니다. 정확하지만 건당 30~45초, 비용도 높습니다.
평가 전략 2단계
| 단계 | 목적 | 지표 | 비용 |
|---|---|---|---|
| Phase 1: 넓은 스윕 | 수십 개 설정을 빠르게 거르기 | Recall@k + Precision@k + SemScore | 낮음 |
| Phase 2: 정밀 비교 | 상위 3~5개 설정 최종 선택 | RAGAS Faithfulness + Context Precision | 높음 |
Phase 1에서 80%를 걸러내고, Phase 2에서 남은 후보만 비싼 지표로 비교하면 됩니다.
튜닝 가능한 Knob 카탈로그
청킹 & 인덱싱
Chunk Size
문서를 몇 토큰 단위로 자를지. 작으면 세밀하지만 맥락이 끊기고, 크면 맥락은 유지되지만 노이즈가 섞입니다.
평가: chunk size만 바꿔가며 Recall@10과 Context Relevance(검색된 청크 중 답변에 실제로 쓸모 있는 토큰 비율)를 측정.
Chunk Size: 200 | 400 | 600 | 800 | 1000
Recall@10: 0.82 | 0.88 | 0.85 | 0.79 | 0.72
Ctx Relev: 0.41 | 0.55 | 0.62 | 0.58 | 0.45
| 결과 | 조치 |
|---|---|
| Recall 높은데 답변 품질 낮음 → 맥락 부족 | chunk size 키우기 |
| Recall 자체가 낮음 → 정보가 묻혀 있음 | chunk size 줄이기 |
| Context Relevance < 0.4 → 노이즈 과다 | chunk size 줄이기 + strategy 재검토 |
| Ctx Relev ≥ 0.7 + Recall도 높음 | sweet spot — 고정 |
3~4개 size(200/400/800)로 경향만 보면 됩니다. 50 단위 미세조정은 시간 낭비입니다.
Chunk Overlap
청크 간 겹치는 구간. 경계에서 정보가 잘리는 걸 방지합니다. chunk size의 10~25%.
평가: chunk size 고정, overlap만 바꿔가며 Recall@k 측정. 특히 경계 쿼리(정답이 두 청크에 걸쳐 있는 케이스)를 따로 확인.
| 결과 | 조치 |
|---|---|
| 경계 쿼리 Recall이 일반 쿼리보다 현저히 낮음 | overlap 늘리기 (20~25%) |
| overlap 0%↔20% Recall 차이 < 2%p | 10%로 낮춰서 인덱스 절약 |
| overlap 늘려도 경계 쿼리 Recall 변화 없음 | semantic chunking 전환 고려 |
Chunking Strategy
| 문서 유형 | 권장 strategy |
|---|---|
| 구조적 (매뉴얼, FAQ, API docs) | recursive — 문단/문장 경계 존중 |
| 비구조적 (회의록, 이메일) | semantic 시도 → 안 되면 recursive |
| 짧고 균일 (채팅, 댓글) | fixed size |
| 길고 구조적 (기술 문서, 논문) | hierarchical — 작은 청크로 검색, 큰 청크 반환 |
Hierarchical (Parent-Child) Chunking: child chunk(100~500 토큰)를 검색 단위로 쓰되, 매칭 시 parent chunk(500~2000 토큰)를 LLM에 반환하는 2단 구조. "검색은 바늘로, 전달은 실타래로." 구조적 문서에서 relevance +20~35%. 자기 완결적 문서(FAQ)에서는 효과 미미.
Contextual Retrieval
청킹 후, 임베딩 전에 각 청크에 문서 맥락 요약을 prepend하는 전처리. Anthropic이 제안한 방식입니다.
"매출이 전분기 대비 3% 증가했습니다"
→ "이 청크는 Acme Corp 2024년 2분기 실적 보고서에서 발췌. 매출 추이 섹션. 매출이 전분기 대비 3% 증가했습니다"
검색 실패율 49% 감소. Reranker 결합 시 67% 감소.
| 파라미터 | 범위 |
|---|---|
| On/Off | 적용 여부 |
| Context 길이 | 50~100 토큰 |
| 적용 대상 | embedding / BM25 / 양쪽 다(가장 효과적) |
평가: on/off의 Recall@k 비교.
| 결과 | 조치 |
|---|---|
| 대명사/지시어 많은 문서에서 검색 실패 잦음 | 적용 |
| 청크가 자기 완결적 (FAQ, API docs) | 생략 — 인덱싱 비용 절약 |
| Recall 향상 < 5%p | 비용 대비 효과 부족 → 생략 |
임베딩
Embedding Model
| 상황 | 권장 |
|---|---|
| 도메인 특화 (의료, 법률, 코드) | 도메인 파인튜닝 또는 multilingual 모델 |
| 범용 | OpenAI text-embedding-3-large / Cohere embed v3 |
| 비용 민감 | text-embedding-3-small / 오픈소스 (BGE, E5) |
Embedding Dimensionality
차원 수를 줄여 속도/비용 절약. MRL 지원 모델이면 품질 손실이 적습니다.
Matryoshka Representation Learning (MRL): 임베딩 벡터의 앞쪽 N차원만 잘라도 성능이 유지되도록 학습하는 기법. 러시아 마트료시카 인형처럼 큰 벡터 안에 작은 벡터가 이미 유효하게 들어있는 구조. OpenAI text-embedding-3, Jina v3 등이 지원.
| 파라미터 | 범위 |
|---|---|
| Target 차원 | 모델별 상이 (OpenAI: 256/512/1536/3072) |
| 방식 | MRL (모델 내장) / PCA (후처리) |
PCA (Principal Component Analysis, 주성분 분석): 고차원 벡터에서 분산이 큰 축을 찾아 저차원으로 압축하는 통계 기법. 임베딩 맥락에서는 생성된 고차원 임베딩을 후처리로 차원 축소할 때 사용합니다.
판단: 차원 50% 축소 시 Recall@10 하락 < 2%p이면 줄이는 게 이득. 5%p 이상이면 유지.
Embedding Quantization
벡터 정밀도를 낮춰 저장/메모리/연산 비용 절감. 1M+ 벡터부터 의미 있습니다.
Quantization: float32(32비트) 벡터를 더 적은 비트로 표현하는 압축 기법. 정밀도를 일부 희생하고 저장 공간과 연산 속도를 얻습니다. 이미지/오디오 압축의 벡터 버전.
| 방식 | 압축률 | 품질 손실 | 비고 |
|---|---|---|---|
| bfloat16 | 2x | ~0% | 무조건 적용 가능 |
| float8 | 4x | <0.3% | 정확도/압축 최적 밸런스 |
| int8 (scalar) | 4x | 소폭 | calibration 필요 |
| binary (1-bit) | 32x | 상당 | full-precision rescoring 필수 |
판단: Recall@10 하락 < 1%p이면 채택. binary는 반드시 rescoring과 조합.
검색 (Retrieval)
Fusion Method: RRF vs Weighted Sum
Dense와 Sparse 결과를 합치는 방법. alpha보다 근본적인 선택입니다.
RRF (Reciprocal Rank Fusion): 각 retriever가 매긴 스코어를 무시하고, 순위만으로 결합하는 방식.
score = Σ 1/(k + rank). BM25 점수와 cosine similarity처럼 스케일이 다른 값을 정규화할 필요가 없어서 out-of-the-box 안정성이 높습니다.
Weighted Score Fusion: alpha × dense_score + (1-alpha) × sparse_score. 스코어 정규화가 필요하지만 세밀한 제어가 가능합니다.
| 파라미터 | 설명 | 기본값 |
|---|---|---|
| Fusion method | RRF vs Weighted Sum | — |
| RRF k | 상위 순위 우대 강도. 작으면 top 편중, 크면 평등 | 60 |
| Score 정규화 (Weighted Sum) | min-max / sigmoid / z-score | min-max |
| 결과 | 조치 |
|---|---|
| Weighted Sum에서 alpha 튜닝해도 결과 불안정 | score 정규화 문제 → RRF로 전환 |
| RRF에서 한쪽 retriever 결과가 무시됨 | retriever별 가중치 추가 또는 Weighted Sum 전환 |
| RRF k=60에서 상위 결과 다양성 부족 | k 낮추기 (20~40) |
Alpha (Dense/Sparse 비율)
Weighted Score Fusion의 핵심 knob. alpha=1이면 pure dense, alpha=0이면 pure sparse.
평가: 키워드 쿼리와 시맨틱 쿼리를 분리해서 alpha 0.0~1.0 구간의 Recall@10을 측정.
Alpha: 0.0 0.2 0.4 0.5 0.6 0.8 1.0
키워드 Recall: 0.92 0.88 0.80 0.75 0.68 0.52 0.41
시맨틱 Recall: 0.35 0.52 0.70 0.76 0.82 0.88 0.90
전체 Recall: 0.63 0.70 0.75 0.76 0.75 0.70 0.66
| 결과 | 조치 |
|---|---|
| 전체 Recall에 명확한 피크 → 그게 최적 | 해당 값으로 고정 |
| 키워드 Recall < 0.7 → sparse 무시되는 중 | alpha 낮추기 |
| 시맨틱 Recall < 0.7 → dense 무시되는 중 | alpha 올리기 |
| 두 곡선의 교차 지점 → 균형점 | 교차점 ±0.1에서 미세조정 |
| 어떤 alpha에서도 양쪽 0.8 이상 안 나옴 | alpha 문제 아님 → 청킹/임베딩 재검토 |
Top-k
검색 결과를 몇 개 가져올지.
평가: k를 5/10/20/30/50으로 바꿔가며 Recall@k와 Precision@k 동시 측정.
k: 5 10 20 30 50
Recall@k: 0.60 0.78 0.88 0.91 0.93
Precision@k: 0.72 0.58 0.38 0.28 0.18
| 결과 | 조치 |
|---|---|
| k=10→20에서 Recall +10%p 이상 → 아직 놓치는 문서 많음 | top-k 늘리기 (reranker 필수) |
| k=10→20에서 Recall +3%p 미만 → 추가분은 노이즈 | top-k=10이면 충분 |
| Precision@k < 0.3 → LLM에 노이즈 과다 | top-k 줄이기 또는 reranker 추가 |
| Reranker 있음 → top-k는 reranker 입력 풀 | top-k 넉넉히(20~30) + top-n으로 필터 |
BM25 k1 / b
Sparse 검색의 세부 파라미터. alpha를 낮췄는데도 키워드 검색이 안 될 때만 건드립니다. 대부분 기본값으로 충분합니다.
k1 (term frequency saturation): 같은 단어가 문서에 반복 등장할 때 점수를 얼마나 더 줄지 조절. 기본 1.2. 높이면 반복에 민감, 낮추면 둔감.
b (length normalization): 문서 길이에 따른 점수 보정. 기본 0.75. 높이면 짧은 문서 유리, 낮추면 긴 문서도 공평.
k1 판단:
| 결과 | 조치 |
|---|---|
| 키워드 반복 문서가 상위에 와야 하는데 안 옴 | k1 올리기 (1.5→2.0) |
| 키워드 1회 문서가 반복 문서에 밀림 | k1 낮추기 (1.2→0.8) |
b 판단:
| 결과 | 조치 |
|---|---|
| 짧은 문서(FAQ)가 과도하게 상위 | b 낮추기 (0.75→0.5) |
| 긴 문서(매뉴얼)의 정보를 못 찾음 | b 낮추기 (0.75→0.3) |
| 문서 길이가 대체로 균일 | 기본값 유지 |
HNSW Index 파라미터
벡터 DB의 ANN 검색 파라미터. 거의 모든 벡터 DB가 HNSW를 쓰는데, 기본값 그대로 두는 경우가 대부분입니다. 데이터가 늘면 조용히 검색 품질이 떨어집니다.
HNSW (Hierarchical Navigable Small World): 벡터 검색을 위한 그래프 기반 인덱스 구조. 모든 벡터를 비교하는 대신, 그래프를 타고 이동하며 근접 이웃을 찾습니다. 정확도와 속도의 트레이드오프를 파라미터로 조절할 수 있습니다.
| 파라미터 | 설명 | 기본값 | 재인덱싱 |
|---|---|---|---|
| ef_search | 쿼리 시 탐색 깊이 | 100 | 불필요 (실시간 조정) |
| M | 노드당 연결 수 | 16 | 필요 |
| ef_construction | 빌드 시 탐색 깊이 | 200 | 필요 |
평가: ef_search를 올려가며 Recall@10과 latency 동시 측정.
| 결과 | 조치 |
|---|---|
| 데이터 1M+ 이후 Recall 하락 (latency 정상) | ef_search 올리기 (200~400) |
| ef_search 올려도 Recall 변화 없음 | M이 낮음 → M 올리고 재인덱싱 |
| 인덱싱 시간 과다 | ef_construction 낮추기 (recall 트레이드오프 확인) |
Similarity Score Threshold
Top-k와 별개로 최소 유사도 설정. 순위 안에 있어도 점수가 낮으면 버립니다.
| 파라미터 | 범위 |
|---|---|
| Score threshold | 예: cosine similarity ≥ 0.7 |
| 적용 시점 | 검색 후 / reranking 후 / 양쪽 |
| Fallback | 전부 탈락 시 최소 1개 반환 vs "관련 정보 없음" |
| 결과 | 조치 |
|---|---|
| 코퍼스에 없는 주제 쿼리 시 엉뚱한 답변 | threshold 추가 (hallucination 방지) |
| 정상 쿼리에서도 결과 부족 | threshold 낮추기 또는 fallback 활성화 |
| Reranker 점수가 bimodal(관련/무관 명확 분리) | reranker 점수 기준 threshold가 효과적 |
후처리 (Post-retrieval)
Reranker + Top-n
검색 결과를 cross-encoder로 다시 정렬. 있고 없고의 차이가 가장 큰 knob 중 하나입니다.
Cross-encoder: 쿼리와 문서를 하나의 입력으로 묶어서 BERT 계열 모델에 통째로 넣는 방식. Bi-encoder(각각 따로 임베딩)보다 훨씬 정확하지만 모든 후보에 대해 개별 추론해야 해서 느립니다. 그래서 검색이 아니라 reranking에 쓰입니다.
평가: reranker 전후 NDCG@k와 SemScore 비교. Recall은 reranker로 바뀌지 않으므로(순서만 바뀜) NDCG가 적합합니다.
Reranker 없음 Reranker 있음
NDCG@10: 0.62 0.81
SemScore: 0.71 0.84
Latency (p50): 120ms 340ms
| 결과 | 조치 |
|---|---|
| NDCG +0.15 이상 → 효과 확실 | 유지 |
| NDCG +0.05 미만 → 효과 미미 | 제거 (latency만 증가) |
| latency 2배 이상 증가 | 경량 모델로 교체 (FlashRank, TinyBERT) |
| top-n=3과 top-n=5의 SemScore 차이 < 2%p | top-n=3으로 절약 |
| top-n=3이 top-n=10보다 SemScore 높음 | top-n 줄이기 — less is more |
Context Window Packing
검색 결과를 LLM 프롬프트에 어떻게 담는지. 검색은 잘 했는데 답변이 이상하면 여기부터 의심하세요.
Lost in the Middle: LLM이 긴 context의 앞부분과 뒷부분은 잘 활용하지만, 중간에 있는 정보는 무시하는 경향. 관련 청크를 중간에만 배치하면 있어도 못 쓰는 상황이 발생합니다.
| 파라미터 | 설명 |
|---|---|
| 청크 순서 | 관련도 높은 것을 앞/뒤에 배치 (Lost in the Middle 대응) |
| Context budget | 모델 context window의 80% 이하 권장 |
| 메타데이터 포함 | 출처, 날짜, 관련도 점수를 같이 넣을지 |
| 중복 제거 | overlap으로 인한 중복 텍스트 제거 |
평가: 동일 검색 결과로 packing 방식만 바꿔가며 SemScore + RAGAS Faithfulness 비교.
| 결과 | 조치 |
|---|---|
| 마지막 청크 정보를 답변이 무시 | 관련 청크를 앞과 뒤 양쪽에 배치 |
| context 길어질수록 SemScore 하락 | budget 줄이기 + top-n 줄이기 |
| 비슷한 내용의 청크가 중복 | 중복 제거 활성화 |
| 메타데이터 포함 시 Faithfulness 향상 | 출처 정보 포함 (grounding 강화) |
쿼리 (Query Processing)
Query Rewriting
원래 쿼리를 LLM이 검색에 적합한 형태로 변환합니다. "아까 그거 다시 알려줘"를 "결제 취소 프로세스 안내"로 바꾸는 식.
평가: 원본 쿼리와 rewritten 쿼리 각각의 Recall@k 비교. 의도 보존율도 체크 — 원본과 rewritten을 LLM에 보여주고 "같은 정보를 요청하는가?" 판단시킵니다.
원본 쿼리 Rewritten 변화
대화형 쿼리 Recall: 0.61 0.79 +0.18 ✓
명확한 쿼리 Recall: 0.85 0.83 -0.02 (무해)
의도 보존율: — 0.94
| 결과 | 조치 |
|---|---|
| 대화형 쿼리 Recall +10%p 이상 | 유지 |
| 명확한 쿼리 Recall -5%p 이상 하락 | 대화형 쿼리에만 조건부 적용 |
| 의도 보존율 < 0.9 | 프롬프트 수정 또는 제거 |
HyDE (Hypothetical Document Embeddings)
쿼리로 가상의 답변을 먼저 생성하고, 그 답변의 임베딩으로 검색합니다.
HyDE: "심부전 치료법은?"이라는 쿼리 대신, LLM이 "심부전 치료에는 ACE 억제제, 베타차단제, 이뇨제 등이 사용되며..."라는 가상 답변을 생성하고, 이 답변과 유사한 실제 문서를 검색하는 방식. 짧거나 추상적인 쿼리의 임베딩 품질을 높여줍니다.
평가: 원본 쿼리 vs HyDE 검색의 Recall@k. 짧은 쿼리(3단어 이하)와 긴 쿼리를 분리.
| 결과 | 조치 |
|---|---|
| 짧은 쿼리 Recall +15%p 이상 | 유지 |
| 긴 쿼리에서 Recall 하락 | 짧은 쿼리에만 조건부 적용 |
| Latency +1.5초 이상 | 캐싱 적용 또는 제거 |
| 가상 답변 hallucination > 20% | 프롬프트 강화 또는 제거 |
Decision Tree #1: 처음 셋업할 때
처음부터 잘 깔면 나중에 튜닝할 게 줄어듭니다. 이 순서대로 접근하면 됩니다.
[1] Chunking Strategy 결정
│
├─ 구조적 문서 (매뉴얼, FAQ, API docs)
│ → recursive + chunk size 300~500
│
├─ 비구조적 문서 (회의록, 이메일)
│ → semantic 시도 → 안 되면 recursive + 500~800
│
├─ 짧고 균일 (채팅, 댓글)
│ → fixed size + 200~400
│
└─ 길고 구조적 (기술 문서, 논문)
→ hierarchical (child 200~400, parent 800~1500)
│
overlap: chunk size의 15~20%
│
[2] Contextual Retrieval
│
├─ 대명사/지시어/상대 참조 많음 → 적용 (양쪽 다)
└─ 청크가 자기 완결적 → 생략
│
[3] Embedding Model
│
├─ 도메인 특화 → 파인튜닝 or multilingual 모델
└─ 범용 → text-embedding-3-large / Cohere embed v3
│
[4] Fusion Method + Alpha
│
├─ 처음이면 → RRF (k=60)로 시작
└─ 세밀한 제어 → Weighted Sum
├─ 고유명사/코드/ID 중심 → alpha 0.3~0.5
├─ 의미 기반 중심 → alpha 0.7~0.8
└─ 불확실 → alpha 0.5 → 평가 후 조정
│
[5] Top-k
│
├─ reranker 쓸 예정 → 20~30
└─ reranker 안 쓸 예정 → 5~10
│
[6] Reranker + Score Threshold
│
├─ 정확도 최우선 → reranker + top-n 3~5
├─ 속도 최우선 → reranker 생략
└─ 둘 다 → reranker + top-n 낮춰서 속도 보상
│
+ score threshold (코퍼스에 없는 쿼리 대비)
│
[7] Context Packing
│
├─ 관련 청크를 앞/뒤 배치 (Lost in the Middle 대응)
├─ context budget ≤ 80%
└─ 중복 제거 활성화
│
[8] Query Processing (필요시)
│
├─ 대화형 쿼리 (챗봇) → query rewriting
├─ 짧고 추상적 쿼리 → HyDE 시도
└─ 명확한 키워드 중심 → 생략
Decision Tree #2: 이미 돌리고 있는데 문제있을 때
증상부터 찾아야 합니다.
관련 문서를 못 찾음 (Recall 낮음)
chunk size 줄이기 → 정보가 큰 덩어리에 묻혀 있을 수 있음
↓ 안 되면
top-k 늘리기
↓ 안 되면
alpha 조정 → sparse 비중 올려보기
↓ 안 되면
contextual retrieval 적용
↓ 안 되면
query rewriting 또는 HyDE 추가
노이즈가 많음 (Precision 낮음)
reranker 추가 → 가장 효과 큼
↓ 이미 있거나 안 되면
top-k / top-n 줄이기
↓ 안 되면
score threshold 추가
↓ 안 되면
chunk size 키우기 → 잘게 쪼갠 무의미한 조각이 상위에 올라오는 경우
↓ 안 되면
context packing 중복 제거 활성화
특정 키워드/ID 검색이 안 됨
alpha에서 sparse 비중 올리기 (0.3~0.4)
↓ 안 되면
BM25 k1 올리기 → 키워드 반복 민감도 높이기
↓ 안 되면
해당 키워드가 청크에 포함되는지 확인 → 청킹에서 잘렸을 수 있음
↓ 구조적 한계면
SPLADE 전환 고려 → 학습 기반 sparse, 동의어 자동 확장
SPLADE (SParse Lexical AnD Expansion): BM25를 대체하는 학습 기반 sparse retriever. BERT의 MLM head를 활용해 (1) 불필요한 단어의 가중치를 낮추고 (2) 원문에 없는 관련 단어를 자동 확장합니다. "심근경색" 검색 시 "heart attack"도 활성화.
답변의 맥락이 끊김
chunk size 키우기
↓ 안 되면
overlap 늘리기 (20~25%)
↓ 안 되면
hierarchical chunking → 작은 청크 검색, 큰 청크 반환
↓ 안 되면
contextual retrieval 적용
검색은 괜찮은데 답변이 이상함
context packing 순서 변경 → Lost in the Middle 대응
↓ 안 되면
top-n 줄이기 → 노이즈 context가 LLM을 혼란시키는 경우
↓ 안 되면
메타데이터 포함 → 출처 정보로 grounding 강화
↓ 여전하면
RAGAS Faithfulness로 hallucination 여부 정밀 진단
응답이 느림
top-k 줄이기
↓ 안 되면
reranker 제거 또는 경량 모델로 교체
↓ 안 되면
embedding 차원 축소 / quantization 적용
↓ 안 되면
HNSW ef_search 낮추기 (recall 트레이드오프 확인)
데이터가 늘어나니 성능 저하
HNSW ef_search 올리기 → 조용한 degradation 의심
↓ 안 되면
embedding quantization → 메모리 절약
↓ 안 되면
embedding 차원 축소
↓ 안 되면
metadata filtering → 검색 범위 사전 축소
마무리: 튜닝 우선순위
한 번에 하나씩 바꾸는 것이 좋습니다. 한꺼번에 바꾸면 뭐가 효과인지 모릅니다.
영향도 ★★★ — 먼저 잡을 것
1. Chunk size + strategy
2. Contextual Retrieval (on/off)
3. Fusion method + Alpha
영향도 ★★☆ — 다음에 잡을 것
4. Top-k + Reranker + Top-n (세트)
5. Score Threshold
6. Context Packing (순서, budget, 중복제거)
7. Query Processing (rewriting, HyDE)
영향도 ★☆☆ — 필요할 때만
8. HNSW params (데이터 1M+ 이후)
9. BM25 k1/b
10. Embedding quantization/dimensionality
각 단계에서 golden test set으로 before/after를 측정합니다. Phase 1(Recall@k + SemScore)로 빠르게 스윕하고, 최종 후보만 Phase 2(RAGAS)로 정밀 비교합니다.
파라미터 튜닝은 감이 아니라 계획입니다. Decision tree를 두고 순서대로 따라가 보는 접근이 필요합니다.