모델 운영

MLOPS / 중급

LLM 비용 추적: 토큰 낭비 없이 운영하기

LLM 서비스의 토큰 비용 구조를 이해하고, 프롬프트 캐싱·시맨틱 캐싱·모델 라우팅·배치 API로 품질을 유지하면서 비용을 절감하는 LLMOps 실전 전략을 다룹니다.

PALDYN Team7 MIN READ

지난 글에서 LLM 시스템의 관측성을 구축하는 방법을 살펴봤다. 관측성이 제대로 갖춰지면 자연스럽게 보이는 것이 있다. 비용이다. 토큰이 어디서 얼마나 쓰이는지 측정하지 않으면 월 청구서를 보고 나서야 놀라게 된다.

프로덕션 LLM 시스템에서 비용은 사용자 수와 거의 선형으로 증가한다. 스케일업할수록 비용이 기하급수적으로 커지는 구조다. 하루 100개 요청일 때는 문제없던 비용 구조가 하루 10만 개 요청이 되면 월 수천 달러 청구서로 돌아온다. 이 글에서는 품질을 희생하지 않고 LLM 비용을 체계적으로 줄이는 방법을 다룬다.

비용 구조 이해

LLM 비용 구조 분해

LLM API 비용의 공식은 단순하다: (입력 토큰 × 입력 단가 + 출력 토큰 × 출력 단가). 그러나 실무에서는 이 간단한 공식 안에 세 가지 숨겨진 비용 드라이버가 있다.

시스템 프롬프트: 수백~수천 토큰의 시스템 프롬프트가 모든 요청에 반복 청구된다. 하루 1만 건 요청에 500토큰 시스템 프롬프트면 하루 500만 토큰이 순수하게 반복 비용이다.

RAG 컨텍스트: 검색된 청크가 프롬프트에 들어가면서 입력 토큰이 급증한다. top-k=10에 청크당 500토큰이면 요청당 5000토큰이 컨텍스트만으로 소비된다.

출력 토큰: 입력보다 단가가 3~5배 비싸다. 불필요하게 긴 출력이 비용을 폭발적으로 키운다.

전략 1: 프롬프트 캐싱 (가장 임팩트 큼)

Claude API는 cache_control로 프롬프트 캐싱을 지원한다. 캐시 히트 시 캐시된 토큰은 원래 입력 단가의 10%만 청구한다. 90% 할인이다.

import anthropic

client = anthropic.Anthropic()

# 긴 시스템 프롬프트를 캐시 대상으로 지정
SYSTEM_PROMPT = """당신은 법률 문서 분석 전문가입니다.
다음 지침을 따르세요:
1. 모든 법적 용어는 한국어로 설명합니다.
2. 계약 위험 사항은 반드시 표시합니다.
3. ...
[매우 긴 지침 - 2000토큰]"""

def analyze_contract(document: str) -> str:
    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=1024,
        system=[
            {
                "type": "text",
                "text": SYSTEM_PROMPT,
                "cache_control": {"type": "ephemeral"},  # 캐시 지정
            }
        ],
        messages=[{"role": "user", "content": f"다음 계약서를 분석하세요:\n\n{document}"}],
    )
    # 첫 호출: 시스템 프롬프트 2000토큰 전액 청구
    # 이후 호출: 2000토큰 중 10%만 청구 (= 200토큰 가격)
    return response.content[0].text

캐시는 5분간 유지된다. 동일 시스템 프롬프트를 쓰는 다중 사용자 환경에서 효과가 극대화된다.

전략 2: 시맨틱 캐싱

의미적으로 동일한 질문에 이미 생성한 답변을 재사용한다. "파이썬이란?"과 "파이썬이 뭔가요?"는 동일한 답변을 돌려줄 수 있다.

import numpy as np
from functools import lru_cache

class SemanticCache:
    def __init__(self, threshold: float = 0.92):
        self.threshold = threshold
        self.cache: list[dict] = []  # {embedding, response}
        self.embed_model = SentenceTransformer("paraphrase-multilingual-MiniLM-L12-v2")

    def _cosine(self, a: np.ndarray, b: np.ndarray) -> float:
        return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))

    def get(self, query: str) -> str | None:
        q_emb = self.embed_model.encode(query)
        for entry in self.cache:
            sim = self._cosine(q_emb, entry["embedding"])
            if sim >= self.threshold:
                return entry["response"]
        return None

    def set(self, query: str, response: str):
        self.cache.append({
            "embedding": self.embed_model.encode(query),
            "response": response,
        })

cache = SemanticCache()

def cached_llm_call(query: str) -> str:
    hit = cache.get(query)
    if hit:
        return hit  # LLM 호출 없이 반환 → 비용 0
    
    response = llm_generate(query)
    cache.set(query, response)
    return response

전략 3: 지능형 모델 라우팅

비용 최적화: 지능형 모델 라우팅

def classify_complexity(question: str) -> str:
    """질문을 복잡도에 따라 분류"""
    word_count = len(question.split())
    
    # 단순 규칙 기반 분류
    simple_patterns = ["번역해", "요약해", "정의해", "뭐야", "알려줘"]
    complex_patterns = ["분석해", "설계해", "비교해서", "왜", "어떻게 최적화"]
    
    is_simple = any(p in question for p in simple_patterns) and word_count < 30
    is_complex = any(p in question for p in complex_patterns) or word_count > 100
    
    if is_simple:
        return "haiku"
    elif is_complex:
        return "opus"
    return "sonnet"

MODEL_MAP = {
    "haiku": "claude-haiku-4-5-20251001",
    "sonnet": "claude-sonnet-4-6",
    "opus": "claude-opus-4-7",
}

def smart_llm_call(question: str, messages: list) -> str:
    tier = classify_complexity(question)
    model = MODEL_MAP[tier]
    
    response = client.messages.create(
        model=model,
        max_tokens=1024,
        messages=messages,
    )
    log_cost(model=model, usage=response.usage, feature="smart_routing")
    return response.content[0].text

전략 4: 배치 API 활용

실시간 응답이 필요 없는 태스크(리포트 생성, 대량 번역, 오프라인 분류)는 배치 API를 사용한다.

# Claude Message Batches API (최대 50% 할인)
import anthropic

client = anthropic.Anthropic()

# 배치 요청 생성
batch = client.messages.batches.create(
    requests=[
        {
            "custom_id": f"doc-{i}",
            "params": {
                "model": "claude-haiku-4-5-20251001",
                "max_tokens": 512,
                "messages": [{"role": "user", "content": f"다음을 3줄로 요약: {doc}"}],
            },
        }
        for i, doc in enumerate(documents)
    ]
)

# 비동기로 결과 폴링 (최대 24시간)
import time
while batch.processing_status == "in_progress":
    time.sleep(60)
    batch = client.messages.batches.retrieve(batch.id)

# 결과 수집
for result in client.messages.batches.results(batch.id):
    if result.result.type == "succeeded":
        print(result.custom_id, result.result.message.content[0].text)

비용 대시보드 구성

from dataclasses import dataclass, field
from collections import defaultdict
import datetime

@dataclass
class CostTracker:
    daily_costs: dict = field(default_factory=lambda: defaultdict(float))
    feature_costs: dict = field(default_factory=lambda: defaultdict(float))
    
    PRICES = {
        "claude-haiku-4-5-20251001": {"input": 0.8e-6, "output": 4.0e-6},
        "claude-sonnet-4-6": {"input": 3.0e-6, "output": 15.0e-6},
        "claude-opus-4-7": {"input": 15.0e-6, "output": 75.0e-6},
    }

    def record(self, model: str, input_tokens: int, output_tokens: int, feature: str):
        p = self.PRICES.get(model, self.PRICES["claude-sonnet-4-6"])
        cost = input_tokens * p["input"] + output_tokens * p["output"]
        
        today = datetime.date.today().isoformat()
        self.daily_costs[today] += cost
        self.feature_costs[feature] += cost

        if self.daily_costs[today] > 20.0:
            self._alert(f"일일 비용 ${self.daily_costs[today]:.2f} 초과")

    def report(self) -> dict:
        return {
            "daily_total": dict(self.daily_costs),
            "by_feature": dict(self.feature_costs),
        }

최적화 효과 측정

전략 적용 대상 절감 효과
프롬프트 캐싱 반복되는 긴 시스템 프롬프트 50~70%
시맨틱 캐싱 FAQ·반복 질문 많은 서비스 20~40%
모델 라우팅 다양한 난이도의 요청 40~60%
배치 API 비실시간 대량 처리 50%
프롬프트 압축 긴 컨텍스트 10~30%

지난 글: LLMOps 관측성: 프로덕션 LLM 시스템 들여다보기

다음 글: LLM 시맨틱 캐시: 반복 요청 비용 제로화 전략


읽어주셔서 감사합니다. 😊

KEEP READING

이어 읽기

AI 기초2026.07.29

추론 모델은 무엇이 다른가: 사고 토큰과 그 청구서

추론 모델이 일반 LLM과 구조적으로 어떻게 다른지, 사고 토큰이 정확도와 비용에 어떤 영향을 주는지, 그리고 어떤 작업에서만 그 대가가 회수되는지 실무 기준으로 정리합니다.

중급14 MIN
모델 운영2026.05.29

파인튜닝 파이프라인 구축: 데이터부터 배포까지

QLoRA 기반 파인튜닝 파이프라인을 처음부터 구축한다. 데이터 준비, 학습 루프, 체크포인트, 평가, GGUF 변환, Ollama 배포까지 엔드-투-엔드 실전 가이드.

중급14 MIN
모델 운영2026.05.29

평가 하네스 구축: LLM 성능을 체계적으로 측정하라

직접 만드는 LLM 평가 시스템 — 테스트셋 설계, 자동 채점(LLM-as-Judge), 사람 평가 워크플로우, 지표 집계, 회귀 감지까지 실전 평가 하네스 구축 가이드.

중급17 MIN