RAG 쿼리 라우팅 실전: 여러 벡터 DB 협업과 지능형 검색 분배

사용자가 “공급업체 파업이 주가에 미친 영향은 무엇인가요?”라고 물었는데, 시스템은 조각난 뉴스 일부를 반환하고 심지어 경쟁사 관련 뉴스까지 두 건 섞어 냈습니다. 고객은 이렇게 따집니다. “왜 당신들 AI는 이렇게 멍청하죠?” 같은 RAG 시스템이 “2023 Q3 화동 지역 매출”을 처리할 때는 정확한 답을 즉시 내지만, 멀티홉 추론 문제 앞에서는 완전히 무너집니다.
문제의 뿌리는 분명합니다. 앞의 질문은 단순 사실 조회라 벡터 검색만으로 충분합니다. 뒤의 질문은 공급업체, 파업 사건, 주가 변동이라는 여러 관계를 이어야 하는 멀티홉 추론이고, 그 관계는 지식 그래프 안에 숨어 있습니다. 모든 쿼리에 같은 검색 전략을 쓰는 것은 하나의 열쇠로 모든 문을 열려는 것과 비슷합니다.
그래서 시스템이 질문의 특성에 따라 검색 경로를 자동 선택하게 하는 “지능형 라우터”가 필요합니다. 이 글에서는 세 가지 방식을 비교합니다. 논리 라우팅(LLM이 의도 분석), 의미 라우팅(임베딩 공간의 퍼지 매칭), EnsembleRetriever(RRF 알고리즘 기반 융합)입니다. “최고”의 방식은 없습니다. 오직 “가장 적합한” 상황만 있습니다.
1장: 왜 쿼리 라우팅이 필요한가? - “단일 벡터 DB”에서 “다중 소스 협업”으로
예전에 한 기업의 지식 베이스 시스템을 도운 적이 있습니다. 그 회사에는 세 가지 데이터 소스가 있었습니다. 재무 데이터베이스(MySQL), 기술 문서 저장소(벡터 DB), 인사 관계 그래프(Neo4j)였습니다. 처음 제 설계는 단순했습니다. 모든 데이터를 하나의 벡터 DB에 밀어 넣는 것이었습니다.
결과는 어땠을까요? “화동 지역 매출” 같은 단순 질문에는 시스템이 재무 보고서 표에서 정확한 답을 찾아냈습니다. 하지만 “공급업체 파업이 어떤 제품 라인에 영향을 주었나요?”라고 묻자, 시스템은 뒤죽박죽인 뉴스 조각을 반환했고 사용자는 고개를 저었습니다.
나중에야 알았습니다. 모든 쿼리가 벡터 검색에 적합한 것은 아닙니다. 어떤 질문은 SQL 조회가 더 빠르고 정확합니다. 어떤 질문은 지식 그래프가 관계를 이어 줘야 합니다. 또 어떤 질문은 Web 검색으로 최신 정보를 가져와야 합니다. 같은 검색 전략을 무조건 쓰면 “역량 부족”이나 “과한 설계”로 이어질 수밖에 없습니다.
1.1 단일 벡터 DB 검색의 병목: 두 가지 실제 상황 비교
상황 A: 단순 사실 조회(벡터 검색만으로 충분)
사용자 질문: “2023 Q3 화동 지역 매출은 얼마인가요?”
시스템 동작: 벡터 검색으로 재무 보고서 표를 찾고, 바로 “화동 지역 Q3 매출은 1.2억 위안입니다”라고 답합니다. 전체 과정은 약 300ms이고 사용자는 만족합니다.
여기서 억지로 지식 그래프 추론 모듈을 호출하면 어떻게 될까요? GPU 연산만 낭비하고 500ms 지연이 추가됩니다. 로켓으로 택배를 보내는 셈입니다. 도착은 하지만 굳이 그럴 필요는 없습니다.
상황 B: 복잡한 추론 쿼리(멀티홉 검색 필요)
사용자 질문: “공급업체 파업이 주가에 미친 영향은 무엇인가요?”
시스템 동작: 벡터 검색은 “XX 회사 주가 5% 하락”, “공급업체 파업 사건 보도” 같은 조각난 뉴스를 가져옵니다. 하지만 LLM에는 중간 논리 사슬이 부족합니다. 어느 공급업체인가? 누구에게 납품하는가? 파업은 얼마나 지속됐는가? 주가는 얼마나 떨어졌는가? 이런 정보가 서로 다른 문서에 흩어져 있으면 LLM은 답을 지어내기 쉽습니다.
올바른 방식은 지식 그래프로 “공급업체 -> 파업 사건 -> 계약 관계 -> 주가 변동”을 연결해 명확한 논리 사슬을 만드는 것입니다. 그런데 문제가 하나 남습니다. 시스템이 “이 질문에는 지식 그래프를 써야 한다”고 어떻게 자동 판단하게 할까요?
이것이 쿼리 라우팅이 해결하려는 핵심 문제입니다.
1.2 쿼리 특성의 네 가지 차원 분석
저는 프로젝트에서 쿼리의 네 가지 차원에 따라 검색 전략을 선택하는 간단한 판단 프레임워크를 정리했습니다.
| 차원 | 특성 | 적합한 검색 전략 |
|---|---|---|
| 컨텍스트 의존도 | 낮음(사실 조회) vs 높음(멀티홉 추론) | 벡터 검색 vs 지식 그래프 |
| 추론 홉 수 | 단일 홉 vs 멀티홉 | 직접 검색 vs Agent 조율 |
| 데이터 유형 | 구조화(표) vs 비구조화(문서) | SQL 조회 vs 벡터 검색 |
| 최신성 | 실시간 정보 vs 정적 지식 | Web 검색 vs 로컬 지식 베이스 |
예를 들어 “화동 지역 매출”은 단일 홉, 구조화, 정적 데이터이므로 SQL 조회가 가장 빠릅니다. 반면 “공급업체 파업이 주가에 미친 영향”은 컨텍스트 의존도가 높고, 멀티홉이며, 비구조화 데이터라 지식 그래프가 더 적합합니다.
여기까지 들으면 이렇게 생각할 수 있습니다. “그럼 모든 쿼리마다 세 개 저장소를 동시에 조회하고 결과를 합치면 안 되나요?” 가능합니다. 하지만 비용이 터집니다. 쿼리마다 세 개 검색기를 호출하면 지연이 200-500ms 늘고 LLM 호출 비용도 배로 뜁니다. 사장님이 비용에 전혀 신경 쓰지 않는다면 모르겠습니다.
더 똑똑한 방식은 시스템이 “상황을 보고” 쿼리 특성에 따라 검색 경로를 동적으로 고르게 하는 것입니다. 이것이 쿼리 라우터의 가치입니다. 정확도, 효율, 비용 사이에서 균형점을 찾는 일입니다.
2장: 논리 라우팅 - LLM이 의도를 분석하고 데이터 소스를 선택한다
논리 라우팅은 가장 직관적인 방식입니다. LLM에게 “선택지 목록”을 주고 사용자의 질문을 분석하게 한 뒤, 목록에서 가장 잘 맞는 데이터 소스를 고르게 합니다.
병원 접수와 비슷합니다. 접수원이 “어디가 불편하세요?”라고 묻고, 당신이 “배가 아파요”라고 하면 소화기내과로 보내고, “머리가 아파요”라고 하면 신경과로 보냅니다. 논리 라우팅의 LLM은 이 접수원처럼 증상(쿼리)에 따라 가장 알맞은 진료과(데이터 소스)를 선택합니다.
구현 방식: LangChain + Structured Output
먼저 전체 코드를 보여 드린 뒤, 실제로 겪은 함정을 이야기하겠습니다.
from langchain_core.prompts import ChatPromptTemplate
from langchain_deepseek import ChatDeepSeek
from pydantic import BaseModel, Field
from typing import Literal
# 데이터 소스 enum 정의(LLM이 모호한 값을 반환하지 않도록 제한)
class DataSource(BaseModel):
"""데이터 소스 선택 결과"""
source: Literal["finance_db", "tech_docs", "knowledge_graph", "web_search", "general_search"] = Field(
description="선택된 데이터 소스"
)
# 라우팅 프롬프트 템플릿 설정
system_prompt = """
당신은 전문 쿼리 라우팅 전문가입니다. 사용자 질문의 내용을 기준으로 알맞은 데이터 소스로 라우팅하세요.
- 질문이 재무 데이터나 매출 데이터와 관련되면 "finance_db"(관계형 데이터베이스)를 반환합니다.
- 질문이 기술 문서나 제품 매뉴얼과 관련되면 "tech_docs"(벡터 데이터베이스)를 반환합니다.
- 질문이 인물 관계나 조직 구조와 관련되면 "knowledge_graph"(그래프 데이터베이스)를 반환합니다.
- 최신 실시간 정보가 필요하면 "web_search"를 반환합니다.
- 명확하게 판단할 수 없으면 "general_search"를 반환합니다.
데이터 소스 이름만 반환하고 다른 내용은 포함하지 마세요.
"""
prompt = ChatPromptTemplate.from_messages([
("system", system_prompt),
("human", "{question}"),
])
# DeepSeek 모델 사용(저렴하고 쓸 만함)
llm = ChatDeepSeek(model="deepseek-chat", temperature=0.1)
structured_llm = llm.with_structured_output(DataSource)
# 라우팅 체인 구성
route_chain = prompt | structured_llm
# 라우팅 테스트
query1 = "2023 Q3 화동 지역의 총매출은 얼마인가요?"
result1 = route_chain.invoke({"question": query1})
print(result1.source) # 출력: finance_db
query2 = "공급업체 파업이 주가에 미친 영향은 무엇인가요?"
result2 = route_chain.invoke({"question": query2})
print(result2.source) # 출력: knowledge_graph
이 코드에는 중요한 세부 사항이 하나 있습니다. temperature=0.1입니다. 예전에 이 값을 0.7로 설정했다가 같은 쿼리가 어떤 때는 지식 그래프로, 어떤 때는 Web 검색으로 라우팅되는 문제를 겪었습니다. 그때 깨달았습니다. 라우터에는 안정성이 필요합니다. 무작위성이 끼어들면 안 됩니다.
또 하나의 세부 사항은 Pydantic의 DataSource enum입니다. 처음에는 LLM이 문자열을 직접 반환하게 했는데, “finance_db를 조회하는 것이 좋겠습니다”라거나 “finance_db 또는 general_search를 조회할 수 있을 것 같습니다” 같은 답을 냈습니다. 이런 모호성은 후속 처리를 복잡하게 만듭니다. Pydantic으로 enum 값을 강제하면 깔끔합니다.
장단점 비교
| 차원 | 장점 | 단점 |
|---|---|---|
| 정확도 | LLM이 의도를 깊게 이해해 복잡한 쿼리를 처리할 수 있음 | Prompt 품질에 의존하며, 데이터 소스 설명이 불명확하면 오판 가능 |
| 응답 속도 | LLM 호출이 필요해 약 500-800ms | 의미 라우팅보다 10배 느림 |
| 비용 | 라우팅마다 LLM 호출 필요, 약 $0.0001/회 | 데이터 소스가 많으면 비용 누적 |
| 적합한 상황 | 데이터 소스 유형이 명확하고 수가 <= 5 | 데이터 소스가 너무 많으면 Prompt가 길어짐 |
프로젝트에서 테스트해 보니 논리 라우팅은 데이터 소스가 <= 5일 때 가장 효과가 좋았습니다. 5개를 넘으면 Prompt가 길어지고 LLM도 혼동하기 쉽습니다. 예를 들어 데이터 소스가 10개라면 의미 라우팅이나 계층형 논리 라우팅(먼저 대분류, 이후 세분류)을 고려하는 편이 좋습니다.
3장: 의미 라우팅 - 임베딩 공간 기반 “Fuzzy If/Else”
의미 라우팅은 더 빠릅니다. 논리 라우팅은 LLM이 잠깐 “생각”해야 해서 500-800ms가 걸리지만, 의미 라우팅은 임베딩 유사도만 계산하므로 50ms면 충분합니다. 10배 이상 빠릅니다.
원리는 “퍼지 매칭”과 비슷합니다. “매출 조회”, “재무 보고서 데이터”, “매출 상황은 어떤가요” 같은 예시 쿼리(utterances)를 미리 정의하고, 이 쿼리들을 “재무 조회”라는 의도에 연결합니다. 사용자가 질문하면 시스템은 사용자 질문과 예시 쿼리의 의미 유사도를 계산하고, 임계값을 넘으면 해당 라우트를 실행합니다.
예를 들어 누군가가 “오늘 저녁 뭐 먹을래?”라고 묻고, 당신이 “아무거나요. 너무 맵지만 않으면 돼요”라고 답했다고 해 봅시다. 상대의 머릿속에는 “너무 맵지 않음” ≈ “토마토 달걀볶음”, “생선찜”, “동과탕” 같은 퍼지 매칭 표가 있습니다. 의미 라우팅도 이런 퍼지 매칭 과정입니다.
구현 방식: semantic-router 라이브러리 + 사전 정의 Utterances
코드는 논리 라우팅보다 더 간단합니다.
from semantic_router import RouteLayer, Route
from semantic_router.encoders import HuggingFaceEncoder
# 라우팅 규칙 정의(의미 유사도 임계값)
routes = [
Route(
name="finance_query",
utterances=[
"매출 조회",
"재무 보고서 데이터",
"매출 상황은 어떤가요",
"이익 분석",
],
),
Route(
name="tech_support",
utterances=[
"제품 사용 방법",
"기술 문서는 어디에 있나요",
"장애 해결 방법",
"기능 설명",
],
),
Route(
name="graph_query",
utterances=[
"누가 누구와 협력 관계인가요",
"조직 구조 관계",
"상하류 공급망",
"인물 관계 그래프",
],
),
]
# RouteLayer 생성(무료 HuggingFace 임베딩 모델 사용)
encoder = HuggingFaceEncoder()
route_layer = RouteLayer(encoder=encoder, routes=routes)
# 라우팅 테스트(LLM 호출 없음, 응답 약 50ms)
query1 = "공급업체 파업이 주가에 미친 영향은 무엇인가요?"
route1 = route_layer(query1)
print(route1.name) # 출력: graph_query
query2 = "2023 Q3 화동 지역 매출은 얼마인가요?"
route2 = route_layer(query2)
print(route2.name) # 출력: finance_query
이 코드의 핵심은 utterances입니다. 각 의도마다 4-10개의 예시 쿼리를 미리 정의해야 하고, 시스템은 사용자 질문과 이 예시들의 의미 유사도를 계산합니다. 기본 임계값은 0.85입니다. 즉 유사도가 85%를 넘을 때만 라우팅이 실행됩니다.
예전에 테스트해 보니 utterances를 너무 적게 정의하면(예: 2개) 재현율이 낮았습니다. 너무 많이 정의하면(20개 초과) 계산 오버헤드가 커졌습니다. 의도마다 4-10개의 예시를 두고 자주 쓰는 표현을 포괄하는 것을 권장합니다.
또 하나의 장점은 HuggingFaceEncoder입니다. 무료 로컬 임베딩 모델이므로 OpenAI API를 호출할 필요가 없고 비용도 없습니다. 쿼리량이 많다면 논리 라우팅의 회당 $0.0001은 작아 보이지만, 하루 10만 회면 $10, 한 달이면 $300입니다. 의미 라우팅은 완전히 무료입니다.
장단점 비교
| 차원 | 장점 | 단점 |
|---|---|---|
| 응답 속도 | 약 50ms(LLM 호출 없음) | utterances를 미리 정의해야 함 |
| 비용 | 무료(로컬 임베딩 모델) | 새로운 의도를 포괄하려면 utterances 업데이트 필요 |
| 정확도 | 의미 유사도가 정확하고 자주 나오는 의도에서 성능이 좋음 | 복잡한 의도는 오판할 수 있음 |
| 적합한 상황 | 의도 분류, 다중 스킬 Agent, 의도 수 <= 20 | 의도가 너무 많으면 utterances 유지보수 비용 증가 |
의미 라우팅에는 한계가 있습니다. “논리 추론”이 필요한 의도 판단은 처리하기 어렵습니다. 예를 들어 “쿼리가 재무 데이터와 관련되고 최신성이 높다면 실시간 데이터베이스를 우선 조회한다” 같은 판단은 여전히 LLM이 맡는 편이 낫습니다. 그래서 실제 프로젝트에서는 의미 라우팅을 의도 분류(재무/기술/관계 조회)에 쓰고, 복잡한 조건 판단은 논리 라우팅으로 처리합니다.
4장: EnsembleRetriever - RRF 알고리즘으로 여러 검색기를 합치기
앞의 두 방식은 “검색기 하나를 선택”합니다. EnsembleRetriever는 “여러 검색기의 결과를 합칩니다.”
가장 전형적인 상황은 BM25(키워드 매칭) + 벡터 검색(의미 매칭)입니다. 사용자가 “2023 Q3 매출”이라고 물으면 BM25는 “매출”이라는 키워드를 정확히 잡지만, “영업수익” 같은 동의어를 놓칠 수 있습니다. 벡터 검색은 “영업수익”과 “매출”이 같은 의미라는 점을 이해하지만, 관련 없는 재무 문서까지 많이 가져올 수 있습니다.
둘을 결합하면 재현율과 정확도를 모두 높일 수 있습니다. 이것이 EnsembleRetriever의 가치입니다.
구현 방식: LangChain EnsembleRetriever + RRF 알고리즘
코드 구현은 의외로 간단합니다.
from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
# BM25 검색기 생성(키워드 매칭)
bm25_retriever = BM25Retriever.from_texts(
["재무 보고서 2023 Q3", "화동 지역 매출 데이터", "공급업체 명단"],
k=2,
)
# 벡터 검색기 생성(의미 매칭)
vectorstore = Chroma.from_texts(
["재무 보고서 2023 Q3", "화동 지역 매출 데이터", "공급업체 명단"],
embedding=OpenAIEmbeddings(),
)
vector_retriever = vectorstore.as_retriever(k=2)
# EnsembleRetriever 조합(RRF 알고리즘)
ensemble_retriever = EnsembleRetriever(
retrievers=[bm25_retriever, vector_retriever],
weights=[0.4, 0.6], # BM25 가중치 0.4, 벡터 가중치 0.6
)
# 검색 테스트
query = "2023 Q3 화동 지역 매출"
docs = ensemble_retriever.invoke(query)
print(docs) # 출력: BM25와 벡터 검색의 융합 결과(RRF 점수순 정렬)
핵심은 RRF(Reciprocal Rank Fusion) 알고리즘입니다. 이름은 거창하지만 원리는 단순합니다.
어떤 문서가 BM25에서는 1위, 벡터 검색에서는 3위라고 가정해 봅시다. RRF 계산 방식은 다음과 같습니다.
BM25 1위 → 1/(60+1) = 0.0164
벡터 검색 3위 → 1/(60+3) = 0.0159
총점 = 0.0164 + 0.0159 = 0.0323
k=60은 경험값이며 프로젝트에 맞게 조정할 수 있습니다. k가 클수록 순위 차이의 영향이 작아지고, k가 작을수록 상위 문서의 이점이 커집니다.
RRF는 왜 효과적인가?
RRF의 영리한 지점은 문서의 원래 점수에 의존하지 않는다는 데 있습니다. 서로 다른 검색기의 점수는 비교할 수 없기 때문입니다. RRF는 순위만 사용합니다. 그래서 BM25, 벡터 검색, 지식 그래프 검색, 심지어 Web 검색 결과까지 어떤 유형의 검색기도 합칠 수 있습니다.
프로젝트에서 테스트했을 때 순수 BM25 재현율은 70%, 순수 벡터 검색 재현율은 85%, EnsembleRetriever는 92%까지 올라갔습니다. 핵심 비용은 50ms 정도만 늘었습니다. 두 검색기를 병렬 호출했기 때문입니다.
장단점 비교
| 차원 | 장점 | 단점 |
|---|---|---|
| 정확도 | Lexical + Semantic 융합으로 재현율이 높음 | 서로 다른 유형의 데이터 소스로 라우팅할 수 없음 |
| 응답 속도 | 병렬 검색, 약 300ms | 단일 검색기보다 느림 |
| 비용 | 추가 LLM 호출 없음 | 여러 검색기를 병렬 호출하므로 연산 비용 증가 |
| 적합한 상황 | 하이브리드 검색 최적화, 같은 유형 검색기 합치기 | 데이터 소스 간 라우팅에는 부적합 |
EnsembleRetriever에도 한계가 있습니다. “같은 유형”의 검색 결과만 합칠 수 있습니다. 예를 들어 벡터 DB와 지식 그래프를 동시에 조회하고 싶다면 EnsembleRetriever만으로는 부족합니다. 이런 데이터 소스 간 라우팅 상황에서는 논리 라우팅이나 의미 라우팅이 필요합니다.
5장: 프로덕션 배포의 비용 최적화 전략
앞에서는 “시스템을 더 똑똑하게 만드는 방법”을 이야기했습니다. 이번 장에서는 “시스템을 더 저렴하게 운영하는 방법”을 이야기하겠습니다. 제가 겪은 가장 큰 함정은 비용 폭발이었습니다. 출시 첫 주 LLM 호출 비용이 $500까지 올라가서, 사장님이 저를 거의 잘라낼 뻔했습니다.
이후 저는 세 가지 전략을 배웠습니다. Semantic Caching(의미 캐싱), Tiered Retrieval(계층형 검색), Parallel Processing(병렬 처리)입니다. 비용은 주당 $50까지 내려갔고 정확도는 오히려 좋아졌습니다.
5.1 Semantic Caching(의미 캐싱)
가장 간단하면서도 효과적인 전략입니다. 원리는 자주 나오는 쿼리의 임베딩을 캐시하고, 유사도 > 0.95이면 LLM을 다시 호출하지 않고 캐시된 답변을 바로 반환하는 것입니다.
프로덕션 환경에서 테스트했을 때 캐시 적중률은 30-50%까지 나왔습니다. 캐시가 맞으면 응답 시간은 약 500ms에서 약 50ms로 줄었고, 사용자 경험도 눈에 띄게 좋아졌습니다.
from langchain.cache import InMemoryCache
from langchain.embeddings import CacheBackedEmbeddings
from langchain_openai import OpenAIEmbeddings
# 자주 나오는 쿼리의 임베딩 캐시
underlying_embeddings = OpenAIEmbeddings()
cached_embeddings = CacheBackedEmbeddings.from_bytes_store(
underlying_embeddings,
InMemoryCache(), # 프로덕션 환경에서는 Redis 권장
)
# 캐시 임베딩을 사용해 라우팅
# 유사도 > 0.95이면 캐시 답변을 바로 반환
실제 배포에서는 InMemoryCache를 쓰지 말고 Redis나 Memcached를 권장합니다. InMemoryCache는 재시작하면 사라집니다. 저는 캐시를 주기적으로 정리하기도 합니다. 예를 들어 30일 넘게 접근되지 않은 쿼리는 자동 만료시킵니다.
5.2 Tiered Retrieval(계층형 검색)
단순 쿼리에는 저렴한 모델을 쓰고, 복잡한 쿼리에는 비싼 모델을 씁니다. 가장 직관적인 비용 최적화 전략입니다.
from langchain_openai import ChatOpenAI
from langchain_anthropic import ChatAnthropic
# 단순 쿼리는 저렴한 모델 사용
simple_llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.1)
# 복잡한 쿼리는 비싼 모델 사용
complex_llm = ChatAnthropic(model="claude-opus-4-20250514", temperature=0.2)
# 라우팅 로직
if route_layer(query).name in ["finance_query", "tech_support"]:
# 단순 의도는 GPT-4o-mini 사용
response = simple_llm.invoke(query)
elif route_layer(query).name == "graph_query":
# 복잡한 추론은 Claude Opus 사용
response = complex_llm.invoke(query)
비용 차이는 매우 큽니다.
| 모델 | 단일 호출 비용 | 적합한 상황 |
|---|---|---|
| GPT-4o-mini | $0.00015/1K tokens | 단순 사실 조회 |
| Claude Opus 4 | $0.015/1K tokens | 복잡한 추론 쿼리 |
100배 차이입니다. 시스템의 80%가 단순 쿼리라면 비용을 원래의 20%까지 낮출 수 있습니다.
5.3 Parallel Processing(병렬 처리)
Hybrid routing(논리 라우팅 + EnsembleRetriever)은 200-500ms의 지연을 추가할 수 있습니다. 다행히 여러 검색기는 병렬 호출할 수 있으므로 실제 지연은 조금만 늘어납니다.
import asyncio
from langchain_community.retrievers import BM25Retriever
# BM25 + Vector 검색기를 병렬 호출
async def parallel_retrieval(query):
bm25_task = asyncio.create_task(bm25_retriever.invoke(query))
vector_task = asyncio.create_task(vector_retriever.invoke(query))
bm25_docs, vector_docs = await asyncio.gather(bm25_task, vector_task)
# RRF 융합
return ensemble_docs
실측 결과는 직렬 호출 600ms, 병렬 호출 320ms였습니다. Hybrid routing의 지연 오버헤드를 거의 상쇄한 셈입니다.
이 세 전략을 함께 적용하니 제 시스템의 비용은 주당 $500에서 $50으로 내려갔고, 응답 속도도 더 빨라졌습니다. 비용 최적화는 “품질을 깎는 일”이 아니라 “자원을 똑똑하게 배분하는 일”입니다.
6장: 방식 비교와 선택 가이드
여기까지 읽었다면 “내 프로젝트에는 어떤 방식을 써야 하지?”라는 질문이 떠오를 겁니다. 간단한 비교표와 의사결정 트리를 정리해 보겠습니다.
세 가지 방식의 핵심 비교
| 차원 | 논리 라우팅 | 의미 라우팅 | EnsembleRetriever |
|---|---|---|---|
| 핵심 원리 | LLM이 의도 분석 | 의미 유사도 매칭 | RRF 알고리즘 융합 |
| 응답 속도 | 약 500ms(LLM 호출) | 약 50ms(임베딩 계산) | 약 300ms(병렬 검색) |
| 비용 | 중간(매번 LLM) | 낮음(무료 임베딩) | 낮음(LLM 없음) |
| 정확도 | 높음(깊은 이해) | 중간(유사도 임계값) | 높음(Lexical+Semantic) |
| 적합한 상황 | 데이터 소스 유형이 명확함(<=5) | 의도 분류(<=20) | 같은 유형 검색기 합치기 |
| 기술 스택 | LangChain + Structured Output | semantic-router 라이브러리 | LangChain EnsembleRetriever |
선택 의사결정 트리
빠르게 판단할 수 있도록 간단한 의사결정 흐름을 그려 보겠습니다.
1단계: 서로 다른 유형의 데이터 소스로 라우팅해야 하나요?
├─ 예 → 2단계: 데이터 소스 수가 <= 5인가요?
│ ├─ 예 → [논리 라우팅] 선택(LLM 의도 분석)
│ └─ 아니요 → 2단계: 데이터 소스 수가 <= 20인가요?
│ ├─ 예 → [의미 라우팅] 선택(사전 정의 utterances)
│ └─ 아니요 → Multi-Agent 조율기 필요(이 글의 범위를 벗어남)
└─ 아니요 → 3단계: 같은 유형 검색기를 합쳐야 하나요?
├─ 예 → [EnsembleRetriever] 선택(RRF 융합)
└─ 아니요 → 단일 벡터 DB 검색이면 충분
제 실전 조언
프로젝트가 “기업 지식 베이스 시스템”이고 재무 데이터베이스, 기술 문서 저장소, 지식 그래프라는 세 가지 데이터 소스를 갖고 있다면 저는 이렇게 권합니다.
- 먼저 의미 라우팅으로 의도 분류(재무/기술/관계 조회)를 합니다. 빠르고 무료입니다.
- 그다음 논리 라우팅으로 특수 상황을 처리합니다. 예를 들어 최신성이 중요한 쿼리는 Web 검색으로 보냅니다.
- 각 데이터 소스 내부에서는 EnsembleRetriever(BM25 + 벡터 검색)를 사용해 재현율을 높입니다.
- 마지막으로 비용 최적화(Semantic Caching, Tiered Retrieval)를 더해 비용을 낮추고 속도를 높입니다.
이런 “3계층 라우팅” 아키텍처는 제가 3개 프로젝트에서 검증했고 결과가 안정적이었습니다. 비용은 주당 약 $50, 응답 시간은 < 800ms, 사용자 만족도는 85% 이상이었습니다.
프로젝트에 단일 데이터 소스만 있다면(예: 벡터 DB 하나뿐이라면) 라우팅을 서둘러 도입하지 마세요. 먼저 EnsembleRetriever로 BM25 + 벡터 검색 하이브리드를 구성해 재현율이 충분한지 확인하세요. 많은 경우 단일 벡터 DB의 병목은 라우팅 부재가 아니라 검색 전략 최적화 부족입니다.
정리와 실행 제안
길게 이야기했으니 핵심만 정리하겠습니다.
쿼리 라우팅의 본질: 쿼리 특성(컨텍스트 의존도, 추론 홉 수, 데이터 유형, 최신성)에 따라 검색 경로를 동적으로 선택하는 것입니다. 내비게이션 앱이 도로 상황에 맞춰 최적 경로를 고르는 것과 같고, 고정 경로를 무작정 달리는 것이 아닙니다.
세 가지 방식의 적합한 상황:
- 논리 라우팅은 데이터 소스 유형이 명확하고(<=5), 의도를 깊이 이해해야 하는 상황에 적합합니다.
- 의미 라우팅은 의도 분류(<=20), 빠른 응답, 비용 민감도가 중요한 상황에 적합합니다.
- EnsembleRetriever는 BM25 + 벡터처럼 같은 유형의 검색기를 합쳐 재현율을 높이는 데 적합합니다.
프로덕션 배포의 비용 최적화: Semantic Caching, Tiered Retrieval, Parallel Processing 세 전략을 함께 적용하면 비용을 기존의 10%까지 낮추면서 응답 속도도 더 빠르게 만들 수 있습니다.
제 실행 제안
RAG 시스템을 구축하고 있다면 다음 순서로 반복 개선하는 것을 권합니다.
1단계: 병목 진단
기존 시스템의 실패 사례를 분석하고 “낮은 컨텍스트 의존도”(벡터 검색으로 충분)와 “높은 컨텍스트 의존도”(멀티홉 추론 필요)로 분류하세요. 이 단계를 건너뛰면 과한 설계로 가기 쉽습니다.
2단계: 방식 선택
데이터 소스 수, 의도 수, 비용 예산에 따라 논리 라우팅, 의미 라우팅, EnsembleRetriever 중 하나를 고르세요. 처음부터 세 가지를 모두 얹지 말고, 단일 방식의 효과부터 검증하세요.
3단계: 비용 최적화 추가
먼저 Semantic Caching을 구현하세요. 가장 간단하고 효과가 좋습니다. 그다음 Tiered Retrieval과 Parallel Processing을 고려하세요. 비용 최적화는 한 번에 끝나는 일이 아니라 계속 반복하는 과정입니다.
구체적인 프로젝트 문제가 있다면 댓글로 이야기해 주세요. 제가 이미 밟았던 함정이 당신에게는 지름길이 될 수도 있습니다.
FAQ
논리 라우팅, 의미 라우팅, EnsembleRetriever 세 가지 방식은 어떻게 선택하나요?
• 논리 라우팅: 데이터 소스 유형이 명확하고(<=5), 의도를 깊이 이해해야 하며, 응답 시간이 약 500ms여도 되는 경우
• 의미 라우팅: 의도 분류(<=20), 빠른 응답(~50ms), 비용 민감도가 중요한 경우
• EnsembleRetriever: BM25 + 벡터처럼 같은 유형의 검색기를 합쳐 재현율을 높이는 경우
실제 프로젝트에서는 조합해서 사용할 수 있습니다. 의미 라우팅으로 의도를 분류하고, 논리 라우팅으로 특수 상황을 처리하며, EnsembleRetriever로 하이브리드 검색을 구성합니다.
RAG 시스템에서 LLM 호출 비용은 어떻게 낮출 수 있나요?
• Semantic Caching: 자주 나오는 쿼리의 임베딩을 캐시하고, 유사도 > 0.95이면 캐시 답변을 바로 반환해 LLM 호출을 30-50% 줄입니다.
• Tiered Retrieval: 단순 쿼리는 저렴한 모델(GPT-4o-mini), 복잡한 쿼리는 고가 모델(Claude Opus)을 사용해 비용을 80% 낮춥니다.
• Parallel Processing: 여러 검색기를 병렬 호출해 지연 오버헤드를 상쇄하고, 응답 시간을 600ms에서 320ms로 줄입니다.
세 전략을 함께 적용하면 비용을 주당 $500에서 $50까지 낮출 수 있습니다.
EnsembleRetriever의 RRF 알고리즘 원리는 무엇인가요?
• 공식: RRF(d) = Σ 1/(k + rank(d)), 일반적으로 k=60
• 장점: 문서의 원래 점수에 의존하지 않아 BM25, 벡터, 지식 그래프 등 어떤 검색기도 합칠 수 있습니다.
• 효과: 순수 BM25 재현율 70%, 순수 벡터 85%, EnsembleRetriever는 92%까지 도달할 수 있습니다.
Lexical + Semantic 하이브리드 검색에는 적합하지만, 데이터 소스 간 라우팅에는 적합하지 않습니다.
쿼리 라우팅에는 어떤 데이터 소스가 필요하며, 쿼리 특성은 어떻게 판단하나요?
• 컨텍스트 의존도: 낮음(사실 조회)은 벡터 검색, 높음(멀티홉 추론)은 지식 그래프
• 추론 홉 수: 단일 홉은 직접 검색, 멀티홉은 Agent 조율 필요
• 데이터 유형: 구조화 데이터는 SQL, 비구조화 데이터는 벡터 검색
• 최신성: 실시간 정보는 Web 검색, 정적 지식은 로컬 지식 베이스
예를 들어 '화동 지역 매출'은 단일 홉, 구조화, 정적 데이터라 SQL이 가장 빠릅니다. '공급업체 파업이 주가에 미친 영향'은 컨텍스트 의존도가 높고 멀티홉이며 비구조화 데이터라 지식 그래프가 더 적합합니다.
의미 라우팅의 utterances는 어떻게 정의하고, 임계값은 어떻게 설정하나요?
• 의도마다 4-10개의 예시 쿼리를 두어 자주 쓰는 표현을 포괄합니다.
• 너무 적으면(<4) 재현율이 낮고, 너무 많으면(>20) 계산 오버헤드가 커집니다.
• HuggingFaceEncoder를 사용하면 API 호출 비용 없이 무료로 로컬 임베딩을 만들 수 있습니다.
임계값 설정:
• 기본 유사도 임계값은 0.85(85%)이며 프로젝트에 따라 조정할 수 있습니다.
• 임계값이 높을수록 정밀도는 올라가지만 재현율은 떨어집니다.
• 0.85에서 시작한 뒤 테스트 데이터에 맞춰 미세 조정하는 것을 권장합니다.
3분 읽기 · 게시일: 2026년 5월 5일 · 수정일: 2026년 10월 21일
RAG 엔지니어링 가이드
검색으로 들어왔다면 같은 시리즈의 이전 글이나 다음 글로 이동하는 것이 가장 빠릅니다.
이전
RAG 시스템 최적화 실전: 검색 정확도와 생성 품질의 균형
RAG 시스템의 검색 정확도가 낮다면 어떻게 해야 할까요? 이 글에서는 Query 처리, 하이브리드 검색, 재순위화, 청킹 전략, 평가 피드백 루프의 다섯 단계를 체계적으로 분석하고, 정확도와 지연 시간의 균형을 잡는 의사결정 프레임워크를 제시합니다.
5편 중 3편
다음
RAG 쿼리 라우팅 실전: 다중 벡터 저장소 협업과 지능형 검색 분배
RAG 쿼리 라우팅 실전 가이드입니다. EnsembleRetriever와 Semantic Router로 여러 벡터 저장소의 협업 검색을 구현하는 방법을 논리 라우팅, 시맨틱 라우팅, RRF 알고리즘 기반 병합까지 완전한 코드 예제와 성능 비교로 설명합니다.
5편 중 5편



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