목차
- 왜 프롬프트 캐싱이 중요한가: 비용보다 먼저 병목이 옵니다
- 프롬프트 캐싱 원리: 쉽게 말해 같은 머리말을 다시 읽지 않게 하는 겁니다
- KV Cache(Key-Value Cache, 어텐션 계산 재사용용 캐시)와의 관계
- 여기서 중요한 포인트!
- 어떤 요청이 캐싱에 잘 맞나: 적용 대상부터 고르셔야 합니다
- 실전 구현 1: 프롬프트를 캐시 친화적으로 재구성하기
- 1. 공통 prefix를 템플릿으로 고정합니다
- 2. 가변 값은 prefix 뒤로 보냅니다
- 3. 문자열 정규화(normalization, 형식 통일)를 적용합니다
- 실전 구현 2: vLLM 기반 추론 서버에 붙이는 방식
- 예시 배포 흐름
- 실전 구현 3: 애플리케이션 레벨에서 캐시 히트율 높이기
- ⚠️ 주의사항과 트러블슈팅: 여기서 많이 막힙니다
- 1. 공백 하나 때문에 캐시가 안 맞는 경우
- 2. 사용자별 문맥을 앞쪽에 너무 많이 붙이는 경우
- 3. 긴 컨텍스트가 무조건 좋은 줄 아는 경우
- 4. 비용만 보고 latency를 안 보는 경우
- 검증 방법: 숫자를 지어내지 말고, 비교 기준을 먼저 고정하세요
- 실제 적용 사례 분석: 어디서 체감이 컸나
- 사례 1. 긴 시스템 프롬프트를 쓰는 운영 챗봇
- 사례 2. RAG에서 문서 앞단 규칙이 반복되는 QA
- 정리와 다음 단계: 프롬프트 캐싱은 옵션이 아니라 설계 문제입니다
- FAQ: 실무에서 자주 나오는 질문
- Q1. 프롬프트 캐싱만 켜면 바로 비용이 줄어드나요?
- Q2. vLLM만 쓰면 자동으로 해결되나요?
- Q3. 어디부터 손대는 게 가장 빠른가요?
- Q4. 어떤 지표를 꼭 봐야 하나요?
[인프라] 프롬프트 캐싱으로 LLM 비용 줄이는 법
프롬프트 캐싱은 요즘 LLM 비용, 응답 속도, GPU 자원 효율을 같이 잡을 때 거의 빠지지 않는 주제입니다. 특히 시스템 프롬프트(system prompt), 공통 지시문(shared instruction), RAG(Retrieval-Augmented Generation, 검색 증강 생성)에서 반복되는 긴 컨텍스트를 계속 넣고 있다면 비용이 생각보다 빨리 불어나거든요. 저도 홈랩에서 vLLM으로 여러 실험을 돌리다가, 모델이 똑같은 앞부분을 계속 읽고 있다는 걸 보고 나서야 이 문제를 제대로 체감했습니다. 처음엔 “어차피 토큰은 토큰 아닌가?” 싶었는데, 실제로 써보니까 반복되는 prefix(프리픽스, 앞부분 문맥)를 얼마나 재사용하느냐가 체감 성능에 꽤 큰 차이를 만들더라고요.
특히 운영 환경에서는 단순히 LLM 비용만 줄이는 게 아니라, LLM Latency까지 안정적으로 줄이는 방향으로 봐야 합니다. 요청이 몰릴 때 매번 긴 지시문을 처음부터 다시 처리하면 지연이 늘고, GPU 메모리도 금방 빡빡해지거든요. 그래서 이번 글에서는 프롬프트 캐싱의 원리, vLLM에서 생각해야 할 포인트, 그리고 실제 서비스에 붙일 때 어떤 식으로 구조를 잡으면 좋은지 제 경험 기준으로 풀어보겠습니다.
공통 시스템 프롬프트와 사용자 요청이 합쳐지고, 반복되는 prefix가 캐시 재사용되는 전체 흐름을 보여주는 이미지입니다.
왜 프롬프트 캐싱이 중요한가: 비용보다 먼저 병목이 옵니다
많이들 비용 절감부터 떠올리시는데, 제가 직접 운영해보니 문제는 그보다 먼저 병목으로 드러났습니다. 예를 들어 팀 공용 챗봇을 만든다고 해볼게요.
- 모든 요청 앞에 긴 역할 지시문을 붙입니다.
- 보안 정책, 응답 형식, 금지어 목록도 같이 넣습니다.
- RAG 문서 헤더나 도메인 설명도 반복됩니다.
이렇게 되면 사용자 질문은 짧은데도 앞단 프롬프트가 너무 길어집니다. 그러면 모델 입장에서는 매 요청마다 같은 앞부분을 다시 prefill(프리필, 입력 토큰을 읽어 내부 상태를 만드는 단계)해야 하죠. 여기서 시간이 들고, 비용이 들고, GPU 사용량도 올라갑니다. 특히 동시 요청이 많아지면 “왜 모델은 놀고 있지 않은데 응답은 느리지?” 같은 상황이 생깁니다. 저도 처음엔 네트워크 문제인 줄 알고 한참 봤었는데, 근본 원인은 반복 prefix였습니다. 삽질 좀 했습니다 ㅎㅎ
프롬프트 캐싱 원리: 쉽게 말해 같은 머리말을 다시 읽지 않게 하는 겁니다
쉽게 말해 프롬프트 캐싱은 모델이 이미 처리한 앞부분 문맥을 재활용하는 방식입니다. 구현 방식은 엔진마다 조금씩 다르지만, 핵심 개념은 비슷합니다.
KV Cache(Key-Value Cache, 어텐션 계산 재사용용 캐시)와의 관계
트랜스포머 기반 모델은 입력 토큰을 처리하면서 attention(어텐션) 계산에 필요한 내부 상태를 만듭니다. 이 상태를 흔히 KV Cache라고 부르는데, 같은 prefix가 다시 들어오면 이 계산 결과를 재사용할 수 있습니다. 그러면 매번 처음부터 다 계산하지 않아도 됩니다.
| 구분 | 캐싱 전 | 캐싱 후 |
|---|---|---|
| 반복 시스템 프롬프트 | 매 요청마다 다시 처리 | 같은 prefix면 재사용 가능 |
| LLM 비용 | 반복 토큰 처리 비용 누적 | 반복 처리 감소 기대 |
| LLM Latency | prefill 구간이 길어짐 | 초반 처리 시간 단축 기대 |
| 효과가 큰 상황 | 짧은 프롬프트 위주 | 긴 공통 지시문, RAG 헤더, 에이전트 템플릿 |
여기서 중요한 포인트!
캐싱은 완전히 같은 prefix일수록 잘 먹힙니다. 문장 하나, 공백 하나, 타임스탬프 하나가 달라도 캐시 히트(hit)가 깨질 수 있습니다. 그래서 실무에서는 모델 엔진만 보는 게 아니라, 애플리케이션 레이어에서 프롬프트를 얼마나 안정적으로 고정하느냐가 정말 중요합니다.
어떤 요청이 캐싱에 잘 맞나: 적용 대상부터 고르셔야 합니다
모든 요청에 프롬프트 캐싱이 큰 효과를 주는 건 아닙니다. 제가 실제로 써보니 아래 패턴에서 특히 체감이 컸습니다.
- 시스템 프롬프트가 길고 거의 고정된 챗봇
- 출력 형식이 엄격한 JSON 생성 작업
- RAG 공통 헤더가 긴 문서 QA
- 멀티턴 에이전트에서 고정 정책이 반복되는 경우
반대로 매 요청마다 앞부분이 크게 달라지는 워크로드는 효과가 제한적일 수 있습니다. 예를 들어 사용자별 정책 블록이 길고 자주 바뀌면 캐시 재사용률이 떨어집니다. 그래서 먼저 해야 할 일은 “우리 요청 중 어디가 반복되나?”를 보는 겁니다.

캐시가 잘 먹도록 공통 prefix와 사용자별 가변 영역을 분리한 프롬프트 설계 예시입니다.
실전 구현 1: 프롬프트를 캐시 친화적으로 재구성하기
엔진 설정 전에 먼저 프롬프트 구조를 손보는 게 맞습니다. 이걸 안 하고 옵션만 켜면 기대보다 효과가 작아요. 저도 처음엔 서버 설정만 바꿨다가 별 차이가 없어서 한참 헤맸거든요.
1. 공통 prefix를 템플릿으로 고정합니다
SYSTEM_PREFIX = """You are an infrastructure assistant.
Follow the security policy below:
1. Do not output secrets.
2. Answer in Korean unless asked otherwise.
3. Use concise technical explanations.
Output must be valid JSON when requested.
"""
DOMAIN_PREFIX = """Service context:
- Environment: self-hosted GPU
- Gateway: internal API
- Logging: request metadata only
"""
def build_prompt(user_question: str) -> str:
return f"{SYSTEM_PREFIX}\n{DOMAIN_PREFIX}\nUser: {user_question}\nAssistant:"
핵심은 SYSTEM_PREFIX와 DOMAIN_PREFIX를 자주 바꾸지 않는 겁니다. 날짜, 요청 ID, 사용자 이름처럼 매번 바뀌는 값은 뒤쪽으로 빼세요.
2. 가변 값은 prefix 뒤로 보냅니다
- 좋은 예: 정책, 역할, 출력 형식은 앞에 고정
- 주의할 예: 현재 시각, 세션 ID, A/B 테스트 라벨은 뒤로 이동
- 나쁜 예: 공통 머리말 중간에 매 요청마다 바뀌는 값 삽입
3. 문자열 정규화(normalization, 형식 통일)를 적용합니다
def normalize_prompt_block(text: str) -> str:
lines = [line.rstrip() for line in text.strip().splitlines()]
return "\n".join(lines)
SYSTEM_PREFIX = normalize_prompt_block(SYSTEM_PREFIX)
DOMAIN_PREFIX = normalize_prompt_block(DOMAIN_PREFIX)
공백, 줄바꿈, 들여쓰기가 달라져도 캐시 히트율이 떨어질 수 있어서, 이런 정규화는 생각보다 효과가 있습니다. 사소해 보여도 운영에서는 꽤 중요하더라고요.
실전 구현 2: vLLM 기반 추론 서버에 붙이는 방식
vLLM은 OpenAI 호환 API 형태로 많이 붙이기 좋고, 반복 prefix 재사용 관점에서도 자주 언급되는 엔진입니다. 세부 옵션은 배포 방식에 따라 다를 수 있으니 공식 문서와 현재 빌드를 꼭 같이 확인하셔야 하지만, 운영 구조는 대체로 비슷합니다.
예시 배포 흐름
- 모델 서버를 띄웁니다.
- 애플리케이션에서 공통 prefix를 최대한 고정합니다.
- OpenAI 호환 엔드포인트로 요청을 보냅니다.
- 로그에서 prefill 지연과 처리량 변화를 확인합니다.
vllm serve meta-llama/Meta-Llama-3-8B-Instruct \
--host 0.0.0.0 \
--port 8000
위 예시는 가장 단순한 형태입니다. 실제 옵션은 GPU 메모리, 병렬 처리, 스케줄링 정책에 따라 달라집니다. 여기서 중요한 건 “옵션을 많이 켠다”가 아니라, 반복 prefix를 안정적으로 유지하는 호출 패턴입니다.
from openai import OpenAI
client = OpenAI(base_url="http://127.0.0.1:8000/v1", api_key="dummy")
SYSTEM_PREFIX = """You are an infrastructure assistant.
Answer in Korean.
Summarize trade-offs clearly.
"""
COMMON_CONTEXT = """Environment:
- Self-hosted inference
- Target: lower cost and lower latency
- Focus: repeated prompt prefixes
"""
user_question = "프롬프트 캐싱이 비용 절감에 왜 중요한지 설명해줘"
response = client.chat.completions.create(
model="meta-llama/Meta-Llama-3-8B-Instruct",
messages=[
{"role": "system", "content": SYSTEM_PREFIX},
{"role": "user", "content": COMMON_CONTEXT + "\n\n" + user_question},
],
temperature=0,
)
print(response.choices[0].message.content)
여기서 제가 많이 보는 실수는, 매 요청마다 시스템 프롬프트에 디버그 정보나 타임스탬프를 섞는 겁니다. 그러면 사실상 같은 prefix가 아니게 되거든요. 그러니 운영 정보는 애플리케이션 로그에 남기고, 프롬프트 본문은 최대한 고정하세요.

vLLM 추론 서버, 애플리케이션 API, 공통 프롬프트 템플릿이 어떻게 연결되는지 보여주는 구성 다이어그램입니다.
실전 구현 3: 애플리케이션 레벨에서 캐시 히트율 높이기
엔진 캐시만 믿으면 아쉽습니다. 애플리케이션에서 조금만 구조를 바꾸면 효과가 훨씬 좋아집니다.
from hashlib import sha256
PREFIX_TEMPLATE = """You are a production assistant.
Policy version: v1
Output format: markdown
Language: ko
"""
def prefix_key(prefix: str) -> str:
return sha256(prefix.encode("utf-8")).hexdigest()
def build_messages(question: str):
stable_prefix = PREFIX_TEMPLATE.strip()
return {
"prefix_key": prefix_key(stable_prefix),
"messages": [
{"role": "system", "content": stable_prefix},
{"role": "user", "content": question},
],
}
이렇게 prefix 해시를 따로 남겨두면 요청 로그에서 어떤 프롬프트가 얼마나 반복되는지 확인하기 편합니다. 즉, “캐시가 될 것 같다”가 아니라 실제로 반복되는 프롬프트를 데이터로 확인할 수 있습니다. 이거 진짜 편하더라고요.
⚠️ 주의사항과 트러블슈팅: 여기서 많이 막힙니다
프롬프트 캐싱은 개념은 단순한데, 운영에서는 생각보다 잘 안 맞을 때가 있습니다. 제가 겪었던 포인트 위주로 적어보겠습니다.
1. 공백 하나 때문에 캐시가 안 맞는 경우
템플릿 엔진이 줄바꿈을 다르게 넣거나, JSON 직렬화 순서가 바뀌면 prefix가 달라질 수 있습니다.
- 해결: 프롬프트 생성 함수를 한 곳으로 모읍니다.
- 해결: trim, newline normalization을 적용합니다.
- 해결: 템플릿 버전을 명시해서 바뀐 시점을 추적합니다.
2. 사용자별 문맥을 앞쪽에 너무 많이 붙이는 경우
권한 정보, 개인화 규칙, 최근 대화 요약을 전부 앞에 넣으면 캐시 공통 구간이 짧아집니다.
- 해결: 진짜 공통인 부분만 앞에 둡니다.
- 해결: 사용자별 정보는 뒤쪽으로 분리합니다.
3. 긴 컨텍스트가 무조건 좋은 줄 아는 경우
RAG에서 문서를 많이 붙이면 정답률이 올라갈 것 같지만, 실제로는 반복되는 공통 헤더만 길고 본문은 자주 바뀌어서 캐시 효율이 낮을 수 있습니다.
- 해결: 공통 지시문과 문서 본문을 분리해서 관찰합니다.
- 해결: 상위 N개 문서 선정 규칙을 안정화합니다.
4. 비용만 보고 latency를 안 보는 경우
LLM 추론 최적화는 비용 절감만이 아닙니다. 사용자는 응답 속도를 더 민감하게 느끼거든요. 프롬프트 캐싱이 잘 되면 prefill 구간이 줄어드는 쪽에서 체감이 옵니다. 그래서 저는 토큰 비용 추정만 보지 않고, 첫 토큰 시간(time-to-first-token)과 전체 응답 시간도 같이 봅니다.
검증 방법: 숫자를 지어내지 말고, 비교 기준을 먼저 고정하세요
이 부분은 특히 조심해야 합니다. 환경마다 GPU, 모델 크기, 동시성, 프롬프트 길이가 다 다르기 때문에 “몇 퍼센트 빨라진다” 같은 고정 수치를 일반화하면 안 됩니다. 대신 아래처럼 비교 기준을 고정해서 보시는 게 좋습니다.
- 같은 모델, 같은 GPU, 같은 동시성 조건을 유지합니다.
- 캐싱 전후에 동일한 요청 집합을 재생합니다.
- 공통 prefix 길이를 일정하게 유지합니다.
- 첫 토큰 시간, 전체 지연, 처리량을 같이 봅니다.
- 반복 요청 비율을 별도로 기록합니다.
curl -s http://127.0.0.1:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "meta-llama/Meta-Llama-3-8B-Instruct",
"messages": [
{"role": "system", "content": "You are an infrastructure assistant. Answer in Korean."},
{"role": "user", "content": "프롬프트 캐싱의 장점을 3가지로 설명해줘"}
],
"temperature": 0
}'
검증 로그는 아래처럼 보는 걸 추천드립니다.
| 항목 | 왜 보나 | 체크 포인트 |
|---|---|---|
| 첫 토큰 시간 | prefill 최적화 체감 확인 | 반복 prefix에서 감소하는지 |
| 전체 응답 시간 | 사용자 체감 품질 확인 | 생성 길이와 분리해서 보기 |
| 요청당 입력 토큰 패턴 | 반복 구간 식별 | 공통 prefix 비율이 높은지 |
| 프롬프트 템플릿 버전 | 캐시 깨짐 원인 추적 | 변경 시점과 성능 변화 연결 |

캐싱 적용 전후의 첫 토큰 시간, 전체 응답 시간, 반복 요청 비율을 비교하는 관측 대시보드 이미지입니다.
실제 적용 사례 분석: 어디서 체감이 컸나
제가 구조적으로 효과를 많이 본 건 아래 두 가지였습니다.
사례 1. 긴 시스템 프롬프트를 쓰는 운영 챗봇
보안 정책, 출력 형식, 응답 톤, 금지 규칙을 길게 붙이는 챗봇은 캐싱 대상이 명확합니다. 이런 경우엔 사용자 질문보다 앞부분이 더 길 때도 있는데요, 이때 공통 prefix만 안정적으로 유지해도 LLM 비용과 LLM Latency가 같이 개선되는 패턴이 잘 나옵니다.
사례 2. RAG에서 문서 앞단 규칙이 반복되는 QA
문서 본문은 바뀌어도, “출처를 명시하라”, “모르면 모른다고 답하라” 같은 지시문은 거의 고정이죠. 이 공통 영역을 템플릿으로 고정하면 효과가 나기 쉽습니다. 반대로 문서 본문 자체는 자주 달라서 완전한 캐시 대상이 되기 어렵습니다. 즉, RAG에서는 전체를 캐싱하려 하지 말고, 고정 지시문부터 분리하는 게 맞습니다.
정리와 다음 단계: 프롬프트 캐싱은 옵션이 아니라 설계 문제입니다
오늘 이야기의 핵심은 단순합니다. 프롬프트 캐싱은 서버 옵션 하나로 끝나는 기능이 아니라, 프롬프트 설계와 요청 구조를 같이 손봐야 제대로 효과가 납니다. 처음엔 저도 엔진만 바꾸면 되는 줄 알았는데, 실제로 써보니까 캐시 친화적인 템플릿 설계가 훨씬 중요했어요. 결국 잘 되는 팀은 모델을 잘 고르는 팀이 아니라, 반복되는 문맥을 얼마나 일관되게 관리하느냐를 잘하는 팀이더라고요.
혹시 지금 운영 중인 LLM 서비스에서 시스템 프롬프트가 길고, 같은 안내문이 반복되고 있다면 이건 바로 점검해볼 만합니다. 다음 글에서는 vLLM 운영 시 KV cache 압박과 동시성 튜닝 쪽을 더 깊게 다뤄보겠습니다. 이전 글에서 다뤘던 추론 서버 모니터링 방법과 같이 보시면 훨씬 감이 오실 거예요.

프롬프트 설계, 가변값 분리, 측정 지표, 배포 체크포인트를 한 장으로 정리한 요약 이미지입니다.
FAQ: 실무에서 자주 나오는 질문
Q1. 프롬프트 캐싱만 켜면 바로 비용이 줄어드나요?
아닙니다. 반복되는 prefix가 실제로 많아야 하고, 그 prefix가 안정적으로 동일해야 합니다. 요청마다 머리말이 바뀌면 효과가 작습니다.
Q2. vLLM만 쓰면 자동으로 해결되나요?
엔진 지원은 중요하지만, 애플리케이션 레벨에서 템플릿을 고정하지 않으면 기대한 만큼 재사용이 안 될 수 있습니다. 저는 이 부분이 더 중요하다고 봅니다.
Q3. 어디부터 손대는 게 가장 빠른가요?
가장 먼저 시스템 프롬프트와 공통 지시문을 분리해서, 매 요청에 완전히 같은 문자열이 들어가는지 확인해보세요. 여기서 절반은 정리됩니다.
Q4. 어떤 지표를 꼭 봐야 하나요?
첫 토큰 시간, 전체 응답 시간, 반복 요청 비율, 프롬프트 템플릿 버전 이 네 가지는 꼭 같이 보시는 걸 추천합니다.