Top-K Retrieval 사용을 중단하세요. 대신 이것을 시도해 보세요.
요약
RAG 시스템에서 단순 Top-K 유사도 검색의 한계를 지적하며, 검색 품질을 높이기 위한 실험 결과를 공유합니다. 12,000개의 데이터를 대상으로 테스트한 결과, 하이브리드 검색 방식이 단순 검색보다 높은 답변 품질을 제공함을 확인했습니다.
핵심 포인트
- 단순 Top-K 유사도 검색은 40% 확률로 부적절한 답변을 생성할 수 있음
- 하이브리드 검색(BM25 + Dense)은 키워드 일치 문제를 해결하여 품질을 19% 개선함
- RAG의 핵심 난제는 생성 단계보다 올바른 청크를 가져오는 검색 단계에 있음
- 정확한 오류 코드나 함수 이름 검색 시 하이브리드 방식이 훨씬 효과적임
Top-K Retrieval 사용을 중단하세요. 대신 이것을 시도해 보세요.
모두가 RAG (Retrieval-Augmented Generation)에 대해 이야기할 때, 어려운 부분이 생성 (generation) 단계라고 말합니다. 하지만 그렇지 않습니다. 진짜 어려운 부분은 애초에 모델 앞에 올바른 청크 (chunks)를 가져다 놓는 것입니다. 저는 이전에 왜 RAG retrieval은 검색 (search) 문제가 아니라 필터링 (filtering) 문제인지에 대해 글을 쓴 적이 있으며, 이번 실험을 통해 이를 확인했습니다.
저는 이 사실을 고통스럽게 배웠습니다. 12,000개의 프로덕션 데이터 청크 (chunks)를 대상으로 5가지의 서로 다른 retrieval 전략을 3주 동안 테스트한 결과, 기본 방식인 단순한 top-k 유사도 검색 (naive top-k similarity search)이 엔지니어들에게 40%의 확률로 쓸모없는 답변을 제공하고 있다는 것을 발견했습니다. 그들은 봇을 신뢰하지 않게 되었습니다.
두 가지 전략은 답변 품질을 측정 가능한 수준으로 개선했습니다. 세 가지 전략은 더 많은 토큰 (tokens)을 소모하면서 상황을 더 악화시켰습니다.
제가 무엇을 테스트했는지, 무엇이 효과가 있었는지, 그리고 왜 뻔한 선택이 실패했는지에 대해 설명하겠습니다.
설정 (The Setup)
지식 베이스 (Knowledge base): 12,000개 청크 (각 512 토큰)
- 내부 엔지니어링 문서 (40%)
- Slack 장애 대응 스레드 (35%)
- 사후 분석 (Post-mortems) 및 런북 (runbooks) (25%)
임베딩 모델 (Embedding model): nomic-embed-text-v1.5 (768 차원)
벡터 DB (Vector DB): HNSW 인덱스를 사용하는 Qdrant
LLM: Ollama Cloud를 통한 Claude Sonnet 5
평가 (Evaluation): 엔지니어들의 실제 질문 50개, 관련성 및 실행 가능성에 따라 1-5점 척도로 채점
베이스라인 (Baseline): 단순한 top-k 유사도 검색 (k=5)
- 평균 점수: 3.2/5
- 쿼리당 토큰 사용량: ~2,800 토큰 (컨텍스트 + 응답)
- 지연 시간 (Latency): 340ms (p50)
베이스라인은... 괜찮았습니다. 하지만 "괜찮다"는 것은 엔지니어들이 40%의 확률로 쓸모없는 답변을 받게 되고 봇을 신뢰하지 않게 된다는 것을 의미했습니다. 저는 더 나은 결과가 필요했습니다.
전략 1: 하이브리드 검색 (Hybrid Search) (BM25 + Dense)
가설: Dense 임베딩 (embeddings)은 정확한 키워드 일치를 놓칩니다. BM25는 이를 잡아냅니다. 두 가지를 결합하십시오.
구현:
- Dense 검색: 코사인 유사도 (cosine similarity) 기준 상위 10개 청크
- BM25 검색: 키워드 일치 기준 상위 10개 청크 (Elasticsearch)
- 결과 병합을 위한 상호 순위 융합 (Reciprocal Rank Fusion)
- 재순위화 (re-ranking) 후 최종 k=5
결과: 평균 점수 3.8/5 (+19%)
- 토큰 사용량 (Token usage): ~3,200 토큰 (+14%)
- 지연 시간 (Latency): 520ms (+53%)
판결 (Verdict): 시도할 가치가 있음.
하이브리드 접근 방식은 순수 밀집 검색 (dense search)이 놓쳤던 질문들을 포착했습니다. 특히 엔지니어들이 정확한 오류 코드, 함수 이름, 또는 설정 키 (config keys)를 사용했을 때 더욱 그러했습니다. 예: "error code 0x80070005"를 검색했을 때, 단순히 의미적으로 유사한 "permission denied" 스레드가 아니라 정확한 Windows 권한 문제 해결 문서를 반환했습니다.
지연 시간 (Latency)의 상승은 실재했으나, 품질 향상을 고려하면 수용 가능한 수준이었습니다. 빈번한 쿼리 (queries)를 캐싱 (caching)하여 p50 지연 시간을 다시 380ms로 낮추었습니다.
전략 2: 부모-자식 검색 (Parent-Child Retrieval)
가설 (Hypothesis): 작은 청크 (chunks)는 검색은 잘 되지만 문맥 (context)이 부족합니다. 큰 청크는 문맥은 풍부하지만 검색 성능이 떨어집니다. 검색에는 작은 청크를 사용하고, 생성 (generation)에는 큰 청크를 사용합니다.
구현 (Implementation):
- 자식 청크 (Child chunks): 512 토큰 (임베딩 (embedding) 및 검색용)
- 부모 청크 (Parent chunks): 2,048 토큰 (자식 청크가 속한 전체 섹션)
- 유사도에 따라 상위 10개의 자식 청크를 검색
- 해당 자식 청크들의 부모 청크를 LLM에 전달
- 최종 문맥 (Final context): 3-5개의 부모 청크 (~6,000-10,000 토큰)
결과: 평균 점수 4.1/5 (+28%)
- 토큰 사용량 (Token usage): ~7,500 토큰 (+168%)
- 지연 시간 (Latency): 410ms (+21%)
판결 (Verdict): 최고의 품질이지만, 비용이 많이 듦.
부모-자식 검색 (Parent-child retrieval)은 LLM이 단순히 키워드 패턴 매칭을 하는 대신, 실제로 질문에 답변할 수 있을 만큼 충분한 문맥을 제공했습니다. 엔지니어들은 파편화된 스니펫 (snippets)이 아닌 전체 절차를 얻을 수 있었습니다.
하지만 토큰 비용이 매우 높았습니다. 우리의 쿼리 규모 (하루 약 800회 쿼리)를 기준으로 할 때, 이는 Ollama Cloud 비용을 3배로 증가시켰을 것입니다. 저는 양자화 모델 (quantized models)을 사용하여 AI 에이전트 비용을 60% 절감한 방법에 대해 글을 쓴 적이 있는데, 이것이 바로 중요하게 다뤄져야 할 비용 분석의 전형적인 사례입니다.
전략 3: LLM을 활용한 쿼리 확장 (Query Expansion with LLM)
가설 (Hypothesis): 엔지니어들은 모호한 질문을 던집니다. 검색 전에 LLM을 사용하여 쿼리를 확장함으로써 더 관련성 높은 청크들을 포착합니다.
구현 (Implementation):
- 사용자 질문: "deployment failed"
- LLM 확장 결과: "deployment failed production kubernetes rollout error troubleshooting rollback"
- 확장된 쿼리를 사용하여 밀집 검색 (Dense Retrieval) 수행
- Top k=5 청크
결과: 평균 점수 2.9/5 (-9%)
- 토큰 사용량: ~3,400 토큰 (+21%)
- 지연 시간 (Latency): 890ms (+162%)
판결: 하지 마세요.
LLM이 관련성이 있어 보이는 방향으로 쿼리를 계속 확장했지만, 실제로는 더 좋지 않은 청크들을 검색했습니다. "Deployment failed"는 엔지니어가 직면한 특정 오류가 아니라, 배포 모범 사례(best practices)에 대한 일반적인 목록이 되어버렸습니다.
지연 시간의 증가가 진짜 치명적이었습니다. 검색 전에 LLM 호출을 추가함으로써 모든 쿼리가 느리게 느껴졌습니다. 엔지니어들은 이를 눈치챘고 봇 사용을 중단했습니다.
전략 4: 메타데이터 필터링 (Metadata Filtering)
가설: 모든 청크가 동일한 가치를 지니는 것은 아니다. 검색 전에 최신성, 문서 유형, 신뢰도 점수(confidence score)를 기준으로 필터링한다.
구현:
- 사전 필터링: 최근 18개월 이내의 청크만 포함
- 사전 필터링: 반응(reaction)이 3개 미만인 Slack 스레드 제외 (낮은 신호)
- 사전 필터링: 임베딩 신뢰도(embedding confidence)가 0.7 미만인 청크 제외
- 그 다음: 단순 Top-k 유사도 검색 (k=5)
결과: 평균 점수 3.6/5 (+13%)
- 토큰 사용량: ~2,600 토큰 (-7%)
- 지연 시간 (Latency): 310ms (-9%)
판결: 무료 업그레이드.
이것은 가장 쉬운 승리였습니다. 오래된 문서와 품질이 낮은 Slack 스레드를 필터링하는 것만으로도 아키텍처 변경 없이 결과를 개선했습니다. 첫 시도에 더 관련성 높은 청크를 검색했기 때문에 토큰 사용량은 실제로 감소했습니다.
한 가지 주의할 점은 메타데이터를 주의 깊게 관리해야 한다는 것이었습니다. 문서가 업데이트될 때, 이전 청크들의 "최종 수정(last modified)" 타임스탬프도 업데이트되어야 했습니다. 그렇지 않으면 잘못 필터링되어 제외될 수 있기 때문입니다.
전략 5: 멀티홉 검색 (Multi-Hop Retrieval)
가설: 어떤 질문들은 여러 번의 검색 단계가 필요하다. 먼저 개념을 찾고, 그다음 절차를 찾는다.
구현:
- 1단계: 핵심 개념에 대한 청크 검색 (k=3)
- 2단계: 1단계 결과에서 핵심 용어 추출
- 3단계: 해당 용어들에 대한 청크 검색 (k=5)
- 4단계: 중복 제거 후 상위 5개의 고유 청크 반환
결과: 평균 점수 3.1/5 (-3%)
- 토큰 사용량 (Token usage): ~4,200 토큰 (+50%)
- 지연 시간 (Latency): 720ms (+112%)
판결: 과잉 설계됨 (Overengineered).
멀티홉 검색 (Multi-hop retrieval)은 이론적으로는 똑똑해 보였다. 하지만 실제로는 오류를 증폭시켰다. 만약 1단계에서 잘못된 개념을 검색했다면, 2단계에서는 경로를 더욱 이탈하게 되었다.
이 방식이 효과가 있었던 유일한 경우는 "Rate limiter가 circuit breaker와 어떻게 상호작용하는가"와 같이 매우 구체적인 기술적 질문을 다룰 때뿐이었다. 즉, 실제로 두 개의 분리된 개념을 검색하여 결합해야 하는 경우 말이다. 하지만 이는 전체 쿼리의 약 5% 정도에 불과했다. 나머지 95%를 위해 복잡성을 감수할 가치는 없었다.
우리가 실제로 배포한 것
3주간의 테스트 끝에, 다음과 같은 내용이 프로덕션(production)에 반영되었다:
기본 쿼리 (트래픽의 90%):
- 메타데이터 필터링 (Metadata filtering) + 하이브리드 검색 (hybrid search)
- k=5 청크
- 토큰 사용량 (Token usage): ~3,200 토큰
- 지연 시간 (Latency): 520ms (캐시 적용 시: 380ms)
중요도가 높은 쿼리 (트래픽의 10%):
- 메타데이터 필터링 (Metadata filtering) + 부모-자식 검색 (parent-child retrieval)
- k=3 부모 청크
- 토큰 사용량 (Token usage): ~7,500 토큰
- 지연 시간 (Latency): 410ms
우리는 의도 분류 (intent classification)를 기반으로 쿼리를 부모-자식 검색으로 라우팅한다. 질문에 "어떻게 하나요 (how do I)", "절차 (procedure)", "단계 (steps)", 또는 "런북 (runbook)"이 포함되어 있으면 부모-자식 검색을 사용한다. 그 외의 모든 것은 하이브리드 검색을 사용한다.
진짜 교훈
검색 전략은 임베딩 모델 (embedding model)보다 더 중요하다. 많은 팀이 nomic-embed-text에서 text-embedding-3-large나 m3e-base로 교체하는 데 집착하는 것을 보지만, 그들은 여전히 필터링되지 않은 청크에 대해 단순한 top-k 검색을 사용하고 있다.
그것은 마치 사각형 바퀴를 그대로 둔 채 자동차 엔진만 업그레이드하는 것과 같다.
메타데이터 필터링 (metadata filtering)부터 시작하세요. 비용이 들지 않고 쉽습니다. 그다음, 정확한 키워드 쿼리 (exact keyword queries)가 필요한 경우 하이브리드 검색 (hybrid search)을 추가하세요. 부모-자식 검색 (parent-child retrieval)은 토큰 비용을 감당할 수 있거나, 잘못된 답변이 실제적인 결과(사고 대응, 의료, 법률 등)를 초래하는 질문에 답해야 하는 경우에만 고려하세요.
매우 구체적인 유스케이스 (use case)가 있어 반드시 필요한 경우가 아니라면, 쿼리 확장 (query expansion)과 멀티홉 검색 (multi-hop retrieval)은 건너뛰세요. 이들은 품질을 획기적으로 개선하지 못하면서 지연 시간 (latency)과 복잡성만 증가시킵니다.
수치 (The Numbers)
전체 비교 결과는 다음과 같습니다:
| 전략 | 평균 점수 | 토큰 사용량 | 지연 시간 (p50) | 적용 여부? |
|---|---|---|---|---|
| 베이스라인 (top-k) | 3.2/5 | 2,800 | 340ms | 아니요 |
| ... |
두 가지 승리자 — 하이브리드 검색 (hybrid search)과 부모-자식 검색 (parent-child retrieval) — 은 모두 한 가지 공통점이 있습니다. 바로 청크 (chunks)를 랭킹 (rank) 매기는 방식과 다르게 검색한다는 점입니다. 하이브리드는 두 가지 서로 다른 알고리즘을 사용합니다. 부모-자식은 두 가지 서로 다른 청크 크기를 사용합니다.
패배자들은 모두 한 번의 패스 (one pass)로 모든 것을 처리하려고 했습니다. 하나의 임베딩 (embedding), 하나의 검색, 하나의 결과. 그것은 더 단순하지만, 답변이 틀린다면 단순함은 아무런 도움이 되지 않습니다.
다음에 테스트할 것들
만약 저에게 3주의 시간이 더 주어진다면, 다음을 테스트하겠습니다:
1. 의도에 따른 쿼리 라우팅 (Query routing by intent). 모든 쿼리에 하나의 검색 전략을 사용하는 대신, 먼저 쿼리를 분류하고 (방법론 vs 문제 해결 vs 개념적 질문) 서로 다른 검색 파이프라인 (retrieval pipelines)으로 라우팅합니다. 우리는 이미 부모-자식 라우팅을 통해 이 방식의 원시적인 버전을 수행하고 있지만, 저는 이를 더 세분화하고 싶습니다.
2. 학습된 재순위화 (Learned re-ranking). 과거의 클릭률 (click-through) 또는 추천 (thumbs-up) 데이터를 기반으로 검색된 청크들의 순위를 다시 매기도록 작은 모델을 학습시킵니다. 검색이 10개의 후보를 가져오면, 재순위화 모델 (re-ranker)이 최적의 5개를 선택합니다. 이것이 대형 RAG 플랫폼들이 하는 방식이지만, 저는 더 단순한 기술로 그 목표의 80%까지 도달할 수 있는지 확인하고 싶었습니다.
3. 청크 크기 튜닝 (Chunk size tuning). 우리는 모두가 사용하기 때문에 512 토큰을 사용했습니다. 하지만 우리의 특정 문서에는 256 또는 1,024 토큰이 더 잘 맞을 수도 있습니다. 이는 비용이 적게 드는 실험입니다. 단순히 다시 임베딩하고 다시 테스트하기만 하면 됩니다.
하지만 현재로서는 이 두 가지 솔루션 구성(two-solution setup)이 잘 작동하고 있습니다. 엔지니어들은 더 나은 답변을 얻고 있으며, 봇을 운영하는 비용은 parent-child 방식만 사용할 때보다 저렴합니다. 그리고 저는 당분간 검색(retrieval) 설정을 조정하는 일을 멈추려 합니다.
때로는 정답이 하나의 완벽한 전략인 것은 아닙니다. 두 가지 불완전한 전략 중 언제 어떤 것을 사용해야 하는지 아는 것이 중요합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기