본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 20. 12:11

vLLM Semantic Router를 사용하여 AgentGateway에 의미론적 두뇌(Semantic Brain) 부여하기 - 나의

요약

기존의 키워드 기반 if-else 라우팅 방식의 한계를 극복하기 위해 vLLM Semantic Router를 AgentGateway에 통합하는 과정을 다룹니다. 단순 키워드 매칭 대신 프롬프트의 의미를 이해하여 최적의 AI 모델로 요청을 전달하는 지능형 라우팅 구현 방법을 설명합니다.

핵심 포인트

  • 키워드 기반 라우팅의 높은 오류율(약 18%)과 한계 지적
  • vLLM Semantic Router를 통한 의미론적(Semantic) 라우팅 도입
  • 요청의 의도를 파악하여 모델별(Ollama, OpenAI, Gemini) 최적 배정
  • AgentGateway와의 통합을 통한 에이전트 시스템 고도화

Homelab AI 시리즈의 3부 — 1부 | 2부

문제는 창피한 수준이었다

1부에서 저는 거실에서 24시간 내내 작동하는 개인용 AI 에이전트(Pi)를 어떻게 구축했는지 보여드렸습니다. 저는 AgentGateway를 사용하여 세 가지 모델로 요청을 라우팅(Routing)했습니다: 코딩을 위한 로컬 Ollama (qwen2.5-coder:7b), 심층 추론(Deep reasoning)을 위한 OpenAI (gpt-4o), 그리고 빠른 일반 작업을 위한 Gemini (gemini-2.5-flash)입니다.

라우팅 두뇌(Routing brain)라고요? Pi와 AgentGateway 사이에 놓인 100줄짜리 Python 스크립트였습니다:

# router.py — 제가 배포하기 부끄러웠던 "AI 두뇌"
coding_keywords = ["code", "python", "javascript", "bash", "script",
                   "function", "bug", "error", "html", "css"]
...

네, 맞습니다. 저의 "지능적인" AI 라우팅은 미화된 if-elif-else 체인에 불과했습니다.

작동은 했습니다 — 작동하지 않기 전까지는 말이죠. "Rust에서 async/await 패턴을 설명해줘"라는 요청은 일치하는 키워드가 없어서 simple로 분류되었습니다. "저녁 메뉴 고르는 것 좀 도와줘"는 키워드 목록에 think가 포함되어 있어 reasoning으로 분류되었습니다. 그리고 힌디어로 된 프롬프트나 혼합 언어 프롬프트는요? 매번 예외 없이 바로 폴백(Fallback)으로 넘어갔습니다.

이 설정을 2주 동안 매일 실행한 후, 몇 가지 대략적인 수치를 수집했습니다:

지표Python 라우터 사용 시
잘못 라우팅된 요청 (샘플 확인)~18%
...

요청의 18%가 잘못된 모델로 가는 것은 단순히 돈을 낭비하는 것뿐만 아니라, _잘못된 답변_을 제공한다는 것을 의미합니다. 저의 크론잡(cron-job) 에이전트가 "이번 주 일정을 요약하고 최적화 방안을 제안해줘"라는 복잡한 요청을 Gemini나 GPT-4o 대신 7B 로컬 모델로 보낼 때, 출력 결과는 눈에 띄게 나빠집니다.

단순히 키워드를 스캔하는 것이 아니라, 프롬프트를 제대로 이해하는(understood) 무언가가 필요했습니다.

vLLM Semantic Router의 등장

AgentGateway의 메인테이너(Maintainers)들과 논의하던 중, Keith MattixJohn 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)

┌─────────────────────────────────────────────────────┐
│                  Client (Pi Agent)                   │
│             POST /v1/chat/completions                │
...

설정하기 (두 개의 YAML 파일)

전체 설정은 두 개의 설정 파일로 정의됩니다. 코드도, Python도 필요 없습니다.

1. Semantic Router 설정 (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-시리즈, 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.jsonconfig.json 등)이 가져와지지 않고 있었습니다. 이는 모델 리졸버 (model resolver)의 다운로드 완전성 (download-completeness) 버그였습니다.

PR #2195에서 수정되었습니다.

진심 어린 감사 🙏

두 문제 모두 vLLM Semantic Router 팀에 의해 며칠 만에 분류 및 수정되었습니다. 특히 FFI 디스패치 수정을 작성한 @WUKUNTAI-0211님과 파일 완전성 수정을 담당한 @theohsiung님께 감사드립니다. 해당 PR들은 현재 main 브랜치에 병합되었습니다. ARM64/Apple Silicon 환경에서 실행 중이라면, 최신 버전을 풀 (pull)하기만 하면 바로 작동합니다. 또한 최근 저에게 리포지토리 (repo) 기여를 독려해 준 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 엔드포인트로 라우팅했으며, 전체 왕복 시간(LLM 생성 포함)은 22.5초 만에 완료되었습니다. 라우팅 오버헤드(overhead)는요? 1ms입니다. 나머지는 단순히 Ollama가 생각하는 시간일 뿐입니다.

3단계: SR 시작 시퀀스

컨테이너 부팅 시, 전체 모델 로딩 파이프라인(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 RoutervLLM Semantic Router
오분류된 요청 (Misrouted requests)~18%~3% (주관적 샘플 점검)
...

비용 절감은 오분류된 요청이 줄어듦으로써 발생합니다. "Rust의 async/await 패턴을 설명해줘"라는 요청이 GPT-4o 대신 로컬 Ollama로 올바르게 전달되면, 이는 $0.03 대신 $0.003의 비용이 드는 요청이 됩니다. Pi의 크론 잡(cron jobs)과 저의 직접적인 사용을 통해 발생하는 수백 건의 일일 요청을 고려하면, 이 차이는 빠르게 누적됩니다.

모든 에이전트 빌더에게 이것이 필요한 이유

Mac Mini에서 실행되는 개인용 Pi이든, Kubernetes 상의 프로덕션 에이전트 플릿(fleet)이든, 에이전트를 구축하고 있다면 프롬프트를 이해하는 라우팅 계층이 필요합니다. 그 이유는 다음과 같습니다:

  1. 비용 제어는 에이전트의 제1 문제입니다. 에이전트는 매우 많은 요청을 생성합니다. 지능형 라우팅이 없다면 모든 요청이 가장 비싼 모델로 전송됩니다. Semantic Router (SR)의 multi_factor 알고리즘은 비용, 지연 시간(latency), 품질을 명시적으로 가중치로 계산합니다.

  2. 키워드 기반 라우팅은 확장성이 없습니다. 에이전트가 예상치 못한 도메인을 처리하는 순간(제 Pi가 레시피 조사를 시작했는데, 제가 설정한 키워드 중에는 "사워도우 스타터 수분율"을 포함하는 것이 없었습니다), 키워드 기반 라우팅은 조용히 실패합니다.

  3. AgentGateway + SR은 프로덕션급입니다. 이것은 취미 수준의 설정이 아닙니다. AgentGateway는 Rust로 구축된 Gateway API 데이터 플레인(data plane)입니다. SR은 vLLM 프로젝트를 기반으로 하며 Go와 Rust로 작성된 Envoy ExtProc 서버입니다. 이는 50개의 모델을 사용하는 Kubernetes 클러스터에 배포할 수 있는 것과 동일한 아키텍처입니다.

  4. 코드 유지보수가 필요 없습니다. 저는 모델 설명을 작성한 이후로 라우팅 설정을 건드리지 않았습니다. SR은 제가 계속 업데이트해야 하는 규칙이 아니라, 모델 설명을 통해 학습합니다.

다음 단계

라우팅 지능 문제를 해결했으므로, 이제 저는 다음 사항에 집중하고 있습니다:

  • Observability (관측 가능성): Pi → AgentGateway → SR → Upstream LLM 및 그 역방향으로 이어지는 모든 요청을 추적하기 위해 Jaeger와 Prometheus를 연결하는 작업입니다. AgentGateway는 이미 OpenTelemetry 호환 스팬(spans)을 방출하고 있으므로, 저는 컬렉터(collectors)만 설정하면 됩니다.
  • 더 많은 모델: 이제 라우팅이 의미론적(semantic)으로 이루어지므로, YAML 파일에 새로운 모델 카드(model card)만 추가하면 특화된 모델(의료용, 법률용 등)을 추가할 수 있습니다. SR이 언제 해당 모델들을 사용할지 자동으로 판단할 것입니다.

만약 여러분이 홈랩(homelab) AI 환경을 운영 중이거나, 어떤 규모로든 에이전트(agents)를 구축하고 있다면, AgentGatewayvLLM 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가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0