(6/6) LLM API 비용 추적 — 모델 라우팅 자동화: 쉬운 작업은 싼 모델로
- 과금 구조와 usage 필드 — 4사 가격표 한눈에
- 파이썬으로 4사 통합 비용 트래커 만들기
- 공식 비용 API 대사와 예산 알림 자동화
- 키가 난립할 때 — 키 인벤토리와 키별 비용 귀속
- Grafana 대시보드로 비용 한눈에 보기
- 모델 라우팅 자동화 — 쉬운 작업은 싼 모델로 ← 지금 글
Summary
5편까지로 비용은 보이게 됐어요. 얼마 쓰는지(2편), 청구서와 맞는지(3편), 누가 쓰는지(4편), 추세가 어떤지(5편)까지. 이제 남은 건 그 데이터를 근거로 줄이는 일입니다.
3편 절감 체크리스트에서 효과 2위로 꼽았던 게 모델 라우팅이었어요 — “분류·추출 같은 쉬운 작업을 플래그십에서 경량 모델로 내리기”. 이번 편은 그걸 사람 판단이 아니라 시스템이 하게 만듭니다. 라우팅 테이블 → 난이도 분류기 → 캐스케이드 순서로, 단순한 것부터 자동화 수준을 올려갈게요.
💡 이 글에서 다루는 것
- 단가 차이가 만드는 기회 — 플래그십 대비 5~40배 싼 경량 모델
- 트래커 데이터에서 “비싼 모델로 가는 쉬운 트래픽” 찾기
- 1단계: 모델 하드코딩 걷어내기 — 라우팅 테이블
- 2단계: 경량 모델 난이도 분류기 — 요청 단위 라우팅
- 3단계: 캐스케이드 — 싼 모델 먼저, 실패하면 위로
- LiteLLM 게이트웨이의 논리 모델 티어와 폴백
- 승급률(escalation rate)로 품질 저하 감시하기
1. 단가 차이가 만드는 기회
1편 가격표에서 라우팅 관점으로 필요한 것만 다시 꺼내봅니다. 기준은 플래그십인 Claude Opus 4.8 이에요.
| 모델 | 입력/출력 ($/1M) | Opus 4.8 대비 |
|---|---|---|
| Claude Opus 4.8 | $5.00 / $25.00 | 기준 |
| Claude Haiku 4.5 | $1.00 / $5.00 | 1/5 |
| Gemini 2.5 Flash | $0.30 / $2.50 | 1/10~1/17 |
| gpt-5.4-nano | $0.20 / $1.25 | 1/20~1/25 |
| Solar Pro 3 | $0.15 / $0.60 | 1/33~1/42 |
핵심은 단가 차이 자체가 아니라, 정답 품질이 차이 나지 않는 작업이 생각보다 많다는 점이에요. 스팸 분류, 태그 추출, 정형 요약 같은 작업은 플래그십과 경량 모델의 결과가 사실상 같습니다. 그 트래픽이 습관적으로 플래그십에 가고 있다면 — 같은 결과를 5~40배 비싸게 사고 있는 거예요.
2. 내릴 후보 찾기 — 트래커 데이터로
감이 아니라 데이터로 후보를 고릅니다. 2편 SQLite 에서 “어떤 작업이, 어떤 모델로, 어떤 모양의 호출을 하는지”를 뽑아요.
rows = conn.execute("""
SELECT app, model,
COUNT(*) AS calls,
CAST(AVG(output_tokens) AS INTEGER) AS avg_out,
ROUND(SUM(cost_usd), 2) AS usd
FROM llm_usage
WHERE ts >= date('now', '-14 days')
GROUP BY app, model
HAVING usd > 1
ORDER BY usd DESC
""").fetchall()
for r in rows:
print(r)
('mlops/doc-tagging', 'claude-opus-4-8', 3120, 84, 18.9)
('data/report-writer', 'claude-opus-4-8', 240, 2210, 11.2)
('mlops/mail-classify', 'gpt-5.5', 1890, 12, 6.4)
('data/bulk-summary', 'claude-sonnet-4-6', 610, 310, 3.1)
읽는 법은 이렇습니다.
- 1행
doc-tagging— 평균 출력 84토큰짜리 태깅 작업을 Opus 로 3천 번. 전형적인 1순위 강등 후보예요. - 3행
mail-classify— 출력 12토큰(라벨 하나)에 플래그십. 역시 후보입니다. - 2행
report-writer— 평균 2천 토큰 넘는 긴 생성. 이런 건 건드리지 않아요. 라우팅은 품질 차이가 안 나는 곳에서만 이겨요.
정리하면 후보의 지문은 “출력이 짧고 정형적인데 모델은 플래그십” 입니다.
3. 1단계 — 모델 하드코딩 걷어내기
자동화의 첫걸음은 허무할 정도로 단순해요. 코드 곳곳에 박힌 model="claude-opus-4-8" 을 전부 걷어내고, 작업 이름 → 모델 매핑 한 곳으로 모읍니다.
# routes.py — 모델 선택은 이 파일만 안다
ROUTES = {
"doc-tagging": ("anthropic", "claude-haiku-4-5"),
"mail-classify": ("openai", "gpt-5.4-nano"),
"bulk-summary": ("google", "gemini-2.5-flash"),
"report-writer": ("anthropic", "claude-opus-4-8"), # 긴 생성은 그대로
}
DEFAULT_ROUTE = ("anthropic", "claude-sonnet-4-6")
def route(task):
return ROUTES.get(task, DEFAULT_ROUTE)
print(route("doc-tagging"))
print(route("새로운-작업"))
('anthropic', 'claude-haiku-4-5')
('anthropic', 'claude-sonnet-4-6')
호출부는 route(task) 로 받은 (provider, model) 을 2편의 track() 래퍼에 그대로 넘기면 됩니다. 이 상태가 되면 모델 교체가 배포 없는 설정 변경이 돼요. 2장의 후보를 하나씩 내려보고, 이상하면 딕셔너리 한 줄만 되돌리면 됩니다.
작업 유형별로 난이도가 균질하다면 사실 여기서 끝이에요. 저는 이 단계만으로 2장의 1·3행이 정리돼서 일 비용이 눈에 띄게 떨어졌습니다.
4. 2단계 — 경량 모델 난이도 분류기
문제는 같은 작업 안에 쉬운 요청과 어려운 요청이 섞여 있는 경우예요. 고객 문의 응대가 대표적입니다 — “영업시간이 언제예요?”와 “3년치 거래 내역 기준으로 환불 규정 해석해줘”가 같은 엔드포인트로 들어와요.
이럴 땐 요청 단위로 라우팅해야 하고, 그 판정을 경량 모델에게 시킵니다. Haiku 4.5 에 구조화 출력으로 easy/hard 만 뽑게 해요.
import json
import anthropic
client = anthropic.Anthropic()
DIFFICULTY_SCHEMA = {
"type": "object",
"properties": {"difficulty": {"type": "string", "enum": ["easy", "hard"]}},
"required": ["difficulty"],
"additionalProperties": False,
}
def classify_difficulty(user_input):
"""요청 난이도를 경량 모델로 판정한다."""
response = client.messages.create(
model="claude-haiku-4-5",
max_tokens=64,
output_config={"format": {"type": "json_schema", "schema": DIFFICULTY_SCHEMA}},
messages=[{
"role": "user",
"content": (
"다음 요청을 처리하는 데 여러 단계의 추론이나 해석이 필요하면 hard, "
"단순 조회·분류·정형 요약이면 easy 로 판정해줘.\n\n"
f"<요청>\n{user_input}\n</요청>"
),
}],
)
return json.loads(response.content[0].text)["difficulty"]
print(classify_difficulty("영업시간이 언제예요?"))
print(classify_difficulty("3년치 거래 내역을 근거로 이 건이 환불 규정 몇 조에 해당하는지 해석해줘"))
easy
hard
“분류 호출 자체가 비용 아닌가?”가 당연한 의문인데, 계산해보면 걱정할 수준이 아니에요. 입력 300토큰 + 출력 10토큰이면 Haiku 단가로 호출당 $0.00035 입니다. easy 판정 하나가 Opus→Haiku 강등으로 아끼는 금액(수 센트)의 1% 수준이에요.
⚠️ 단, 분류기는 비싼 작업 앞에만 붙이세요. 이미 nano/Flash 로 가는 호출에까지 붙이면 절감액보다 분류비가 커지는, 배보다 배꼽인 상황이 됩니다.
5. 3단계 — 캐스케이드: 싼 모델 먼저, 실패하면 위로
분류기는 “시도 전 예측”이라 가끔 틀립니다. 더 공격적인 전략은 일단 싼 모델로 시도하고, 결과가 검증에 실패하면 상위 모델로 승급하는 캐스케이드예요.
def cascade(messages, validate, task="doc-tagging"):
"""싼 모델부터 시도하고, 검증 실패 시 상위 모델로 승급한다."""
tiers = ["claude-haiku-4-5", "claude-opus-4-8"]
for i, model in enumerate(tiers):
tag = task + (":escalated" if i > 0 else "")
response = track("anthropic", model, client.messages.create(
model=model,
max_tokens=1024,
messages=messages,
), app=tag)
if validate(response):
return response
return response # 최상위 모델 결과는 그대로 반환
validate 가 이 패턴의 성립 조건입니다. 기계적으로 검증 가능한 작업에만 쓰세요.
- ✅ JSON 파싱이 되는가, 라벨이 허용 목록 안인가, 필수 필드가 다 있는가, 추출값이 원문에 존재하는가
- 🚨 “글이 좋은가” 같은 열린 생성 — 검증 함수를 못 만들면 캐스케이드도 없습니다. 그런 작업은 3장의 정적 라우팅으로 플래그십에 두세요.
포인트 하나는 승급 시 app 태그에 :escalated 를 붙여 승급 자체를 트래커에 기록한다는 거예요. 이게 다음 장의 품질 감시 지표가 됩니다.
6. 게이트웨이에서 라우팅 — 논리 모델 티어
3~4편의 LiteLLM 게이트웨이를 쓰고 있다면, 라우팅 테이블을 앱 코드가 아니라 게이트웨이 설정으로 올릴 수 있어요. 물리 모델명 대신 easy-tier/hard-tier 같은 논리 이름을 만들어두는 방식입니다.
# litellm_config.yaml (발췌)
model_list:
# 같은 model_name 을 여러 개 두면 그 안에서 로드밸런싱됩니다
- model_name: easy-tier
litellm_params:
model: anthropic/claude-haiku-4-5
api_key: os.environ/ANTHROPIC_API_KEY
- model_name: easy-tier
litellm_params:
model: gemini/gemini-2.5-flash
api_key: os.environ/GEMINI_API_KEY
- model_name: hard-tier
litellm_params:
model: anthropic/claude-opus-4-8
api_key: os.environ/ANTHROPIC_API_KEY
router_settings:
fallbacks: [{"easy-tier": ["hard-tier"]}]
앱은 이제 model="easy-tier" 처럼 논리명만 호출해요. 얻는 것이 세 가지입니다.
| 효과 | 내용 |
|---|---|
| 교체 무배포 | 티어 구성 변경이 yaml 수정으로 끝 — 앱 코드는 논리명만 앎 |
| 로드밸런싱 | easy-tier 안의 Haiku·Flash 로 트래픽 분산, 한쪽 장애·rate limit 흡수 |
| 장애 폴백 | easy-tier 가 에러로 죽으면 hard-tier 로 자동 재시도 |
💡 헷갈리기 쉬운 구분 하나 — 게이트웨이의
fallbacks는 호출이 에러로 실패했을 때의 승급이에요. 5장의 캐스케이드는 호출은 성공했는데 결과 품질이 검증에 걸렸을 때의 승급입니다. 전자는 게이트웨이가, 후자는 앱 코드가 담당하는 게 자연스러워요.
4편의 가상 키와 조합하면 “이 팀 키는 easy-tier 만 호출 가능” 같은 정책도 가능해집니다. 비용 통제가 모델 선택 단계까지 내려오는 거예요.
7. 효과 측정과 품질 감시
라우팅을 켰으면 두 가지를 지켜봅니다. 돈이 실제로 줄었는가, 그리고 품질을 대가로 치르고 있지 않은가.
돈은 이미 도구가 다 있어요. 2편 일별 리포트와 5편 Grafana 의 모델별 패널에서, 라우팅 적용일을 기점으로 Opus 막대가 줄고 Haiku·Flash 가 늘어나는 그림이 보이면 성공입니다. 제 경우 2장의 1·3행을 내린 것만으로 일 비용이 $7.60 에서 $3.90 수준으로 내려왔어요.
품질은 5장에서 심어둔 :escalated 태그로 감시합니다.
SELECT date(ts) AS day,
ROUND(100.0 * SUM(app LIKE '%:escalated') / COUNT(*), 1) AS escalated_pct
FROM llm_usage
WHERE app LIKE 'mlops/doc-tagging%'
GROUP BY day
ORDER BY day;
이 승급률(escalation rate) 이 라우팅의 건강 지표예요.
- 5% 안팎 — 건강합니다. 경량 모델이 대부분 처리하고 가끔 승급.
- 30% 이상 — 경량 모델이 그 작업을 사실 못 하는 거예요. 승급이 잦으면 호출을 두 번씩 하는 셈이라 오히려 비싸집니다. 라우팅을 되돌리거나 티어를 한 단계 올리세요.
5편 대시보드에 이 쿼리를 패널로 하나 추가해두면, 품질 저하가 감이 아니라 숫자로 잡힙니다. 여기에 주 1회 승급된 케이스 몇 개를 눈으로 훑는 루틴을 더하면 감시 체계는 충분해요.
8. 시리즈 정리
여섯 편의 전체 그림입니다.
| 계층 | 도구 | 답하는 질문 |
|---|---|---|
| 구조 이해 (1편) | 가격표 · usage 필드 | 뭐가 얼마인가 |
| 상시 관측 (2편) | track() + SQLite |
지금 어디에 쓰고 있나 |
| 정산 대사 (3편) | 공식 Usage/Cost API | 청구서와 맞나 |
| 통제 (3편) | 게이트웨이 예산 | 초과를 막을 수 있나 |
| 귀속 (4편) | 키 인벤토리 · api_key_id 그룹핑 · 가상 키 |
누가 쓰고 있나 |
| 시각화 (5편) | Grafana + SQLite/Infinity | 추세가 보이나 |
| 최적화 (6편) | 라우팅 테이블 · 분류기 · 캐스케이드 | 덜 쓸 수 있나 |
“비용이 폭발했는데 추적할 방법이 없다”에서 출발한 시리즈가, 추적을 넘어 비용을 구조적으로 줄이는 데까지 왔어요. 관측이 없으면 라우팅할 근거도, 라우팅이 망가졌을 때 알아챌 방법도 없다는 점에서 — 결국 1~5편이 만든 데이터가 6편을 가능하게 한 셈입니다. 시리즈는 여기서 완결이에요. 🎉
일단 오늘은 여기까지….. 여섯 편을 따라와 주셔서 감사해요. 다음에는 새로운 주제로 찾아뵐게요.