프롬프트 캐싱 설계법: OpenAI와 Claude API 비용을 줄이는 입력 구조
LLM API 비용을 줄이고 싶다면 모델만 바꾸기 전에 프롬프트 캐싱을 먼저 봐야 합니다. 프롬프트 캐싱은 반복되는 입력 prefix를 재사용해 처리 시간과 비용을 줄이는 방식입니다. OpenAI는 최근 모델에서 자동 캐싱과 prompt_cache_key, explicit breakpoint를 제공하고, Claude API는 cache_control을 통해 자동 캐싱 또는 명시적 cache breakpoint를 설정할 수 있습니다.
핵심은 간단합니다. 변하지 않는 내용은 앞에, 자주 바뀌는 내용은 뒤에 둡니다. 캐시는 “같은 prefix”가 다시 들어올 때 이득이 생기기 때문입니다. 그런데 실제 서비스 프롬프트는 이 원칙을 자주 어깁니다. 사용자 이름, 현재 시간, 요청 ID, 최근 이벤트를 프롬프트 맨 앞에 넣어 캐시를 매번 깨뜨립니다.
프롬프트 캐싱이 먹히는 구조
프롬프트 캐싱은 모든 요청에 마법처럼 적용되지 않습니다. 캐시가 맞으려면 앞부분이 같아야 합니다. 따라서 서비스 프롬프트를 다음 순서로 정리해야 합니다.
- 고정 시스템 정책
- 역할과 답변 스타일
- 출력 스키마
- 예시 few-shot
- 도구 설명 또는 함수 정의
- 고정 지식 베이스 일부
- 사용자별 변동 데이터
- 이번 요청 질문
앞 16번은 가능한 한 안정적으로 유지합니다. 78번은 뒤로 보냅니다. 이렇게 하면 같은 테넌트, 같은 기능, 같은 도구 세트를 쓰는 요청에서 캐시 히트 가능성이 올라갑니다.
나쁜 예시는 다음입니다.
- “현재 시간은 2026-07-27 23:00입니다”를 시스템 프롬프트 첫 줄에 넣는다.
- request_id를 도구 설명 앞에 넣는다.
- 사용자별 프로필을 few-shot 예시보다 앞에 넣는다.
- 매 요청마다 도구 목록 순서가 달라진다.
이런 작은 변화가 캐시 prefix를 깨뜨립니다.
OpenAI prompt_cache_key를 언제 써야 하나
OpenAI 문서에 따르면 prompt_cache_key는 긴 공통 prefix를 공유하는 요청을 같은 캐시로 라우팅하는 데 도움을 줍니다. 특히 GPT-5.6 계열 이후에서는 더 일관된 캐싱을 위해 key 사용이 중요해졌습니다. 단, 하나의 key에 너무 많은 서로 다른 prefix를 몰아넣으면 오히려 히트율이 떨어질 수 있습니다.
좋은 key 설계는 “같은 prefix를 공유하는 요청끼리” 묶는 것입니다.
예시:
- tenant:acme:support-bot:v4
- feature:invoice-parser:schema-v2
- repo:mobile-app:code-review-policy-v3
- kb:pricing-docs:2026-07
나쁜 key 설계:
- user:random-user-id:timestamp
- all-requests
- prod
- chat
너무 넓은 key는 서로 다른 prefix를 섞습니다. 너무 좁은 key는 같은 prefix 요청을 분산시킵니다. 테넌트, 기능, 프롬프트 버전을 기준으로 묶는 편이 가장 관리하기 쉽습니다.
Claude cache_control은 자동과 명시적을 구분하라
Claude API는 cache_control을 통해 prompt caching을 사용할 수 있습니다. 자동 캐싱은 요청 top-level에 cache_control을 추가해 가장 마지막 cacheable block까지 캐시하도록 맡기는 방식입니다. 멀티턴 대화처럼 히스토리가 계속 늘어나는 경우 시작점으로 좋습니다.
반대로 명시적 cache breakpoint는 어떤 content block까지 캐시할지 직접 표시하는 방식입니다. 도구 설명, 긴 정책 문서, 고정 예시처럼 “여기까지는 거의 안 바뀐다”가 분명한 경우에 적합합니다.
실무 기준은 이렇습니다.
- 빠르게 적용하고 싶다: 자동 캐싱부터 켠다.
- 비용이 큰 기능이다: 명시적 breakpoint를 설계한다.
- 프롬프트 버전 관리가 되어 있다: 명시적 breakpoint가 유리하다.
- 사용자별 데이터가 앞뒤로 섞여 있다: 먼저 프롬프트 구조를 정리한다.
중요한 것은 캐싱 기능을 켜기 전에 프롬프트를 정돈하는 것입니다. 구조가 지저분한 상태에서 캐싱만 켜면 히트율이 낮고, 왜 비용이 줄지 않는지 추적하기 어렵습니다.
비용 계산은 cache write와 cache read를 나눠야 한다
캐시는 처음 저장할 때와 다시 읽을 때 비용 구조가 다를 수 있습니다. OpenAI 문서도 cache_write_tokens와 cached_tokens 같은 지표를 통해 캐시 쓰기와 읽기를 구분해 측정할 수 있다고 설명합니다. Claude도 usage 필드를 통해 캐시 생성·읽기 토큰을 확인하는 흐름이 필요합니다.
따라서 대시보드에는 다음 지표를 분리해야 합니다.
| 지표 | 의미 |
|---|---|
| uncached input tokens | 캐시 없이 처리된 입력 |
| cache write tokens | 새로 캐시에 쓴 입력 |
| cached input tokens | 캐시에서 읽은 입력 |
| output tokens | 모델이 생성한 출력 |
| cache hit rate | 캐시 가능한 요청 중 실제 hit 비율 |
| cost per successful task | 성공한 작업 1건당 총비용 |
단순히 총 토큰만 보면 캐싱 효과를 모릅니다. 캐시 write가 많고 read가 적으면 아직 이득이 아닐 수 있습니다. 반복 요청이 충분한 기능에서 read가 늘어날 때 비용 절감이 시작됩니다.
프롬프트 버전 관리를 하지 않으면 캐시가 깨진다
프롬프트 캐싱을 운영하려면 버전 관리가 필요합니다. 시스템 프롬프트 한 문장을 바꾸면 prefix가 달라져 캐시가 새로 만들어질 수 있습니다. 배포마다 프롬프트가 자주 바뀌는 팀은 캐시 히트율이 불안정해집니다.
추천 방식은 프롬프트를 코드처럼 관리하는 것입니다.
- prompt_id를 둔다.
- prompt_version을 둔다.
- 변경 이유를 changelog에 남긴다.
- 캐시 key에 version을 포함한다.
- A/B 테스트 중인 프롬프트는 key를 분리한다.
- 날짜, request_id 같은 동적 값은 prefix 뒤로 보낸다.
이렇게 하면 “지난주부터 비용이 왜 늘었지?”라는 질문에 답할 수 있습니다. 프롬프트 버전이 바뀌었는지, cache hit rate가 떨어졌는지, 요청량이 늘었는지 분리해서 볼 수 있기 때문입니다.
실제 리팩터링 예시
기존 프롬프트가 다음 구조라고 가정해보겠습니다.
- 현재 시간
- 사용자 프로필
- 시스템 역할
- 출력 JSON 형식
- 예시 3개
- 질문
이 구조는 캐싱에 불리합니다. 현재 시간과 사용자 프로필이 앞에 있어서 매번 prefix가 달라집니다. 리팩터링 후에는 다음처럼 바꿉니다.
- 시스템 역할
- 답변 정책
- 출력 JSON 형식
- 예시 3개
- 도구 설명
- 현재 시간
- 사용자 프로필
- 질문
그리고 cache breakpoint는 5번 뒤에 둡니다. 이렇게 하면 시간과 사용자 정보가 바뀌어도 앞부분은 재사용할 수 있습니다.
바로 적용할 체크리스트
- 프롬프트를 고정 영역과 변동 영역으로 나눈다.
- 고정 시스템 정책, 스키마, 예시, 도구 설명을 앞에 둔다.
- 현재 시간, request_id, 사용자 데이터는 뒤로 보낸다.
- OpenAI는 prompt_cache_key를 테넌트·기능·버전 기준으로 설계한다.
- Claude는 자동 캐싱으로 시작하고 고비용 기능은 명시적 breakpoint를 둔다.
- cache write tokens와 cached tokens를 분리해 본다.
- 프롬프트 변경 이력을 버전으로 남긴다.
- cache hit rate를 기능별로 모니터링한다.
프롬프트 캐싱은 비용 최적화 기능이지만, 실제로는 프롬프트 구조화 훈련에 가깝습니다. 입력을 재사용 가능한 부분과 매번 바뀌는 부분으로 나누는 팀이 캐싱 이득을 가져갑니다. 모델 교체보다 먼저 이 구조를 잡는 것이 보통 더 싸고 안전합니다.