프롬프트 캐싱으로 LLM 비용 70~90% 깎는 법: OpenAI/Anthropic 2026년 6월 기준 실전 설계
들어가며
LLM 비용이 커지는 지점은 대부분 “매 호출마다 똑같이 다시 보내는 긴 프롬프트”입니다. 예를 들어 시스템 지침, tool schema(JSON Schema), 정책/가드레일, 예시 few-shot, 고정 RAG 문서 번들, 에이전트 메모리 등이 매번 수천~수만 토큰씩 들어가면 입력(pre-fill) 비용이 폭발합니다.
2026년 기준 주요 API들은 이를 줄이기 위해 prompt caching(=prefix caching, KV cache 재사용)을 제공합니다. OpenAI는 동일 prefix를 자동 캐시해 cached input 할인을 적용하고(요청 구조 최적화로 히트율을 끌어올리는 게 핵심), Anthropic은 cache_control로 “이 구간을 캐시로 고정”하는 방식이 더 노골적입니다. (platform.openai.com)
언제 쓰면 좋은가
- 시스템 프롬프트/툴 정의가 길고(≥ 1k~수만 tokens), 호출 빈도가 높거나(분당 다수), 세션 내 반복이 많을 때
- agentic workflow에서 “거의 불변인 prefix + 매번 달라지는 tail” 구조가 명확할 때 (platform.openai.com)
언제 쓰면 안 되는가(혹은 기대를 낮춰야 하는가)
- 트래픽이 낮고 띄엄띄엄 들어오는 서비스: 캐시 TTL 만료로 히트율이 급락(“매번 캐시 write만 하고 끝”)할 수 있음 (reddit.com)
- 요청마다 prefix가 자주 바뀌는 설계(동적으로 tool schema 생성, 시스템 지침에 매번 timestamp/trace id 삽입 등)
- 캐시 write 프리미엄이 큰 쪽(특히 Anthropic)에서 “긴 prefix를 자주 다시 쓰는 구조”라면 오히려 write 비용이 발목 잡을 수 있음 (www-cdn.anthropic.com)
🔧 핵심 개념
1) Prompt caching이 “왜” 돈을 아끼는가: KV cache 재사용
LLM inference에서 입력 프롬프트를 처리하는 prefill 단계는 긴 문맥일수록 비싸고, 여기서 생성되는 attention의 K/V tensors(KV cache)가 계산량의 핵심입니다. Prompt caching은 동일한 prefix에 대해 이 KV를 재사용(또는 보관/오프로딩)해서, 다음 요청에서는 prefix prefill을 거의 다시 하지 않도록 만듭니다. (platform.openai.com)
즉 “토큰을 다시 보내지 않는다”가 아니라, “보내더라도 내부적으로 prefix 계산을 재사용해 비용/지연을 할인한다”에 가깝습니다.
2) OpenAI vs Anthropic: “암묵적 자동 캐시” vs “명시적 캐시 블록”
OpenAI
- 자동으로 prefix cache를 시도합니다(최근 모델들에 기본 활성). 캐시 히트는 “동일한 prefix의 exact match”가 전제이며, 실무적으로는 “정적 내용을 앞에 몰고, 변동 내용을 뒤로 보내라”가 정답입니다. (platform.openai.com)
- 요청이 특정 서버로 라우팅되며, prefix hash(+
prompt_cache_key선택 파라미터)를 통해 같은 prefix가 같은 머신으로 가도록 유도해 히트율을 올립니다. 트래픽이 일정 수준(문서엔 대략 분당 ~15회 수준 언급) 이상이면 overflow로 다른 머신으로 분산되어 캐시 효율이 떨어질 수 있습니다. (platform.openai.com) - 캐시 보존은 짧은 편(공개 문서 기준 “몇 분~1시간 내 정리” 뉘앙스)이고, 일부 모델은 최대 24h retention 옵션이 따로 존재합니다. (platform.openai.com)
Anthropic
- 메시지의 특정 구간(대개 system/도구 정의/고정 문서 번들)에
cache_control을 걸어 “여기까지를 prefix 캐시로 묶는다”는 형태가 일반적입니다(명시적). - 가격 구조가 특징적: cache read(히트)는 base input의 0.1×(90% 할인) 수준인 대신, cache write는 5m 기준 1.25×, 1h는 2× 같은 프리미엄이 붙습니다. (정확 수치는 모델/티어 표를 확인) (www-cdn.anthropic.com)
- 결론: Anthropic은 “히트가 잘 나면 미친 듯이 싸지고”, “히트가 안 나면 write 프리미엄 때문에 더 아플 수” 있습니다. (claudefabel.com)
3) 캐시 히트율을 결정하는 3가지: Prefix 안정성 / 라우팅 / TTL
1) Prefix 안정성: 공백 하나, JSON key 순서, tool schema의 필드 정렬, system prompt에 들어간 동적 값 하나가 prefix를 바꿔 히트를 깨뜨립니다(“exact prefix match”). (platform.openai.com)
2) 라우팅: OpenAI는 prefix hash 기반으로 서버를 선택하므로, 같은 prefix가 같은 곳에 모이도록 prompt_cache_key를 “테넌트/앱/워크플로우 단위”로 잡는 식의 전략이 유효합니다. (platform.openai.com)
3) TTL/트래픽 패턴: 캐시는 보통 “짧게 유지”되므로(예: OpenAI는 통상 몇 분 단위로 정리된다고 안내), 호출 간격이 길면 히트가 잘 안 납니다. Anthropic은 5m/1h 같은 TTL 선택이 비용과 직결됩니다. (openai.com)
💻 실전 코드
현실적인 시나리오: SaaS 백엔드에서 “정적 정책 + 고정 tool schema + 고객사별 규정 문서(긴 텍스트)”를 붙여 “지원 티켓 분류/답변 초안”을 생성합니다.
포인트는:
- 정적/반정적 덩어리를 prefix로 고정
- 사용자 티켓/메타데이터는 tail로 이동
- (OpenAI)
prompt_cache_key로 라우팅 안정화 - (Anthropic)
cache_control로 캐시 경계를 명시
아래 예제는 Node.js(TypeScript) 기준이며, 환경변수로 API 키를 받습니다.
0) 설치/환경
1
2
3
4
npm init -y
npm i openai @anthropic-ai/sdk zod
npm i -D typescript ts-node @types/node
npx tsc --init
1) 공통: “캐시 친화적” 프롬프트 빌더
- 절대 하면 안 되는 것: system prompt 맨 앞에
request_id,timestamp, “이번 호출에서만 유효한” 추적정보 넣기 - 해야 하는 것: 고정 번들을 앞에, 변동은 뒤에
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
// promptBundle.ts
import { z } from "zod";
export const Ticket = z.object({
ticketId: z.string(),
customerId: z.string(),
subject: z.string(),
body: z.string(),
priority: z.enum(["low", "medium", "high"]),
});
export type Ticket = z.infer<typeof Ticket>;
// 정적 정책(예: 회사 가이드라인) - 길수록 캐싱 이득이 커짐
export function buildStaticPolicyBundle() {
return [
"You are a senior support engineer. Follow company policy strictly.",
"Policy:",
"- Never ask for secrets.",
"- Provide reproducible steps.",
"- If uncertain, ask 1-2 clarifying questions.",
"Style:",
"- Use bullet points.",
"- End with a short next-step checklist.",
].join("\n");
}
// 반정적: 고객사별 규정 문서(변경은 드물지만 길 수 있음)
export function buildCustomerPlaybook(customerId: string) {
// 현실에선 DB/오브젝트스토리지에서 가져오고, 변경 시 버전이 올라감
return `Customer Playbook (customerId=${customerId}, version=v7):
- SLA: P1 within 1h, P2 within 8h
- Allowed actions: reset user token, reindex project
- Forbidden: delete project without explicit approval
(… imagine 10k+ tokens of internal runbook …)`;
}
// 변동: 티켓 본문/메타
export function buildDynamicTail(ticket: Ticket) {
return `Ticket:
id=${ticket.ticketId}
priority=${ticket.priority}
subject=${ticket.subject}
body:
${ticket.body}
Task:
1) Classify root cause category (one of: auth, billing, outage, bug, question)
2) Draft a reply email
3) List internal actions/tools to run`;
}
2) OpenAI: prompt_cache_key로 히트율을 “테넌트 단위”로 끌어올리기
OpenAI 문서상 캐시는 prefix exact match, 라우팅은 prefix hash(+ prompt_cache_key)를 사용합니다. 멀티테넌트 서비스라면 prompt_cache_key = customerId 혹은 workflowId로 잡아 “같은 고객사의 반복 호출이 같은 캐시에 붙도록” 설계하는 게 실전에서 효과가 큽니다. (platform.openai.com)
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
// openaiCached.ts
import OpenAI from "openai";
import { buildStaticPolicyBundle, buildCustomerPlaybook, buildDynamicTail, Ticket } from "./promptBundle";
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
export async function draftWithOpenAI(ticketInput: unknown) {
const ticket = Ticket.parse(ticketInput);
const staticPolicy = buildStaticPolicyBundle();
const customerPlaybook = buildCustomerPlaybook(ticket.customerId);
const tail = buildDynamicTail(ticket);
const res = await client.responses.create({
model: "gpt-4o", // 실제 운영 모델로 교체
// 캐시 라우팅 안정화 키(문서에 안내)
prompt_cache_key: `support-workflow:${ticket.customerId}`,
input: [
{
role: "system",
content: [
// 정적/반정적 덩어리를 앞에 몰아 prefix를 최대한 안정화
{ type: "text", text: staticPolicy + "\n\n" + customerPlaybook },
],
},
{
role: "user",
content: [{ type: "text", text: tail }],
},
],
});
// 응답에서 prompt_tokens / cached 여부 관련 필드를 관찰(모델/엔드포인트별 제공 필드 상이)
return {
id: res.id,
output_text: res.output_text,
usage: res.usage,
};
}
예상 출력(개략)
- 첫 호출: cache miss → input 전액 과금(또는 cached=0)
- 같은 customerId로 5~10분 내 반복 호출: cached input이 증가 → input 비용 절감 + TTFT 감소 (openai.com)
3) Anthropic: cache_control로 “캐시 경계”를 확실히 잡기(5m vs 1h)
Anthropic은 가격표에 cache write/read가 분리되어 있고, 히트(read)는 0.1×로 매우 싸지만 write 프리미엄이 있습니다. 따라서 “무조건 캐시”가 아니라, ‘write를 낼 가치가 있는 prefix’만 캐시해야 합니다. (www-cdn.anthropic.com)
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
// anthropicCached.ts
import Anthropic from "@anthropic-ai/sdk";
import { buildStaticPolicyBundle, buildCustomerPlaybook, buildDynamicTail, Ticket } from "./promptBundle";
const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
export async function draftWithAnthropic(ticketInput: unknown) {
const ticket = Ticket.parse(ticketInput);
const staticPolicy = buildStaticPolicyBundle();
const customerPlaybook = buildCustomerPlaybook(ticket.customerId);
const tail = buildDynamicTail(ticket);
const msg = await anthropic.messages.create({
model: "claude-sonnet-4.6", // 실제 운영 모델로 교체
max_tokens: 800,
system: [
// 여기까지를 캐시 대상으로(예: ephemeral 5m/1h는 SDK/문서 기준으로 설정)
// 정확한 type 명칭은 Anthropic의 최신 API 스펙에 맞춰 조정 필요.
{
type: "text",
text: staticPolicy + "\n\n" + customerPlaybook,
cache_control: { type: "ephemeral" }, // 5m 캐시 블록(개념)
},
],
messages: [
{
role: "user",
content: [{ type: "text", text: tail }],
},
],
});
return {
id: msg.id,
text: msg.content?.map((c: any) => c.text).join("") ?? "",
usage: msg.usage, // 여기서 cache_creation_input_tokens / cache_read_input_tokens 류를 반드시 로깅
};
}
운영에서 반드시 추가할 로깅/지표
cache_creation_input_tokens(write) vscache_read_input_tokens(hit) 비율- “write만 잔뜩 쌓이고 hit이 거의 없다”면 설계가 실패한 것(혹은 트래픽 패턴/TTL 미스매치) (claudefabel.com)
⚡ 실전 팁 & 함정
Best Practice 1) “프롬프트를 두 덩어리로” 설계하라: Immutable Prefix / Volatile Tail
- Prefix: system + tool schema + 정책 + 예시 + (가능하면) 고정 RAG 번들
- Tail: 티켓 본문, 사용자 입력, 실시간 조회 결과, timestamp, trace id
이 원칙 하나로 캐시 히트율이 급상승합니다(특히 OpenAI는 exact prefix match). (platform.openai.com)
Best Practice 2) 고객사/워크플로우 버전으로 “캐시 키/버전”을 분리하라
- 고객사별 playbook이 다르면
prompt_cache_key(OpenAI) 또는 캐시 블록(Anthropic)을 테넌트 단위로 분리 - playbook이 바뀌면
version=v8처럼 버전 문자열을 prefix 안에 넣어 “의도적으로 캐시를 갈아타게” 만들기
(안 그러면 ‘바뀐 문서’가 섞여 디버깅이 지옥이 됩니다.)
Best Practice 3) Anthropic에서는 “write 프리미엄”을 KPI로 관리
Anthropic은 5m write가 base 대비 1.25×, 1h는 2× 같은 구조라서, 긴 prefix를 자주 다시 쓰면 비용이 튑니다. “히트가 2번 이상 날 워크로드인가?”를 보고 1h를 선택해야 합니다(낮은 트래픽이면 1h가 오히려 이득일 수 있음). (www-cdn.anthropic.com)
흔한 함정/안티패턴
- 동적 tool schema 생성: JSON schema의 필드 순서/설명 문구가 매번 바뀌면 prefix가 깨져 캐시가 사실상 무력화
- 세션 중간에 system prompt 수정: 캐시된 prefix가 통째로 무효화되어 “write 폭탄”을 맞기 쉬움(특히 Anthropic) (claudefabel.com)
- 저트래픽 서비스에서 캐시를 만능으로 기대: TTL 만료로 miss가 대부분이면, 캐싱은 “비용 절감”이 아니라 “복잡도 추가”가 됩니다. 이 경우 (a) 24h retention 가능한 모델/옵션(OpenAI) 검토, (b) batch 처리로 묶기, (c) prefix 자체를 줄이기(프롬프트 다이어트)가 더 큰 레버가 될 수 있습니다. (platform.openai.com)
비용/성능/안정성 트레이드오프
- 비용: 캐시 히트율이 일정 수준을 넘으면 가장 큰 레버(입력 토큰이 긴 앱일수록)
- 성능: TTFT가 크게 줄 수 있음(특히 긴 컨텍스트) (platform.openai.com)
- 안정성/보안: “캐시가 어디까지/어떻게 격리되는가”는 게이트웨이(OpenRouter 등)나 멀티테넌트 구조에서 별도 검토 대상(연구들도 존재). 민감 데이터는 Zero Data Retention/정책을 포함해 반드시 확인하세요. (arxiv.org)
🚀 마무리
정리하면, 2026년의 prompt caching은 “있으면 좋음”이 아니라 긴 prefix를 반복하는 제품에선 1순위 비용 레버입니다. OpenAI는 자동 캐시이므로 구조를 잘 짜고 prompt_cache_key 같은 라우팅 힌트를 활용해 히트율을 공학적으로 끌어올리는 것이 핵심입니다. (platform.openai.com)
Anthropic은 cache_control로 캐시 경계를 더 명확히 잡는 대신, cache write 프리미엄이 있어 “히트가 날 워크로드인가?”를 KPI로 관리해야 합니다. (www-cdn.anthropic.com)
도입 판단 기준(실무 체크리스트) 1) 내 요청의 입력 토큰 중 “정적/반정적 prefix”가 전체의 50% 이상인가? 2) 동일 prefix가 TTL 안에 2회 이상 재사용되는가? (저트래픽이면 retention/배치/프롬프트 다이어트가 우선) 3) 캐시 write/read 토큰을 메트릭으로 뽑아 히트율을 주 단위로 개선할 수 있는가?
다음 학습 추천
- “캐시가 깨지는 패턴”을 실험으로 정리한 평가 연구(에이전트/툴 호출에서 특히 유용) (arxiv.org)
- OpenAI Prompt Caching 내부 동작/라우팅 및 24h retention 문서(모델별 지원 범위 포함) (platform.openai.com)
- Anthropic 가격표(5m/1h write, hit(read) 단가를 모델/티어별로 정확히 대입해 손익분기 계산) (www-cdn.anthropic.com)