LLM 시스템의 비용 최적화: 비용이 실제로 어디로 가는가
가치가 있는 곳에 토큰을 사용하세요.
LLM 비용은 사용량에 따라 선형적으로 증가합니다. 하루에 10,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: qwen2.5-1.5b
summarize:
max_tokens: 500
model: qwen2.5-7b
code_review:
max_tokens: 2000
model: qwen2.5-coder-7b
reason:
max_tokens: 4000
model: qwen2.5-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 토큰) | 로컬 비용/시간 | 손익분기점 |
|---|---|---|---|
| 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 | ~1일 4시간 |
| Qwen2.5-32B | $0.30 / $1.20 | ~$0.20 | ~1일 2시간 |
| Qwen2.5-7B | $0.10 / $0.40 | ~$0.05 | ~1일 1시간 |
하드웨어 계산:
| 하드웨어 | 초기 투자비 | 월간 전기세 | API 대비 손익분기점 |
|---|---|---|---|
| RTX 3090 (중고) | $600 | $15 | ~4개월 |
| RTX 4090 | $1,500 | $20 | ~6개월 |
| RTX 5080 | $1,000 | $18 | ~5개월 |
| DGX Spark | $2,000 | $30 | ~8개월 |
적당한 사용량(하루 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": "qwen2.5-32b", "cost": 0.001},
{"model": "qwen2.5-7b", "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": "qwen2.5-1.5b", "latency": 0.5},
{"model": "qwen2.5-7b", "latency": 2.0},
{"model": "qwen2.5-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 — 시스템 설계의 기둥: 라우팅, 비용, 가드레일 및 오케스트레이션