왜 출력 토큰이 비용의 뇌관인가
LLM API 요금은 입력보다 출력 토큰 단가가 보통 3~5배 높다. 그런데 실무에서 max_tokens를 습관적으로 크게(예: 4096) 잡아두는 경우가 많다. 문제는 두 가지다. 첫째, 모델이 실제로 그만큼 길게 응답하면 예산이 순식간에 소진된다. 둘째, 스트리밍 환경에서 상한이 너무 크면 무한 반복·잡담으로 흘러도 조기에 끊어내지 못한다. 즉 max_tokens는 단순 안전장치가 아니라 건당 비용 상한을 결정하는 계약값이다.
비용 구조부터 계산하기
핵심은 "이 요청의 최악 비용"을 호출 전에 확정할 수 있다는 점이다. 입력 토큰은 요청 시점에 이미 알 수 있고, 출력 토큰의 상한은 max_tokens로 우리가 정한다. 따라서 상한 비용은 결정론적이다.
def max_cost(input_tokens: int, max_out: int,
in_price: float, out_price: float) -> float:
# 단가는 1M 토큰 기준(USD)
return (input_tokens * in_price + max_out * out_price) / 1_000_000
# 예: 입력 1,200토큰, 출력 상한 512
print(max_cost(1200, 512, in_price=3.0, out_price=15.0))
# -> 0.0114 USD (이 요청은 절대 이 값을 넘지 않는다)
이 함수 하나로 라우팅 로직에서 "예산 초과 요청은 거부"를 구현할 수 있다.
작업 유형별 상한 예측
모든 요청에 같은 상한을 주는 것은 낭비다. 작업 성격에 따라 필요한 출력 길이는 예측 가능하다. 과거 로그에서 작업 유형별 출력 토큰 분포를 뽑아 p95를 상한으로 잡으면, 대부분의 정상 응답은 잘리지 않으면서 롱테일 폭주만 차단된다.
| 작업 유형 | 출력 p50 | 출력 p95 | 권장 max_tokens |
|---|---|---|---|
| 분류/라벨링 | 8 | 20 | 32 |
| 요약 | 180 | 420 | 512 |
| 코드 생성 | 350 | 1100 | 1280 |
| 자유 대화 | 210 | 900 | 1024 |
로그에서 상한을 산출하는 쿼리
상한값을 감으로 정하지 말고 실제 출력 토큰 로그에서 뽑는다. 요청마다 task_type과 output_tokens를 적재해 두면 다음처럼 유형별 p95를 계산해 설정 테이블에 반영할 수 있다.
SELECT
task_type,
APPROX_QUANTILES(output_tokens, 100)[OFFSET(95)] AS p95,
-- 20% 여유를 두고 32의 배수로 올림
CAST(CEIL(APPROX_QUANTILES(output_tokens, 100)[OFFSET(95)] * 1.2 / 32) * 32
AS INT64) AS suggested_max_tokens
FROM llm_requests
WHERE created_at >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)
AND finish_reason = 'stop' -- 정상 종료만 포함
GROUP BY task_type;
finish_reason = 'stop'으로 필터링하는 이유는, 이미 length로 잘린 응답을 포함하면 상한이 실제 필요치보다 낮게 왜곡되기 때문이다.
런타임에 상한을 주입하기
산출된 값은 코드에 하드코딩하지 말고 설정으로 분리한다. 배포 없이 조정할 수 있어야 실험과 사고 대응이 빠르다.
max_tokens:
default: 512
by_task:
classify: 32
summarize: 512
codegen: 1280
chat: 1024
# 글로벌 하드 리미트: 어떤 요청도 이 값을 못 넘음
hard_ceiling: 2048
def resolve_max_tokens(task: str, cfg: dict) -> int:
v = cfg["by_task"].get(task, cfg["default"])
return min(v, cfg["hard_ceiling"])
주의점: 상한이 품질을 깎지 않게
상한을 너무 조이면 응답이 중간에 잘려 finish_reason=length가 늘어난다. 이때는 재시도로 이어지며 오히려 비용이 증가한다. 따라서 length 절단율을 반드시 모니터링하고, 특정 유형에서 1~2%를 넘으면 상한을 올린다. 또한 JSON 등 구조화 출력은 잘리면 파싱 자체가 실패하므로, 스키마 크기를 근거로 상한을 넉넉히 잡아야 한다.
정리
max_tokens는 비용 상한을 확정하는 값이므로, 요청 전에 "최악 비용"을 계산해 예산 게이트를 세우고, 작업 유형별 p95를 로그에서 산출해 설정으로 주입하며, length 절단율을 지표로 삼아 상한을 조정한다. 이 세 가지만 지켜도 출력 비용의 롱테일 폭주를 사전에, 결정론적으로 막을 수 있다.