birchholt 직접 해보고 남기는 기록

LLM 대화를 매번 통째로 보내면 서버는 다시 계산할까 — 프롬프트 캐싱 실측

LLM API로 대화를 이어 가면 매 요청에 앞 대화 전체가 다시 실려 갑니다. 그렇다면 서버는 이미 본 앞부분을 매번 처음부터 다시 계산할까요? 대화 배열을 직접 들고 LLM API를 호출해 봤고 요금이나 지연이 턴마다 커지는 것이 신경 쓰였던 분을 위한 글입니다. 읽고 나면 제공사가 이미 보낸 앞부분을 어떻게 알아보는지, 캐시가 덜어 주는 것과 덜어 주지 않는 것이 무엇인지, 하네스 코드에서 무엇을 지켜야 캐시가 살아나는지를 알 수 있습니다.

이 글은 「에이전트 하네스를 직접 만들어 본다」 시리즈의 첫 번째 내용인 대화 배열 글에서 이어지는 글입니다. 그 글에서는 대화가 내 코드의 배열 하나이고 매 호출마다 그 배열 전체를 다시 보내기 때문에 입력 토큰이 턴마다 쌓인다는 것을 로그로 확인했습니다.

결론부터 말하면 서버는 요청의 앞부분이 전과 같은지 확인해 그만큼의 계산을 다시 쓰지만 클라이언트는 여전히 배열 전체를 보내고 그 토큰은 컨텍스트 윈도우를 그대로 차지합니다. 이번 Gemini 무료 등급 실측에서는 자동 캐시가 12턴 내내 한 번도 맞지 않았고 이름으로 부르는 명시적 캐시는 5,809토큰을 캐시에서 읽었습니다.

이미 보낸 앞부분은 같은 내용입니다

대화 배열 글의 세 턴짜리 실험(v1-2)을 다시 보면 세 번째 요청의 앞 원소 세 개는 두 번째 요청 전체와 똑같습니다. 요청 맨 앞에서부터 이어지는 이 공통 부분을 프리픽스(prefix, 앞부분)라고 부릅니다. 대화 배열은 끝에만 붙어 가므로 턴이 바뀌어도 프리픽스는 그대로 남습니다. 제공사들은 이 성질을 이용해 앞 요청에서 계산해 둔 프리픽스의 상태를 다시 씁니다. 이것을 프롬프트 캐싱(또는 프리픽스 캐싱)이라고 합니다.

같은 앞부분인지는 해시로 맞춰 봅니다

방식을 문서로 공개한 구현들은 들어온 요청의 앞부분이 전에 계산한 것과 같은지 해시로 판단합니다. 해시는 클라이언트가 만들어 보내는 값이 아니고 서버가 받은 토큰으로 직접 계산합니다. Gemini 캐싱 문서는 맞춰 보는 방식을 적어 두지 않았습니다. 해시는 길이와 상관없이 데이터를 짧은 고정 길이 값으로 바꾼 지문이고 내용이 조금만 달라도 값이 달라집니다. 구현마다 해시를 만드는 단위가 다릅니다.

제공사가 주는 세 가지 방식

  1. 요청마다 프리픽스를 맞춰 보는 캐싱. 클라이언트는 하던 대로 배열 전체를 보내고 서버가 앞부분을 맞춰 봅니다.
    • Gemini: 2.5 이후 모델은 암묵적 캐싱이 기본으로 켜져 있고 최소 입력은 3.6 Flash 기준 4,096토큰입니다. 적중하면 할인이 자동으로 반영됩니다. Google 문서: Context caching.
    • OpenAI: 기본으로 켜져 있고 GPT-5.6 이후 모델의 최소 길이는 1,024토큰, 할인은 최대 95%입니다. 다만 GPT-5.6 이후 모델은 캐시 쓰기가 일반 입력가의 1.25배(기본 30분 보관)로 따로 매겨집니다. 적중 토큰 수는 usage.input_tokens_details.cached_tokens에 나옵니다.
    • Anthropic: 캐시할 지점을 cache_control로 표시합니다. 블록마다 직접 달 수도 있고 요청 최상위에 하나만 달면 마지막 블록에 자동으로 붙어 대화를 따라 앞으로 옮겨 갑니다. 캐시 항목의 TTL(지우기 전까지 보관하는 시간)은 기본 5분이고 쓸 때마다 무료로 연장되며 1시간 옵션도 있습니다. 쓰기는 기본 입력가의 1.25배(1시간은 2배), 읽기는 0.1배(Opus 5.5·Sonnet 5.5는 0.05배)이고 캐시할 수 있는 최소 길이는 모델에 따라 512~4,096토큰입니다.
  2. 이름으로 부르는 명시적 캐시(Gemini). cachedContents.create로 앞부분을 TTL과 함께 미리 올려 두고 이후 요청에서는 그 캐시의 이름(cache.name)만 참조합니다. 이 경우에는 캐시한 부분을 요청 본문에 다시 싣지 않습니다. generateContent API의 Gemini 3·2.5 모델에서만 되고 Interactions API와 Gemini 4 Argon 이후 모델은 지원하지 않습니다. 기본 TTL은 문서에 나와 있지 않습니다. 유료 등급 가격은 3.6 Flash 기준 일반 입력 $0.75/1M토큰, 캐시 읽기 $0.075/1M토큰으로 약 90% 싸고 명시적 캐시는 저장료 $0.50/1M토큰/시간이 따로 붙습니다(2026-12-31까지 가격). 무료 등급은 캐싱과 저장 모두 무료로 표시돼 있습니다. Google 가격 페이지: Gemini API pricing.
  3. 서버가 대화를 들고 있는 방식. OpenAI Responses API는 previous_response_id로 앞 응답을 가리키면 기록을 다시 보내지 않아도 됩니다. 다만 체인에 있는 이전 입력 토큰은 모두 다시 입력 토큰으로 과금된다고 문서에 적혀 있습니다. OpenAI 문서: Conversation state. Gemini Interactions API의 previous_interaction_id도 서버가 기록을 불러와서 전체 기록을 다시 보낼 필요가 없게 해 줍니다(store=true가 기본, 유료 등급은 55일 보관). tools와 system_instruction은 매번 다시 지정해야 하고 이전 턴 토큰이 다시 과금되는지는 문서에 나와 있지 않습니다. Google 문서: Interactions API overview.

대화 배열 글의 실험은 셋 중 어느 것도 쓰지 않고 배열을 직접 들고 다녔습니다.

직접 재 본 결과: 자동 캐시는 한 번도 맞지 않았습니다

자동 캐싱이 실제로 맞는지 보려고 최소 4,096토큰을 넘는 긴 첫 메시지를 만들었습니다. 원 레포의 MIT 라이선스 전문에 번호를 붙여 24번 이어 붙였고 그 뒤로 짧은 후속 질문 5개를 보냈습니다. 매 턴 배열 전체를 다시 보냈고 턴 사이에는 2.5초를 기다렸습니다.

턴   1회차 prompt   2회차 prompt
1        5809           5809
2        5960           5907
3        6148           6056
4        6268           6177
5        6422           6303
6        6483           6392

제가 돌려 보니 12턴 모두 응답에 cachedContentTokenCount 필드가 아예 없었습니다. 캐시 적중 0토큰이라는 뜻입니다. 입력은 5,809~6,483토큰으로 문서의 최소 4,096을 넘었고 2회차 1턴은 1회차 1턴과 똑같은 앞부분을 40초쯤 뒤에 보냈는데도 맞지 않았습니다. 문서가 권하는 대로 큰 공통 내용을 앞에 두고 비슷한 요청을 짧은 간격으로 보냈지만 문서는 적중을 약속하지 않습니다. 왜 맞지 않았는지는 확인하지 못했습니다. prompt가 자라는 방식은 대화 배열 글의 실험과 같았습니다. 1회차 2턴의 5,960은 직전 total 5,948보다 12 많고 2회차도 5,907로 직전 5,895보다 12 많습니다. 이 12가 새 질문 몫으로 보입니다.

같은 시스템 메시지와 라이선스 24벌로 명시적 캐시를 TTL 300초로 만들자 HTTP 200과 함께 totalTokenCount 5,809가 돌아왔고 만료 시각은 만든 시각에서 약 5분 뒤였습니다. 그 캐시를 참조하고 짧은 질문 하나를 보낸 호출의 사용량입니다.

// runs/cache-probe-1.json  explicit.generate.usage (발췌)
{
  "promptTokenCount": 5820,
  "cachedContentTokenCount": 5809,
  "candidatesTokenCount": 39,
  "thoughtsTokenCount": 273,
  "totalTokenCount": 6132
}

2회차도 같은 5,809토큰이 캐시에서 읽혔습니다(출력 33, 추론 297, 합계 6,150). 다 쓴 캐시는 DELETE로 지웠고 HTTP 200이 왔습니다. 이 호출의 추론 토큰 273은 이번 호출이 새로 만든 출력이라 캐시에서 읽은 5,809와는 따로 세어졌습니다.

캐시가 덜어 주는 것과 그대로 남는 것

위 호출의 promptTokenCount 5,820에는 캐시에서 읽은 5,809가 들어 있습니다. Gemini API 문서도 cachedContent를 쓸 때 promptTokenCount는 캐시된 토큰을 포함한 전체 프롬프트 크기라고 적습니다. Google API 레퍼런스: generateContent. Anthropic 컨텍스트 윈도우 문서도 input_tokens, cache_read_input_tokens, cache_creation_input_tokens 셋 모두 윈도우에 들어간다고 적습니다.

캐시가 적중해도 그 토큰은 컨텍스트 윈도우를 그대로 차지합니다. 캐시가 덜어 주는 것은 그 프리픽스를 다시 계산하는 일, 그만큼의 응답 지연, 그리고 해당 토큰의 단가입니다. 자동 캐싱에서는 클라이언트가 여전히 배열 전체를 보내고 Gemini 명시적 캐시는 요청 본문을 줄여 주지만 모델이 보는 입력 크기는 같습니다. 레이트 리밋 처리는 제공사마다 다릅니다. OpenAI는 캐시된 입력 토큰도 TPM(분당 토큰 한도)에 포함한다고 적고 Anthropic은 캐시 적중분이 레이트 리밋에 차감되지 않는다고 적습니다.

하네스가 지킬 규칙: 앞은 그대로 두고 뒤에만 붙입니다

해시가 앞에서부터 누적되기 때문에 배열 앞쪽이 한 글자만 바뀌어도 바뀐 지점부터 뒤 전체가 캐시와 맞지 않아 다시 계산됩니다. 맨 앞이 바뀌면 전부 다시 계산됩니다. 하네스 쪽에서 지킬 일은 이렇습니다.

확인하지 못한 것

실습 환경

원 레포: harness-engineering-101. 본문은 CC BY-NC-ND 4.0이고 이 글은 본문을 인용하거나 번역하지 않았습니다. 코드는 MIT, Copyright (c) 2026 Isuru Wijesiri(LICENSE-CODE)입니다. 이 글은 그 코드를 직접 실행한 기록입니다.