블로그 목록
AI

Claude API 프롬프트 캐싱으로 토큰 비용 줄이기 — 같은 컨텍스트를 매번 다시 보내지 말자

문서 기반 Q&A 기능을 붙이다가 청구서를 보고 좀 놀랐다. 매 요청마다 긴 시스템 프롬프트랑 참고 문서를 통째로 같이 보내고 있었는데, 그게 전부 입력 토큰으로 잡히고 있었다. 사용자가 짧은 질문 하나를 던져도 그 앞에 깔린 2만 토큰짜리 컨텍스트가 매번 풀 가격으로 과금되는 구조였다.

생각해보면 그 앞부분은 요청마다 한 글자도 안 바뀐다. 바뀌는 건 맨 끝의 질문뿐이다. 이걸 매번 새로 처리할 이유가 없다.

프롬프트 캐싱이 푸는 문제

Claude API의 프롬프트 캐싱은 프롬프트의 앞부분(prefix)을 캐싱해두고, 다음 요청에서 그 부분이 똑같으면 캐시에서 읽어온다. 캐시에서 읽는 토큰은 일반 입력 가격의 약 0.1배다. 즉 90% 싸진다.

핵심은 "prefix 매칭"이라는 점이다. 캐시 키는 캐시 지점까지의 바이트를 그대로 해싱한 값이라, 앞부분에서 단 한 글자라도 바뀌면 그 뒤로는 전부 캐시가 깨진다. 그래서 고정된 내용은 앞에, 매번 바뀌는 내용(질문, 타임스탬프)은 뒤에 둬야 한다.

적용 방법

cache_control을 붙이고 싶은 콘텐츠 블록에 달아주면 된다. 보통 큰 시스템 프롬프트의 마지막 블록에 건다.

import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic();

const response = await client.messages.create({
  model: "claude-opus-4-8",
  max_tokens: 1024,
  system: [
    {
      type: "text",
      text: LARGE_DOCUMENT, // 매번 똑같은 큰 컨텍스트
      cache_control: { type: "ephemeral" }, // 여기까지 캐싱
    },
  ],
  messages: [{ role: "user", content: userQuestion }], // 이건 매번 바뀜
});

ephemeral 캐시는 기본 5분 TTL이다. 트래픽이 5분 간격보다 자주 들어오면 캐시가 계속 살아있어서 그대로 효과를 본다.

캐시가 진짜 먹었는지 확인

응답의 usage를 보면 알 수 있다.

console.log(response.usage.cache_creation_input_tokens); // 캐시에 쓴 토큰
console.log(response.usage.cache_read_input_tokens);     // 캐시에서 읽은 토큰
console.log(response.usage.input_tokens);                // 풀 가격으로 처리된 토큰

첫 요청에서는 cache_creation_input_tokens에 값이 잡히고(쓰기 비용은 약 1.25배), 두 번째 요청부터 cache_read_input_tokens로 넘어가야 정상이다. 만약 같은 prefix로 계속 요청하는데 cache_read_input_tokens가 0이라면, 어딘가에서 prefix를 깨뜨리고 있는 거다.

내 경우가 딱 그랬는데, 시스템 프롬프트 맨 앞에 현재 시각: ${new Date()}를 넣어두고 있었다. 이거 하나 때문에 매 요청의 prefix가 달라져서 캐시가 한 번도 안 먹었다. 시각 같은 동적 값은 시스템 프롬프트에서 빼고 메시지 쪽으로 옮기니까 바로 해결됐다.

정리

캐시 읽기는 0.1배, 쓰기는 1.25배라서 같은 prefix를 두 번만 재사용해도 본전을 뽑는다. 최소 캐시 길이(모델에 따라 1024~4096 토큰)만 넘으면 적용할 가치가 있다. 긴 컨텍스트를 반복해서 보내는 구조라면, 동적 값을 prefix에서 걷어내고 cache_control 한 줄만 달아도 청구서가 눈에 띄게 줄어든다.