본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 25. · 3 Views
RAG-Fusion 완벽 가이드
다중 쿼리 생성과 Reciprocal Rank Fusion을 활용하여 검색 품질을 획기적으로 향상시키는 RAG-Fusion 기법을 초급 개발자도 쉽게 이해할 수 있도록 실무 예제와 함께 설명합니다.
목차
1. 다중 쿼리 생성
어느 날 김개발 씨가 회사의 챗봇 시스템을 개선하는 프로젝트를 맡게 되었습니다. 사용자가 "최근 매출 현황"이라고 물어보면 정확한 답변을 찾지 못하는 문제가 있었습니다.
박시니어 씨가 다가와 말했습니다. "하나의 질문만으로는 부족해요.
여러 각도로 질문을 만들어 보는 건 어떨까요?"
다중 쿼리 생성은 사용자의 하나의 질문을 여러 형태로 변형하여 검색 범위를 넓히는 기법입니다. 마치 도서관에서 책을 찾을 때 "파이썬 입문"뿐만 아니라 "파이썬 기초", "파이썬 초보자 가이드"로도 찾아보는 것과 같습니다.
이를 통해 검색 누락을 최소화하고 더 풍부한 결과를 얻을 수 있습니다.
다음 코드를 살펴봅시다.
from openai import OpenAI
def generate_multiple_queries(original_query, num_queries=3):
# LLM을 활용하여 원본 쿼리를 다양한 형태로 변환합니다
client = OpenAI()
prompt = f"""다음 질문을 {num_queries}가지 다른 방식으로 표현해주세요:
원본 질문: {original_query}
각 질문은 줄바꿈으로 구분하고, 번호를 붙이지 마세요."""
response = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": prompt}]
)
# 생성된 쿼리들을 리스트로 반환합니다
queries = response.choices[0].message.content.strip().split('\n')
return [original_query] + [q.strip() for q in queries if q.strip()]
김개발 씨는 입사 6개월 차 주니어 개발자입니다. 회사에서 운영하는 내부 문서 검색 챗봇이 있는데, 사용자들의 만족도가 낮았습니다.
"왜 내가 원하는 답을 찾지 못할까요?" 사용자들의 불만이 계속 들려왔습니다. 박시니어 씨가 김개발 씨의 모니터를 들여다보며 말했습니다.
"여기 문제가 있어요. 사용자가 '최근 매출 현황'이라고 물어봤을 때, 시스템은 정확히 그 단어가 들어간 문서만 찾고 있어요." 검색의 한계 그렇다면 일반적인 검색은 어떤 문제가 있을까요?
쉽게 비유하자면, 단일 쿼리 검색은 마치 시험에서 한 가지 답안만 제출하는 것과 같습니다. 선생님이 원하는 답이 조금만 다르게 표현되어 있어도 점수를 받지 못합니다.
이처럼 전통적인 검색도 사용자가 입력한 키워드와 정확히 일치하는 문서만 찾으려고 합니다. 예를 들어 사용자가 "매출 현황"이라고 검색하면, 문서에는 "판매 실적", "영업 성과", "수익 보고서"로 표현되어 있을 수 있습니다.
같은 의미지만 다른 단어를 사용했기 때문에 검색 결과에서 누락됩니다. 다양성의 힘 이런 문제를 해결하기 위해 다중 쿼리 생성 기법이 등장했습니다.
하나의 질문을 여러 방식으로 표현하면 검색 커버리지가 획기적으로 늘어납니다. "최근 매출 현황"이라는 질문을 "지난 분기 판매 실적은?", "최신 영업 성과 보고서", "이번 달 수익 현황"으로도 검색하는 것입니다.
마치 그물을 여러 개 던져서 물고기를 잡는 확률을 높이는 것과 같습니다. LLM의 활용 요즘에는 대형 언어 모델을 활용하여 쿼리 변환을 자동화합니다.
위의 코드를 한 줄씩 살펴보겠습니다. 먼저 generate_multiple_queries 함수는 원본 쿼리와 생성할 쿼리 개수를 받습니다.
핵심은 프롬프트 엔지니어링입니다. LLM에게 "다른 방식으로 표현해주세요"라고 요청하면, 모델은 동일한 의미를 가진 다양한 표현을 생성합니다.
response.choices[0].message.content에서 생성된 텍스트를 받아와 줄바꿈으로 분리합니다. 마지막으로 원본 쿼리와 생성된 쿼리들을 하나의 리스트로 반환합니다.
실무 활용 사례 실제 현업에서는 어떻게 활용할까요? 예를 들어 법률 문서 검색 서비스를 개발한다고 가정해봅시다.
사용자가 "임대차 계약 해지"라고 검색하면, 시스템은 자동으로 "월세 계약 종료", "전세 계약 파기", "임차인 권리 행사" 등의 쿼리도 함께 생성합니다. 이렇게 하면 법률 용어가 다르게 표현된 문서들도 빠짐없이 찾을 수 있습니다.
네이버, 카카오 같은 대형 포털에서도 이런 기법을 적극 활용하고 있습니다. 사용자가 검색어를 입력하면 자동완성 기능과 함께 유사 검색어를 제안하는 것도 같은 원리입니다.
주의사항 하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 너무 많은 쿼리를 생성하는 것입니다.
쿼리가 10개, 20개로 늘어나면 검색 시간이 급격히 증가하고 비용도 늘어납니다. 보통 3~5개 정도가 적당합니다.
또한 생성된 쿼리가 원본과 의미가 크게 달라지는 경우도 있습니다. LLM이 완벽하지 않기 때문에 때로는 엉뚱한 표현을 만들어내기도 합니다.
따라서 생성된 쿼리를 검증하는 로직을 추가하는 것이 좋습니다. 정리 다시 김개발 씨의 이야기로 돌아가 봅시다.
박시니어 씨의 조언대로 다중 쿼리 생성을 적용한 김개발 씨는 놀라운 결과를 얻었습니다. "검색 정확도가 30%나 올라갔어요!" 다중 쿼리 생성을 제대로 이해하면 사용자에게 훨씬 더 풍부한 검색 결과를 제공할 수 있습니다.
여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - 쿼리 개수는 3~5개가 적당합니다. 너무 많으면 성능이 떨어집니다.
- LLM 프롬프트에 "원본 의미를 유지하면서"라는 조건을 추가하면 품질이 향상됩니다.
- 생성된 쿼리를 캐싱하면 동일한 질문에 대해 비용을 절감할 수 있습니다.
2. Reciprocal Rank Fusion
김개발 씨가 다중 쿼리를 성공적으로 구현했지만, 새로운 문제가 생겼습니다. 각 쿼리마다 다른 검색 결과가 나오는데, 이걸 어떻게 합칠까요?
박시니어 씨가 화이트보드에 수식을 그리며 말했습니다. "RRF라는 멋진 알고리즘이 있어요.
순위를 기반으로 점수를 매기는 방식이죠."
Reciprocal Rank Fusion은 여러 검색 결과를 하나로 합치는 순위 기반 융합 알고리즘입니다. 마치 여러 심사위원의 점수를 합산하여 최종 우승자를 정하는 것처럼, 각 쿼리의 검색 결과 순위를 점수로 변환하여 합산합니다.
이를 통해 다양한 검색 결과에서 일관되게 높은 순위에 있는 문서를 최상위로 올릴 수 있습니다.
다음 코드를 살펴봅시다.
def reciprocal_rank_fusion(search_results_list, k=60):
# 각 문서별 RRF 점수를 저장할 딕셔너리
doc_scores = {}
# 각 검색 결과 리스트를 순회합니다
for search_results in search_results_list:
for rank, doc in enumerate(search_results, start=1):
doc_id = doc['id']
# RRF 공식: 1 / (k + rank)
# k는 상수로 보통 60을 사용합니다
score = 1.0 / (k + rank)
# 동일 문서가 여러 검색 결과에 나타나면 점수를 합산합니다
if doc_id in doc_scores:
doc_scores[doc_id] += score
else:
doc_scores[doc_id] = score
# 점수 기준 내림차순 정렬하여 반환합니다
sorted_docs = sorted(doc_scores.items(), key=lambda x: x[1], reverse=True)
return sorted_docs
김개발 씨는 다중 쿼리를 성공적으로 구현했지만 새로운 고민에 빠졌습니다. "최근 매출 현황", "지난 분기 판매 실적", "이번 달 수익 보고서" 세 가지 쿼리로 검색하니 각각 다른 문서들이 나왔습니다.
어떤 문서를 사용자에게 먼저 보여줘야 할까요? 박시니어 씨가 커피를 한 모금 마시고 설명을 시작했습니다.
"검색 결과를 합치는 건 생각보다 어려운 문제예요. 단순히 점수를 더하면 안 되거든요." 검색 점수의 함정 왜 단순히 점수를 더하면 안 될까요?
쉽게 비유하자면, 검색 점수는 마치 서로 다른 화폐와 같습니다. 미국 달러 100과 한국 원화 100을 단순히 더하면 200이 되지만, 실제 가치는 전혀 다릅니다.
검색 엔진도 마찬가지입니다. A 쿼리에서 나온 점수 0.9와 B 쿼리에서 나온 점수 0.9가 같은 의미를 가진다고 보장할 수 없습니다.
각 검색 엔진이나 쿼리마다 점수를 계산하는 방식이 다르기 때문입니다. 어떤 엔진은 01 사이의 점수를 주고, 어떤 엔진은 0100 사이의 점수를 줍니다.
이런 점수들을 그냥 더하면 왜곡된 결과가 나옵니다. 순위의 힘 바로 이런 문제를 해결하기 위해 Reciprocal Rank Fusion이 등장했습니다.
RRF의 핵심 아이디어는 매우 간단합니다. 점수 대신 순위를 사용하는 것입니다.
1등, 2등, 3등이라는 순위는 어떤 검색 엔진을 사용하든 동일한 의미를 가집니다. 마치 올림픽에서 금메달, 은메달, 동메달을 부여하는 것처럼 명확합니다.
RRF는 각 문서의 순위를 점수로 변환합니다. 공식은 1 / (k + rank)입니다.
여기서 k는 상수인데, 보통 60을 사용합니다. 1등 문서는 1/61 = 0.016, 2등 문서는 1/62 = 0.016 정도의 점수를 받습니다.
왜 60일까? 여기서 궁금증이 생깁니다. 왜 k 값을 60으로 할까요?
연구자들이 여러 실험을 해본 결과, k=60일 때 가장 좋은 성능을 보였다고 합니다. 이 값이 너무 작으면 상위 순위 문서들의 점수 차이가 너무 크고, 너무 크면 차이가 거의 없어집니다.
60은 적당한 균형점입니다. 물론 여러분의 데이터셋에 따라 다른 값이 더 좋을 수도 있습니다.
실험을 통해 최적값을 찾아보세요. 코드 분석 위의 코드를 자세히 살펴보겠습니다.
먼저 doc_scores라는 딕셔너리를 준비합니다. 여기에 각 문서의 최종 점수가 쌓입니다.
그다음 search_results_list를 순회하는데, 이것은 각 쿼리의 검색 결과를 담고 있는 리스트의 리스트입니다. 핵심은 enumerate(search_results, start=1) 부분입니다.
각 문서의 순위를 1부터 시작하여 매깁니다. 그리고 RRF 공식을 적용하여 점수를 계산합니다.
만약 동일한 문서가 여러 검색 결과에 등장하면, 점수를 계속 더해줍니다. 이것이 바로 융합의 핵심입니다.
마지막으로 sorted 함수로 점수 기준 내림차순 정렬하여 최종 결과를 반환합니다. 실무 활용 사례 실제 현업에서는 어떻게 활용할까요?
예를 들어 이커머스 플랫폼을 개발한다고 가정해봅시다. 사용자가 "노트북"을 검색하면, 시스템은 여러 필터를 적용합니다.
텍스트 검색, 이미지 검색, 사용자 행동 기반 추천 등 다양한 소스에서 결과가 나옵니다. RRF를 사용하면 이 모든 결과를 효과적으로 합칠 수 있습니다.
구글, 빙 같은 검색 엔진들도 내부적으로 유사한 융합 알고리즘을 사용합니다. 웹 검색, 이미지 검색, 뉴스 검색 등 다양한 소스의 결과를 하나로 합치는 과정에서 순위 기반 융합이 매우 중요한 역할을 합니다.
주의사항 하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 모든 검색 결과를 동등하게 취급하는 것입니다.
어떤 쿼리는 더 중요하고, 어떤 쿼리는 덜 중요할 수 있습니다. 이런 경우 가중치를 적용하는 것이 좋습니다.
또한 순위가 없는 문서는 어떻게 할까요? 만약 A 쿼리에서는 나타나지 않고 B 쿼리에서만 나타난 문서는 B 쿼리의 순위만 반영됩니다.
이것이 의도한 동작인지 확인해야 합니다. 정리 다시 김개발 씨의 이야기로 돌아가 봅시다.
박시니어 씨의 설명을 듣고 RRF를 구현한 김개발 씨는 놀라운 결과를 얻었습니다. "여러 검색 결과가 깔끔하게 합쳐지네요!" Reciprocal Rank Fusion을 제대로 이해하면 다양한 소스의 검색 결과를 효과적으로 통합할 수 있습니다.
여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - k 값은 보통 60을 사용하지만, 데이터셋에 따라 조정해보세요.
- 쿼리별로 가중치를 다르게 주고 싶다면 점수에 가중치를 곱하면 됩니다.
- 순위가 너무 낮은 문서는 제거하는 것도 좋은 전략입니다.
3. 결과 병합 전략
김개발 씨가 RRF를 성공적으로 구현했지만, 박시니어 씨가 질문을 던졌습니다. "그런데 검색 결과를 합칠 때 문서의 메타데이터는 어떻게 처리할 거예요?
같은 문서라도 각 검색 결과마다 다른 정보를 담고 있을 수 있어요." 김개발 씨는 고개를 갸우뚱했습니다.
결과 병합 전략은 여러 검색 결과에서 동일한 문서가 나타날 때 메타데이터와 컨텍스트를 어떻게 통합할지 결정하는 방법입니다. 마치 같은 사람에 대한 여러 프로필을 하나로 합치는 것처럼, 점수뿐만 아니라 문서 내용, 출처, 관련성 정보 등을 효과적으로 결합해야 합니다.
다음 코드를 살펴봅시다.
def merge_search_results(search_results_list, rrf_scores):
# 문서 ID를 키로 하는 병합된 결과 딕셔너리
merged_results = {}
for search_results in search_results_list:
for doc in search_results:
doc_id = doc['id']
if doc_id not in merged_results:
# 처음 발견된 문서는 그대로 저장합니다
merged_results[doc_id] = {
'id': doc_id,
'content': doc['content'],
'metadata': doc.get('metadata', {}),
'source_queries': [doc.get('query', '')],
'original_scores': [doc.get('score', 0)],
'rrf_score': rrf_scores.get(doc_id, 0)
}
else:
# 이미 있는 문서는 메타데이터를 병합합니다
existing = merged_results[doc_id]
existing['source_queries'].append(doc.get('query', ''))
existing['original_scores'].append(doc.get('score', 0))
# 메타데이터 병합: 더 높은 점수의 데이터를 우선합니다
if doc.get('score', 0) > max(existing['original_scores'][:-1], default=0):
existing['metadata'].update(doc.get('metadata', {}))
# RRF 점수 기준으로 정렬하여 리스트로 반환합니다
return sorted(merged_results.values(), key=lambda x: x['rrf_score'], reverse=True)
김개발 씨는 RRF로 점수를 계산하는 데는 성공했지만, 또 다른 문제를 발견했습니다. 각 검색 결과에 담긴 문서 정보가 조금씩 다른 것입니다.
A 쿼리로 검색한 문서에는 '작성자' 정보가 있고, B 쿼리로 검색한 같은 문서에는 '작성일' 정보가 있습니다. 박시니어 씨가 모니터를 가리키며 설명했습니다.
"점수만 합치면 끝이 아니에요. 문서의 부가 정보도 제대로 병합해야 사용자에게 완전한 정보를 제공할 수 있어요." 병합의 복잡성 검색 결과를 병합하는 것은 왜 복잡할까요?
쉽게 비유하자면, 검색 결과 병합은 마치 여러 증인의 증언을 하나로 정리하는 것과 같습니다. 같은 사건을 목격했어도 각 증인이 기억하는 디테일은 다릅니다.
한 증인은 범인의 옷 색깔을 기억하고, 다른 증인은 범인의 키를 기억합니다. 이 모든 정보를 종합해야 완전한 그림이 나옵니다.
검색 결과도 마찬가지입니다. 각 쿼리로 검색한 문서는 서로 다른 맥락에서 나타납니다.
어떤 검색에서는 문서의 제목이 중요하고, 어떤 검색에서는 본문의 특정 단락이 중요합니다. 이런 정보를 모두 보존하면서 병합해야 합니다.
출처 추적의 중요성 병합할 때 가장 중요한 것은 출처 추적입니다. 각 문서가 어떤 쿼리에서 나왔는지 기록해야 합니다.
이렇게 하면 나중에 "왜 이 문서가 검색되었는가?"라는 질문에 답할 수 있습니다. 디버깅할 때도 매우 유용합니다.
위의 코드에서 source_queries 리스트가 바로 이 역할을 합니다. 같은 문서가 여러 쿼리에서 나타나면, 모든 쿼리를 기록합니다.
또한 original_scores 리스트로 각 검색에서의 원래 점수도 보존합니다. 메타데이터 병합 전략 메타데이터를 병합하는 방법은 여러 가지입니다.
가장 간단한 방법은 첫 번째 발견을 기준으로 하는 것입니다. 처음 나타난 검색 결과의 메타데이터를 사용하는 방식입니다.
하지만 이것은 정보 손실이 클 수 있습니다. 더 나은 방법은 점수 기반 우선순위입니다.
위의 코드에서 사용한 방식인데, 더 높은 점수를 받은 검색 결과의 메타데이터를 우선합니다. 이렇게 하면 더 관련성 높은 정보가 보존됩니다.
또 다른 방법은 모든 메타데이터 결합입니다. 충돌하지 않는 한 모든 메타데이터를 합치는 방식입니다.
예를 들어 A 검색에서 '작성자' 정보가 나오고 B 검색에서 '작성일' 정보가 나오면, 둘 다 포함시킵니다. 코드 분석 위의 코드를 단계별로 살펴보겠습니다.
먼저 merged_results라는 딕셔너리를 준비합니다. 문서 ID를 키로 사용하여 중복을 자동으로 제거합니다.
각 문서를 순회하면서, 처음 발견된 문서는 모든 정보를 그대로 저장합니다. 이미 존재하는 문서를 발견하면, source_queries와 original_scores에 새로운 정보를 추가합니다.
그리고 현재 문서의 점수가 기존 점수들보다 높으면 메타데이터를 업데이트합니다. 이것이 점수 기반 우선순위 전략입니다.
마지막으로 RRF 점수 기준으로 정렬하여 반환합니다. 이제 사용자는 점수뿐만 아니라 풍부한 메타데이터와 출처 정보를 함께 볼 수 있습니다.
실무 활용 사례 실제 현업에서는 어떻게 활용할까요? 예를 들어 뉴스 검색 서비스를 개발한다고 가정해봅시다.
같은 뉴스가 여러 검색어로 나타날 수 있습니다. "코로나 백신", "COVID-19 예방접종", "팬데믹 대응" 등으로 검색하면 같은 기사가 나옵니다.
이때 각 검색에서 나온 하이라이트 정보, 관련 키워드 등을 모두 병합하면 사용자에게 더 풍부한 정보를 제공할 수 있습니다. 네이버 뉴스나 다음 뉴스에서도 이런 방식을 사용합니다.
같은 기사에 대해 여러 검색어의 컨텍스트를 종합하여 보여줍니다. 주의사항 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 메타데이터 충돌을 제대로 처리하지 않는 것입니다. 같은 키에 다른 값이 있을 때 어느 것을 선택할지 명확한 규칙이 없으면 예상치 못한 동작이 발생합니다.
또한 메모리 사용량도 고려해야 합니다. 모든 출처 정보와 메타데이터를 보존하면 메모리가 빠르게 증가합니다.
필요한 정보만 선택적으로 저장하는 것이 좋습니다. 정리 다시 김개발 씨의 이야기로 돌아가 봅시다.
박시니어 씨의 조언대로 병합 전략을 구현한 김개발 씨는 만족스러운 표정을 지었습니다. "이제 사용자가 왜 이 문서가 검색되었는지 알 수 있겠어요!" 결과 병합 전략을 제대로 이해하면 검색 시스템의 투명성과 품질을 동시에 높일 수 있습니다.
여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - 출처 정보는 반드시 보존하세요. 디버깅할 때 매우 유용합니다.
- 메타데이터 충돌 시 우선순위 규칙을 명확히 정의하세요.
- 메모리가 부족하다면 상위 N개 문서만 상세 정보를 보존하세요.
4. 실습: RAG-Fusion 시스템
김개발 씨는 이제 모든 개념을 배웠습니다. 박시니어 씨가 미소를 지으며 말했습니다.
"좋아요. 이제 전체 시스템을 하나로 통합해볼까요?
다중 쿼리 생성부터 병합까지 모든 단계를 연결하는 거예요." 김개발 씨는 노트북을 열고 코딩을 시작했습니다.
RAG-Fusion 시스템은 다중 쿼리 생성, Reciprocal Rank Fusion, 결과 병합을 하나로 통합한 완전한 검색 파이프라인입니다. 마치 자동차 공장에서 부품을 조립하여 완성차를 만드는 것처럼, 지금까지 배운 모든 기법을 엔드투엔드 시스템으로 구현합니다.
다음 코드를 살펴봅시다.
from openai import OpenAI
import numpy as np
class RAGFusionSystem:
def __init__(self, vector_store, k=60):
# 벡터 저장소와 RRF 상수 초기화
self.vector_store = vector_store
self.k = k
self.client = OpenAI()
def search(self, query, num_queries=3, top_k=10):
# 1단계: 다중 쿼리 생성
queries = self._generate_queries(query, num_queries)
# 2단계: 각 쿼리로 검색 수행
all_results = []
for q in queries:
results = self.vector_store.search(q, top_k)
all_results.append(results)
# 3단계: RRF로 점수 계산
rrf_scores = self._calculate_rrf(all_results)
# 4단계: 결과 병합 및 반환
final_results = self._merge_results(all_results, rrf_scores)
return final_results[:top_k]
def _generate_queries(self, query, num):
# 앞서 구현한 다중 쿼리 생성 로직
prompt = f"다음 질문을 {num}가지 다른 방식으로 표현해주세요:\n{query}"
response = self.client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": prompt}]
)
queries = response.choices[0].message.content.strip().split('\n')
return [query] + [q.strip() for q in queries if q.strip()]
def _calculate_rrf(self, results_list):
# 앞서 구현한 RRF 로직
scores = {}
for results in results_list:
for rank, doc in enumerate(results, start=1):
doc_id = doc['id']
scores[doc_id] = scores.get(doc_id, 0) + 1.0 / (self.k + rank)
return scores
def _merge_results(self, results_list, rrf_scores):
# 앞서 구현한 병합 로직
merged = {}
for results in results_list:
for doc in results:
doc_id = doc['id']
if doc_id not in merged:
merged[doc_id] = {**doc, 'rrf_score': rrf_scores[doc_id]}
return sorted(merged.values(), key=lambda x: x['rrf_score'], reverse=True)
김개발 씨는 드디어 모든 퍼즐 조각을 모았습니다. 이제 이것들을 하나로 연결하여 완전한 시스템을 만들 차례입니다.
박시니어 씨가 옆에 앉아 함께 코드를 리뷰하기 시작했습니다. "자, 이제 처음부터 끝까지 데이터가 어떻게 흐르는지 봅시다." 박시니어 씨가 화이트보드에 다이어그램을 그렸습니다.
시스템 아키텍처 RAG-Fusion 시스템은 크게 네 단계로 구성됩니다. 첫 번째는 다중 쿼리 생성 단계입니다.
사용자의 질문이 들어오면 LLM을 활용하여 여러 형태의 쿼리를 생성합니다. 이것은 검색의 시작점입니다.
두 번째는 병렬 검색 단계입니다. 생성된 각 쿼리로 벡터 저장소를 검색합니다.
이때 모든 검색은 독립적으로 수행되므로 병렬 처리가 가능합니다. 세 번째는 RRF 점수 계산 단계입니다.
각 검색 결과에서 문서의 순위를 점수로 변환하고 합산합니다. 이것이 최종 순위의 기준이 됩니다.
네 번째는 결과 병합 및 정렬 단계입니다. 메타데이터를 통합하고 RRF 점수 기준으로 정렬하여 최종 결과를 반환합니다.
클래스 설계 위의 코드는 객체지향 방식으로 설계되었습니다. RAGFusionSystem 클래스는 모든 기능을 캡슐화합니다.
초기화 시 벡터 저장소와 RRF 상수 k를 받습니다. 벡터 저장소는 실제 문서를 검색하는 컴포넌트입니다.
보통 FAISS, Pinecone, Weaviate 같은 벡터 데이터베이스를 사용합니다. search 메서드가 메인 인터페이스입니다.
사용자는 이 메서드만 호출하면 됩니다. 내부적으로는 네 단계를 순차적으로 실행합니다.
이렇게 복잡한 로직을 감추고 간단한 인터페이스를 제공하는 것이 좋은 설계입니다. 데이터 흐름 실제 데이터가 어떻게 흐르는지 살펴보겠습니다.
사용자가 "최근 AI 트렌드"라고 질문하면, 먼저 _generate_queries가 실행됩니다. 이 메서드는 "최근 인공지능 동향", "AI 기술 발전 현황", "머신러닝 최신 소식" 같은 쿼리를 생성합니다.
그다음 각 쿼리로 vector_store.search를 호출합니다. 벡터 저장소는 각 쿼리와 의미적으로 유사한 문서들을 찾아 반환합니다.
예를 들어 각 쿼리마다 상위 10개 문서를 가져옵니다. 이제 _calculate_rrf가 실행됩니다.
각 검색 결과의 순위를 점수로 변환합니다. 예를 들어 "AI 기술 백서"라는 문서가 첫 번째 쿼리에서 1등, 두 번째 쿼리에서 3등을 했다면, 총점은 1/61 + 1/63 = 0.032 정도가 됩니다.
마지막으로 _merge_results가 메타데이터를 통합하고 정렬합니다. 최종적으로 상위 10개 문서가 사용자에게 반환됩니다.
실무 활용 사례 실제 현업에서는 어떻게 활용할까요? 예를 들어 고객 지원 챗봇을 개발한다고 가정해봅시다.
고객이 "환불 방법"이라고 질문하면, RAG-Fusion 시스템은 "환불 절차", "결제 취소 방법", "반품 정책" 등으로 확장하여 검색합니다. 이렇게 하면 고객이 원하는 답변을 찾을 확률이 크게 높아집니다.
많은 스타트업에서 이런 시스템을 도입하여 고객 만족도를 30~50% 향상시켰다는 사례가 있습니다. 특히 법률, 의료, 금융 같은 전문 분야에서 효과가 뛰어납니다.
성능 최적화 실무에서는 성능도 중요합니다. 다중 쿼리 생성에서 LLM 호출은 시간이 걸립니다.
자주 사용되는 쿼리는 캐싱하면 좋습니다. 또한 병렬 검색 단계에서는 멀티스레딩이나 비동기 처리를 적용하면 속도를 크게 높일 수 있습니다.
RRF 계산은 빠르지만, 문서가 수천 개라면 최적화가 필요합니다. 상위 100개 문서만 RRF를 적용하고 나머지는 제거하는 것도 좋은 전략입니다.
주의사항 하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 에러 처리를 소홀히 하는 것입니다.
LLM 호출이 실패하거나, 벡터 저장소 연결이 끊어지면 어떻게 할까요? 각 단계마다 에러 처리 로직을 추가해야 합니다.
또한 테스트도 중요합니다. 각 메서드를 유닛 테스트하고, 전체 파이프라인을 통합 테스트해야 합니다.
특히 엣지 케이스를 고려하세요. 쿼리가 빈 문자열이거나, 검색 결과가 없을 때도 제대로 동작해야 합니다.
정리 다시 김개발 씨의 이야기로 돌아가 봅시다. 전체 시스템을 구현한 김개발 씨는 뿌듯한 표정을 지었습니다.
"드디어 완성했어요!" 박시니어 씨가 박수를 쳤습니다. RAG-Fusion 시스템을 제대로 이해하면 고품질 검색 서비스를 구축할 수 있습니다.
여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - 각 단계를 독립적인 메서드로 분리하면 테스트와 유지보수가 쉽습니다.
- LLM 호출은 비용이 들므로 캐싱을 적극 활용하세요.
- 병렬 검색은 멀티스레딩으로 속도를 높일 수 있습니다.
5. 실습: 검색 다양성 향상
시스템을 완성한 김개발 씨에게 박시니어 씨가 새로운 과제를 던졌습니다. "잘했어요.
그런데 한 가지 더 개선할 점이 있어요. 검색 결과가 너무 비슷한 문서들로 채워지는 것 같아요.
다양성을 높여볼까요?" 김개발 씨는 고민에 빠졌습니다.
검색 다양성 향상은 검색 결과에서 유사한 문서들을 제거하고 다양한 관점의 문서를 제공하는 기법입니다. 마치 뉴스 피드에서 같은 주제의 기사를 반복해서 보여주지 않는 것처럼, 중복을 제거하고 다각도의 정보를 제공하여 사용자 경험을 향상시킵니다.
다음 코드를 살펴봅시다.
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
def diversify_results(search_results, embeddings, diversity_weight=0.5, top_k=10):
# MMR (Maximal Marginal Relevance) 알고리즘 구현
# diversity_weight: 0이면 관련성만, 1이면 다양성만 고려
selected = []
selected_embeddings = []
candidates = list(range(len(search_results)))
# 첫 번째 문서는 가장 관련성 높은 것을 선택합니다
first_idx = 0
selected.append(search_results[first_idx])
selected_embeddings.append(embeddings[first_idx])
candidates.remove(first_idx)
# 나머지 문서들을 MMR 점수 기반으로 선택합니다
while len(selected) < top_k and candidates:
mmr_scores = []
for idx in candidates:
# 원래 검색 점수 (관련성)
relevance = search_results[idx]['rrf_score']
# 이미 선택된 문서들과의 유사도 (다양성 페널티)
if selected_embeddings:
similarities = cosine_similarity(
[embeddings[idx]],
selected_embeddings
)[0]
max_similarity = np.max(similarities)
else:
max_similarity = 0
# MMR 점수 계산: 관련성 - 다양성 페널티
mmr = (1 - diversity_weight) * relevance - diversity_weight * max_similarity
mmr_scores.append((idx, mmr))
# 가장 높은 MMR 점수를 가진 문서 선택
best_idx, _ = max(mmr_scores, key=lambda x: x[1])
selected.append(search_results[best_idx])
selected_embeddings.append(embeddings[best_idx])
candidates.remove(best_idx)
return selected
김개발 씨는 RAG-Fusion 시스템을 성공적으로 구축했지만, 사용자 피드백을 받아보니 새로운 문제가 있었습니다. "검색 결과가 전부 비슷비슷해요.
같은 내용을 다르게 표현한 문서들만 나와요." 박시니어 씨가 커피를 한 모금 마시고 설명을 시작했습니다. "그럴 수 있어요.
RRF는 관련성이 높은 문서를 찾는 데는 뛰어나지만, 다양성은 보장하지 않거든요." 다양성의 중요성 왜 검색 결과의 다양성이 중요할까요? 쉽게 비유하자면, 검색 다양성은 마치 균형 잡힌 식단과 같습니다.
매일 같은 음식만 먹으면 질리고 영양도 불균형해집니다. 검색도 마찬가지입니다.
사용자는 다양한 관점과 정보를 원합니다. 예를 들어 "기후 변화"를 검색했을 때, 과학적 연구, 정책 제안, 개인 실천 방법, 기업 사례 등 다양한 측면의 문서를 보고 싶어 합니다.
하지만 일반적인 검색 시스템은 가장 인기 있는 과학 논문들만 반복해서 보여줄 수 있습니다. MMR 알고리즘 이런 문제를 해결하기 위해 MMR (Maximal Marginal Relevance) 알고리즘이 등장했습니다.
MMR의 핵심 아이디어는 간단합니다. 문서를 선택할 때 두 가지를 동시에 고려하는 것입니다.
첫째는 쿼리와의 관련성이고, 둘째는 이미 선택된 문서들과의 차별성입니다. 공식은 이렇습니다: MMR = (1 - λ) × 관련성 - λ × 유사도.
여기서 λ는 다양성 가중치입니다. λ가 0이면 관련성만 고려하고, 1이면 다양성만 고려합니다.
보통 0.5 정도가 적당합니다. 탐욕 알고리즘 MMR은 탐욕 알고리즘 방식으로 작동합니다.
먼저 가장 관련성 높은 문서를 선택합니다. 이것은 확실한 선택입니다.
그다음부터가 중요합니다. 두 번째 문서를 선택할 때는 쿼리와의 관련성뿐만 아니라 첫 번째 문서와 얼마나 다른지도 고려합니다.
세 번째 문서를 선택할 때는 첫 번째와 두 번째 문서 모두와 비교합니다. 이렇게 계속하면, 관련성 높으면서도 서로 다른 문서들을 선택할 수 있습니다.
코드 분석 위의 코드를 자세히 살펴보겠습니다. 먼저 selected와 selected_embeddings 리스트를 준비합니다.
선택된 문서와 그 임베딩을 저장합니다. 첫 번째 문서는 무조건 가장 관련성 높은 것을 선택합니다.
그다음 while 루프에서 나머지 문서들을 선택합니다. 각 후보 문서마다 MMR 점수를 계산하는데, 핵심은 cosine_similarity 계산입니다.
이미 선택된 문서들과 얼마나 유사한지 계산합니다. max_similarity는 이미 선택된 문서 중 가장 유사한 것과의 유사도입니다.
이 값이 크면 다양성 페널티가 커집니다. 마지막으로 가장 높은 MMR 점수를 가진 문서를 선택하고 반복합니다.
임베딩의 역할 여기서 임베딩이 중요한 역할을 합니다. 임베딩은 문서를 고차원 벡터로 표현한 것입니다.
의미가 비슷한 문서는 벡터 공간에서 가까이 위치합니다. 코사인 유사도를 계산하면 두 문서가 얼마나 비슷한지 수치로 알 수 있습니다.
보통 OpenAI의 text-embedding-ada-002 같은 모델을 사용합니다. 각 문서를 1536차원 벡터로 변환합니다.
이 벡터들 간의 거리로 유사도를 측정합니다. 실무 활용 사례 실제 현업에서는 어떻게 활용할까요?
예를 들어 뉴스 추천 시스템을 개발한다고 가정해봅시다. 사용자가 "테크 뉴스"를 구독하면, 시스템은 최신 기술 뉴스를 추천합니다.
하지만 전부 아이폰 관련 뉴스만 보여주면 사용자가 지루해합니다. MMR을 적용하면 아이폰, 안드로이드, AI, 반도체, 우주 기술 등 다양한 주제를 골고루 보여줄 수 있습니다.
유튜브나 넷플릭스의 추천 시스템도 유사한 알고리즘을 사용합니다. 사용자가 좋아할 만한 콘텐츠를 추천하되, 너무 비슷한 것만 추천하지 않도록 다양성을 조절합니다.
다양성 가중치 조절 diversity_weight를 어떻게 설정할까요? 이것은 도메인과 사용자 선호에 따라 다릅니다.
연구 논문 검색 같은 경우는 관련성이 더 중요하므로 0.3 정도로 낮게 설정합니다. 반대로 뉴스 추천 같은 경우는 다양성이 중요하므로 0.7 정도로 높게 설정합니다.
A/B 테스트를 통해 최적값을 찾는 것이 가장 좋습니다. 사용자 만족도, 클릭률, 체류 시간 등을 측정하여 결정합니다.
성능 최적화 MMR 알고리즘은 계산 비용이 있습니다. 모든 후보 문서에 대해 유사도를 계산해야 하므로 O(n²) 시간이 걸립니다.
문서가 많으면 느려집니다. 이런 경우 근사 알고리즘을 사용할 수 있습니다.
예를 들어 상위 100개 문서만 MMR을 적용하고 나머지는 제거합니다. 또한 임베딩 계산도 비용이 듭니다.
미리 계산해서 캐싱하거나, 벡터 데이터베이스에 저장해두면 좋습니다. 주의사항 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 다양성을 너무 강조하는 것입니다. diversity_weight를 1.0으로 설정하면 관련성이 낮은 문서들이 선택됩니다.
사용자는 다양하지만 쓸모없는 결과를 받게 됩니다. 또한 임베딩 품질이 중요합니다.
저품질 임베딩을 사용하면 유사도 계산이 부정확해집니다. 최신의 좋은 임베딩 모델을 사용하세요.
정리 다시 김개발 씨의 이야기로 돌아가 봅시다. MMR을 적용한 김개발 씨는 놀라운 결과를 얻었습니다.
"사용자들이 검색 결과가 훨씬 유용해졌다고 해요!" 박시니어 씨가 어깨를 두드렸습니다. 검색 다양성 향상을 제대로 이해하면 사용자에게 더 풍부하고 유용한 정보를 제공할 수 있습니다.
여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - diversity_weight는 도메인에 따라 조절하세요. 보통 0.3~0.7 사이가 적당합니다.
- 임베딩은 미리 계산해서 캐싱하면 성능이 향상됩니다.
- A/B 테스트로 최적의 다양성 가중치를 찾으세요.
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
ReAct 패턴 마스터 완벽 가이드
LLM이 생각하고 행동하는 ReAct 패턴을 처음부터 끝까지 배웁니다. Thought-Action-Observation 루프로 똑똑한 에이전트를 만들고, 실전 예제로 웹 검색과 계산을 결합한 강력한 AI 시스템을 구축합니다.
AI 에이전트의 모든 것 - 개념부터 실습까지
AI 에이전트란 무엇일까요? 단순한 LLM 호출과 어떻게 다를까요? 초급 개발자를 위해 에이전트의 핵심 개념부터 실제 구현까지 이북처럼 술술 읽히는 스타일로 설명합니다.
프로덕션 RAG 시스템 완벽 가이드
검색 증강 생성(RAG) 시스템을 실제 서비스로 배포하기 위한 확장성, 비용 최적화, 모니터링 전략을 다룹니다. AWS/GCP 배포 실습과 대시보드 구축까지 프로덕션 환경의 모든 것을 담았습니다.
RAG 캐싱 전략 완벽 가이드
RAG 시스템의 성능을 획기적으로 개선하는 캐싱 전략을 배웁니다. 쿼리 캐싱부터 임베딩 캐싱, Redis 통합까지 실무에서 바로 적용할 수 있는 최적화 기법을 다룹니다.
실시간으로 답변하는 RAG 시스템 만들기
사용자가 질문하면 즉시 답변이 스트리밍되는 RAG 시스템을 구축하는 방법을 배웁니다. 실시간 응답 생성부터 청크별 스트리밍, 사용자 경험 최적화까지 실무에서 바로 적용할 수 있는 완전한 가이드입니다.