포스트

토큰을 “덜 쓰는” 게 아니라 “비싼 토큰을 안 쓰는” 법: 2026년 5월 LLM API 비용 최적화 Routing 심층 가이드

토큰을 “덜 쓰는” 게 아니라 “비싼 토큰을 안 쓰는” 법: 2026년 5월 LLM API 비용 최적화 Routing 심층 가이드

들어가며

2026년 5월 기준 LLM API 비용은 더 이상 “프롬프트 조금 줄이자” 수준으로는 안 내려갑니다. 이유는 간단합니다. 프롬프트 최적화로 줄일 수 있는 토큰은 한계가 있는데, 모델 티어 간 단가 격차(= 같은 토큰이라도 가격 차이)가 너무 커졌기 때문입니다. 예를 들어 OpenAI GPT‑4.1은 입력/출력 $2/$8 per 1M tokens이고, 동일 계열에서 더 저렴한 mini/nano가 존재하며, cached input은 $0.50까지 내려갑니다. (developers.openai.com) 또한 OpenAI는 Batch API 50% 할인을 명시합니다. (openai.com) Anthropic도 Haiku/Sonnet/Opus 간 5–25x 비용 스프레드가 생기기 쉬워 “routing이 가장 큰 레버”가 되었다고 정리합니다. (cloudzero.com)

이 글에서 다루는 “routing”은 단순히 “작은 모델로 먼저 던져보기”가 아닙니다. (1) 토큰을 캐시 가능한 구조로 재배열하고, (2) 요청을 분류/리스크 스코어링해서, (3) 필요한 경우에만 상위 모델/장문 출력으로 에스컬레이션하는 전체 파이프라인입니다.

언제 쓰면 좋은가

  • 트래픽이 있고(일/주 단위로 수천~수만 호출), 요청의 난이도가 다양함(대부분은 쉬운데 가끔 매우 어렵다)
  • 프롬프트에 고정 prefix(시스템 지침/툴 스키마/정책/코딩 스타일 가이드)가 크다
  • “정확도 1%”보다 “월 비용 30%”가 더 중요한 제품 구간이 있다(예: 운영 자동화, 백오피스 요약/추출, 고객응대 1차)

언제 쓰면 안 되는가

  • 트래픽이 작고(최적화 투자 대비 절감액이 작음) 장애 비용이 큰 초기 PoC
  • 도메인이 단일하고 대부분이 고난도라 항상 상위 모델이 필요한 워크로드
  • 평가(evals) 인프라 없이 “감으로 라우팅”하려는 경우(절감보다 품질 사고가 먼저 납니다)

🔧 핵심 개념

1) “토큰 절약”의 3가지 레버

1) 단가 절약(모델 라우팅): 같은 토큰이라도 모델별 $/token이 다르니, “어려운 요청”만 비싼 모델로 보낸다. Anthropic은 Haiku(저가)↔Opus(고가) 사이에 큰 스프레드가 있고, routing이 비용 레버라고 강조합니다. (cloudzero.com)
2) 입력 절약(prompt caching): 고정 prefix를 provider 캐시에 태워 cached input 단가로 청구되게 만든다. OpenAI GPT‑4.1은 cached input $0.50/1M로 명시되어 있고, 새 모델에서 캐시 할인 폭을 키웠다고 설명합니다. (developers.openai.com)
3) 출력 절약(출력 토큰이 더 비쌈): 많은 벤더에서 output이 input보다 훨씬 비쌉니다(예: Claude는 output이 input의 5x라고 설명). (cloudzero.com) 즉 “잘 쓰는 프롬프트”란 장문 답변을 유도하는 게 아니라 필요할 때만 길게 내보내는 프롬프트입니다.

2) 라우팅의 내부 흐름(실전형)

실전 파이프라인은 보통 아래처럼 3단 구조가 안정적입니다.

A. Gate(저가 모델)

  • 목적: “이 요청은 쉬운가/어려운가?”, “RAG 필요한가?”, “정책/보안 민감도 높은가?”
  • 출력: route = {tier, need_rag, max_output_tokens, cache_key_hint} 같은 구조화된 decision

B. Workhorse(중간 모델)

  • 목적: 대부분의 요청을 처리(요약/추출/FAQ/정형화 응답)
  • 전략: 고정 prefix는 캐시, 동적 컨텍스트는 뒤로 몰아 캐시 효율 최대화
    (프롬프트 캐싱 연구에서도 동적 내용을 프롬프트 뒤로 밀고, 캐시 블록을 통제하는 게 효과적이라고 보고합니다.) (arxiv.org)

C. Escalation(상위 모델 또는 reasoning 모델)

  • 목적: 낮은 확신/고난도/실패 케이스만 처리
  • 트리거: self-check(정답 검증), tool 결과 불일치, 사용자의 “이상하다” 피드백, 규칙 기반 리스크 스코어 등

3) 다른 접근과의 차이점

  • 프롬프트 다이어트만 하는 접근: 5~15% 절감은 가능하지만, 모델 티어 스프레드를 못 먹습니다.
  • 무조건 작은 모델 우선: 실패율이 쌓이면 재시도/에스컬레이션으로 오히려 output 토큰이 폭증합니다(특히 “사과문/장황한 설명” 패턴).
  • routing + caching 결합이 핵심: 캐시는 “반복 prefix”가 큰 시스템(에이전트/툴콜/정책프롬프트)에서 45~80% 절감까지 보고되며, 단순 적용보다 “캐시 블록 통제”가 중요하다는 결과가 있습니다. (arxiv.org)

💻 실전 코드

아래 예제는 “SaaS 고객지원 자동화” 시나리오입니다.

  • 요청은 ticket(제목/본문/최근 대화 일부)로 들어옴
  • 1단계: Gate 모델이 난이도/리스크/필요 출력 길이를 판정(JSON)
  • 2단계: Workhorse 모델이 답변 초안 생성(짧게, 정책 포함)
  • 3단계: Gate가 “고난도/법무/환불/보안”으로 분류하면 Escalation 모델로 재작성
  • 고정 prefix(정책/톤/출력 형식)는 캐시 가능한 prefix로 분리(“동적 입력은 뒤로”)

실행 전 준비

  • Python 3.11+
  • pip install openai pydantic tenacity
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
import os, json, hashlib, time
from typing import Literal, Optional
from pydantic import BaseModel, Field
from tenacity import retry, wait_exponential, stop_after_attempt
from openai import OpenAI

client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])

# -----------------------------
# 1) Routing decision schema
# -----------------------------
class RouteDecision(BaseModel):
    tier: Literal["nano", "mini", "flagship"] = Field(
        description="nano/mini: 저가, flagship: 고가(고난도/리스크)"
    )
    need_rag: bool = Field(description="지식베이스 조회 필요 여부")
    max_output_tokens: int = Field(description="출력 토큰 상한(비용 제어)")
    risk: Literal["low", "med", "high"] = Field(description="정책/법무/보안 리스크")

# -----------------------------
# 2) Cache-friendly prompt design
#    - static prefix: 정책/톤/형식 (캐시 대상)
#    - dynamic suffix: 티켓 내용 (캐시 비대상)
# -----------------------------
STATIC_PREFIX = """You are a senior customer-support engineer.
Follow these rules:
- Be concise. Prefer bullet points.
- If user asks for refund/legal/security, mark as high risk and suggest escalation.
- Never hallucinate product policy; if unsure, ask a clarifying question.
Return JSON only when asked.
"""

def stable_cache_key(text: str) -> str:
    return hashlib.sha256(text.encode("utf-8")).hexdigest()[:16]

@retry(wait=wait_exponential(min=1, max=8), stop=stop_after_attempt(3))
def route_ticket(ticket: dict) -> RouteDecision:
    """
    Gate 모델: 매우 저렴한 모델(nano/mini 계열)로 routing.
    """
    dynamic = json.dumps(ticket, ensure_ascii=False)
    prompt = (
        STATIC_PREFIX
        + "\nDecide routing for the following support ticket.\n"
        + "Return JSON with keys: tier, need_rag, max_output_tokens, risk.\n\n"
        + f"TICKET:\n{dynamic}\n"
    )

    # NOTE: 모델명은 조직에서 실제 사용하는 것으로 교체하세요.
    # 여기서는 GPT-4.1 라인업의 tier 개념만 사용합니다.
    resp = client.responses.create(
        model="gpt-4.1-nano",
        input=prompt,
        # 운영에서는 temperature 낮게(결정 흔들림 방지)
        temperature=0.1,
        max_output_tokens=400,
    )

    text = resp.output_text.strip()
    return RouteDecision.model_validate_json(text)

@retry(wait=wait_exponential(min=1, max=8), stop=stop_after_attempt(3))
def draft_answer(ticket: dict, decision: RouteDecision, kb_snippets: Optional[str] = None) -> str:
    """
    Workhorse/Flagship 실행: decision에 따라 모델 선택 + 출력 토큰 상한.
    """
    dynamic_parts = {
        "ticket": ticket,
        "kb_snippets": kb_snippets,
        "constraints": {
            "risk": decision.risk,
            "max_output_tokens": decision.max_output_tokens
        }
    }
    dynamic = json.dumps(dynamic_parts, ensure_ascii=False)

    # 캐시 효율 관점: STATIC_PREFIX는 가능한 한 변경하지 않는다.
    # 동적 데이터는 뒤에 몰아 넣는다.
    prompt = (
        STATIC_PREFIX
        + "\nWrite a support reply. Keep it short unless high complexity.\n"
        + "If risk is high, propose next step instead of firm commitments.\n\n"
        + f"CONTEXT:\n{dynamic}\n"
    )

    if decision.tier == "nano":
        model = "gpt-4.1-nano"
    elif decision.tier == "mini":
        model = "gpt-4.1-mini"
    else:
        model = "gpt-4.1"

    resp = client.responses.create(
        model=model,
        input=prompt,
        temperature=0.2,
        max_output_tokens=decision.max_output_tokens,
    )
    return resp.output_text.strip()

def maybe_fetch_kb(ticket: dict) -> str:
    """
    (예시) 사내 검색/RAG 결과. 실제로는 벡터DB/검색엔진 호출.
    """
    # 현실적인 포인트: RAG는 "항상"이 아니라, Gate가 필요하다고 한 경우만.
    return (
        "- Refund policy: refunds within 14 days if usage < 1000 credits.\n"
        "- Security: SSO issues require org_id and last login timestamp.\n"
    )

def handle(ticket: dict) -> dict:
    t0 = time.time()
    decision = route_ticket(ticket)

    kb = maybe_fetch_kb(ticket) if decision.need_rag else None
    answer = draft_answer(ticket, decision, kb_snippets=kb)

    return {
        "route": decision.model_dump(),
        "answer": answer,
        "latency_sec": round(time.time() - t0, 2),
        # 운영에서는 resp.usage로 prompt_tokens/completion_tokens를 수집해
        # 라우팅/캐시 전략을 주기적으로 재학습/튜닝합니다.
    }

if __name__ == "__main__":
    ticket = {
        "subject": "Billing question: refund?",
        "body": "I was charged twice. I want a refund. Also I think my account was hacked.",
        "recent_messages": [
            "User: I see two charges",
            "Agent: Can you share invoice ids?"
        ]
    }

    result = handle(ticket)
    print(json.dumps(result, ensure_ascii=False, indent=2))

예상 출력(형태 예시)

  • route.tierflagship, risk=high, max_output_tokens가 낮게(예: 300~600) 설정되어 “길게 떠드는” 비용을 막고
  • 답변은 “환불/보안은 확인 필요 → 필요한 정보 요청 + 에스컬레이션 안내”처럼 보수적으로 생성

⚡ 실전 팁 & 함정

Best Practice 1) “출력 토큰 상한”을 라우팅 결과에 포함하라

많은 팀이 input 최적화만 하다가 비용이 안 내려갑니다. 실제로 Anthropic은 output이 input 대비 5x라고 정리합니다. (cloudzero.com)

  • 쉬운 요청: max_output_tokens=200~400
  • 요약/추출: 포맷 강제 + max_output_tokens 낮게
  • 고난도: 길게 쓰게 두기보다 단계화(추가 질문 → 다음 턴)로 비용 분산

Best Practice 2) 캐시를 “되게” 만드는 프롬프트 구조를 먼저 설계하라

캐싱은 “옵션 켜기”가 아니라 프롬프트의 정적/동적 분리가 본체입니다. 캐싱 연구는 전략적 캐시 블록 통제가 비용 45~80% 절감에 기여할 수 있고, 단순(naive) 캐싱은 오히려 역효과가 날 수 있음을 지적합니다. (arxiv.org)

  • 시스템/정책/툴 스키마는 최대한 고정
  • 사용자 입력, RAG 결과, tool 결과는 뒤쪽에
  • “매 요청마다 바뀌는 타임스탬프/추적ID”를 prefix에 넣는 실수를 피하기

Best Practice 3) 라우팅은 “정확도”가 아니라 “기대비용(Expected Cost)”로 최적화

안티패턴은 “nano로 던져보고 실패하면 flagship”인데, 실패 기준이 애매하면 재시도 + 장문 사과문으로 output 토큰이 터집니다.

  • Gate에서 risk/high면 처음부터 상위 모델(단, 출력 상한은 낮게)
  • “자기확신(self-check)”를 Gate로 다시 돌릴 때는 짧은 판정 프롬프트로(판정 비용이 더 들면 본말전도)

흔한 함정) 라우팅 모델이 비싸면 전체가 무너진다

Gate는 “싸고 빠르게”가 핵심입니다. 분류/판정은 nano/mini가 담당하고, 비싼 모델은 “생성”에만 쓰는 구조가 일반적으로 안정적입니다. (OpenAI도 GPT‑4.1 mini/nano 같은 저가 라인업을 제공하며, GPT‑4.1은 입력/출력 $2/$8로 명시됩니다.) (developers.openai.com)


🚀 마무리

2026년 5월의 LLM 비용 최적화는 “토큰을 줄이는 기술”이라기보다 (1) 모델 티어를 나눠 기대비용을 최적화하고, (2) 캐시 친화적 프롬프트로 입력 단가를 낮추고, (3) 출력 길이를 통제해 비싼 output 토큰을 억제하는 엔지니어링입니다. OpenAI는 GPT‑4.1에서 cached input 가격과 Batch 할인 같은 비용 레버를 공식 문서/소개 글에 명시하고 있고, (developers.openai.com) Anthropic 역시 모델 tier 간 비용 스프레드와 caching/batch/routing을 핵심 절감 수단으로 정리합니다. (cloudzero.com)

도입 판단 기준(제가 현업에서 쓰는 체크리스트)

  • 요청의 70% 이상이 “정형/저난도”로 떨어지는가? → Yes면 routing 가치 큼
  • 고정 prefix가 큰가(정책/툴/가이드가 1K+ tokens)? → Yes면 caching 최우선
  • 품질을 숫자로 볼 수 있는가(evals/샘플링 QA/CSAT)? → 없으면 먼저 이것부터

다음 학습 추천

  • 프롬프트 캐싱의 블록 설계/동적 컨텐츠 배치에 대한 최신 연구(에이전트 워크로드에서 특히 중요) (arxiv.org)
  • 비용/제약 하에서 라우팅을 최적화하는 최근 라우팅 연구(“쿼리별”이 아니라 “배치/풀 단위” 관점까지 확장) (arxiv.org)
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.