LLM 시스템의 비용 최적화: 돈이 실제로 어디로 가는가
중요한 곳에 토큰을 투자하세요.
LLM(대형 언어 모델) 비용은 사용량에 따라 선형적으로 증가합니다. 하루에 1,000개의 요청을 처리하고 요청당 비용이 $0.01인 시스템의 경우, 일일 비용은 $100이며 연간 비용은 $365입니다. 기업 규모에서는 이 비용이 $10,000을 넘을 수 있습니다.
비용 최적화는 구색을 맞추기 위한 것이 아닙니다. 중요한 곳에 토큰을 올바르게 사용하는 것입니다.
낭비하는 토큰 하나하나가 더 나은 답변을 위해 사용할 수 있는 토큰입니다.

토큰 예산 관리
비용을 통제하는 가장 간단한 방법은 제한을 설정하는 것입니다. 세션당, 작업당, 또는 일일 기준으로 설정할 수 있습니다.
전략 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개의 요청만 하는 시스템에 대해 예산 관리, 폴백, 캐싱의 복잡성은 가치가 없습니다.
먼저 기본 흐름이 작동하도록 하십시오. 청구서가 도착할 때 최적화를 추가하십시오.
트레이드오프
| 전략 | 비용 | 품질 | 복잡도 |
|---|---|---|---|
| 최적화 없음 | 가장 높음 | 일관됨 | 가장 낮음 |
| 토큰 예산 관리 | 중간 | 변동 있음 | 중간 |
| 폴백 모델 | 낮음~중간 | 변동 있음 | 중간 |
| 캐싱 | 가장 낮음 | 높음 (캐시 히트 시) | 중간 |
| 하이브리드 | 최적화됨 | 최적화됨 | 가장 높음 |
프로덕션 시스템은 일반적으로 하이브리드 방식을 사용합니다. 세션당 예산을 설정하고, 품질 또는 지연 시간에 따라 폴백하며, 가능한 것을 캐싱합니다. 복잡성은 실제 존재하지만, 절감 효과도 실제 존재합니다.
관련 자료
- Model Routing Strategies — 기능 기반, 비용 인지, 지연 시간 인지 라우팅
- LLM Guardrails in Practice — 입력 검증, 출력 필터링, 안전성
- Multi-Model System Design — 다중 모델 아키텍처
- LLM Architecture — 시스템 설계의 핵심: 라우팅, 비용, 가드레일, 오케스트레이션