프롬프트 캐싱으로 LLM 비용 50~90% 줄이기: 2026년 6월 OpenAI·Anthropic 실전 설계와 히트율 최적화
들어가며
프로덕션 LLM 비용이 새는 가장 흔한 지점은 “매 요청마다 똑같은 긴 prefix( system instructions, tool schema, 정책, 예시, RAG 상단 컨텍스트 )를 다시 prefill”하는 구간입니다. Prompt Caching은 이 prefix의 KV cache(Attention key/value 텐서)를 재사용해 입력 비용과 TTFT(time-to-first-token) 를 크게 줄입니다. OpenAI는 자동 prefix 캐싱으로 입력 비용/지연을 줄이고, Anthropic은 cache_control로 “여기까지는 재사용”을 명시해 cache read(0.1x) 할인을 노리는 구조가 핵심입니다. (platform.openai.com)
언제 쓰면 좋은가
- 동일한 시스템 프롬프트/툴 정의/정책/코드베이스를 반복 호출하는 워크로드: 코딩 에이전트, 고객센터 챗봇, 내부 지식 Q&A, 대량 분류/추출 파이프라인
- 한 요청의 입력 토큰이 크고(>1k~4k), 사용자별로 바뀌는 부분은 뒤쪽에 붙는 형태 (platform.openai.com)
언제 쓰면 안 되는가 (혹은 기대치를 낮춰야 하는가)
- 프롬프트가 짧거나(특히 Anthropic은 모델별 최소 캐시 길이가 큼), 매번 prefix 자체가 달라지는 경우(동적 타임스탬프/랜덤 ID/정렬 변화/툴 schema 순서 변경)
- 병렬로 “동일 prefix를 동시에” 쏘는 구조: 첫 요청이 캐시를 만들기 전엔 나머지가 히트할 수 없어, 오히려 cache write가 늘어날 수 있음 (platform.claude.com)
🔧 핵심 개념
1) Prompt Caching이 실제로 캐싱하는 것: “텍스트”가 아니라 KV cache
모델은 입력 토큰을 한 번 쭉 읽으며(=prefill) attention의 key/value 텐서를 쌓고, 그 위에서 출력 토큰을 생성합니다. 캐시는 보통 이 prefill 결과(KV 텐서) 를 재사용하는 최적화입니다. 그래서 캐시 히트 시 “앞부분 prefill”을 건너뛰고 곧바로 생성 단계로 진입해 TTFT가 줄어듭니다. (platform.openai.com)
2) OpenAI: “자동 prefix 매칭 + 라우팅”이 본질
OpenAI는 정확히 같은 prefix 가 들어오면 서버가 해당 prefix를 최근 처리한 머신으로 라우팅하고, 거기서 캐시 조회를 합니다. 중요한 디테일:
- 캐싱은 프롬프트 길이가 1024 토큰 이상일 때 의미 있게 동작(표시되는
cached_tokens가 0이 아닌 형태) (platform.openai.com) - 라우팅 해시는 보통 “초기 일부 토큰(예: 256 토큰)”을 사용하며,
prompt_cache_key를 주면 라우팅을 더 안정적으로 만들어 히트율을 올릴 수 있음 (platform.openai.com) - 동일 prefix + 동일 cache_key 조합이 분당 ~15회 수준을 넘으면 overflow로 라우팅이 분산되어 히트율이 떨어질 수 있음(고QPS 서비스는 키/샤딩 전략이 필요) (platform.openai.com)
- 캐시 유지: 기본은 in-memory(대체로 5~10분 유휴 후 제거, 최대 1시간), 일부 모델은
prompt_cache_retention: "24h"로 확장 가능 (platform.openai.com)
관측은 응답의 usage.prompt_tokens_details.cached_tokens로 합니다. (platform.openai.com)
3) Anthropic: “명시적 캐시 브레이크포인트 + 가격 모델(Write/Read)”이 본질
Anthropic은 요청 payload에서 tools → system → messages 순으로 누적 해시를 만들고, cache_control이 찍힌 블록까지를 캐시 대상으로 잡습니다. (platform.claude.com)
핵심은 “어디까지가 재사용 가능한 prefix인가”를 개발자가 구조로 설계한다는 점입니다.
또한 OpenAI와 가장 큰 차이는 비용 모델이 Write/Read로 분리되어 있다는 것:
- 기본 TTL은 ephemeral 5분, 필요 시
ttl:"1h"선택(추가 비용) (platform.claude.com) - 모델별 최소 캐시 길이가 크고(예: Opus/Haiku는 4096, Sonnet 일부는 1024 등) 이 미만이면
cache_control을 줘도 캐싱이 안 됩니다. (platform.claude.com) - 효과 측정은 응답 usage의
cache_creation_input_tokens,cache_read_input_tokens같은 필드로 봅니다. (docs.claude.com)
왜 Anthropic은 설계가 더 중요하냐?
cache write가 유료(그리고 보통 표준 input보다 비쌈)라서, 히트율이 낮으면 “캐싱을 켰는데 비용이 늘어나는” 구간이 생깁니다. 반대로 히트가 안정적으로 나면 cache read가 매우 싸서(0.1x) 장기적으로 강력합니다. (이 write/read trade-off 때문에 “히트율 최적화”가 설계의 중심이 됩니다.) (platform.claude.com)
💻 실전 코드
현실적인 시나리오: 고객지원 티켓 분류 + 정책 준수 + 사내 용어사전(고정) + 티켓 본문(가변)
목표: 고정 prefix(정책/스키마/사전)를 캐시로 고정하고, 매 요청마다 달라지는 티켓 본문은 뒤로 보내 히트율을 올립니다. 또한 “히트율(캐시된 토큰 비율)”을 로깅해 비용 모델에 반영합니다.
0) 환경/의존성
1
2
3
4
5
python -m venv .venv
source .venv/bin/activate
pip install openai anthropic python-dotenv
export OPENAI_API_KEY="..."
export ANTHROPIC_API_KEY="..."
1) OpenAI (Responses API) — prefix 안정화 + prompt_cache_key로 라우팅 힌트
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
import os, json, time
from openai import OpenAI
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
STATIC_PREFIX = """You are a senior support triage agent.
Policy:
- Output MUST be valid JSON.
- Choose exactly one label from: ["billing","bug","feature","abuse","account","other"].
- Provide "confidence" 0..1 and "rationale" <= 2 sentences.
Company glossary:
- "ACME Pass" is the paid subscription.
- "Workspace" is a tenant boundary.
Schema:
{"label": "...", "confidence": 0.0, "rationale": "...", "escalate": true/false}
Examples:
(omitted but in real life you'd include several)
"""
def classify_ticket_openai(ticket_id: str, ticket_text: str):
# 핵심: (1) static을 앞에 (2) dynamic은 끝에 (3) cache_key는 "모델+정책버전+워크플로" 단위로 고정
resp = client.responses.create(
model="gpt-5.1", # 예시. 실제 사용 모델로 교체
prompt_cache_retention="24h", # 지원 모델에서만 의미 있음 ([platform.openai.com](https://platform.openai.com/docs/guides/prompt-caching?utm_source=openai))
prompt_cache_key=f"triage:v3", # 라우팅 안정화 힌트 ([platform.openai.com](https://platform.openai.com/docs/guides/prompt-caching?utm_source=openai))
input=[
{
"role": "system",
"content": STATIC_PREFIX
},
{
"role": "user",
"content": f"TICKET_ID={ticket_id}\n\nTICKET_TEXT:\n{ticket_text}"
}
],
response_format={"type": "json_object"},
temperature=0.0,
)
usage = resp.usage
cached = usage["prompt_tokens_details"].get("cached_tokens", 0) # ([platform.openai.com](https://platform.openai.com/docs/guides/prompt-caching?utm_source=openai))
prompt_tokens = usage["prompt_tokens"]
hit_ratio = (cached / prompt_tokens) if prompt_tokens else 0.0
out = resp.output_text
return json.loads(out), {"prompt_tokens": prompt_tokens, "cached_tokens": cached, "hit_ratio": hit_ratio}
if __name__ == "__main__":
# 첫 호출은 보통 miss(캐시 warm), 두 번째부터 hit가 나기 쉬움
ticket = "Customer says ACME Pass charged twice after upgrade. Needs refund."
r1, m1 = classify_ticket_openai("T-1001", ticket)
time.sleep(1)
r2, m2 = classify_ticket_openai("T-1002", "ACME Pass renewal failed; card valid. Please investigate.")
print("R1:", r1, "METRICS:", m1)
print("R2:", r2, "METRICS:", m2)
예상 출력(형태)
- R1은
cached_tokens=0에 가깝고, - R2는
cached_tokens가 STATIC_PREFIX 토큰 대부분을 차지하면서hit_ratio가 크게 증가하는 패턴이 정상입니다. (프롬프트 총 길이가 1024 토큰 미만이면 계속 0일 수 있으니, 실제론 정책/예시/툴 스키마가 충분히 길어야 합니다.) (platform.openai.com)
2) Anthropic (Messages API) — cache_control 브레이크포인트로 “여기까지는 고정”을 선언
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
import os, json, time
from anthropic import Anthropic
client = Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
GLOSSARY_AND_POLICY = """You are a senior support triage agent.
Policy:
- Output MUST be valid JSON.
- Choose exactly one label from: ["billing","bug","feature","abuse","account","other"].
- Provide "confidence" 0..1 and "rationale" <= 2 sentences.
Company glossary:
- "ACME Pass" is the paid subscription.
- "Workspace" is a tenant boundary.
Schema:
{"label": "...", "confidence": 0.0, "rationale": "...", "escalate": true/false}
(Include real examples & tool schemas here to exceed minimum cache length.)
"""
def classify_ticket_claude(ticket_id: str, ticket_text: str):
# Anthropic: tools → system → messages 순으로 누적되며,
# cache_control이 찍힌 블록 "까지"가 캐시 prefix가 됨 ([platform.claude.com](https://platform.claude.com/docs/en/build-with-claude/prompt-caching?via=adil36&utm_source=openai))
msg = client.messages.create(
model="claude-sonnet-4.6", # 예시
max_tokens=300,
temperature=0.0,
system=[
{
"type": "text",
"text": GLOSSARY_AND_POLICY,
"cache_control": {"type": "ephemeral", "ttl": "1h"} # 기본 5m, 필요 시 1h ([platform.claude.com](https://platform.claude.com/docs/en/build-with-claude/prompt-caching?via=adil36&utm_source=openai))
}
],
messages=[
{
"role": "user",
"content": [
{"type": "text", "text": f"TICKET_ID={ticket_id}\n\nTICKET_TEXT:\n{ticket_text}"}
]
}
],
)
# usage 필드에서 creation/read를 보고 read/creation 비율로 효율 판단
usage = msg.usage # docs 예시: cache_creation_input_tokens / cache_read_input_tokens ([docs.claude.com](https://docs.claude.com/ja/docs/build-with-claude/prompt-caching?utm_source=openai))
metrics = {
"input_tokens": usage.get("input_tokens"),
"output_tokens": usage.get("output_tokens"),
"cache_creation_input_tokens": usage.get("cache_creation_input_tokens", 0),
"cache_read_input_tokens": usage.get("cache_read_input_tokens", 0),
}
text = msg.content[0].text
return json.loads(text), metrics
if __name__ == "__main__":
r1, m1 = classify_ticket_claude("T-2001", "Refund request: charged twice for ACME Pass.")
time.sleep(1)
r2, m2 = classify_ticket_claude("T-2002", "Upgrade failed; workspace stuck on trial but billing active.")
print("R1:", r1, "METRICS:", m1)
print("R2:", r2, "METRICS:", m2)
중요: Anthropic은 모델별 최소 캐시 길이가 있어, system이 충분히 길지 않으면 첫 번째 요청부터 “생성/읽기 토큰이 0”처럼 나와도 버그가 아니라 “캐시 불가”일 수 있습니다. (platform.claude.com)
⚡ 실전 팁 & 함정
Best Practice 1) “Prefix 불변성”을 제품 스펙으로 취급하라
캐시는 “정확히 같은 prefix”에만 히트합니다. 그래서 아래가 히트율을 박살냅니다:
- system prompt에 날짜/세션ID/요청ID 삽입
- tool schema의 필드 순서가 런타임마다 바뀜(JSON stringify 비결정성)
- RAG 컨텍스트를 system 앞쪽에 붙임(검색 결과가 매번 달라져 prefix가 흔들림)
정석은 고정(정책/툴/예시/사전) 을 앞에, 가변(사용자 입력/검색 결과/티켓 본문) 을 뒤에 배치입니다. OpenAI도 “정확 prefix 매칭”을 전제로 이 구조를 권장합니다. (platform.openai.com)
Anthropic 역시 cache_control을 “재사용 구간의 끝”에 두라고 가이드합니다. (platform.claude.com)
Best Practice 2) OpenAI는 prompt_cache_key를 “세그먼트 키”로 설계하라
OpenAI의 캐시는 “최근 처리한 머신”에 붙어 있고, 라우팅이 분산되면 히트가 깨질 수 있습니다. 고QPS에서 추천 패턴:
prompt_cache_key = f"{workflow}:{policy_version}:{tenant_bucket}"처럼 너무 세분화도, 너무 통합도 아닌 수준으로 잡기- 동일 prefix+키 조합이 너무 뜨거워(문서상 대략 15 rpm 이상) overflow가 나면, tenant를 몇 개 bucket으로 샤딩해 키를 분산(=의도적 hit-rate/분산 trade-off) (platform.openai.com)
Best Practice 3) Anthropic은 “Write 비용”을 상쇄할 히트 빈도를 먼저 계산하라
Anthropic은 캐싱이 “공짜 최적화”가 아니라, cache write(프리미엄) → cache read(할인) 구조입니다. 그래서 다음이 중요합니다:
- TTL(기본 5m, 옵션 1h) 안에 동일 prefix가 충분히 재사용되는가? (platform.claude.com)
- “동일 prefix를 병렬로 여러 개 먼저 쏘는” 구조면, 첫 요청이 캐시를 만들기 전이라 read가 안 나와 write만 누적될 수 있음(첫 warmup 후 fan-out 하거나, 큐로 순서를 제어) (platform.claude.com)
- 최소 캐시 길이 미만이면 아예 캐시가 안 되니, 시스템 프롬프트/툴 정의가 짧은 워크로드는 효과가 제한적 (platform.claude.com)
흔한 함정/안티패턴
- “대화 히스토리 전체를 통째로 고정 prefix로 만들겠다”: 중간에 도구 결과(tool result)나 동적 텍스트가 끼면 prefix가 계속 깨져 히트율이 들쭉날쭉해집니다. 장기 에이전트 태스크에서 “동적 블록을 뒤로 밀어라”는 연구 결과도 있습니다. (arxiv.org)
- 게이트웨이/프록시 계층(예: 여러 라우터, 멀티 제공자)에서 캐시 격리/동작이 달라질 수 있음: 보안/프라이버시 및 히트율 예측이 어려워질 수 있습니다(특히 timing 기반 감사 연구들이 있음). (arxiv.org)
비용/성능/안정성 트레이드오프 한 줄 요약
- 캐시를 “크게” 잡으면(긴 prefix) 히트 시 절감 폭은 커지지만, prefix가 조금만 흔들려도 미스가 나서 변동성이 커집니다.
- 캐시를 “작게” 잡으면 히트 안정성은 올라가지만, 절감 폭이 작아집니다.
- 그래서 실무에서는 (고정 prefix 최소화 + 완전 불변화) → 그 다음 길이 확장 순으로 튜닝하는 게 승률이 높습니다.
🚀 마무리
2026년 6월 기준 prompt caching은 “옵션 기능”이 아니라, 반복 prefix가 있는 서비스에선 가장 큰 비용 레버입니다. OpenAI는 자동 prefix 캐싱 + prompt_cache_key/retention으로 “라우팅과 유지시간”을 튜닝하는 게임이고, Anthropic은 cache_control로 “재사용 경계”를 설계하며 write/read 경제성을 맞추는 게임입니다. (platform.openai.com)
도입 판단 기준(체크리스트) 1) 내 요청에서 “매번 반복되는 prefix”가 1k~4k+ 토큰 이상인가? (Anthropic은 모델별 최소 길이 확인) (platform.claude.com)
2) TTL(5m/1h/24h) 내에 동일 prefix가 반복되는가? (platform.claude.com)
3) observability가 있는가? (cached_tokens, cache_read_input_tokens 등으로 히트율을 SLO처럼 관리) (platform.openai.com)
4) prefix를 “완전 불변”으로 고정할 수 있는가? (툴 schema 안정화, RAG 위치 뒤로, 동적 값 제거)
다음 학습 추천
- OpenAI Prompt Caching 동작/파라미터(
prompt_cache_key,prompt_cache_retention)를 공식 문서 기준으로 정리해 팀 가이드로 고정 (platform.openai.com) - Anthropic
cache_control블록 설계(여러 breakpoint, 최소 캐시 길이, TTL) 패턴을 “프롬프트 아키텍처”로 문서화 (platform.claude.com) - 장기 에이전트에서 “동적 블록이 캐시를 깨는 지점”을 측정하고, 동적 컨텍스트를 뒤로 미는 전략을 실험(연구 결과 참고) (arxiv.org)