쿼리 라우팅 기법
쿼리 라우팅은 입력 쿼리를 분석하여 가장 적합한 검색 경로·파이프라인으로 동적으로 분기하는 기법이다. RAG Routing의 상위 개념이며, 라우터 구현 방식에 따라 Semantic, LLM, Fallback, Ensemble, Hierarchical 라우팅으로 나뉜다.
Semantic Routing
Semantic Routing은 쿼리 임베딩과 각 경로의 대표 임베딩 간 코사인 유사도를 계산하여 가장 가까운 경로로 분기하는 기법이다.
동작 원리
각 라우팅 경로(도메인·파이프라인)를 대표하는 예시 쿼리들을 미리 임베딩해 저장한다. 런타임에 입력 쿼리를 임베딩하고, 저장된 대표 임베딩들과 유사도를 비교해 가장 높은 경로를 선택한다.
from sentence_transformers import SentenceTransformer
import numpy as np
model = SentenceTransformer("BAAI/bge-m3")
route_embeddings = {
"legal": model.encode(["계약", "소송", "법률 해석", "판례"]),
"medical": model.encode(["진단", "약 복용", "혈압", "증상"]),
"tech": model.encode(["코드 오류", "API", "버그", "아키텍처"]),
}
# 각 경로의 대표 임베딩을 평균으로 압축
route_centroids = {k: v.mean(axis=0) for k, v in route_embeddings.items()}
def semantic_route(query: str) -> str:
q_emb = model.encode(query)
scores = {
route: np.dot(q_emb, centroid) / (np.linalg.norm(q_emb) * np.linalg.norm(centroid))
for route, centroid in route_centroids.items()
}
return max(scores, key=scores.get)장단점
| 항목 | 내용 |
|---|---|
| 장점 | 동의어·paraphrase에 강건, 규칙 작성 불필요, 빠른 추론 속도 |
| 단점 | 대표 임베딩 품질에 의존, 경로가 많아질수록 관리 복잡도 증가 |
적합한 상황
도메인 수가 10개 미만이고, 도메인 간 의미적 거리가 충분히 클 때 효과적이다. 도메인이 중첩되거나 경계가 모호하면 오분류율이 높아진다.
LLM Routing
LLM Routing은 분류 판단을 LLM에 위임하는 기법이다. 자연어로 경로 설명을 작성하면 LLM이 쿼리를 읽고 가장 적합한 경로를 선택한다.
동작 원리
분류 프롬프트에 각 경로의 설명과 선택 기준을 명시한다. LLM은 쿼리를 분석해 경로 레이블 하나를 반환한다. 구조화된 출력(JSON, enum)을 강제하면 파싱 오류를 줄일 수 있다.
import anthropic
import json
client = anthropic.Anthropic()
ROUTES = {
"legal": "법률, 계약서, 판례, 소송과 관련된 질문",
"medical": "의학 정보, 약물, 증상, 진단과 관련된 질문",
"tech": "소프트웨어, 코드, 아키텍처, 인프라와 관련된 질문",
"general": "위 카테고리에 해당하지 않는 일반 질문",
}
def llm_route(query: str) -> str:
route_desc = "\n".join(f"- {k}: {v}" for k, v in ROUTES.items())
prompt = f"""다음 쿼리를 분석하여 가장 적합한 카테고리를 선택하라.
카테고리 목록:
{route_desc}
쿼리: {query}
JSON 형식으로만 응답하라: {{"route": "<카테고리명>"}}"""
response = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=64,
messages=[{"role": "user", "content": prompt}],
)
return json.loads(response.content[0].text)["route"]비용 최적화
LLM Routing은 레이턴시·비용이 높으므로 소형 모델(Haiku, GPT-4o-mini)을 사용하거나, 결과를 캐싱해 동일 쿼리의 반복 호출을 방지한다.
장단점
| 항목 | 내용 |
|---|---|
| 장점 | 복잡한 의도 파악 가능, 경로 설명을 자연어로 관리, 유지보수 편리 |
| 단점 | 추가 레이턴시(200~800ms), API 비용 발생, 비결정적 출력 |
Fallback Routing
Fallback Routing은 기본 경로에서 충분한 품질의 결과를 얻지 못할 때 상위(더 강력하거나 비용이 높은) 경로로 단계적으로 에스컬레이션하는 기법이다. Cascading Routing이라고도 한다.
동작 원리
각 경로에 품질 임계값을 설정하고, 임계값을 충족하지 못하면 다음 경로를 시도한다. 빠르고 저렴한 경로를 먼저 실행해 대부분의 쿼리를 처리하고, 소수의 어려운 쿼리만 고비용 경로로 넘긴다.
from dataclasses import dataclass
from typing import Optional
@dataclass
class SearchResult:
docs: list
score: float
THRESHOLD = 0.75
def fallback_route(query: str) -> SearchResult:
# 1단계: 빠른 BM25 검색
result = bm25_retriever.search(query)
if result.score >= THRESHOLD:
return result
# 2단계: Dense retriever로 에스컬레이션
result = dense_retriever.search(query)
if result.score >= THRESHOLD:
return result
# 3단계: LLM 기반 고품질 검색으로 최종 에스컬레이션
return llm_retriever.search(query)임계값 설정
임계값은 검색 스코어의 분포를 분석해 설정한다. 너무 낮으면 에스컬레이션이 거의 발생하지 않고, 너무 높으면 대부분의 쿼리가 최상위 경로로 몰려 비용이 증가한다.
장단점
| 항목 | 내용 |
|---|---|
| 장점 | 비용과 품질의 균형, 단순한 구조, 디버깅 용이 |
| 단점 | 임계값 튜닝 필요, 레이턴시 누적(최악의 경우 모든 단계 실행) |
Ensemble Routing
Ensemble Routing은 복수의 경로를 병렬로 실행하고 결과를 통합하는 기법이다. 단일 경로 선택이 아니라 모든 경로의 결과를 함께 활용해 재현율(recall)을 극대화한다.
동작 원리
각 경로가 독립적으로 검색 결과를 반환하면, 병합 전략(score fusion, RRF, voting)을 적용해 최종 결과 집합을 생성한다.
import asyncio
from typing import List
async def ensemble_route(query: str) -> List[dict]:
# 복수 retriever를 병렬 실행
results = await asyncio.gather(
bm25_retriever.async_search(query),
dense_retriever.async_search(query),
colbert_retriever.async_search(query),
)
# Reciprocal Rank Fusion으로 병합
return reciprocal_rank_fusion(results)
def reciprocal_rank_fusion(result_lists: list, k: int = 60) -> list:
scores: dict = {}
for ranked_list in result_lists:
for rank, doc in enumerate(ranked_list):
doc_id = doc["id"]
scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank + 1)
return sorted(scores.keys(), key=lambda d: scores[d], reverse=True)병합 전략 비교
| 전략 | 방식 | 특징 |
|---|---|---|
| Score Fusion | 각 경로 스코어를 정규화 후 합산 | 스코어 분포가 유사할 때 효과적 |
| Reciprocal Rank Fusion (RRF) | 순위 기반 역수 합산 | 스코어 단위 불일치에 강건 |
| Voting | 복수 경로에 등장한 문서 우선 | 단순하지만 드문 정답 놓칠 수 있음 |
관련 개념: Reciprocal Rank Fusion, Score Fusion, Naive Merge
장단점
| 항목 | 내용 |
|---|---|
| 장점 | 재현율 극대화, 단일 경로 실패 시에도 결과 보장 |
| 단점 | 실행 비용이 경로 수에 비례, 병합 오버헤드 |
Hierarchical Routing
Hierarchical Routing은 라우팅을 단계적으로 좁혀가는 기법이다. 상위 레벨에서 큰 카테고리를 먼저 선택하고, 하위 레벨에서 세부 경로를 선택하는 트리 구조로 동작한다.
동작 원리
라우터를 트리 형태로 구성한다. 루트 라우터가 대분류를 결정하고, 해당 대분류 내에 특화된 하위 라우터가 세부 경로를 결정한다.
Query
└─ 루트 라우터 (도메인 분류)
├─ tech
│ └─ 기술 라우터 (backend / frontend / infra)
│ ├─ backend → DB 인덱스
│ ├─ frontend → UI 인덱스
│ └─ infra → DevOps 인덱스
├─ legal → 법률 인덱스
└─ general → 범용 인덱스
class HierarchicalRouter:
def __init__(self):
self.root_router = SemanticRouter(routes=["tech", "legal", "general"])
self.tech_router = SemanticRouter(routes=["backend", "frontend", "infra"])
def route(self, query: str) -> str:
top_level = self.root_router.classify(query)
if top_level == "tech":
sub_level = self.tech_router.classify(query)
return f"tech/{sub_level}"
return top_level # "legal" | "general"설계 원칙
- 각 레벨의 라우터는 해당 레벨의 분류만 담당하도록 책임을 분리한다.
- 상위 레벨 오분류가 하위 레벨 전체를 오염시키므로, 상위 라우터의 정확도가 가장 중요하다.
- 하위 라우터가 많아질수록 레이턴시가 누적되므로 깊이는 2~3단계로 제한한다.
장단점
| 항목 | 내용 |
|---|---|
| 장점 | 경로 수가 많아도 각 라우터의 분류 클래스 수를 소규모로 유지 가능 |
| 단점 | 상위 오분류가 전파, 트리 설계·유지보수 비용, 레이턴시 누적 |
기법 비교 요약
| 기법 | 결정 주체 | 병렬 실행 | 비용 | 재현율 | 적합 규모 |
|---|---|---|---|---|---|
| Semantic | 임베딩 유사도 | 선택 1개 | 낮음 | 중간 | 소~중 |
| LLM | LLM 분류 | 선택 1개 | 높음 | 중간 | 소~중 |
| Fallback | 품질 임계값 | 직렬(단계별) | 가변 | 중간 | 소~대 |
| Ensemble | 없음(전부 실행) | 복수 병렬 | 높음 | 높음 | 소~중 |
| Hierarchical | 트리 라우터 | 선택 1개 | 중간 | 중간 | 대규모 |
관련 개념
- RAG Routing: 쿼리 라우팅의 상위 개념
- Retriever Routing: 검색기(retriever) 수준의 분기
- Reciprocal Rank Fusion: Ensemble 결과 병합에 사용되는 기법
- Tiered Retrieval: 비용-품질 트레이드오프 기반 계층적 검색