
100줄짜리 Python 라우터를 vLLM 시맨틱 라우터로 교체하자 AI 에이전트가 훨씬 똑똑해진 경험
요약
기존의 단순 키워드 기반 Python 라우팅 방식의 한계를 극복하기 위해 vLLM 시맨틱 라우터를 도입한 사례를 소개합니다. 단순 if-else 문 대신 프롬프트의 의미를 이해하는 시맨틱 라우팅을 통해 AI 에이전트의 지능적인 모델 선택 성능을 높였습니다.
핵심 포인트
- 키워드 기반 라우팅의 한계와 의미론적 이해의 필요성
- vLLM 시맨틱 라우터를 활용한 지능적 모델 라우팅 구현
- AgentGateway와 Envoy ExtProc 사이드카를 통한 아키텍처 개선
Homelab AI 시리즈의 파트 3 — Part 1 | Part 2
문제는 창피할 정도였다
Part 1에서 저는 거실에서 24시간 내내 작동하는 개인 AI 에이전트(Pi)를 구축한 방법을 보여주었습니다. 이 과정에서 AgentGateway를 사용하여 세 가지 모델로 요청을 라우팅했습니다: 코딩용 로컬 Ollama(qwen2.5-coder:7b), 심층 추론용 OpenAI (gpt-4o), 그리고 빠른 일반 작업용 Gemini (gemini-2.5-flash).
라우팅 두뇌는 무엇이었을까요? Pi와 AgentGateway 사이에 놓인 100줄짜리 Python 스크립트였습니다:
# router.py — 제가 배포하기 창피했던 'AI 두뇌'
coding_keywords = ["code", "python", "javascript", "bash", "script",
"function", "bug", "error", "html", "css"]
...
네, 맞습니다. 저의 '지능적인' AI 라우팅은 그저 화려한 if-elif-else 체인에 불과했습니다.
작동했지만, 작동하지 않는 순간도 있었습니다.
단순히 키워드를 스캔하는 것이 아니라, 프롬프트를 이해하는 무언가가 필요했습니다.
vLLM 시맨틱 라우터 (Semantic Router)의 등장
AgentGateway의 유지 관리자(Maintainers)들과 논의하던 중, Keith Mattix와 John Howard 덕분에 vLLM 시맨틱 라우터 (Semantic Router)와의 일급(first-class) 통합 기능을 발견했습니다. 아키텍처가 즉시 머릿속에 그려졌습니다.
제 Python 스크립트가 AgentGateway의 앞에 조잡한 리버스 프록시 (reverse proxy)로 자리 잡는 대신, 시맨틱 라우터 (Semantic Router)는 **Envoy ExtProc 사이드카 (sidecar)**로 실행됩니다. AgentGateway는 요청을 일시 중단하고, HTTP 바디를 SR의 gRPC 엔드포인트로 보낸 뒤, 헤더 변조 (x-selected-model: qwen-coder)를 돌려받아 라우팅을 재개합니다. 프록시 홉 (proxy hops)도 없고, Python 프로세스도 필요 없습니다. 그저 게이트웨이 자체의 요청 라이프사이클 (request lifecycle) 내부에서 gRPC 네이티브 지능이 작동할 뿐입니다.
SR은 내장된 mmBERT 모델 (2D Matryoshka 임베딩 모델, 약 130MB)을 사용하여 모든 프롬프트를 시맨틱하게 분류하고, 이를 사용자가 YAML에 작성한 모델 설명과 비교합니다. 키워드 목록도, 정규 표현식 (regex)도 필요 없습니다. 실제 임베딩 (embeddings)을 사용합니다.
아키텍처 (The Architecture)
┌─────────────────────────────────────────────────────┐
│ 클라이언트 (Pi Agent) │
│ POST /v1/chat/completions │
...
설정하기 (두 개의 YAML 파일)
전체 설정은 두 개의 설정 파일로 정의됩니다. 코드도, Python도 필요 없습니다.
1. 시맨틱 라우터 설정 (config.yaml)
이 파일은 SR에 모델 정보와 모델 간 라우팅 방법을 알려줍니다:
version: v0.3
providers:
...
핵심 통찰은 다음과 같습니다: 각 모델이 무엇을 _잘하는지_를 자연어로 설명하면, SR(Semantic Router)이 해당 설명을 시맨틱 앵커 (semantic anchors)로 사용한다는 점입니다. 유지 관리해야 할 키워드 목록이 필요 없습니다. 새로운 프롬프트가 도착하면, SR은 이를 임베딩 (embedding)하고 코사인 유사도 (cosine similarity)를 사용하여 이 설명들과 비교합니다. 프롬프트와 가장 유사한 설명을 가진 모델이 선택됩니다.
2. AgentGateway 설정 (homelab_config.yaml)
이 설정은 AgentGateway가 SR을 ExtProc 사이드카 (sidecar)로 사용하고, SR이 설정한 헤더를 기반으로 라우팅하도록 지시합니다:
# 게이트웨이 레벨 정책: Semantic Router로의 ExtProc
policies:
- name:
...
여기서 **관심사의 분리 (separation of concerns)**를 주목하십시오. Semantic Router는 API 키를 절대 건드리지 않습니다. SR은 프롬프트를 분류하고 헤더를 변형할 뿐입니다. 다운스트림 인증 (downstream auth)은 AgentGateway가 담당합니다. 이것이 바로 인프라 팀이 프로덕션 게이트웨이를 설계하는 방식입니다. 즉, 라우팅 지능을 보안 태세 (security posture)로부터 분리하는 것입니다.
그리고 failureMode: failOpen 설정은 무엇일까요? 이는 만약 SR 컨테이너가 충돌하거나 재시작되는 경우, AgentGateway가 끊김 없이 기본 Gemini 경로로 전환됨을 의미합니다. 제가 직접 테스트해 보았습니다. SR 컨테이너가 재시작되는 동안에도 Pi의 요청은 단 하나의 에러 없이 처리되었습니다. 에이전트는 눈치채지도 못했습니다.
ARM64의 늪 (두 개의 버그, 두 개의 PR)
여기서부터 이야기가 진짜 시작됩니다. 저는 이것을 Apple Silicon Mac Mini (M-series, ARM64)에서 실행하고 있습니다. 모든 것이 문제없이 설치되었습니다. SR 컨테이너도 시작되었습니다. 그런데 다음과 같은 상황이 발생했습니다:
{
"msg": "embedding_models_init_completed",
"embedding_ready": false,
...
}
mmBERT 모델은 로드되었지만, 임베딩 런타임 (embedding runtime)이 준비 상태가 되지 않았습니다. 모든 라우팅 시도에서 다음과 같은 로그가 남았습니다:
Failed to embed model qwen-coder: failed to generate batched embedding (status: -1)
버그 #1: 잘못된 FFI 디스패치 (#2172)
SR 소스 코드를 심층 분석한 결과, 문제의 원인을 발견했습니다. Go 라우터가 모든 모델 유형에 대해 candle_binding.GetEmbeddingBatched()를 호출하고 있었지만, Rust FFI 백엔드는 qwen3 아키텍처에 대해서만 배치 임베딩 (batched embeddings)을 지원하고 있었습니다. 기본값인 mmbert의 경우, status: -1을 반환했습니다.
해결책 (PR #2192)은 우아했습니다. 디스패치 체크 (dispatch check)를 추가하는 단 15줄의 변경이었습니다.
// qwen3만 배치 FFI를 지원합니다. 나머지는 단일 텍스트 FFI를 사용합니다.
func candleEmbeddingSupportsBatched(modelType string) bool {
return modelType == "qwen3"
...
qwen3가 아닌 모델의 경우, ARM64에서 완벽하게 작동하는 GetEmbeddingWithModelType()으로 자연스럽게 폴백 (fallback) 됩니다.
버그 #2: 첫 부팅 시 모델 파일 누락 (#2173)
두 번째 문제는 더 미묘했습니다. SR 컨테이너가 첫 부팅 시 HuggingFace에서 mmBERT 모델 파일을 다운로드할 때, 몇몇 필수 파일(tokenizer.json 및 config.json 등)이 가져와지지 않고 있었습니다. 이는 모델 리졸버 (model resolver)의 다운로드 완전성 (download-completeness) 버그였습니다.
PR #2195에서 수정되었습니다.
진심으로 감사합니다 🙏
두 문제 모두 vLLM Semantic Router 팀에 의해 며칠 만에 분류 및 수정되었습니다. 특히 FFI 디스패치 수정을 작성해 준 @WUKUNTAI-0211님과 파일 완전성 수정을 담당해 준 @theohsiung님께 감사드립니다. 해당 PR들은 현재 main 브랜치에 병합되었습니다. ARM64/Apple Silicon 환경에서 실행 중이라면, 최신 버전을 풀 (pull) 하기만 하면 바로 작동합니다. 또한 최근 저에게 리포지토리에 기여하도록 격려해 준 AayushSaini101님께도 감사를 전합니다.
이것이 바로 오픈 소스의 진정한 모습입니다. 재현 단계와 로그 스니펫 (log snippets)을 포함하여 두 개의 이슈를 제출했고, 업스트림 (upstream) 리포지토리에 작동하는 수정 사항이 병합되었습니다. 이 프로젝트의 커뮤니티 측면은 매우 탁월합니다.
증거: 실제 라우팅 로그
요청이 실제로 어떻게 흐르는지 보여드리겠습니다. 저는 다음과 같이 요청을 보냅니다:
curl http://localhost:3000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
...
1단계: SR이 프롬프트를 분류함 (1ms!)
{
"msg": "routing_decision",
"original_model": "MoM",
...
단 1밀리초(ms)가 걸렸습니다. SR은 프롬프트를 임베딩 (embedding)하고, 세 가지 모델 설명과 비교한 뒤, 이것이 코딩 작업임을 판단하여 qwen-coder로 결정했습니다.
2단계: AgentGateway가 Ollama로 라우팅
info request
gateway=default/default
route=default/route0
...
AgentGateway는 x-selected-model: qwen-coder 헤더를 매칭하여 로컬 Ollama 엔드포인트 (endpoint)로 라우팅했습니다. LLM 생성 (generation)을 포함한 전체 왕복 시간 (round-trip)은 22.5초가 소요되었습니다. 라우팅 오버헤드 (overhead)는 얼마였을까요? 바로 1ms입니다. 나머지는 단순히 Ollama가 생각하는 시간일 뿐입니다.
3단계: SR 시작 시퀀스 (Startup Sequence)
컨테이너 부팅 시, 전체 모델 로딩 파이프라인 (pipeline)을 확인할 수 있습니다:
{"msg":"embedding_models_init_started","mmbert_configured":true,"use_cpu":true}
INFO: mmBERT embedding model registered with 2D Matryoshka support
{"msg":"embedding_models_initialized","use_batched":false}
{"msg":"selection_factory_initialized","selector_count":14}
{"msg":"startup_complete","embedding_ready":false,"sem_cache_enabled":true,
"model_selection":true,"extproc_port":50051,"decisions":"MoM"}
기본적으로 14개의 선택 알고리즘 (selection algorithms)을 사용할 수 있습니다. 다요소 (Multi-factor), ELO, 강화학습 (reinforcement-learning) 기반, 하이브리드 (hybrid), 지연 시간 인식 (latency-aware), 세션 인식 (session-aware), KNN, SVM, K-means — 이 모든 것이 등록되어 준비되어 있습니다. 저는 비용 가중치가 높은 multi_factor를 사용하고 있지만, YAML 설정 하나만 바꾸면 이 중 어떤 것으로든 전환할 수 있습니다. Python 키워드 리스트로 이걸 해보려고 시도해 보세요.
2주 후의 수치
Pi와 함께 SR 기반 설정을 2주 동안 실행한 후의 비교 결과는 다음과 같습니다:
| 지표 (Metric) | Python 라우터 (Python Router) | vLLM 시맨틱 라우터 (vLLM Semantic Router) |
|---|---|---|
| 오라우팅 요청 (Misrouted requests) | ~18% | ~3% (주관적 샘플 점검) |
| ... |
비용 절감은 오라우팅(misroutes)이 줄어듦으로써 발생합니다. "Rust의 async/await 패턴을 설명해줘"라는 요청이 GPT-4o 대신 로컬 Ollama로 올바르게 전달되면, 이는 $0.03 대신 $0.003짜리 요청이 됩니다. Pi의 크론 잡(cron jobs)과 저의 직접적인 사용을 통해 발생하는 수백 건의 일일 요청을 고려하면, 이 차이는 빠르게 누적됩니다.
모든 에이전트 빌더에게 이것이 필요한 이유
여러분이 에이전트를 구축하고 있다면 — 그것이 Mac Mini에서 실행되는 개인용 Pi이든, Kubernetes에서 실행되는 프로덕션급 에이전트 군단이든 — 프롬프트를 이해하는 라우팅 계층(routing layer)이 필요합니다. 그 이유는 다음과 같습니다:
-
비용 제어는 에이전트의 제1 문제입니다. 에이전트는 매우 많은 요청을 생성합니다. 지능형 라우팅이 없다면 모든 요청이 가장 비싼 모델로 전송됩니다. 시맨틱 라우터(SR)의
multi_factor알고리즘은 비용, 지연 시간(latency), 품질을 명시적으로 가중치로 계산합니다. -
키워드 라우팅은 확장성이 없습니다. 에이전트가 예상치 못한 도메인을 처리하는 순간 (제 Pi가 레시피 조사를 시작했는데, 제가 설정한 키워드 중에는 "사워도우 스타터 수분율(sourdough starter hydration)"을 커버하는 것이 없었습니다), 키워드 기반 라우팅은 소리 없이 실패합니다.
-
AgentGateway + SR은 프로덕션급입니다. 이것은 취미 수준의 설정이 아닙니다. AgentGateway는 Rust로 구축된 Gateway API 데이터 플레인(data plane)입니다. SR은 vLLM 프로젝트를 기반으로 하며 Go와 Rust로 작성된 Envoy ExtProc 서버입니다. 이는 50개의 모델을 가진 Kubernetes 클러스터에 배포할 법한 것과 동일한 아키텍처입니다.
-
코드 유지보수가 필요 없습니다. 저는 모델 설명(model descriptions)을 작성한 이후로 라우팅 설정을 건드린 적이 없습니다. SR은 제가 계속 업데이트해야 하는 규칙이 아니라, 그 설명으로부터 학습합니다.
다음 단계
라우팅 지능 문제를 해결했으므로, 이제 저는 다음 사항에 집중하고 있습니다:
- Observability (관측 가능성): Pi → AgentGateway → SR → Upstream LLM 및 그 역방향으로 이어지는 모든 요청을 추적하기 위해 Jaeger와 Prometheus를 연결하는 작업입니다. AgentGateway는 이미 OpenTelemetry 호환 스팬(spans)을 방출하고 있으므로, 저는 컬렉터(collectors)만 설정하면 됩니다.
- 더 많은 모델: 라우팅이 시맨틱(semantic)하게 이루어지므로, 이제 YAML 파일에 새로운 모델 카드(model card)를 추가하는 것만으로 특화된 모델(의료용, 법률용 등)을 추가할 수 있습니다. SR이 언제 해당 모델들을 사용할지 자동으로 판단할 것입니다.
만약 여러분이 홈랩(homelab) AI 환경을 운영 중이거나, 어떤 규모로든 에이전트를 구축하고 있다면, AgentGateway와 vLLM Semantic Router의 조합은 현재 AI 생태계에서 가장 과소평가된 인프라 조합이라고 생각합니다. 이 조합은 저의 조잡한 Python 키워드 매처(keyword matcher)를 제대로 된 ML 기반 라우팅 플레인(routing plane)으로 바꿔 놓았습니다.
그리고 이것은 제 거실에 있는 Mac Mini에서 돌아가고 있습니다. 🏠
이 파이프라인에 완전한 관측 가능성(observability)을 추가하고, 새벽 3시에 Pi가 꿈을 꿀 때 정확히 어떤 일이 일어나는지 트레이스(traces)와 함께 보여드릴 파트 4를 기대하며 저를 팔로우해 주세요.
#ai #agents #architecture #opensource
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기