하이브리드 LLM 라우팅 - 모델 선택기가 아니다.
중년개발자
@loxo
약 10시간 전
LLM을 호출하기 전에, 누가 답할지 결정하는 코드
개념부터 프롬프트·오픈소스·실제 처리 흐름까지
1. LLM Router란 무엇인가?
LLM Router는 사용자 요청을 분석해 어떤 모델과 처리 경로를 사용할지 결정하는 계층이다.
단순한 형태는 두 모델 중 하나를 고른다.
- 단순 요청 → 빠르고 저렴한 모델
- 복잡한 요청 → 비싸지만 성능이 좋은 모델
실제 서비스에서는 더 많은 조건을 고려한다.
- 개인정보가 있으면 사내 모델
- 이미지가 있으면 비전 모델
- 검색이 필요하면 도구 사용 모델
- 고위험 요청이면 고성능 모델과 사람 검토
- 기본 모델에 장애가 생기면 대체 모델
- 출력 형식이 깨지면 재시도 또는 상위 모델
따라서 하이브리드 LLM 라우팅은 다음 구조다.
확실한 조건은 규칙으로 판단하고, 의미와 난이도는 AI가 분류하며, 실제 호출과 장애 전환은 게이트웨이가 처리한다.
2. 실제 요청 처리 순서
사용자 요청
↓
개인정보·위험·기능 규칙
↓
작업 유형과 난이도 분석
↓
FAST / DEEP / PRIVATE 경로 선택
↓
해당 경로의 모델 호출
↓
출력 형식과 품질 검증
↓
실패하면 재시도 또는 상위 모델여기서 라우터는 하나의 모델 이름을 직접 반환하기보다 FAST, DEEP, PRIVATE 같은 논리적인 경로를 선택하는 것이 좋다.
그래야 모델이 교체되더라도 라우팅 정책은 유지할 수 있다.
3. 가장 먼저 필요한 요청 정보
라우터에는 사용자 프롬프트만 전달하면 안 된다. 다음과 같은 메타데이터도 필요하다.
prompt: 사용자 질문
data_class: public / internal / confidential
risk: low / high
expected_format: text / json
required_capability: text / image / search / tool예를 들어 다음 요청을 생각해 보자.
“첨부한 사내 인사평가 자료를 요약해 줘.”
질문 자체만 보면 단순 요약이다. 하지만 data_class가 confidential이라면 외부 모델로 보내면 안 된다.
이것이 “질문 난이도”만으로 라우팅해서는 안 되는 이유다.
4. 규칙과 AI 판단을 분리한다
규칙으로 처리할 것
결과가 확실해야 하는 조건이다.
[RULE: 기밀정보]
data_class가 confidential이면 PRIVATE 경로
[RULE: 고위험 작업]
risk가 high이면 DEEP 또는 REVIEW 경로
[RULE: 이미지]
이미지가 있으면 비전 기능이 없는 모델 제외
[RULE: 장애]
기본 서버가 응답하지 않으면 대체 서버 호출개인정보와 접근 권한처럼 중요한 정책을 LLM의 확률적 판단에만 맡기면 안 된다.
AI 라우터가 판단할 것
문장의 의미를 이해해야 하는 조건이다.
- 작업이 요약인지 분석인지
- 여러 단계의 추론이 필요한지
- 사용자 의도가 모호한지
- 고성능 모델의 추가 가치가 큰지
5. 실제 라우터 프롬프트
라우터 모델에게 특정 제품이나 모델 이름을 선택하게 하지 않는다.
다음처럼 작업 신호만 반환하도록 만든다.
당신은 사용자 요청에 답변하는 모델이 아니라,
요청의 처리 조건을 분석하는 라우터입니다.
사용자 질문에 직접 답하지 말고 다음 JSON만 반환하세요.
{
"task": "summary | translation | extraction | writing | analysis | coding | search",
"complexity": "low | high",
"risk": "low | high",
"sensitivity": "none | possible",
"confidence": 0.0,
"reason": "판단 이유 한 문장"
}
판단 기준:
- 단순 변환, 짧은 요약, 분류는 complexity=low
- 여러 조건의 비교, 원인 분석, 다단계 추론은 complexity=high
- 법률, 의료, 금융, 보안 또는 실제 데이터 변경은 risk=high
- 개인정보나 회사 내부 정보가 의심되면 sensitivity=possible
- 판단이 애매하면 confidence를 낮게 설정하세요.사용자 요청이 다음과 같다고 해보자.
“두 사업계획의 수익성과 위험을 비교하고 반대 의견까지 검토해 줘.”
라우터 결과는 다음과 같다.
{
"task": "analysis",
"complexity": "high",
"risk": "low",
"sensitivity": "none",
"confidence": 0.93,
"reason": "여러 대안을 비교하고 반론까지 검토해야 한다."
}애플리케이션은 이 신호를 보고 DEEP 경로를 선택한다.
6. 실제 흐름을 보여주는 최소 구현
다음 코드는 구조를 이해하기 위한 최소 Python 예제다.
- 라우터 모델과 사내 모델은 Ollama에서 실행
- 여러 모델 호출은 LiteLLM으로 통합
- 기밀정보는 규칙으로 먼저 처리
- 판단 확신도가 낮으면 고성능 모델로 승격
- 호출 또는 출력 검증에 실패하면 Fallback 실행
import json
import os
import litellm
LOCAL_URL = os.getenv("LOCAL_LLM_URL", "http://localhost:11434")
ROUTER_MODEL = "ollama/qwen3:4b"
MODEL_CHAINS = {
"FAST": [
"ollama/qwen3:8b",
"openai/gpt-5-mini",
],
"DEEP": [
"openai/gpt-5-mini",
],
"PRIVATE": [
"ollama/qwen3:14b",
],
}
ROUTER_PROMPT = """
당신은 요청 처리 조건을 분석하는 라우터입니다.
사용자 질문에 답하지 말고 다음 JSON만 반환하세요.
{
"task": "summary | translation | extraction | writing | analysis | coding",
"complexity": "low | high",
"risk": "low | high",
"sensitivity": "none | possible",
"confidence": 0.0,
"reason": "한 문장"
}
"""
async def call_model(model, messages):
options = {
"model": model,
"messages": messages,
"temperature": 0,
}
if model.startswith("ollama/"):
options["api_base"] = LOCAL_URL
return await litellm.acompletion(**options)
def apply_hard_rules(request):
if request["data_class"] == "confidential":
return "PRIVATE"
if request["risk"] == "high":
return "DEEP"
return None
async def classify_request(request):
response = await call_model(
ROUTER_MODEL,
[
{"role": "system", "content": ROUTER_PROMPT},
{"role": "user", "content": request["prompt"]},
],
)
signal = json.loads(response.choices[0].message.content)
if signal["sensitivity"] == "possible":
route = "PRIVATE"
elif signal["complexity"] == "high":
route = "DEEP"
elif signal["risk"] == "high":
route = "DEEP"
elif signal["confidence"] < 0.75:
route = "DEEP"
else:
route = "FAST"
return route, signal
def validate_output(text, expected_format):
if not text or not text.strip():
return False
if expected_format == "json":
try:
json.loads(text)
except json.JSONDecodeError:
return False
return True
async def handle_request(request):
route = apply_hard_rules(request)
signal = {"source": "hard_rule"}
if route is None:
route, signal = await classify_request(request)
last_error = None
for model in MODEL_CHAINS[route]:
try:
response = await call_model(
model,
[{"role": "user", "content": request["prompt"]}],
)
answer = response.choices[0].message.content
if validate_output(answer, request["expected_format"]):
return {
"route": route,
"model": model,
"signal": signal,
"answer": answer,
}
last_error = ValueError("출력 검증 실패")
except (
litellm.Timeout,
litellm.RateLimitError,
litellm.APIError,
) as error:
last_error = error
raise RuntimeError(f"모든 모델 호출 실패: {last_error}")API 키는 코드에 작성하지 않고 실행 환경에 보관해야 한다. 모델 이름 역시 예시이므로 실제 운영 모델에 맞게 교체한다.
7. 코드는 실제로 어떻게 움직일까?
다음 요청이 들어왔다고 해보자.
prompt: "이 회의 내용을 세 줄로 요약해 줘."
data_class: "public"
risk: "low"
expected_format: "text"처리 과정
- 기밀정보가 아니므로 하드 규칙을 통과한다.
- 로컬 라우터 모델이 요청을 분석한다.
summary,complexity=low,confidence=0.96을 반환한다.- 애플리케이션이
FAST경로를 선택한다. - Ollama의 소형 모델을 호출한다.
- 답변이 비어 있지 않으면 사용자에게 반환한다.
- 로컬 모델에 장애가 생기면 다음 모델로 Fallback한다.
이번에는 다음 요청을 보자.
prompt: "사내 인사평가 문서에서 승진 후보자를 정리해 줘."
data_class: "confidential"
risk: "high"
expected_format: "json"처리 과정이 달라진다.
data_class=confidential규칙이 즉시 적용된다.- 라우터 모델의 판단을 기다리지 않고
PRIVATE경로를 선택한다. - 외부 모델은 후보에서 제외된다.
- 사내 모델을 호출한다.
- 결과가 올바른 JSON인지 검사한다.
- 사내 모델이 모두 실패해도 외부 모델로 Fallback하지 않는다.
Fallback도 보안 정책을 위반해서는 안 된다.
8. 오픈소스 프레임워크는 어디에 들어갈까?
Semantic Router
Aurelio Labs의 Semantic Router는 대표 문장과 사용자 요청의 의미를 비교해 경로를 선택한다. MIT 라이선스의 오픈소스다. 공식 저장소
다음과 같이 업무 경로가 명확할 때 적합하다.
- 환불
- 배송
- 계약 검토
- 기술 장애
- 일반 대화
위 예제의 classify_request()를 Semantic Router로 대체할 수 있다.
직접 만들어야 하는 것:
- 경로 이름
- 경로별 대표 문장
- 유사도 임계값
- 판단 불가 시 처리 방법
RouteLLM
RouteLLM은 약한 모델과 강한 모델의 응답 선호 데이터를 이용해, 강한 모델이 더 나은 결과를 낼 가능성을 계산한다. Apache 2.0 라이선스의 오픈소스다. 공식 저장소
다음 상황에 적합하다.
- 소형 모델과 고성능 모델 사이의 비용을 최적화하고 싶다.
- 실제 질문과 모델별 평가 결과가 있다.
- 강한 모델 호출 비율을 임계값으로 조정하고 싶다.
위 예제에서 complexity == high 같은 단순 조건을 RouteLLM의 예상 승률로 대체할 수 있다.
LiteLLM
LiteLLM은 여러 공급자와 로컬 모델을 같은 호출 형식으로 연결하는 오픈소스 AI 게이트웨이다. 오픈소스 코어는 MIT이며 일부 엔터프라이즈 기능은 별도 라이선스다. 공식 저장소
위 코드에서는 다음 역할을 담당한다.
- Ollama와 외부 API를 같은 방식으로 호출
- 공급자별 오류를 공통 오류로 변환
- 시간 초과와 호출 제한 처리
- 재시도와 Fallback
- 토큰과 비용 추적
LiteLLM은 호출을 관리하지만 회사의 업무 라우팅 기준까지 자동으로 만들어 주지는 않는다.
vLLM과 Ollama
두 프로젝트는 라우터가 아니라 선택된 로컬 모델을 실행하는 엔진이다.
개발 단계에서는 Ollama로 시작하고, 운영 트래픽과 동시 요청이 많아지면 vLLM을 검토할 수 있다.
프로그램 라이선스와 실행하는 모델 가중치의 라이선스는 별도로 확인해야 한다.
Langfuse
Langfuse는 라우팅 결과를 기록하고 평가하는 LLM 운영 플랫폼이다. 공식 저장소는 ee 폴더를 제외한 부분에 MIT 라이선스를 적용한다. 공식 저장소
다음 내용을 기록한다.
- 선택된 경로와 모델
- 적용된 규칙
- 라우터 확신도
- 응답 시간과 비용
- 출력 검증 실패
- Fallback 발생
- 사용자 재질문
라우터는 실제 기록을 보면서 임계값을 조정해야 한다.
정책 엔진
초기에는 apply_hard_rules()처럼 애플리케이션 내부에 규칙을 작성해도 된다.
규칙이 많아지고 여러 서비스에서 공유해야 한다면 별도의 정책 엔진을 사용할 수 있다.
정책 엔진은 규칙을 실행해 주지만, 회사의 개인정보·비용·위험 정책 자체는 개발자가 정의해야 한다.
9. 처음부터 모든 프레임워크가 필요한 것은 아니다
가장 단순한 시작
자체 규칙
+ 프롬프트 기반 소형 라우터
+ LiteLLM
+ 소형 모델과 고성능 모델업무 종류가 명확한 서비스
자체 규칙
+ Semantic Router
+ LiteLLM
+ 업무별 모델호출량이 많고 비용 최적화가 중요한 서비스
정책 엔진
+ Semantic Router
+ RouteLLM
+ LiteLLM
+ vLLM과 외부 모델
+ Langfuse먼저 두세 개 경로로 시작한 뒤 실제 요청 데이터를 보며 확장하는 편이 안전하다.
10. 운영 전에 반드시 확인할 지표
좋은 라우터는 고성능 모델 호출을 무조건 줄이는 라우터가 아니다.
다음 지표를 함께 봐야 한다.
- FAST 경로의 정답률
- DEEP 경로로 승격된 비율
- 잘못 FAST로 보낸 요청의 비율
- 모델별 출력 형식 실패율
- Fallback 발생률
- 라우터 판단 시간
- 요청당 평균 비용
- 사용자 재질문율
- 기밀정보가 외부로 전송된 건수
특히 가장 위험한 지표는 다음이다.
고성능 모델이 필요했지만 소형 모델로 잘못 보낸 비율
비용 절감보다 이 오류를 먼저 관리해야 한다.
결론
LLM Router는 단순한 모델 선택문이 아니다.
실제 구현은 다음 역할의 조합이다.
- 규칙이 개인정보와 위험 조건을 강제한다.
- 라우터가 작업 유형과 난이도를 분석한다.
- 정책이
FAST,DEEP,PRIVATE경로를 선택한다. - LiteLLM이 실제 모델과 서버를 호출한다.
- Ollama나 vLLM이 로컬 모델을 실행한다.
- 출력 검증기가 결과를 확인한다.
- 실패하면 재시도하거나 상위 모델로 승격한다.
- Langfuse가 모든 판단과 결과를 기록한다.
오픈소스는 라우팅에 필요한 엔진을 제공한다. 하지만 어떤 요청을 어디로 보내고, 어느 정도의 실패를 허용하며, 언제 사람에게 넘길지는 서비스가 직접 결정해야 한다.
하이브리드 LLM 라우팅의 핵심은 모델을 많이 연결하는 것이 아니라, 요청마다 가장 안전하고 경제적인 실행 경로를 선택하는 것이다.