본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 06. 03:59

분류 체계 수술, Cosine = 1.0000, 그리고 라우팅을 인프라 속으로 사라지게 만들기

요약

Adaptive Model Routing 시리즈의 3부로, 임베딩 기반 라우팅 시스템의 카테고리 분류 체계를 최적화하는 과정을 다룹니다. 데이터의 기하학적 구조에 맞춰 불필요한 카테고리를 통합함으로써 정확도를 높이고 시스템을 효율화하는 방법을 설명합니다.

핵심 포인트

  • 분류 체계(Taxonomy)는 모델의 임베딩 기하학적 구조에 맞춰 설계해야 함
  • 동일한 티어를 공유하며 혼동되는 카테고리는 통합하여 정확도 개선 가능
  • 데이터 재라벨링을 통해 다운타임 없이 시스템 성능 향상 가능
  • 라우팅 로직을 애플리케이션 계층에서 인프라 계층으로 이동하는 방향성 제시

이 글은 Adaptive Model Routing 시리즈의 3부입니다. 1부에서는 Groq을 사용하여 8개 카테고리, 3개 티어(tier)로 구성된 LLM 분류기를 구축했습니다. 2부에서는 섀도 모드(shadow mode)에서 k-NN 임베딩 조회(embedding lookup)를 추가하여 83%의 티어 정확도를 확인했고, 이론적으로 61%의 비용 절감을 발견했습니다. 이 포스트에서는 그 이후에 일어난 일을 다룹니다.

Phase 2가 끝났을 때, 저는 crab-bot 내부의 섀도 모드에서 작동하는 임베딩 풀(embedding pool)을 확보한 상태였습니다. 카테고리 정확도는 78.6%에 머물러 있었습니다. 나쁘지 않은 수치였지만, 세부 내역에는 살펴볼 가치가 있는 무언가가 숨겨져 있었습니다.

Phase 3: 검증 결과 특정 카테고리가 존재할 필요가 없음을 알게 되었을 때

카테고리별 Leave-one-out 정확도가 진짜 이야기를 들려주었습니다:

카테고리정확도티어 (Tier)
casual94%cheap
...

두 개의 카테고리는 기본적으로 동전 던지기 수준이었습니다. 그리고 그들은 서로를 혼동하고 있었습니다. analysis의 오분류 대부분이 research_lookup에 집중되었고, 그 반대도 마찬가지였습니다.

당연한 조치는 분류기 프롬프트(prompt)를 수정하거나, LLM을 튜닝하거나, 더 많은 라벨링된 데이터를 수집하는 것이었습니다. 제가 그 길로 가려던 찰나, 정확도 옆의 열을 주목하게 되었습니다. 두 카테고리 모두 **동일한 티어(tier)**인 Medium에 매핑되어 있었습니다.

그것이 모든 것을 바꾸어 놓았습니다. 질문은

-- category가 'research_lookup'이었던 243개 행의 라벨을 재지정합니다.
UPDATE routing_log
SET category = 'analysis'
...

임베딩 (embeddings)은 변하지 않았습니다. 벡터 (vectors)는 이미 올바른 상태였으며, 단지 그와 함께 저장된 라벨 (label)만이 잘못되어 있었을 뿐입니다. 향후 모든 감사 쿼리 (audit query)가 매핑 시대를 기준으로 필터링할 수 있도록 설정 (config)에서 tier_mapping_versionv1에서 v2로 올렸습니다.

결과: 전체 카테고리 정확도 (category accuracy)가 78.6%에서 **82.0%**로 상승했습니다 (+3.4%). 특히 Medium 티어 정확도는 79.9%에서 **82.1%**로 올라갔습니다. 7개였던 카테고리는 6개가 되었습니다. 다운타임 (downtime)은 전혀 없었으며, 단지 봇 재시작만 수행했습니다.

이 과정을 통해 얻은 원칙은 다음과 같습니다: 분류 체계 (taxonomy)는 모델의 기하학적 구조 (geometry)에 맞춰야 하며, 그 반대가 되어서는 안 된다. 검증 지표 (validation metric)가 두 카테고리를 구별할 수 없다고 말하면서 동시에 두 카테고리가 동일한 목적지를 공유하고 있다면, 그 경계는 잘못된 것입니다. 삭제하십시오.

4단계: 라우터를 인프라 속으로 이동시키기

이 시점에서 라우팅 로직 (routing logic)은 특정 애플리케이션인 crab-bot 내부에 존재했습니다. 이는 스마트한 모델 선택을 원하는 다른 클라이언트가 있다면, 각자 자신만의 분류기 (categorizer)를 구축하고, 자신만의 임베딩 풀 (embedding pool)을 유지하며, 자신만의 세션 캐시 (session cache)를 관리해야 함을 의미했습니다. 이를 복제하는 것은 매우 많은 작업입니다.

thrift-flow는 이미 나의 모든 모델 호출 앞에 위치하고 있는 OpenAI 호환 LLM 프록시 (proxy)입니다. 이곳이 라우팅을 위한 자연스러운 안식처였습니다.

나는 thrift-flow의 proxy/router.pyEmbeddingRouterModelRouter를 추가했습니다. 동일한 intfloat/multilingual-e5-small 모델을 사용하였고, e5 제품군이 요구하는 동일한 query: / passage: 접두사 (prefix) 관례를 따랐습니다. 하지만 풀 마이그레이션 (pool migration)을 진행하기 전에 한 가지 질문에 답해야 했습니다: crab-bot의 모델 인스턴스에서 생성된 임베딩이 thrift-flow가 생성할 임베딩과 호환되는가?

5분간의 확인 작업:

from sentence_transformers import SentenceTransformer
import numpy as np

...

코사인 유사도 (Cosine similarity) 1.0000. 동일한 모델 가중치 (model weights), 동일한 접두사 관례 — 완전히 동일한 벡터 공간 (vector space)이었습니다. 풀 (pool)은 완벽하게 이식 가능했습니다.

저는 crab-bot의 routing.db에서 1,311개의 항목을 마이그레이션(migration)했습니다. 중복 제거(동일한 프롬프트 해시가 여러 번 나타나는 경우)를 거친 후, thrift-flow는 876개의 고유 풀(pool) 항목으로 안착했으며, 이는 k-NN 조회를 활성화하기 위한 최소 기준인 20개를 훨씬 상회하는 수치였습니다. 이를 섀도 모드(shadow mode)로 전환하고 배포했습니다.

서버 측 배선(wiring)은 간단합니다. model="auto"와 함께 요청이 들어오고 라우팅(routing)이 활성화되어 있으면, ModelRouter가 가로챕니다:

if model_requested == "auto" and _model_router is not None:
    _last_user_msg = next(
        (m.get("content") for m in reversed(messages)
...

이제 thrift-flow에 연결하는 모든 클라이언트는 model="auto"를 설정함으로써 적응형 라우팅(adaptive routing)을 이용할 수 있습니다. 클라이언트는 티어(tier), 임베딩(embedding), 또는 분류기(categorizer)에 대해 아무것도 알 필요가 없습니다.

5단계: crab-bot이 순수한 챗봇이 되다

thrift-flow가 라우팅을 처리하게 되면서, crab-bot 자체의 ModelRouter는 이제 불필요한 짐이 되었습니다. 더 나쁜 것은, 두 개의 라우팅 레이어를 병렬로 실행하면 분류를 위한 Groq API 호출이 두 배로 늘어나고 잠재적으로 충돌하는 결정이 내려질 수 있다는 점이었습니다.

마이그레이션은 세 가지 설정 변경으로 끝났습니다:

# 이전
OPENAI_API_BASE = "https://api.openai.com/v1"
AI_MODEL = "gpt-5.5"
...

그리고 crab-bot의 라우팅 설정에서:

llm_categorizer_enabled: false
embedding_lookup_enabled: false

그게 전부입니다. crab-bot은 "모델 라우팅도 수행하는 챗봇"에서 "챗봇"으로 변모했습니다. 분류(categorization), 임베딩 조회(embedding lookup), 세션 캐싱(session caching), 로깅(logging)을 포함한 모든 라우팅 로직은 이제 thrift-flow에서 실행되며 애플리케이션 레이어(application layer)에서는 보이지 않습니다.

thrift-flow는 모델 별칭(model aliases)이 구성된 상태로 8888 포트에 배포됩니다:

models:
  aliases:
    cheap:  "openai/gpt-5.4-mini"
...

crab-bot이 model="auto"와 함께 요청을 보내면, thrift-flow는 이를 분류하고, 티어를 선택하며, 결정을 기록한 뒤 실제 모델로 전달합니다. 봇의 코드는 다시는 티어 이름을 직접 다룰 일이 없습니다.

이 시리즈가 실제로 나에게 가르쳐준 것

검증 지표(Validation metrics)는 특정 카테고리가 존재할 필요가 없는 시점을 알려줄 수 있습니다. 저는 분석 결과가 59%의 정확도를 기록한 것에 대해 걱정하며 시간을 보냈습니다. 하지만 정작 걱정해야 했던 것은 그 혼동(confusion)이 잘못된 라우팅(routing) 결정으로 이어지는지 여부였습니다. 결과는 그렇지 않았습니다. 모델이 틀린 것이 아니라 분류 체계(taxonomy)가 틀렸던 것입니다.

모델과 접두사(prefix)를 제어할 수 있다면 임베딩(Embeddings)은 이식 가능합니다. 코사인(cosine) 체크는 5분밖에 걸리지 않았고, 1,300개의 학습 예시를 시스템 간에 이동시키는 과정의 리스크를 완전히 제거했습니다. 동일한 체크포인트(checkpoint)에서 나온 모델을 동일한 입력 형식으로 사용한다면, 동일한 벡터 공간(vector space)을 얻게 될 것입니다. 수학을 믿으세요.

운영 데이터를 안전하게 재라벨링(Re-labeling)하는 것은 대부분 스키마(schema) 문제입니다. 라우팅 로그에 tier_mapping_version을 포함해 두었기에 확신을 가지고 UPDATE 문을 실행할 수 있었습니다. 향후 어떤 쿼리라도 현재 매핑에 해당하는 행만 필터링할 수 있기 때문입니다. 재라벨링은 데이터 파이프라인이 아닌 단 한 줄의 SQL 문으로 끝났습니다.

라우팅은 애플리케이션이 아닌 인프라(infrastructure)에 속해야 합니다. 5단계(Phase 5) 이전에는 새로운 클라이언트에게 스마트 라우팅을 추가하려면 방대한 코드를 복사해야 했습니다. 5단계 이후에는 model="auto"를 설정하고 올바른 베이스 URL(base URL)을 가리키기만 하면 됩니다. 애플리케이션 계층은 라우팅 메커니즘을 알 필요가 없어야 합니다.

현재 풀(pool)은 876개의 항목으로 구성되어 있으며 계속 늘어나고 있습니다. 다음 단계는 thrift-flow의 임베딩 라우터를 섀도우(shadow) 모드에서 라이브(live) 모드로 전환하는 것입니다. 그리고 k-NN과 LLM 분류기 간의 일치 여부를 측정하여, 신뢰도가 높은 풀 히트(pool hits)의 경우 Groq 호출을 완전히 제거하는 것이 타당한지 확인할 것입니다. 바로 그 지점에서 진정한 지연 시간(latency) 절감 효과가 나타납니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0