토큰 효율 LLM 라우터 — 로컬↔클라우드 라우팅과 예산 원장

  • llm
  • cost
  • routing
  • agents

에이전트 시스템을 빠르고 저렴하게, 그리고 비용 상한 안에 가두는 패턴.

문제

2026년 현재, 프로덕션에서 에이전트 시스템이 무너지는 1순위 원인은 환각도 잘못된 도구 호출도 아닙니다 — 비용입니다. 에이전트는 루프를 돕니다. 재시도가 쌓입니다. 몇 센트면 끝났어야 할 멀티스텝 계획이 조용히 수천 번의 모델 호출로 번져 나갑니다. 이 바닥 사람이라면 한 번쯤 들어봤을 경고담: 방치된 에이전트가 아무도 눈치채기 전에 63시간 동안 약 $4,200을 태웠습니다 — 악의도, 모델 버그도 아닌, 그냥 매 스텝마다 프런티어 모델을 부르는 무한 루프 하나 때문에요.

순진한 해법은 “더 싼 모델 쓰자”입니다. 너무 둔한 도구죠. 싼 모델은 어려운 20%에서 실패하고, 프런티어 모델로의 폴백은 보통 이미 실패한 싼 시도의 비용을 치른 뒤에 일어납니다. 돈은 재시도에서 샙니다.

더 나은 해법은 라우터입니다 — 토큰을 쓰기 전에, 각 요청을 어디서 돌릴지(그리고 애초에 돌릴지 말지)를 정합니다. 이걸 작동시키는 세 부품:

  1. 결정론적 난이도 분류기(제로 토큰) — 쉬운 일은 로컬로, 어려운 일은 클라우드로.
  2. 프롬프트 캐싱 — 반복되는 컨텍스트에 두 번 과금되지 않게.
  3. 예산 원장(budget ledger) + 하드 상한 + 실패 에스컬레이션 — 폭주 루프가 신용카드 한도가 아니라 에 부딪히도록.

이게 제 ai-research-lab 라우터의 패턴입니다. 만드는 법을 정리합니다.

접근

1. 모델 호출 없이 난이도 분류

본능적으로 LLM에게 “이거 어려워?“라고 묻고 싶지만 — 그건 매 요청마다 토큰을 쓰는, 닭이 먼저냐 달걀이 먼저냐의 비용입니다. 대신 싼 신호로 결정론적으로 분류합니다:

  • 입력 길이와 구조
  • 코드·수식·멀티홉 추론 표지의 존재
  • 필요한 도구 호출 / 외부 조회
  • 호출자의 명시적 힌트(상류에서 넘어온 complexity 플래그)

정규식+휴리스틱 분류기는 0 토큰, 마이크로초 단위로 돕니다. 완벽할 필요 없습니다 — 싸고 보수적이면 됩니다. 애매하면 에스컬레이트하고, 예산 원장이 최후의 안전망이 됩니다.

2. 라우팅: 쉬움 → 로컬, 어려움 → 클라우드

쉬운 요청(요약, 분류, 포매팅, 짧은 Q&A)은 로컬 Ollama 모델로. 한계비용이 사실상 0이고 네트워크 지연도 없습니다. 진짜 복잡한 요청 — 장기 추론, 코드 생성, 분류기가 높게 표시한 것 — 만 클라우드 토큰을 쓸 수 있게 허용합니다.

3. 공격적으로 캐싱

에이전트 토큰 소비의 큰 비중은 같은 컨텍스트를 다시 보내는 것입니다: 시스템 프롬프트, 도구 스키마, 검색된 문서. 프롬프트 캐싱은 그 컨텍스트를 한 번만 과금하고 이후엔 싸게 참조하게 합니다. 시스템 프롬프트와 도구 표면이 안정적인 에이전트라면, 이것만으로도 큰 절감입니다.

4. 원장 + 상한 + 에스컬레이션

모든 호출(로컬이든 클라우드든)은 예산 원장에 기록됩니다. 호출 전에 라우터가 남은 예산을 확인합니다. 어떤 요청이 상한을 넘길 것 같으면 조용히 실행하지 않고 거부하거나 다운그레이드합니다. 로컬 실패는 클라우드로 한 번만 에스컬레이트하며, 그 에스컬레이션 자체도 예산에 계상됩니다.

코드 스케치

일반적·예시용 — 비밀키 없음, 특정 벤더 키 없음.

from dataclasses import dataclass, field
import re

@dataclass
class Ledger:
    ceiling_usd: float
    spent_usd: float = 0.0
    calls: list = field(default_factory=list)

    def can_afford(self, est_usd: float) -> bool:
        return (self.spent_usd + est_usd) <= self.ceiling_usd

    def charge(self, route: str, usd: float):
        self.spent_usd += usd
        self.calls.append((route, usd))


def classify_difficulty(prompt: str, hint: str | None = None) -> str:
    """제로 토큰, 결정론적. 'simple' 또는 'complex' 반환."""
    if hint in {"simple", "complex"}:
        return hint
    signals = 0
    if len(prompt) > 2000:
        signals += 1
    if re.search(r"```|def |class |SELECT |import ", prompt):
        signals += 1          # 코드 존재
    if re.search(r"\b(prove|derive|optimi[sz]e|multi-step|plan)\b", prompt, re.I):
        signals += 1          # 추론 표지
    if re.search(r"\b(then|after that|finally|step \d)\b", prompt, re.I):
        signals += 1          # 멀티홉
    return "complex" if signals >= 2 else "simple"


# 대략적 호출당 비용(USD); 로컬은 한계비용 ~0
COST = {"local": 0.0, "cloud": 0.03}

def route(prompt: str, ledger: Ledger, hint: str | None = None) -> str:
    difficulty = classify_difficulty(prompt, hint)
    target = "local" if difficulty == "simple" else "cloud"

    if not ledger.can_afford(COST[target]):
        # 하드 상한: 다운그레이드하거나 거부, 절대 조용히 초과지출 금지
        if target == "cloud" and ledger.can_afford(COST["local"]):
            target = "local"   # 우아하게 격하
        else:
            raise BudgetExceeded(f"would exceed ceiling at {ledger.spent_usd:.2f}")

    result, ok = call_model(target, prompt)   # 당신의 모델 어댑터

    # 로컬 우선 + 통제된 단일 에스컬레이션
    if not ok and target == "local" and ledger.can_afford(COST["cloud"]):
        ledger.charge("local", COST["local"])
        result, ok = call_model("cloud", prompt)
        target = "cloud"

    ledger.charge(target, COST[target])
    return result


class BudgetExceeded(Exception):
    pass

핵심은 정확한 휴리스틱이 아니라 형태입니다: 공짜 분류기, 라우팅 결정, 지출 의 원장 확인, 그리고 그 자체가 예산에 잡히는 에스컬레이션.

결과와 트레이드오프

이 패턴이 실제로 사주는 것:

  • 대다수 요청이 클라우드를 건드리지 않는다. 전형적인 에이전트 워크로드에서 단순·반복 호출이 건수로 압도적입니다. 이걸 로컬로 보내면 물량의 대부분이 한계비용 ~0이 됩니다.
  • 비용이 ’희망’이 아니라 ’경계’가 된다. 폭주 루프는 원장 상한에 부딪혀 멈춥니다. “$4,200 폭탄”이 “작업이 $20 예산에서 멈추고 나를 호출함”으로 바뀝니다.
  • 지연이 준다 — 로컬로 보낸 다수는 네트워크 왕복이 없습니다.

받아들이는 트레이드오프:

  • 분류기 오류는 비용이다. “쉬움”으로 잘못 보낸 로컬 요청이 실패하면 로컬 시도 더하기 에스컬레이션을 치릅니다. 분류기를 보수적으로 유지하고 정밀도를 측정하세요.
  • 로컬 용량은 실제 제약이다. 하드웨어와 모델 관리(웜 모델, keep_alive)가 없으면 로컬 지연이 튑니다.
  • 원장은 정확한 비용 추정이 필요하다. 추정이 엉터리면 상한도 무의미합니다. 추정 vs 실제 지출을 정기적으로 대조하세요.

교훈

  • 쓰기 전에 결정하라. 가장 싼 토큰은 우회해서 안 쓴 토큰이다. 모든 요청 앞단의 제로 토큰 분류기가, 뒷단의 약간 더 싼 모델보다 가치 있다.
  • 예산 상한은 재무 기능이 아니라 안전 기능이다. 파국적 폭주를 기록되고 복구 가능한 정지로 바꾸는 차단기다.
  • 로컬 우선은 비용 전략인 동시에 지연 전략이다. 클라우드는 기본값이 아니라 비싼 에스컬레이션 경로로 다뤄라.
  • 라우팅 정밀도를 모델 정확도처럼 측정하라. 라우터는 당신 워크로드의 모델이다; 틀리면 하류 전부가 비용을 치른다.

다음에 만들 것: 원장의 에스컬레이션 이력에서 라우팅 임계값을 스스로 학습하는 적응형 분류기 — 손으로 맞춘 휴리스틱 대신, 워크로드가 바뀌면 로컬/클라우드 분배를 자동으로 튜닝하도록.