LLM 시스템의 비용 최적화: 돈이 실제로 어디로 가는가

중요한 곳에 토큰을 투자하세요.

Page content

LLM(대형 언어 모델) 비용은 사용량에 따라 선형적으로 증가합니다. 하루에 1,000개의 요청을 처리하고 요청당 비용이 $0.01인 시스템의 경우, 일일 비용은 $100이며 연간 비용은 $365입니다. 기업 규모에서는 이 비용이 $10,000을 넘을 수 있습니다.

비용 최적화는 구색을 맞추기 위한 것이 아닙니다. 중요한 곳에 토큰을 올바르게 사용하는 것입니다.

낭비하는 토큰 하나하나가 더 나은 답변을 위해 사용할 수 있는 토큰입니다.

LLM cost optimization strategies

토큰 예산 관리

비용을 통제하는 가장 간단한 방법은 제한을 설정하는 것입니다. 세션당, 작업당, 또는 일일 기준으로 설정할 수 있습니다.

전략 1: 세션당 예산

세션당 예산은 직관적입니다:

class SessionBudget:
    def __init__(self, budget_tokens: int = 10000):
        self.budget = budget_tokens
        self.used = 0

    def allocate(self, tokens: int) -> bool:
        if self.used + tokens <= self.budget:
            self.used += tokens
            return True
        return False

    def remaining(self) -> int:
        return self.budget - self.used

전략 2: 작업당 예산

작업당 예산은 더 유용합니다. 서로 다른 작업에는 서로 다른 양의 컨텍스트가 필요하기 때문입니다:

task_budgets:
  classify:
    max_tokens: 100
    model: qwen3-1.7b
  summarize:
    max_tokens: 500
    model: qwen3-8b
  code_review:
    max_tokens: 2000
    model: qwen2.5-coder-7b
  reason:
    max_tokens: 4000
    model: qwen3-32b

전략 3: 적응형 예산

적응형 예산은 실제 발생 상황에 따라 조정됩니다. 분류 작업이 일관되게 80개의 토큰을 사용한다면, 100개를 할당하는 것을 중단해야 합니다:

class AdaptiveBudget:
    def __init__(self):
        self.task_history = {}

    def allocate(self, task_type: str) -> int:
        if task_type in self.task_history:
            return int(self.task_history[task_type] * 1.5)
        return 1000

    def record(self, task_type: str, tokens_used: int):
        if task_type not in self.task_history:
            self.task_history[task_type] = tokens_used
        else:
            self.task_history[task_type] = (
                0.9 * self.task_history[task_type] + 0.1 * tokens_used
            )

지수 이동 평균(0.9 가중치)은 최근 사용량이 과거 기록보다 더 중요함을 의미합니다. 작업 부하의 변동성에 따라 가중치를 조정하십시오.

API 대 로컬 추론

규모가 커질수록 로컬 추론이 더 저렴합니다. 손익분기점은 하드웨어와 API 요금에 따라 달라집니다.

모델 API ($/M tokens) 로컬 비용/시간 손익분기점
GPT-4o $2.50 / $10.00 N/A
Claude Sonnet 4 $3.00 / $15.00 N/A
Qwen2.5-72B $0.50 / $2.00 ~$0.50 ~4 hours/day
qwen3-32b $0.30 / $1.20 ~$0.20 ~2 hours/day
qwen3-8b $0.10 / $0.40 ~$0.05 ~1 hour/day

하드웨어 비용 계산:

하드웨어 초기 비용 월간 전기세 API 대비 손익분기점
RTX 3090 (중고) $600 $15 ~4 months
RTX 4090 $1,500 $20 ~6 months
RTX 5080 $1,000 $18 ~5 months
DGX Spark $2,000 $30 ~8 months

적당한 사용량(하루 1시간 이상)에서는 로컬 추론이 자체 비용을 상쇄합니다. 고사용량에서는 절감 효과가 현저합니다. 단점이라면 초기 자본 투자가 필요하다는 것입니다. RTX 5080은 $1,000입니다. API 요금은 일시 중지할 수 있지만, 하드웨어는 그렇지 않습니다.

폴백(Fallback) 전략

선호하는 모델이 너무 비싸거나 느릴 때, 더 저렴한 대안으로 폴백합니다. 핵심은 품질이 “충분하다"는 시점을 아는 것입니다.

전략 1: 품질 기반 폴백

품질 기반 폴백은 출력이 임계값을 충족할 때까지 모델을 시도합니다:

class QualityFallback:
    def __init__(self, quality_threshold: float = 0.8):
        self.threshold = quality_threshold
        self.models = [
            {"model": "claude-sonnet-4", "cost": 0.015},
            {"model": "qwen2.5-72b", "cost": 0.002},
            {"model": "qwen3-32b", "cost": 0.001},
            {"model": "qwen3-8b", "cost": 0.0004},
        ]

    def route(self, prompt: str) -> str:
        for model_config in self.models:
            result = self.call_model(model_config["model"], prompt)
            if self.evaluate_quality(result) >= self.threshold:
                return result
        return self.call_model(self.models[0]["model"], prompt)

문제는 평가 자체에 있습니다. 다른 모델을 호출하지 않고 어떻게 품질을 측정할 수 있을까요? 일부 시스템은 작은 분류기를 사용합니다. 다른 시스템은 휴리스틱 검사(길이, 구조, 키워드 존재 여부)를 사용합니다. 이 방법들은 완벽하지 않습니다.

전략 2: 지연 시간 기반 폴백

지연 시간 기반 폴백은 더 간단합니다. 시간 예산을 충족하는 가장 빠른 모델로 라우팅합니다:

class LatencyFallback:
    def __init__(self, max_latency: float = 5.0):
        self.max_latency = max_latency
        self.models = [
            {"model": "qwen3-1.7b", "latency": 0.5},
            {"model": "qwen3-8b", "latency": 2.0},
            {"model": "qwen3-32b", "latency": 10.0},
            {"model": "claude-sonnet-4", "latency": 5.0},
        ]

    def route(self, prompt: str) -> str:
        for model_config in sorted(self.models, key=lambda x: x["latency"]):
            if model_config["latency"] <= self.max_latency:
                return self.call_model(model_config["model"], prompt)
        return self.call_model(self.models[0]["model"], prompt)

캐싱

캐싱은 가장 과소평가된 비용 최적화 방법입니다. 동일한 프롬프트가 생각보다 더 자주 발생합니다 — 분류 요청, FAQ 스타일의 쿼리, 반복적인 도구 호출 등.

전략 1: 프롬프트 캐싱

정확한 프롬프트 캐싱은 간단합니다:

import hashlib

class PromptCache:
    def __init__(self, max_size: int = 1000):
        self.cache = {}
        self.max_size = max_size

    def get(self, prompt: str) -> str | None:
        key = hashlib.sha256(prompt.encode()).hexdigest()
        return self.cache.get(key)

    def set(self, prompt: str, response: str):
        key = hashlib.sha256(prompt.encode()).hexdigest()
        if len(self.cache) >= self.max_size:
            self.cache.pop(next(iter(self.cache)))
        self.cache[key] = response

전략 2: 시맨틱 캐싱

시맨틱 캐싱은 더 유용합니다. 서로 다르지만 동일한 의미를 가진 프롬프트를 캐치합니다:

from sentence_transformers import SentenceTransformer

class SemanticCache:
    def __init__(self, similarity_threshold: float = 0.95):
        self.model = SentenceTransformer('all-MiniLM-L6-v2')
        self.cache = {}
        self.threshold = similarity_threshold

    def get(self, prompt: str) -> str | None:
        prompt_embedding = self.model.encode([prompt])[0]
        for cached_prompt, cached_response in self.cache.items():
            cached_embedding = self.model.encode([cached_prompt])[0]
            similarity = self.cosine_similarity(
                prompt_embedding, cached_embedding
            )
            if similarity >= self.threshold:
                return cached_response
        return None

    def set(self, prompt: str, response: str):
        self.cache[prompt] = response

임계값은 중요합니다. 0.95는 공격적입니다 — 매우 유사한 프롬프트만 매칭됩니다. 0.85는 더 관대하지만 잘못된 답변을 반환할 위험이 있습니다. 미스율을 측정하고 조정하십시오.

일반적인 쿼리에 대한 응답 캐싱도 가치가 있습니다. 사용자가 “날씨가 어때?“나 “몇 시야?“를 반복해서 묻는다면, 정확한 프롬프트뿐만 아니라 패턴을 캐싱하십시오:

class ResponseCache:
    def __init__(self):
        self.common_queries = {
            "what is the weather": "Check weather API",
            "what is the time": "Check system time",
            "who is the president": "Check current president",
        }

    def get(self, query: str) -> str | None:
        query_lower = query.lower()
        for common_query, response in self.common_queries.items():
            if common_query in query_lower:
                return response
        return None

이 방법은 정교하지는 않지만 작동합니다. 일반적인 쿼리는 일반적인 이유 때문에 존재합니다.

최적화가 도움이 되는 경우

대량 처리, 혼합 작업 부하 실행, 또는 누적되는 API 비용을 지불할 때 최적화가 중요합니다.

의사결정 과정(프로토타이핑), 단일 모델 사용, 또는 저부하 처리 시에는 중요하지 않습니다. 하루 100개의 요청만 하는 시스템에 대해 예산 관리, 폴백, 캐싱의 복잡성은 가치가 없습니다.

먼저 기본 흐름이 작동하도록 하십시오. 청구서가 도착할 때 최적화를 추가하십시오.

트레이드오프

전략 비용 품질 복잡도
최적화 없음 가장 높음 일관됨 가장 낮음
토큰 예산 관리 중간 변동 있음 중간
폴백 모델 낮음~중간 변동 있음 중간
캐싱 가장 낮음 높음 (캐시 히트 시) 중간
하이브리드 최적화됨 최적화됨 가장 높음

프로덕션 시스템은 일반적으로 하이브리드 방식을 사용합니다. 세션당 예산을 설정하고, 품질 또는 지연 시간에 따라 폴백하며, 가능한 것을 캐싱합니다. 복잡성은 실제 존재하지만, 절감 효과도 실제 존재합니다.

관련 자료

구독하기

시스템, 인프라, AI 엔지니어링에 관한 새 글을 받아보세요.