MultiQueryRetriever

다중 쿼리 생성을 통한 RAG 검색 성능 향상

사용자의 단일 쿼리를 여러 관점의 다양한 쿼리로 확장하여 검색 결과를 풍부하게 만드는 MultiQueryRetriever의 원리와 구현 방법을 다룬다. 거리 기반 벡터 검색의 한계를 극복하고 더 포괄적인 검색 결과를 얻는 전략을 학습한다.

AI
RAG
LangChain
Agent
저자

Kwangmin Kim

공개

2024년 05월 07일

1 MultiQueryRetriever 개요

1.1 핵심 개념

MultiQueryRetriever는 LLM이 사용자의 단일 쿼리를 기반으로 여러 개의 다양한 쿼리를 자동 생성하여 검색 성능을 향상시키는 고급 검색 기법이다.

1.1.1 기본 원리: “하나로 질문하고, 여러 관점으로 검색한다”

  1. 쿼리 확장: 사용자의 원본 질문을 LLM을 통해 여러 관점의 질문들로 변환
  2. 다중 검색: 생성된 각 쿼리로 독립적인 검색 수행
  3. 결과 합성: 모든 검색 결과를 합집합하여 포괄적인 문서 집합 구성

1.2 왜 필요한가?

1.2.1 사용자의 쿼리 한계

게으른 사용자 문제

대부분의 사용자는 상세하고 구체적인 질문을 작성하지 않는다. 이로 인해:
- LLM 입력 데이터가 불충분해짐
- 검색 결과의 품질과 범위가 제한됨
- 사용자가 원하는 정보를 놓칠 가능성 증가

비논리적 질문 문제

사람마다 논리적 개성이 강하기 때문에 다음과 같은 문제가 발생한다:
- 논리적 비약이 포함된 질문
- 구체성이 부족한 모호한 표현
- 비유기적이고 정돈되지 않은 문장 구조
- 질문자만 이해하고 타인은 이해하기 어려운 표현

1.2.2 거리 기반 벡터 검색의 한계

1.2.2.1 임베딩 공간의 제약

거리 기반 벡터 데이터베이스 검색은 고차원 공간에서 쿼리 임베딩과 문서 임베딩 간의 ’거리’를 기준으로 유사한 문서를 찾는다. 하지만 다음과 같은 한계가 있다:

1.2.2.2 쿼리의 세부적인 차이가 검색 결과에 큰 영향을 미치는 이유

벡터 공간에서의 거리 계산은 매우 민감하게 작동한다:

  • 고차원 공간의 차원 저주: 768차원(BERT) 또는 1536차원(OpenAI) 공간에서 작은 변화도 거리에 큰 영향
  • 단어 순서와 표현의 미묘한 차이: “어떻게 사용하나요?” vs “사용법이 무엇인가요?”는 의미상 동일하지만 벡터 공간에서는 다른 위치
  • 동의어와 유의어 처리의 한계: “방법”과 “방식”, “사용법”과 “활용법”이 벡터 공간에서 멀리 떨어져 위치할 수 있음

구체적인 예시:

쿼리 A: "OpenAI API 사용방법"     → 벡터 위치 [0.1, 0.8, -0.3, ...]
쿼리 B: "OpenAI API 활용법"      → 벡터 위치 [0.2, 0.7, -0.1, ...]
쿼리 C: "OpenAI API를 어떻게 써요?" → 벡터 위치 [0.5, 0.4, 0.2, ...]

동일한 의도의 질문이지만 벡터 공간에서 서로 다른 위치에 배치되어
완전히 다른 문서들이 검색될 수 있음

1.2.2.3 거리 계산의 한계

  • 코사인 유사도의 함정: 벡터의 방향성만 고려하여 의미의 강도나 맥락적 뉘앙스 손실
  • 임베딩 모델의 편향: 학습 데이터의 특성에 따라 특정 표현 방식에 치우친 결과
  • 문맥 길이의 영향: 짧은 쿼리와 긴 쿼리가 동일한 공간에서 비교되어 부정확한 유사도 계산
  • 임베딩이 데이터의 의미를 완전히 포착하지 못할 수 있음
  • 단일 관점의 검색으로 인한 정보 누락 가능성
  • 수동 프롬프트 엔지니어링의 번거로움

1.3 MultiQueryRetriever의 해결책

1.3.1 자동 쿼리 확장

MultiQueryRetriever는 LLM(Language Learning Model)을 활용하여 주어진 사용자 입력 쿼리에 대해 다양한 관점에서 여러 쿼리를 자동으로 생성한다. 이를 통해 프롬프트 튜닝 과정을 자동화한다.

1.3.2 포괄적 검색 결과

각각의 생성된 쿼리에 대해 관련 문서 집합을 검색하고, 모든 쿼리를 아우르는 고유한 문서들의 합집합을 추출하여 잠재적으로 관련된 더 큰 문서 집합을 얻는다.

1.3.3 논리적 재구성 효과

사용자의 비정돈된 질문을 MultiQuery를 통해 논리정연하고 완전한 형식의 문장 및 문단으로 paraphrasing하는 효과가 있다. 이러한 paraphrased text들은 LLM이 이해하기 적합한 형태로 나오기 때문에 답변 성능에 긍정적인 효과를 기대할 수 있다.

1.3.4 어휘 선택의 민감성 문제

사용자의 무의식적 어휘 선택

대부분의 사용자는 질문을 작성할 때 어휘 선택(vocabulary choice)을 세심하게 고려하지 않는다. 하지만 벡터 임베딩에서는 각 단어가 고유한 의미 공간을 차지하므로, 유사한 의미의 단어들도 상당한 거리 차이를 보이는 벡터를 생성한다.

동의어 간 벡터 거리의 문제

동일하거나 유사한 개념을 표현하는 단어들이 임베딩 공간에서 예상보다 멀리 떨어져 배치되는 현상:

한국어 예시:
- "이용" → 벡터 A [0.2, 0.8, -0.1, ...]
- "사용" → 벡터 B [0.1, 0.6, 0.3, ...]  
- "활용" → 벡터 C [0.4, 0.2, -0.2, ...]

영어 예시:
- "use" → 벡터 D [0.3, 0.7, 0.1, ...]
- "utilize" → 벡터 E [0.1, 0.4, -0.3, ...]
- "exploit" → 벡터 F [-0.2, 0.5, 0.4, ...]
- "take advantage of" → 벡터 G [0.6, -0.1, 0.2, ...]

실제 검색 영향 사례

사용자 질문 A: "OpenAI API를 이용하는 방법"
사용자 질문 B: "OpenAI API를 사용하는 방법"  
사용자 질문 C: "OpenAI API를 활용하는 방법"

→ 동일한 의도이지만 완전히 다른 검색 결과를 얻을 수 있음
→ 특정 단어로 작성된 문서만 검색되고 나머지는 누락될 위험

언어적 뉘앙스의 벡터화 한계

  • 격식의 정도: “사용”(일반적) vs “이용”(격식) vs “활용”(적극적)
  • 맥락적 의미: “exploit”(부정적 뉘앙스) vs “utilize”(중립적) vs “use”(일반적)
  • 언어 혼재: 한국어-영어 혼용 시 더욱 복잡한 벡터 공간 형성

이러한 어휘 선택의 민감성으로 인해 사용자가 선택한 특정 단어에 따라 검색 품질이 크게 좌우되는 문제가 발생한다.

1.3.5 검색 한계 극복

여러 관점에서 동일한 질문을 생성함으로써, MultiQueryRetriever는 거리 기반 검색의 제한을 일정 부분 극복하고, 더욱 풍부한 검색 결과를 제공할 수 있다.

2 환경 설정

2.1 API 키 및 추적 설정

# API 키를 환경변수로 관리하기 위한 설정 파일
from dotenv import load_dotenv

# API 키 정보 로드
load_dotenv()
# LangSmith 추적을 설정한다. https://smith.langchain.com
# !pip install langchain-teddynote
from langchain_teddynote import logging

# 프로젝트 이름을 입력한다.
logging.langsmith("CH10-Retriever")
# 샘플 벡터DB 구축
from langchain_community.document_loaders import WebBaseLoader
from langchain.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter

# 블로그 포스트 (Parent Document)  로드
loader = WebBaseLoader(
    "https://teddylee777.github.io/openai/openai-assistant-tutorial/", encoding="utf-8"
)

# 문서 분할
text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=0)
docs = loader.load_and_split(text_splitter)

# 임베딩 정의
openai_embedding = OpenAIEmbeddings()

# 벡터DB 생성
db = FAISS.from_documents(docs, openai_embedding)

# retriever 생성
retriever = db.as_retriever()

# 문서 검색
query = "OpenAI Assistant API의 Functions 사용법에 대해 알려주세요."
relevant_docs = retriever.invoke(query)

# 검색된 문서의 개수 출력
len(relevant_docs)
USER_AGENT environment variable not set, consider setting it to identify your requests.
4

검색된 결과 중 1개 문서의 내용을 출력한다.

# 1번 문서를 출력합니다.
print(relevant_docs[1].page_content)
가장 강력한 도구로서, Assistant에게 사용자 정의 함수를 지정할 수 있습니다. 이는 Chat Completions API에서의 함수 호출과 매우 유사합니다.


Function calling(함수 호출) 도구를 사용하면 Assistant 에게 사용자 정의 함수 를 설명하여 호출해야 하는 함수를 인자와 함께 지능적으로 반환하도록 할 수 있습니다.


Assistant API는 실행 중에 함수를 호출할 때 실행을 일시 중지하며, 함수 호출 결과를 다시 제공하여 Run 실행을 계속할 수 있습니다. (이는 사용자 피드백을 받아 재게할 수 있는 의미이기도 합니다. 아래 튜토리얼에서 상세히 다룹니다).

3 MultiQueryRetriever 사용법

3.1 기본 사용법

MultiQueryRetriever에 사용할 LLM을 지정하고 질의 생성에 사용하면, retriever가 나머지 작업을 처리한다.

3.1.1 MultiQueryRetriever 생성

from langchain.retrievers.multi_query import MultiQueryRetriever
from langchain_openai import ChatOpenAI


# ChatOpenAI 언어 모델을 초기화한다. temperature는 0으로 설정한다.
llm = ChatOpenAI(temperature=0, model="gpt-4o-mini")

multiquery_retriever = MultiQueryRetriever.from_llm(  # MultiQueryRetriever를 언어 모델을 사용하여 초기화한다.
    # 벡터 데이터베이스의 retriever와 언어 모델을 전달한다.
    retriever=db.as_retriever(),
    llm=llm,
)

아래는 다중 쿼리를 생성하는 중간 과정을 디버깅하기 위하여 실행하는 코드이다.

먼저 "langchain.retrievers.multi_query" 로거를 가져온다. 이는 logging.getLogger() 함수를 사용하여 수행된다. 그 다음, 이 로거의 로그 레벨을 INFO로 설정하여, INFO 레벨 이상의 로그 메시지만 출력되도록 할 수 있다.

# 쿼리에 대한 로깅 설정
import logging

logging.basicConfig()
logging.getLogger("langchain.retrievers.multi_query").setLevel(logging.INFO)

3.1.2 MultiQueryRetriever 실행 및 결과 분석

이 코드는 multiquery_retriever 객체의 invoke 메서드를 사용하여 주어진 question과 관련된 문서를 검색한다. 검색된 문서들은 unique_docs라는 변수에 저장되며, 이 변수의 길이를 확인함으로써 검색된 관련 문서의 총 개수를 알 수 있다. 이 과정을 통해 사용자의 질문에 대한 관련 정보를 효과적으로 찾아내고 그 양을 파악할 수 있다.

# 질문을 정의한다.
question = "OpenAI Assistant API의 Functions 사용법에 대해 알려주세요."
# 문서 검색
relevant_docs = multiquery_retriever.invoke(question)

# 검색된 고유한 문서의 개수를 반환합니다.
print(
    f"===============\n검색된 문서 개수: {len(relevant_docs)}",
    end="\n===============\n",
)

# 검색된 문서의 내용을 출력합니다.
print(relevant_docs[0].page_content)
INFO:langchain.retrievers.multi_query:Generated queries: ['OpenAI Assistant API에서 Functions 기능을 사용하는 방법에 대해 설명해 주세요.  ', 'OpenAI Assistant API의 Functions를 활용하는 방법은 무엇인가요?  ', 'OpenAI Assistant API의 Functions 사용에 대한 가이드를 제공해 주실 수 있나요?']
  • 질문 쿼리가 3개로 늘어남
===============
검색된 문서 개수: 5
===============
OpenAI의 새로운 Assistants API는 대화와 더불어 강력한 도구 접근성을 제공합니다. 본 튜토리얼은 OpenAI Assistants API를 활용하는 내용을 다룹니다. 특히, Assistant API 가 제공하는 도구인 Code Interpreter, Retrieval, Functions 를 활용하는 방법에 대해 다룹니다. 이와 더불어 파일을 업로드 하는 내용과 사용자의 피드백을 제출하는 내용도 튜토리얼 말미에 포함하고 있습니다.



주요내용
  • 단일 쿼리 질문에서는 검색된 문서 개수가 4개
  • 추가된 질문 쿼리를 합하여 검색된 문서 개수가 5개 (1개가 더 늘어남)

3.2 고급 기법: LCEL Chain 활용 방법

3.2.1 사용자 정의 프롬프트로 커스터마이징

기본 MultiQueryRetriever 대신 LCEL(LangChain Expression Language) Chain을 활용하여 더욱 세밀한 제어가 가능한 커스텀 리트리버를 만들 수 있다.

주요 특징:
- 사용자 정의 프롬프트 정의 가능
- 생성될 쿼리 개수 조정 가능 (예: 5개)
- 생성된 쿼리를 "\n" 구분자로 구분하여 반환
- 사용자 정의 Chain과 함께 사용 가능

3.2.2 LCEL Chain 구현

from langchain_core.runnables import RunnablePassthrough
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser

# 프롬프트 템플릿을 정의한다.(5개의 질문을 생성하도록 프롬프트를 작성하였다)
prompt = PromptTemplate.from_template(
    """You are an AI language model assistant. 
Your task is to generate five different versions of the given user question to retrieve relevant documents from a vector database. 
By generating multiple perspectives on the user question, your goal is to help the user overcome some of the limitations of the distance-based similarity search. 
Your response should be a list of values separated by new lines, eg: `foo\nbar\nbaz\n`

#ORIGINAL QUESTION: 
{question}

#Answer in Korean:
"""
)

# 언어 모델 인스턴스를 생성한다.
llm = ChatOpenAI(temperature=0, model="gpt-4o-mini")

# LLMChain을 생성한다.
custom_multiquery_chain = (
    {"question": RunnablePassthrough()} | prompt | llm | StrOutputParser()
)

# 질문을 정의한다.
question = "OpenAI Assistant API의 Functions 사용법에 대해 알려주세요."

# 체인을 실행하여 생성된 다중 쿼리를 확인한다.
multi_queries = custom_multiquery_chain.invoke(question)
# 결과를 확인한다.(5개 질문 생성)
multi_queries
'OpenAI Assistant API의 Functions 기능을 사용하는 방법을 설명해 주세요.  \nOpenAI Assistant API에서 Functions를 활용하는 방법이 궁금합니다.  \nOpenAI Assistant API의 Functions를 어떻게 사용할 수 있는지 알려주세요.  \nFunctions를 사용하여 OpenAI Assistant API를 활용하는 방법에 대해 설명해 주세요.  \nOpenAI Assistant API의 Functions 사용법에 대한 자세한 정보를 제공해 주세요.  '

3.2.3 출력 포맷 및 고려사항

중요한 프롬프트 지시사항:

Your response should be a list of values separated by new lines, eg: `foo\nbar\nbaz\n`

이 지시사항이 중요한 이유:
- 파싱 용이성: 나중에 list 형식으로 파싱이 용이하게 출력이 되도록 하기 위해서다.
- 강력한 구분자: 리스트 형식으로 스플릿을 구현할 때 명확한 구분자(new line)를 인식시켜야 오류가 발생하지 않는다.
- 시스템 안정성: 일관된 출력 형식으로 후속 처리의 안정성을 보장한다.

3.2.4 Custom Chain을 MultiQueryRetriever에 적용

이전에 생성한 Chain을 MultiQueryRetriever에 전달하여 검색을 수행할 수 있다.

multiquery_retriever = MultiQueryRetriever.from_llm(
    llm=custom_multiquery_chain, retriever=db.as_retriever()
)

MultiQueryRetriever를 사용하여 문서를 검색하고 결과를 확인한다.

# 결과
relevant_docs = multiquery_retriever.invoke(question)

# 검색된 고유한 문서의 개수를 반환한다.
print(
    f"===============\n검색된 문서 개수: {len(relevant_docs)}",
    end="\n===============\n",
)

# 검색된 문서의 내용을 출력한다.
print(relevant_docs[0].page_content)
INFO:langchain.retrievers.multi_query:Generated queries: ['OpenAI Assistant API의 Functions 사용법에 대해 설명해 주세요.  ', 'OpenAI Assistant API에서 Functions를 어떻게 활용할 수 있나요?  ', 'OpenAI Assistant API의 Functions 기능에 대한 정보를 제공해 주세요.  ', 'Functions를 사용하여 OpenAI Assistant API를 어떻게 사용할 수 있는지 알려주세요.  ', 'OpenAI Assistant API의 Functions 사용법에 대한 자세한 내용을 알고 싶습니다.']
===============
검색된 문서 개수: 5
===============
OpenAI의 새로운 Assistants API는 대화와 더불어 강력한 도구 접근성을 제공합니다. 본 튜토리얼은 OpenAI Assistants API를 활용하는 내용을 다룹니다. 특히, Assistant API 가 제공하는 도구인 Code Interpreter, Retrieval, Functions 를 활용하는 방법에 대해 다룹니다. 이와 더불어 파일을 업로드 하는 내용과 사용자의 피드백을 제출하는 내용도 튜토리얼 말미에 포함하고 있습니다.
===============

4 성능 분석 및 효과

4.1 실험 결과 비교

4.1.1 단일 쿼리 vs 다중 쿼리 비교

방식 생성된 쿼리 수 검색된 문서 수 장점 단점
단일 쿼리 1개 4개 빠른 처리, 간단한 구조 제한된 검색 범위
기본 MultiQuery 3개 5개 자동 쿼리 확장 제한된 커스터마이징
Custom MultiQuery 5개 5개 높은 커스터마이징, 더 다양한 관점 복잡한 설정

4.1.2 주요 개선 효과

1. 검색 범위 확장
- 단일 쿼리: 4개 문서 → 다중 쿼리: 5개 문서 (25% 증가)
- 더 포괄적인 정보 수집 가능

2. 쿼리 다양성 향상

원본: "OpenAI Assistant API의 Functions 사용법에 대해 알려주세요."

생성된 다양한 관점:
1. "기능을 사용하는 방법을 설명해 주세요"
2. "Functions를 활용하는 방법이 궁금합니다"  
3. "Functions를 어떻게 사용할 수 있는지 알려주세요"
4. "Functions를 사용하여 API를 활용하는 방법"
5. "Functions 사용법에 대한 자세한 정보"

3. 검색 품질 개선
- 다양한 표현 방식으로 누락될 수 있는 관련 문서까지 포괄
- 사용자 의도를 더 정확하게 해석하여 검색

4.2 실무 적용 가이드

4.2.1 언제 사용할까?

✅ MultiQueryRetriever 사용 권장 상황
1. 복잡하거나 모호한 쿼리: 사용자 질문이 불명확하거나 여러 해석이 가능한 경우
2. 포괄적 검색이 필요: 관련된 모든 정보를 놓치지 않고 수집해야 하는 경우
3. 다양한 표현의 문서: 동일한 개념이 여러 방식으로 표현된 문서들이 있는 경우
4. 사용자 경험 개선: 사용자가 정확한 키워드를 모르는 상황에서도 좋은 결과를 제공해야 하는 경우

❌ 일반 Retriever 사용 권장 상황
1. 명확하고 구체적인 쿼리: 이미 정확한 키워드나 구문이 포함된 경우
2. 빠른 응답이 중요: 실시간 응답이 중요하고 약간의 정확도 손실을 감수할 수 있는 경우
3. 제한된 리소스: LLM API 호출 비용을 최소화해야 하는 경우
4. 단순한 FAQ: 간단한 질문-답변 시스템에서 과도한 기능일 수 있는 경우

4.2.2 최적화 전략

1. 쿼리 생성 수 조정

# 간단한 질문: 3개
# 복잡한 질문: 5-7개
# 매우 복잡한 분석: 10개까지

prompt = PromptTemplate.from_template(
    """Generate {num_queries} different versions of the question:
    {question}"""
)

2. 도메인별 프롬프트 최적화

# 기술 문서용
tech_prompt = """Generate technical variations focusing on:
- Implementation details
- API usage patterns  
- Code examples
- Troubleshooting scenarios"""

# 비즈니스 문서용  
business_prompt = """Generate business-oriented variations focusing on:
- Use cases and applications
- Benefits and advantages
- Cost and ROI considerations
- Best practices"""

3. 성능 모니터링

# 검색 결과 품질 추적
def evaluate_retrieval_quality(original_query, generated_queries, results):
    return {
        'query_diversity': calculate_diversity(generated_queries),
        'result_coverage': len(set(results)),
        'relevance_score': calculate_relevance(original_query, results)
    }

4.3 핵심 요약

4.3.1 MultiQueryRetriever의 핵심 가치

“하나로 질문하고, 여러 관점으로 검색한다”

  1. 자동 쿼리 확장: 사용자의 단일 질문을 다양한 관점의 여러 질문으로 자동 변환
  2. 검색 범위 확대: 단일 쿼리로는 놓칠 수 있는 관련 문서들까지 포괄적으로 수집
  3. 사용자 경험 향상: 불완전하거나 모호한 질문도 의도를 파악하여 적절한 답변 제공
  4. 유연한 커스터마이징: LCEL Chain을 통해 도메인별, 용도별 최적화 가능

4.3.2 주요 장점

  1. 검색 정확도 향상: 다양한 관점에서의 접근으로 누락 정보 최소화
  2. 사용성 개선: 사용자가 정확한 키워드를 몰라도 좋은 결과 제공
  3. 자동화: 프롬프트 엔지니어링 과정을 LLM이 자동으로 수행
  4. 확장성: 기존 Retriever 위에 간단히 추가하여 성능 향상 가능

4.3.3 실무 적용 포인트

  • 적정 쿼리 수: 3-5개 (복잡도에 따라 조정)
  • 비용 대비 효과: LLM 호출 비용 증가 vs 검색 품질 향상의 트레이드오프 고려
  • 도메인 특화: 업계별, 용도별 프롬프트 템플릿 개발로 효과 극대화
  • 성능 모니터링: 검색 결과의 다양성과 관련성을 지속적으로 평가

MultiQueryRetriever는 RAG 시스템의 검색 단계를 크게 개선할 수 있는 강력한 도구이다. 특히 사용자의 질문이 불명확하거나 포괄적인 정보 수집이 필요한 상황에서 그 가치를 발휘한다.

Subscribe

Enjoy this blog? Get notified of new posts by email: