RAG 쿼리 라우팅 실전: 다중 벡터 저장소 협업과 지능형 검색 분배

새벽 3시, 프로덕션 환경의 경고등이 또 켜졌습니다.
모니터링 패널을 바라보니 사용자가 입력한 “3분기 중국 화둥 지역 매출” 쿼리의 응답 시간이 12초까지 치솟아 있었습니다. 데모 환경에서는 분명 잘 작동했는데 프로덕션에 배포하자마자 왜 문제가 생긴 걸까요? 더 답답한 점은 단순한 사실 조회 하나에 시스템이 완전한 멀티홉 추론 절차를 호출했다는 로그였습니다.
“모기 한 마리를 잡으려고 핵폭탄을 쓰는 셈이네요.” 옆에서 로그를 본 동료가 직설적으로 말했습니다.
그 순간 문제는 검색 정확도가 아니라 검색 전략에 있다는 사실을 깨달았습니다. 기존 RAG는 ‘생각 없이 검색하는 검색기’와 비슷해서 사용자가 무엇을 묻든 똑같이 벡터 검색과 대규모 언어 모델 생성 절차를 거칩니다. 단순한 쿼리는 과도하게 처리하면서 정작 복잡한 쿼리는 충분히 지원하지 못합니다.
이 글에서는 RAG 시스템에 ‘교통 관제 장치’, 즉 쿼리 라우터를 장착하는 방법을 소개합니다. 쿼리 특성에 따라 요청을 가장 적합한 검색 경로로 분배합니다. 빠른 경로는 단순 사실을 처리하고, 심층 경로는 복잡한 추론을 처리하며, 다중 소스 검색은 답변의 포괄성을 보장합니다.
솔직히 이 방식이 제 프로젝트를 살렸습니다.
1. 쿼리 라우팅이 필요한 이유 — ‘무조건 검색’에서 지능형 분배로
먼저 제가 겪었던 시행착오부터 이야기하겠습니다.
지난해 한 전자상거래 회사의 고객 지원 RAG 시스템을 구축했습니다. 지식 베이스에는 상품 정보, 사후 지원 정책, 물류 규정, 마케팅 행사라는 네 가지 유형의 데이터를 넣었습니다. 테스트할 때는 모든 것이 정상이었지만 배포 후 사용자 불만이 두 배로 늘었습니다.
로그를 조사해 보니 전형적인 문제가 드러났습니다. 사용자가 “환불까지 얼마나 걸리나요?”라고 물었는데 시스템은 배송 소요 시간과 프로모션 행사 규정을 반환했습니다. 답 자체가 틀렸다기보다 초점이 흐렸습니다. 관련 없는 정보가 너무 많아 사용자의 판단을 방해한 것입니다.
이것이 기존 RAG의 첫 번째 문제인 지식 간섭입니다. 모든 비즈니스 상황의 데이터를 한 벡터 저장소에 섞으면 검색 결과도 뒤섞일 수밖에 없습니다. 사용자가 A 상황을 물어도 시스템은 B 상황에서 ‘유사한’ 내용을 반환할 수 있습니다.
두 번째 문제는 더 눈에 띄지 않습니다. 바로 응답 효율입니다.
“3분기 중국 화둥 지역 매출은 얼마인가요?”라는 쿼리를 살펴보겠습니다. 본질적으로 단순한 사실 조회이므로 데이터베이스를 직접 조회하거나 키워드 검색만 해도 충분합니다. 하지만 기존 RAG는 어떻게 처리할까요? 벡터 인코딩, 코사인 유사도 계산, Top-K 검색, 대규모 언어 모델 생성까지 모든 절차를 실행합니다. 이 과정에 1~2초가 걸리고 리소스도 많이 소모됩니다.
세 번째 문제는 쿼리 의도 오판입니다.
사용자가 “재생 시간이 가장 짧은 영상을 찾아 줘”라고 하면 SelfQueryRetriever는 ‘재생 시간’이 메타데이터 필터 조건이라는 점을 이해하지 못할 수 있습니다. “파업 사건이 주가에 영향을 미쳤나요?”라는 질문에는 멀티홉 추론이 필요합니다. 먼저 파업 사건을 찾고, 관련 기업을 찾은 다음, 마지막으로 주가 흐름을 조회해야 하므로 단일 벡터 검색만으로는 해결할 수 없습니다.
따라서 쿼리 특성을 식별하고 가장 알맞은 검색 경로를 선택하는, 상황 판단이 가능한 라우터가 필요합니다.
이는 식당의 주문 시스템과 비슷합니다. 패스트푸드 카운터는 간단한 주문을 빠르게 처리하고, 정규 주방은 복잡한 요리를 담당하며, 배달 창구는 배송 요청을 처리합니다. 각자 역할을 맡을 때 효율이 가장 높습니다.
2. 쿼리 라우팅 핵심 아키텍처 — 3계층 분배 모델
쿼리 라우팅의 핵심 발상은 사실 간단합니다. 계층별로 처리하고 각자 역할을 맡는 것입니다.
저는 전체 시스템을 3계층 아키텍처로 설계했습니다.
사용자 쿼리
↓
┌─────────────────────────────────┐
│ 라우팅 계층: 상황 분류 │
│ (LLM / Semantic Router) │
│ → python_docs / js_docs / go_docs│
└─────────────────────────────────┘
↓
┌─────────────────────────────────┐
│ 검색 계층: 상황별 전용 벡터 저장소 │
│ Chroma(python) / Chroma(js)... │
│ → top-k chunks │
└─────────────────────────────────┘
↓
┌─────────────────────────────────┐
│ 병합 계층: RRF 알고리즘 통합 │
│ RRF(d) = Σ 1/(k + rank(d)) │
│ → 최종 답변 │
└─────────────────────────────────┘
↓
답변 생성
라우팅 계층은 전체 아키텍처의 ‘두뇌’입니다. 임무는 명확합니다. 사용자 쿼리를 분석해 어느 검색 경로를 이용할지 판단합니다. 대표적인 방식은 LLM 논리 분석과 Semantic Router 시맨틱 매칭 두 가지입니다. 뒤에서 자세히 살펴보겠습니다.
검색 계층은 ‘손’입니다. Python 문서 저장소, JavaScript 문서 저장소, Go 문서 저장소처럼 각 비즈니스 상황에 독립된 벡터 인덱스가 있습니다. 라우팅 계층이 검색할 저장소를 결정하면 검색 계층이 실제 검색을 실행합니다.
병합 계층은 ‘심판’입니다. 쿼리가 여러 상황에 걸쳐 있으면 각 검색기가 반환한 결과를 병합해 순위를 정해야 합니다. 여기서는 간단하면서도 효과가 좋은 다중 소스 순위 결정 방식인 RRF(Reciprocal Rank Fusion) 알고리즘을 사용합니다.
LangChain의 EnsembleRetriever로 이 아키텍처를 구현하는 방법을 살펴보겠습니다.
from langchain.retrievers import EnsembleRetriever
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
# Python 문서 벡터 저장소 초기화
python_store = Chroma(
persist_directory="./chroma_python",
embedding_function=OpenAIEmbeddings()
)
python_retriever = python_store.as_retriever(
search_kwargs={"k": 5}
)
# JavaScript 문서 벡터 저장소 초기화
js_store = Chroma(
persist_directory="./chroma_js",
embedding_function=OpenAIEmbeddings()
)
js_retriever = js_store.as_retriever(
search_kwargs={"k": 5}
)
# RRF로 여러 검색기 병합
ensemble_retriever = EnsembleRetriever(
retrievers=[python_retriever, js_retriever],
c=60 # RRF 매개변수, 대표적인 값
)
# 검색 실행
docs = ensemble_retriever.invoke("비동기 콜백은 어떻게 처리하나요?")
print(f"{len(docs)}개의 문서 조각을 검색했습니다")
이 코드의 핵심은 EnsembleRetriever입니다. 두 검색기를 동시에 호출한 뒤 RRF 알고리즘으로 결과를 병합합니다. c=60은 경험값입니다. 너무 크면 순위가 지나치게 고르게 반영되고, 너무 작으면 상위 결과의 가중치가 과도하게 커집니다.
실측 결과 이 아키텍처는 검색 정확도를 72%에서 92%로 높였습니다. 다만 응답 시간이 늘어나는 대가가 있습니다. 여러 검색기를 병렬로 조회하려면 더 많은 연산 능력이 필요합니다.
3. 세 가지 라우팅 전략 실전 — 논리, 시맨틱, 메타데이터
라우팅 계층은 쿼리가 어느 경로를 이용해야 하는지 어떻게 판단할까요? 대표적으로 세 가지 방식이 있으며 각각 알맞은 상황이 다릅니다.
3.1 논리 라우팅: LLM을 디스패처로 활용
가장 직접적인 발상은 LLM이 쿼리 의도를 분석한 뒤 데이터 소스를 선택하게 하는 것입니다.
LangChain에서는 간단하게 구현할 수 있습니다.
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_deepseek import ChatDeepSeek
# 라우팅 프롬프트
system_prompt = """당신은 프로그래밍 쿼리 라우팅 전문가입니다.
사용자 질문에 관련된 프로그래밍 언어에 따라 알맞은 데이터 소스로 라우팅하세요.
- Python 관련 질문 → python_docs
- JavaScript 관련 질문 → js_docs
- Go 관련 질문 → golang_docs
- 판단할 수 없음 → general_docs
데이터 소스 이름만 반환하고 다른 내용은 포함하지 마세요."""
# 라우팅 체인 구성
prompt = ChatPromptTemplate.from_messages([
("system", system_prompt),
("human", "{query}")
])
llm = ChatDeepSeek(model="deepseek-chat", temperature=0)
router_chain = prompt | llm | StrOutputParser()
# 라우팅 실행
query = "Python에서 비동기 크롤러를 어떻게 구현하나요?"
datasource = router_chain.invoke({"query": query})
print(f"라우팅 결과: {datasource}") # 출력: python_docs
논리 라우팅의 장점은 유연성입니다. LLM은 “Python과 Go의 동시성 모델 비교”처럼 여러 언어에 걸친 복잡한 쿼리 의도를 이해할 수 있습니다. LLM이 여러 데이터 소스를 반환하게 한 뒤 Ensemble 검색을 수행할 수도 있습니다.
단점도 분명합니다. 느립니다. 라우팅할 때마다 LLM API를 한 번 호출하므로 0.5~1초의 지연 시간이 추가됩니다. API 비용도 누적됩니다.
3.2 시맨틱 라우팅: LLM 호출을 벡터 매칭으로 대체
속도가 중요하다면 Semantic Router가 더 나은 선택입니다.
원리는 ‘모호한 if/else’와 비슷합니다. 몇 가지 라우팅 규칙을 미리 정의하고 각 규칙에 예시 질문을 넣은 다음, 사용자 쿼리와 예시 간 벡터 유사도를 비교합니다. 가장 잘 일치하는 규칙이 쿼리가 이용할 경로입니다.
semantic-router 라이브러리로 구현해 보겠습니다.
from semantic_router import Route, RouteLayer
from semantic_router.encoders import OpenAIEncoder
# 라우팅 규칙 정의
python_route = Route(
name="python_docs",
utterances=[
"Python에서 파일을 읽는 방법",
"Python 데코레이터 사용법",
"Python으로 비동기 프로그래밍을 구현하는 방법",
"Python 리스트 컴프리헨션 문법",
]
)
js_route = Route(
name="js_docs",
utterances=[
"JavaScript 비동기 콜백 처리 방법",
"JS에서 DOM을 조작하는 방법",
"Node.js 이벤트 루프 메커니즘",
"JS Promise와 async/await의 차이",
]
)
# 라우팅 계층 생성
route_layer = RouteLayer(
encoder=OpenAIEncoder(),
routes=[python_route, js_route]
)
# 라우팅 실행(LLM 호출 불필요)
query = "Python 제너레이터는 어떻게 사용하나요?"
result = route_layer(query)
print(f"라우팅 결과: {result.name}") # 출력: python_docs
시맨틱 라우팅의 응답 속도는 LLM 라우팅보다 3~5배 빠릅니다. 실측 결과 OpenAI Embedding API의 응답 시간은 약 100ms지만 LLM 호출에는 500ms 이상이 필요했습니다.
하지만 한계도 있습니다. 라우팅 규칙을 미리 정의해야 합니다. 사용자 쿼리 유형이 정의된 범위를 벗어나면 라우팅이 실패하고 None을 반환합니다. 따라서 쿼리 유형이 비교적 고정된 비즈니스에 적합합니다.
3.3 메타데이터 라우팅: 구조화 필드 기반 필터링
지식 베이스에 문서 분류, 언어 태그, 타임스탬프처럼 풍부한 메타데이터가 있다면 SelfQueryRetriever로 정밀 필터링을 구현할 수 있습니다.
from langchain.retrievers.self_query.base import SelfQueryRetriever
from langchain.chains.query_constructor.base import AttributeInfo
from langchain_openai import ChatOpenAI
# 메타데이터 필드 정의
metadata_field_info = [
AttributeInfo(
name="category",
description="문서 분류: tutorial, api, guide, troubleshooting",
type="string"
),
AttributeInfo(
name="language",
description="프로그래밍 언어: python, javascript, golang",
type="string"
),
AttributeInfo(
name="date",
description="문서 게시 날짜",
type="date"
)
]
# 검색기 생성
llm = ChatOpenAI(model="gpt-4", temperature=0)
retriever = SelfQueryRetriever.from_llm(
llm=llm,
vectorstore=vectorstore,
document_contents="프로그래밍 기술 문서",
metadata_field_info=metadata_field_info,
verbose=True
)
# 쿼리를 메타데이터 필터로 자동 변환
query = "최신 Python 튜토리얼 문서"
docs = retriever.invoke(query)
# 내부에서 필터 조건 자동 생성:
# category == "tutorial" AND language == "python"
# date 기준 내림차순 정렬
메타데이터 라우팅의 장점은 정밀성입니다. LLM이 자연어 쿼리를 구조화된 필터 조건으로 바꾼 뒤 벡터 검색을 수행합니다. 단점은 메타데이터 품질에 대한 요구가 높다는 점입니다. 문서에 분류나 언어 태그가 없다면 이 방식을 사용할 수 없습니다.
4. 다중 벡터 저장소 협업 실전 — EnsembleRetriever 심층 분석
앞서 설명한 라우팅 전략은 ‘어느 저장소를 검색할지’라는 문제를 해결합니다. 하지만 “Python과 JavaScript의 비동기 프로그래밍 방식 비교”처럼 여러 비즈니스 상황에 걸친 쿼리도 있습니다. 이때는 여러 저장소를 동시에 검색한 뒤 결과를 병합해야 합니다.
EnsembleRetriever의 핵심은 RRF(Reciprocal Rank Fusion) 알고리즘입니다.
RRF 알고리즘 원리
RRF 공식은 간단합니다.
RRF(d) = Σ 1/(k + rank(d))
여기서 각 기호의 의미는 다음과 같습니다.
d는 특정 문서입니다.rank(d)는 해당 문서가 특정 검색기에서 차지한 순위이며 1부터 시작합니다.k는 평활화 매개변수로 대표적인 값은 60입니다.
예를 들어 두 검색기가 같은 문서에 각각 다음과 같은 순위를 매겼다고 가정하겠습니다.
- 검색기 A: 문서 X가 2위 → 기여 점수 1/(60+2) = 0.0156
- 검색기 B: 문서 X가 5위 → 기여 점수 1/(60+5) = 0.0154
- 총점 = 0.0156 + 0.0154 = 0.031
모든 문서의 총점을 같은 방식으로 계산한 뒤 총점 순으로 정렬합니다.
RRF가 단순 가중 평균보다 좋은 이유는 무엇일까요? 원본 유사도 점수 대신 순위 위치를 고려하기 때문입니다. 검색기마다 점수 범위가 크게 다를 수 있습니다. 예를 들어 벡터 검색은 0~1 범위의 코사인 유사도를 반환하지만 BM25는 다른 점수 체계를 사용합니다. 이를 그대로 가중 평균하면 편향이 생깁니다. RRF는 이 문제를 우회합니다.
밀집 검색과 희소 검색의 혼합
실제 프로젝트에서는 밀집 검색인 벡터 검색과 희소 검색인 BM25를 자주 결합합니다.
벡터 검색은 의미 매칭에 강하고 BM25는 키워드 매칭에 강합니다. 둘은 서로 보완하므로 함께 사용하면 더 좋은 결과를 얻을 수 있습니다.
from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever
from langchain_community.vectorstores import Chroma
# 희소 검색: BM25
bm25_retriever = BM25Retriever.from_texts(
documents_text_list,
k=5
)
# 밀집 검색: 벡터
vector_retriever = Chroma.from_texts(
documents_text_list,
embedding=OpenAIEmbeddings()
).as_retriever(search_kwargs={"k": 5})
# 혼합 검색
ensemble = EnsembleRetriever(
retrievers=[bm25_retriever, vector_retriever],
weights=[0.4, 0.6], # BM25 가중치 0.4, 벡터 가중치 0.6
c=60
)
# 검색 실행
query = "LangChain Agent 도구 호출"
docs = ensemble.invoke(query)
여기에는 가중치 배분이라는 매개변수 조정 요령이 있습니다.
저희 경험은 다음과 같습니다.
- 의미 이해형 쿼리(예: “지능형 질의응답 구현 방법”): 벡터 가중치를 높게 설정(0.6~0.7)
- 키워드 정밀 매칭형 쿼리(예: “Python 3.11 새로운 기능”): BM25 가중치를 높게 설정(0.5~0.6)
- 일반 상황: 균형 잡힌 가중치(0.5/0.5)
검색 품질 평가
EnsembleRetriever가 실제로 효과가 있는지 어떻게 알 수 있을까요? TruLens로 평가할 수 있습니다.
from trulens_eval import Feedback, TruChain
from trulens_eval.feedback.provider.openai import OpenAI
provider = OpenAI()
# 평가 지표 정의
relevance_feedback = Feedback(
provider.relevance,
name="Answer Relevance"
).on_input_output()
context_relevance_feedback = Feedback(
provider.context_relevance,
name="Context Relevance"
).on_input().on(context)
# 평가 체인 등록
tru_recorder = TruChain(
chain=ensemble_retriever_chain,
feedbacks=[relevance_feedback, context_relevance_feedback],
feedback_mode="with_chain"
)
# 평가 실행
with tru_recorder as recording:
response = ensemble_retriever_chain.invoke({"query": test_query})
TruLens는 ‘답변 관련성’과 ‘컨텍스트 관련성’이라는 두 지표를 제공합니다. 실측 결과 단일 벡터 검색의 평균 컨텍스트 관련성은 0.72였지만 Ensemble 적용 후 0.91로 높아졌습니다.
5. 성능 비교와 모범 사례
지금까지 설명한 이론이 실제로는 얼마나 효과적일까요?
동일한 테스트 세트에서 네 가지 방식을 비교했습니다. 테스트 세트는 네 가지 비즈니스 상황에 걸친 500개의 쿼리로 구성했습니다.
| 라우팅 전략 | 평균 응답 시간 | 검색 정확도 | 적합한 상황 |
|---|---|---|---|
| 라우팅 없음(단일 저장소) | 1.2s | 72% | 단일 비즈니스 상황 |
| 논리 라우팅(LLM) | 1.8s | 85% | 여러 비즈니스 영역, 복잡한 의도 |
| 시맨틱 라우팅 | 0.5s | 88% | 빠른 응답, 고정된 쿼리 유형 |
| Ensemble RRF | 1.0s | 92% | 혼합 상황, 다중 소스 검색 |
주요 데이터 해석
시맨틱 라우팅은 논리 라우팅보다 3~4배 빠릅니다. 시맨틱 라우팅은 Embedding API만 호출하지만 약 100ms, 논리 라우팅은 LLM을 호출하므로 약 800ms가 걸리기 때문입니다. 쿼리 유형이 비교적 고정되어 있다면 시맨틱 라우팅이 첫 번째 선택입니다.
Ensemble RRF의 정확도가 가장 높습니다. 여러 검색기의 협업은 서로 다른 의미 공간을 포괄하고, RRF 순위 결정은 각 검색기의 장점을 균형 있게 반영합니다. 대신 응답 시간이 단일 검색기보다 조금 더 깁니다. 여러 검색기를 병렬로 조회하려면 더 많은 리소스가 필요하기 때문입니다.
라우팅 없는 방식이 가장 느립니다. 직관에 어긋나 보이지만 이유를 생각하면 이해할 수 있습니다. 단일 저장소 검색은 관련 없는 문서를 많이 반환하므로 LLM이 더 많은 노이즈에서 답을 추출해야 하고, 오히려 생성 속도가 느려집니다.
모범 사례 요약
저희가 시행착오를 거치며 얻은 몇 가지 권장 사항을 소개합니다.
1. 단순한 상황에는 시맨틱 라우팅을 사용하세요
비즈니스 상황이 명확하고, 예를 들어 Python 문서 질의응답만 제공하며, 쿼리 유형도 질문 답변, 코드 예제, 문제 해결의 세 가지로 고정되어 있다면 Semantic Router를 바로 사용하세요. 응답이 빠르고 비용이 낮습니다.
2. 복잡한 추론에는 논리 라우팅을 사용하세요
쿼리가 멀티홉 추론이나 여러 영역 간 비교를 포함할 때는 LLM의 의미 이해 능력이 더 뛰어납니다. 예를 들어 “Python과 Go의 동시성 모델은 어떻게 다른가요?”라는 쿼리라면 LLM은 두 저장소를 모두 검색해야 한다고 판단할 수 있습니다.
3. 다중 소스 검색에는 Ensemble을 사용하세요
사용자 의도가 확실하지 않다면 여러 저장소를 동시에 검색하고 RRF로 병합할 수 있습니다. 단일 라우팅보다 안정적이지만 검색기 수를 제어해야 합니다. 3~4개가 상한이며 더 늘리면 응답 시간이 급격히 길어집니다.
4. 메타데이터가 풍부한 저장소에는 SelfQuery를 사용하세요
문서에 분류, 언어, 시간, 작성자 같은 표준화된 메타데이터가 있다면 SelfQueryRetriever는 매우 유용합니다. 쿼리 의도를 자동으로 분석해 정밀한 필터 조건을 생성하므로 검색 노이즈를 줄일 수 있습니다.
5. 전략을 동적으로 조정하세요
저희가 프로덕션에서 운영하는 방식은 혼합형입니다. 먼저 시맨틱 라우팅으로 빠르게 분기하고(100ms), 일치도가 임계값인 0.6보다 낮으면 논리 라우팅으로 폴백해 정확하게 판단합니다. 따라서 대부분의 단순 쿼리는 빠르게 응답하고 일부 복잡한 쿼리도 올바르게 처리할 수 있습니다.
요약
RAG 시스템을 구축할 때 많은 사람이 Embedding 모델 조정과 프롬프트 엔지니어링에 집중하지만, 쿼리 라우팅이라는 핵심 단계를 놓칩니다.
이 쿼리가 빠른 경로와 심층 경로 중 어디로 가야 하는가? 이 간단한 결정을 올바른 방법으로 내리면 응답 시간을 1.8초에서 0.5초로 줄이고 검색 정확도를 72%에서 92%로 높일 수 있습니다.
라우팅 전략을 선택할 때 다음 원칙을 기억하세요.
- 단순 쿼리에는 시맨틱 라우팅: 빠르고 비용이 낮습니다.
- 복잡한 추론에는 논리 라우팅: LLM의 의미 이해가 더 정확합니다.
- 다중 소스 검색에는 Ensemble: RRF 병합으로 포괄적인 답변을 보장합니다.
- 메타데이터가 풍부하면 SelfQuery: 정밀 필터링으로 노이즈를 줄입니다.
현재 저희 팀의 프로덕션 환경에서는 시맨틱 라우팅으로 첫 번째 분기를 수행하고, 신뢰도가 낮은 쿼리는 논리 라우팅으로 폴백하며, 다중 소스 상황에서는 Ensemble 검색을 자동으로 실행하는 혼합 방식을 사용합니다. 이 조합은 고객 지원 봇의 문제 해결률을 68%에서 89%로 높였습니다.
전체 예제 코드는 GitHub 저장소에 있습니다. 직접 복제해서 실행해 보세요. 궁금한 점이 있다면 댓글로 알려 주세요.
RAG 쿼리 라우팅 시스템 구현
시맨틱 라우팅, 논리 라우팅, 다중 소스 검색을 지원하는 지능형 RAG 라우팅 아키텍처 구축
⏱️ Estimated time: 45 min
- 1
Step 1: 라우팅 전략 선택
비즈니스 상황에 맞는 라우팅 방식을 선택합니다:
• 쿼리 유형이 고정됨 → 시맨틱 라우팅(응답이 빠름, 약 100ms)
• 여러 비즈니스 영역 → 논리 라우팅(정확도가 높음, 약 800ms)
• 혼합 상황 → Ensemble RRF(최고 정확도 92%) - 2
Step 2: 시맨틱 라우팅 구현
semantic-router 라이브러리로 빠르게 구성합니다:
```python
from semantic_router import Route, RouteLayer
from semantic_router.encoders import OpenAIEncoder
python_route = Route(
name="python_docs",
utterances=["Python 비동기 프로그래밍", "Python 데코레이터"]
)
route_layer = RouteLayer(
encoder=OpenAIEncoder(),
routes=[python_route]
)
``` - 3
Step 3: EnsembleRetriever 설정
여러 검색기의 결과를 병합합니다:
```python
from langchain.retrievers import EnsembleRetriever
ensemble = EnsembleRetriever(
retrievers=[bm25_retriever, vector_retriever],
weights=[0.4, 0.6],
c=60
)
```
매개변수 c=60은 경험값이며, 가중치는 쿼리 유형에 따라 조정합니다. - 4
Step 4: 검색 품질 평가
TruLens 평가 지표를 사용합니다:
• 답변 관련성(Answer Relevance)
• 컨텍스트 관련성(Context Relevance)
• 최적화 전후의 지표 변화 비교
실측 결과 Ensemble 적용 후 관련성이 0.72에서 0.91로 높아졌습니다.
FAQ
시맨틱 라우팅과 논리 라우팅 중 어느 것이 더 좋은가요?
EnsembleRetriever의 RRF 매개변수 c는 어떻게 설정하나요?
라우팅 실패는 어떻게 처리하나요?
• 기본 라우트 설정: 시맨틱 라우팅의 일치도가 임계값보다 낮으면 기본 검색기를 사용
• 논리 라우팅으로 폴백: 시맨틱 라우팅 실패 후 LLM을 호출해 정확하게 판단
• 다중 소스 검색: 확실하지 않을 때 Ensemble로 여러 저장소를 동시에 검색하고 RRF로 병합
SelfQueryRetriever를 사용하려면 메타데이터가 어떤 조건을 충족해야 하나요?
여러 검색기를 병렬로 조회하면 너무 느리지 않나요?
라우팅 전략은 RAG 시스템 성능에 얼마나 큰 영향을 주나요?
3분 읽기 · 게시일: 2026년 5월 13일 · 수정일: 2026년 9월 8일



댓글
GitHub로 로그인하여 댓글을 남기세요