왜 문제가 되는가
LLM 애플리케이션은 매 요청마다 동일한 앞부분을 반복해서 보낸다. 긴 시스템 프롬프트, 도구 정의(function/tool schema), 문서 컨텍스트, few-shot 예시가 대표적이다. 이 토큰들은 요청마다 다시 인코딩되고 다시 과금된다. RAG나 에이전트처럼 컨텍스트가 수천~수만 토큰인 경우 입력 토큰이 전체 비용의 대부분을 차지하고, 첫 토큰까지의 지연(TTFT)도 프리필(prefill) 구간에서 길어진다.
핵심은 "변하지 않는 접두부(prefix)"가 존재한다는 점이다. 프롬프트 캐싱은 이 접두부의 계산 결과(KV 캐시)를 서버에 재사용해 프리필을 건너뛴다. 결과적으로 반복 호출에서 입력 비용과 지연을 동시에 줄인다.
캐시가 동작하는 원리
캐시는 프롬프트의 앞에서부터 정확히 일치하는 구간에만 적용된다. 접두부가 1바이트라도 달라지면 그 지점 이후는 캐시 미스가 난다. 따라서 프롬프트를 "고정 → 준고정 → 가변" 순으로 배치하는 것이 설계의 전부라고 해도 과언이 아니다.
- 고정: 시스템 프롬프트, 도구 정의, 정책 텍스트
- 준고정: 세션 문서, 대화 히스토리(누적)
- 가변: 이번 턴의 사용자 질문
흔한 실수는 프롬프트 맨 앞에 타임스탬프, 요청 ID, 랜덤 정렬된 JSON을 넣는 것이다. 이러면 매번 접두부가 달라져 캐시가 전부 무효화된다.
Anthropic 방식: 명시적 캐시 브레이크포인트
Claude API는 cache_control로 캐시할 지점을 명시한다. 브레이크포인트 이전의 모든 내용이 캐시 후보가 된다. 캐시 쓰기는 일반 입력보다 비싸지만(약 1.25배), 캐시 읽기(히트)는 매우 저렴(약 0.1배)하므로 재사용이 2회 이상이면 대개 이득이다. 기본 TTL은 약 5분이며 히트할 때마다 갱신된다.
from anthropic import Anthropic
client = Anthropic()
resp = client.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
system=[
{"type": "text", "text": "너는 사내 규정 답변 봇이다. 아래 규정만 근거로 답하라."},
{
"type": "text",
"text": LONG_POLICY_DOCUMENT, # 수만 토큰의 고정 문서
"cache_control": {"type": "ephemeral"}, # 여기까지 캐시
},
],
messages=[{"role": "user", "content": "연차 이월 규정 알려줘"}], # 가변부
)
print(resp.usage) # cache_creation_input_tokens / cache_read_input_tokens 확인
usage의 cache_read_input_tokens가 커지고 있는지로 히트율을 검증한다. 이 값이 0에 머물면 접두부가 매번 바뀌고 있다는 신호다.
OSS 방식: 프리픽스 캐시 자동화
vLLM, SGLang 같은 추론 서버는 자동 프리픽스 캐싱(APC)을 지원한다. 명시적 브레이크포인트 없이 요청 간 공통 접두부를 해시로 매칭해 KV 블록을 재사용한다. 운영자는 켜기만 하면 된다.
# vLLM: 자동 프리픽스 캐싱 활성화
vllm serve meta-llama/Llama-3.1-8B-Instruct \
--enable-prefix-caching \
--gpu-memory-utilization 0.90 \
--max-model-len 32768
# 확인: /metrics 에서 prefix cache hit rate 관찰
curl -s localhost:8000/metrics | grep -i prefix_cache
이 방식은 트래픽에 동일 접두부가 많이 몰릴수록(같은 시스템 프롬프트를 쓰는 다중 사용자) 효과가 크다. 대신 GPU 메모리를 캐시 블록에 쓰므로 동시 처리량과의 트레이드오프가 있다.
방식 비교
| 항목 | Anthropic 명시적 캐시 | vLLM/SGLang APC |
|---|---|---|
| 제어 | cache_control로 지점 지정 | 자동(해시 매칭) |
| 과금 | 쓰기 비쌈·읽기 저렴 | 추가 과금 없음(자원 소모만) |
| 유효기간 | TTL 기반(약 5분) | 메모리 압박 시 LRU 축출 |
| 적합 | 매니지드 API 사용 | 자체 호스팅·멀티테넌트 |
실무 주의점
첫째, 최소 캐시 길이가 있다. 접두부가 너무 짧으면(수백 토큰 미만) 캐시 대상이 안 되거나 이득이 없다. 짧은 프롬프트는 굳이 캐싱하지 말라.
둘째, 대화 히스토리를 캐싱할 때는 히스토리를 앞, 새 발화를 뒤에 두어야 누적 접두부가 유지된다. 히스토리를 요약해 재작성하면 접두부가 바뀌어 히트율이 떨어진다.
셋째, 히트율은 반드시 계측하라. 지표 없이 "캐싱 켰으니 됐다"는 위험하다. 접두부에 섞여 들어간 동적 값(사용자명, 현재 시각) 하나가 히트율을 0으로 만든다.
넷째, TTL과 트래픽 패턴을 맞춰라. 5분 TTL인데 요청 간격이 10분이면 매번 재생성 비용만 낸다. 이 경우 가벼운 워밍 요청이나 트래픽 배칭을 고려한다.
정리
프롬프트 캐싱의 성패는 라이브러리가 아니라 프롬프트 레이아웃에서 갈린다. 고정 접두부를 앞에 모으고, 동적 값을 뒤로 밀고, 히트율을 지표로 확인하는 것 — 이 세 가지만 지키면 반복 호출이 많은 워크로드에서 입력 비용과 TTFT를 함께 낮출 수 있다.