* 이 게시물은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
(컨텍스트 엔지니어링 ②) 예산
1편에서 컨텍스트를 ‘설계 대상’으로 봐야 한다는 얘길 했다. 이번 편은 그 설계의 가장 현실적인 제약, 돈이다. LLM 토큰 비용 절감이라고 하면 거창하게 들리지만 결국 ‘같은 품질을 더 적은 토큰으로’라는 한 문장이고 — 이 글에선 그걸 감이 아니라 숫자로 해본다.
컨텍스트 윈도우를 창고로 생각하는 사람이 많은데, 나는 예산으로 본다. 창고면 많이 쌓을수록 좋지만, 예산이면 아껴 써야 한다. 관련 없는 토큰을 밀어넣는 건 예산을 낭비하는 거고, 그 낭비는 성능 저하와 비용 증가로 동시에 돌아온다.
모델이 프롬프트 가운데 낀 내용을 양 끝보다 잘 놓친다는 건 잘 알려진 현상이다(lost in the middle). 문서 서른 개를 욱여넣으면 정작 중요한 스무 번째가 묻힌다. 그러니 “일단 다 넣고 모델이 알아서 고르겠지”는 대체로 나쁜 전략이다.
예산을 지키는 도구는 몇 가지가 있다.
- 검색(RAG) — 통째로 넣지 말고 질문에 필요한 조각만 골라 넣는다.
- 정리(context editing) — 오래돼 쓸모없어진 도구 결과를 컨텍스트에서 잘라낸다.
- 요약(compaction) — 대화가 길어지면 앞부분을 요약본으로 압축한다.
- 캐싱(prompt caching) — 매번 똑같은 부분은 캐시해서 비용을 낮춘다.
이 중 프롬프트 캐싱은 효과가 즉각적이다. 시스템 지시처럼 안 변하는 부분을 캐시해두면, 그 부분은 다음 호출부터 정가의 10분의 1 수준으로 처리된다. 요령은 안 변하는 걸 앞에, 매번 바뀌는 질문을 뒤에 두는 것. 캐시는 앞에서부터 일치하는 데까지만 유효해서, 앞쪽에 현재 시각 같은 게 하나 끼면 통째로 깨진다.
코드로 옮기면 고정된 지시는 캐시에 걸고, 질문은 그 뒤에 둔다. 그리고 캐시가 진짜 맞는지 응답에서 확인한다.
resp = client.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
system=[{
"type": "text",
"text": STABLE_INSTRUCTIONS, # 고정 → 캐시 대상
"cache_control": {"type": "ephemeral"},
}],
messages=[{"role": "user", "content": question}], # 매번 바뀜
)
print(resp.usage.cache_read_input_tokens) # 0이면 캐시가 안 걸린 것
프롬프트 캐싱 손익분기 — 언제 이득이고 언제 손해인가
캐싱이 만능처럼 들리지만 공짜는 아니다. 캐시에 처음 쓰는 순간은 정가보다 비싸다 — 쓰기는 기본 입력 단가의 1.25배 수준, 대신 그 뒤의 읽기는 10분의 1 수준이다(정확한 배수는 모델·TTL마다 다르니 공식 문서에서 확인). 그러니까 “써놨는데 아무도 안 읽으면” 손해 보는 구조다. 어디가 손익분기인지 계산해 보자.
이 글의 비용 계산은 전부 대표 모델(Claude Opus 4.8)의 입력 $5/100만 토큰, 출력 $25/100만 토큰 — 2026년 기준 — 으로 했다. 단가와 캐시 배수는 모델·시점마다 달라지니, 실제 견적을 낼 땐 공식 요금 문서의 숫자로 바꿔 끼우자. 계산 구조 자체는 그대로 쓰면 된다.
시스템 지시 + few-shot 예시 + 정책 문서를 합쳐 2,000토큰짜리 고정 프리픽스가 있다고 하자(캐시는 대략 1,024토큰 이상 프리픽스부터 걸리니 이 조건은 넘는다). 입력 단가 $5/100만 토큰이면 호출당 정가는 2,000 × $5/1M = $0.01. 캐시 쓰기는 1.25배인 $0.0125, 읽기는 0.1배인 $0.001이다. TTL 안에서 N회 호출한다면:
- 캐시 없이: N × $0.01
- 캐싱 적용: $0.0125(쓰기 1회) + (N−1) × $0.001(읽기)
부등식 1.25 + 0.1(N−1) < N을 풀면 N > 1.28. 즉 TTL 안에서 같은 프리픽스로 두 번째 호출이 나가는 순간 이미 이득이다. 횟수가 쌓일수록 격차는 벌어진다.
| 호출 수(TTL 내) | 캐시 없이 | 캐싱 적용 | 절감 |
|---|---|---|---|
| 2회 | $0.020 | $0.0135 | 33% |
| 10회 | $0.100 | $0.0215 | 79% |
| 100회 | $1.000 | $0.1115 | 89% |
반대로 손해 보는 경우도 분명하다. 호출 간격이 TTL(기본 5분)보다 길어 매번 캐시 미스가 나면, 매번 쓰기 요금만 내는 셈이라 오히려 25% 비싸진다. 트래픽이 뜸하면 TTL을 1시간으로 늘리는 옵션이 있는데 쓰기 배수도 같이 올라가니(내가 확인했을 땐 2배 — 역시 공식 문서 기준으로 재확인) 손익을 다시 계산해야 한다. 그리고 앞 문단에서 말한 대로, 프리픽스 앞쪽에 타임스탬프 하나만 끼어도 이 계산은 전부 물거품이 된다.
정보 밀도를 높여라
같은 예산이라면 밀도가 높은 토큰을 넣는다. 장황한 원문보다 핵심을 추린 것, 중복 없는 것. 같은 내용을 세 번 다르게 말한 문서보다, 한 번 정확히 말한 문서가 낫다. 예산 관점에선 ‘짧고 정확한‘이 거의 항상 이긴다.
같은 작업, 긴 프롬프트 vs 다이어트한 프롬프트
말로만 하면 안 믿기니 직접 재봤다. 고객 문의 티켓을 요약하는 시스템 지시문을 두 벌 준비했다. 하나는 흔히 보는 장황한 버전 — “매우 신중하게 읽어주세요”, “빠뜨리지 말아주세요”, “감사합니다” 같은 당부에 예시 세 개가 붙은 것. 다른 하나는 같은 요구사항을 불릿 세 줄로 압축한 다이어트 버전이다.
import tiktoken
enc = tiktoken.get_encoding("o200k_base") # 오프라인 근사치용
verbose = """You are a highly advanced, extremely capable AI assistant.
Your job is to summarize customer support tickets. Please make sure that you
always read the entire ticket very carefully before you begin summarizing.
(...같은 톤의 당부가 이어지고, few-shot 예시 3개까지 총 18줄...)"""
diet = """Summarize the support ticket in 3 bullet points:
- issue (one line)
- order number (if any)
- customer sentiment: neutral / frustrated / angry
"""
for name, text in [("verbose", verbose), ("diet", diet)]:
print(name, len(enc.encode(text)), "tokens")
실행 결과:
verbose 194 tokens
diet 35 tokens
지시 내용은 같은데 194 대 35, 82%가 빠졌다. 비용으로 환산하면 — 호출 10만 건, 입력 단가 $5/100만 토큰 기준으로 장황한 버전은 194 × 100,000 = 1,940만 토큰 → $97, 다이어트 버전은 350만 토큰 → $17.5. 지시문 한 벌 고쳐서 10만 호출마다 $79.5가 남는다. 실전 프롬프트는 few-shot 예시가 통째로 들어간 수천 토큰짜리가 흔하니 격차는 이보다 훨씬 크게 벌어진다.
| 구분 | 실측 토큰 | 10만 호출 입력 비용 |
|---|---|---|
| 장황한 버전 | 194 | $97.00 |
| 다이어트 버전 | 35 | $17.50 (−82%) |
정중한 당부는 사람에게 하는 것이고, 모델에는 요구사항만 남긴다. ‘신중하게’, ‘꼼꼼하게’ 같은 부사는 출력을 거의 바꾸지 못하면서 토큰만 차지하는 대표 항목이다. 단, few-shot 예시를 다 지우면 출력 형식이 흔들릴 수 있으니 하나쯤 남기고 전후 품질을 비교하면서 줄이는 게 안전하다.
긴 대화는 이력이 비용이다 — 요약·절단 before/after
챗봇 API는 스테이트리스다. 서버가 대화를 기억해 주는 게 아니라, 매 턴 지금까지의 이력을 전부 다시 보낸다. 그래서 대화 비용은 턴 수에 비례하는 게 아니라 제곱에 가깝게 붙는다 — 턴마다 보내는 이력이 계속 자라니까.
턴당 평균 900토큰(질문 150 + 답변 750)씩 쌓이는 대화를 가정하면, 20턴째 요청 하나의 입력이 벌써 약 17,250토큰이다. 20턴 누적 입력은 약 174,000토큰. 여기에 요약·절단을 걸어보자 — 최근 6턴은 원문 유지(5,400토큰), 그 앞 이력은 600토큰짜리 요약으로 교체. 20턴째 입력이 약 6,150토큰으로 떨어진다.
| 구분 | 20턴째 입력 | 20턴 누적 입력 |
|---|---|---|
| 전체 이력 재전송 | 약 17,250토큰 | 약 174,000토큰 |
| 최근 6턴 + 요약 600토큰 | 약 6,150토큰 (−64%) | 약 100,000토큰 (−43%) |
물론 요약도 공짜는 아니다. 요약을 만드는 호출 자체의 비용이 있고, 요약에서 빠진 디테일은 영영 사라진다. 어느 턴까지 원문으로 남길지, 서버 쪽 압축(compaction) 기능은 어떻게 쓰는지 — 긴 대화의 컨텍스트 관리는 4편 ‘기억’에서 따로 다뤘다.
실측 사례 — 예산의 71%를 먹고 있던 것
측정이 왜 먼저인지 보여주는 실전 사례 하나. 로컬 소형 LLM 에이전트를 만들며 컨텍스트 예산을 재봤더니, 컨텍스트 10,000토큰 중 도구 정의가 7,244토큰(71%)을 매 요청 고정으로 먹고 있었다. 대화에 남는 자리는 28%. 자주 쓰는 도구만 남기고 나머지 정의를 지연 로딩으로 돌리자 2,670토큰으로 줄어 대화 여유가 73%가 됐다 — 모델도 프롬프트 문장도 그대로인데, 예산 재배치만으로 체감이 달라졌다.
포인트는 ‘어디가 큰지’를 재기 전엔 이 낭비가 보이지도 않았다는 것이다. 이 사례의 전말은 폐쇄망 에이전트 시리즈 8편에 있다.
얼마나 쓰는지 재보기
감이 아니라 숫자로 접근한다. 토큰 카운팅 API나 SDK로 프롬프트가 실제 몇 토큰인지 재고, 어느 부분이 큰지 본다. ‘이 문서 블록이 3천 토큰이나 먹네’를 알아야 뺄지 요약할지 결정할 수 있다. 측정 없는 최적화는 없다 — 이건 서버 튜닝이든 컨텍스트든 똑같다. 다음 편은 이 예산을 실제로 아끼는 도구 설계(3편)다.
앞에서 쓴 tiktoken은 어디까지나 근사치다 — OpenAI 토크나이저 기준이라 Claude 같은 다른 모델에선 숫자가 어긋난다. 정확하게 재려면 쓰는 모델의 공식 카운팅 기능을 쓴다. Claude라면 요청을 보내기 전에 토큰 카운팅 엔드포인트로 정확한 입력 토큰 수를 받아볼 수 있고(과금 여부 등 정책은 공식 문서 확인), 호출 후에는 응답의 usage 필드가 실제 청구 기준이 되는 정답지다.
# 호출 전: 이 요청이 몇 토큰인지 미리 확인
resp = client.messages.count_tokens(
model="claude-opus-4-8",
system=[{"type": "text", "text": STABLE_INSTRUCTIONS}],
messages=[{"role": "user", "content": question}],
)
print(resp.input_tokens) # 예: 2412
# 호출 후: 실제 청구 기준은 usage 필드
resp = client.messages.create(...)
print(resp.usage.input_tokens) # 정가로 계산된 입력
print(resp.usage.cache_read_input_tokens) # 캐시에서 읽어 할인된 입력
print(resp.usage.output_tokens) # 출력 — 입력보다 단가가 몇 배 비싸다
그리고 반드시 측정한다. 토큰을 실제로 세보고, 캐시가 정말 맞고 있는지 응답의 캐시 적중 수치를 확인한다. 아낀다고 생각만 하고 실제로는 매번 새로 계산하고 있는 경우가 은근히 많다. 감이 아니라 숫자로 봐야 한다.
실무 체크리스트 — 전략별 절감폭과 트레이드오프
LLM 토큰 비용 절감 전략을 한 표로 정리하면 이렇다. 절감폭은 이 글의 예시 계산 기준이니, 실제 값은 각자의 워크로드에서 재보고 판단하자.
| 전략 | 기대 절감폭(예시 기준) | 트레이드오프 |
|---|---|---|
| 프롬프트 다이어트 | 지시부 82% (194→35토큰) | 잃는 것 거의 없음. few-shot을 다 지우면 형식이 흔들림 — 하나는 남기기 |
| 프롬프트 캐싱 | 반복 프리픽스 최대 90% 수준 | TTL 내 재사용 없으면 쓰기 비용만 25%가량 추가. 프리픽스 안정 설계 필요 |
| RAG 선별 주입 | 문서 전체 주입 대비 수 배~수십 배 | 검색 품질이 답변 품질의 상한이 됨. 인덱스 관리 비용 |
| 이력 요약·절단 | 20턴 기준 누적 43%↓ | 요약에서 빠진 디테일 복구 불가 + 요약 호출 자체의 비용 |
| 컨텍스트 정리 | 에이전트 루프에서 케이스별 | 지운 도구 결과가 다시 필요해지면 재호출 |
| 출력 제한 | 출력 단가는 입력의 몇 배(대표 모델 5배)라 체감 큼 | max_tokens로 자르면 답이 중간에 끊기는 사고 — 지시로 줄이는 게 우선 |
순서도 중요하다. 먼저 재고(카운팅), 큰 덩어리부터 줄이고(다이어트·RAG), 반복되는 건 캐시하고(프롬프트 캐싱), 길어지는 건 요약한다. 이 순서만 지켜도 대부분의 워크로드에서 청구서가 눈에 띄게 얇아진다.
자주 묻는 질문 (FAQ)
토큰 비용을 어떻게 줄이나요?
불필요한 맥락 제거·요약, 프롬프트 캐싱, 필요한 자료만 검색해 주입, 출력 길이 제한 등이 기본입니다. 큰 컨텍스트를 습관처럼 넣지 마세요.
컨텍스트는 무조건 많이 넣는 게 좋나요?
아닙니다. 길수록 비싸고 느리며, 중간 정보가 묻혀 정확도가 떨어질 수 있습니다. ‘관련 있는 것만’ 넣는 게 원칙입니다.
프롬프트 캐싱은 언제 손해인가요?
캐시 쓰기는 기본 입력 단가보다 비쌉니다(대표 모델 기준 1.25배 수준 — 정확한 배수는 공식 문서 확인). TTL 안에 같은 프리픽스로 재호출이 없으면 쓰기 비용만 내는 셈이라 오히려 손해입니다. 반대로 TTL 안에서 두 번째 호출부터는 이득이며, 응답의 cache_read_input_tokens 값으로 캐시가 실제로 맞고 있는지 꼭 확인하세요.

답글 남기기