프롬프트 캐시 중단(Prompt Cache Break): 단 40줄의 코드로 히트율(Hit-Rate)이 100%에서 40%로 급락하다
요약
프롬프트 캐싱의 효율을 떨어뜨리는 '프롬프트 캐시 중단(Prompt Cache Break)' 현상을 분석합니다. 타임스탬프나 도구 순서 변경 등 미세한 접두사 변화가 캐시 히트율을 급락시키고 비용을 증가시키는 문제를 다룹니다.
핵심 포인트
- 프롬프트 캐싱은 바이트 단위로 완전히 동일한 접두사에만 적용됨
- 타임스탬프 등 동적 데이터 삽입 시 캐시 미스로 인한 비용 상승 발생
- 캐싱 활성화 여부보다 실제 캐시 히트율(Hit-Rate) 측정이 중요함
- cache_break.py를 통해 접두사 세그먼트 해싱 및 히트율 검증 가능
요약: **프롬프트 캐시 중단 (prompt cache-break)**이란 프롬프트 접두사(prefix) 상단에 발생한 단 하나의 변화 — 새로운 타임스탬프(timestamp), 재정렬된 도구 블록(tool block) 등 — 로 인해 그 지점부터 캐시 미스(cache miss)가 발생하여, 캐시된 읽기(cached reads)가 조용히 새로운 입력(fresh input)으로 다시 과금되는 현상을 말합니다. cache_break.py는 각 접두사 세그먼트를 해싱(hashing)하여 중단 지점을 찾아내고 CI를 실패하게 만듭니다. 제 테스트 환경(fixture)에서는 타임스탬프 하나가 예상 캐시 히트율(cache-hit-rate)을 100%에서 40%로 떨어뜨렸습니다.
AI 공개 사항: 저는 AI 글쓰기 어시스턴트를 사용하여 이 글을 초안했습니다. 사용된 도구, 두 개의 테스트 환경(fixtures), 그리고 아래의 모든 수치는 tiktoken o200k_base를 사용한 실제 로컬 실행 결과입니다. 저는 직접 실행하고, 종료 코드(exit codes)를 확인했으며, 결과가 결정론적(deterministic)임을 확인하기 위해 출력을 두 번 해싱했으며, 게시하기 전에 모든 줄을 편집했습니다.
프롬프트 캐싱(prompt caching)을 활성화하는 것은 마치 공짜 승리처럼 느껴집니다. 하지만 대개 여러분이 생각하는 그런 승리가 아닙니다.
여기에 함정이 있습니다. Anthropic과 OpenAI 모두 캐시 읽기(cache read) 비용을 새로운 입력 토큰(fresh input token) 비용의 일부로 책정합니다. Anthropic은 캐시된 읽기 비용을 새로운 입력 대비 약 $0.30/M 대 $3.00/M로 제시하며, 캐시된 부분에 대해 약 10배의 차이를 둡니다 (Anthropic prompt caching docs). 기능을 켜고, 대시보드를 확인하고, 다음 단계로 넘어가면 됩니다. 하지만 그 할인은 이미 캐시된 것과 바이트 단위로 완전히 동일한(byte-for-byte identical) 접두사에 대해서만 적용됩니다. 상단 근처의 문자 하나만 바꿔도 그 지점부터 캐시 미스가 발생합니다. 여러분은 여전히 캐싱을 켜둔 상태이지만, 캐시를 *적중(hitting)*시키지 못하고 있는 것입니다. 그리고 여러분의 설정(config) 중 그 어떤 것도 이에 대해 경고를 보내지 않습니다.
따라서 실제로 중요한 수치는 "캐싱이 활성화되었는가"가 아닙니다. 바로 실제 **캐시 히트율 (cache-hit-rate)**이며, 거의 아무도 이를 측정하지 않습니다.
요약 (TL;DR). 프롬프트 캐싱 (Prompt caching)은 정확한 접두사 일치 (exact-prefix match) 시에만 이득을 줍니다. 에이전트들은 자신도 모르게 이 일치를 깨뜨립니다. 시스템 블록 내의 동적인 now=... 값, 순서가 바뀐 두 개의 도구 정의 (tool definitions), 상단에 삽입된 메모리 조각 등이 그 예입니다. 접두사가 깨지면, 그 지점 아래의 캐시는 미스 (cache miss) 처리되며 새로운 입력으로 다시 비용이 청구됩니다. cache_break.py (아래, 키리스(keyless), 오프라인 방식)는 매 단계마다 각 접두사 세그먼트를 해싱하고, 기준점(baseline)에서 벗어나는 첫 번째 세그먼트를 찾아내어 히트율 (hit-rate)을 계산하고 제어합니다. 깨끗한 트레이스 (trace)에서는 **100%**의 히트율을 추정하며 종료 코드 0을 반환했습니다. 하지만 타임스탬프가 하나 주입된 동일한 콘텐츠에서는 **40%**를 기록했으며, 3단계의 0번 세그먼트에서 캐시 중단 (cache-break)이 감지되어 종료 코드 1을 반환했습니다.
반전 포인트: "캐싱이 켜져 있다"는 "캐싱이 작동하고 있다"는 뜻이 아닙니다
대부분의 캐싱 관련 글들은 _어떻게 켜는가_에서 멈춥니다. 접두사를 표시하고, cache_control을 설정하면 끝입니다. 그것은 쉬운 80%에 불과합니다. 비용이 많이 드는 나머지 20%는, 실행 중인 에이전트에서 이후에 조용히 일치 여부를 무효화하여 전체 비용 측면에서는 절대 알아차릴 수 없는 모든 요소들입니다.
설정을 건드리지 않고도 에이전트가 스스로 캐시를 깨뜨리는 세 가지 방법은 다음과 같습니다:
- 시스템 프롬프트 내의 동적인 값. 전형적인 사례는 모델이 "현재 시간을 알 수 있도록" 시스템 블록에 찍히는 현재 타임스탬프 —
now=2026-06-21T08:14:03Z— 입니다. 이는 호출할 때마다 변경됩니다. 이것이 맨 상단에 위치하면, 모든 단계에서 모든 내용에 대해 캐시 미스가 발생합니다. - 순서가 바뀐 도구 정의 (tool definitions). 프레임워크가 딕셔너리 (dict)나 세트 (set)로부터 도구 스키마 (tool schemas)를 직렬화 (serialize)할 때 발생합니다. 두 개의 도구를 실행할 때 순서가 뒤바뀌면 바이트 (bytes)가 달라지며, 접두사가 더 이상 일치하지 않게 됩니다. 도구는 동일하지만 캐시는 깨집니다.
- 앞부분에 삽입된 메모리 조각. "지난 세션에서 배운 내용"을 앞에 붙이는 검색 증강 메모리 (Retrieval-augmented memory)는 가변적인 블록을 안정적인 블록 위에 배치합니다. 그 아래에 있는 모든 내용은 이제 새로운 입력이 됩니다.
반증 가능한 주장: 깨끗한 트레이스(trace)와 접두사(prefix) 순서만 깨진 _바이트 단위로 동일한 내용(byte-identical-content)_의 트레이스를 비교했을 때, 실제 측정 결과가 깨진 트레이스에서는 히트율(hit-rate) 급락을 보여주고 깨끗한 트레이스에서는 높은 상태를 유지해야 합니다. 만약 급락하지 않는다면 제 생각이 틀린 것이며 이 도구는 쓸모가 없습니다. 결과는 급락했습니다 — 100%에서 40%로 — 그리고 탐지기는 정확한 세그먼트(segment)를 지목했습니다. 실행 결과는 아래와 같습니다.
현재 연구 분야에서는 이러한 실패 모드(failure mode)를 부르는 명칭도 있습니다. arXiv 노트인 Don't Break the Cache (2601.06007, Lumer et al., Jan 2026)는 캐싱된 에이전트 추론(agentic inference)을 위한 접두사 안정성(prefix-stability) 규율에 대해 다루고 있으며, 공식적인 이론적 접근을 원하신다면 읽어볼 가치가 있습니다. 여기서 사용된 70% 기본 임계값(gate)은 그들의 것이 아니라 제가 직접 정한 것입니다. 즉, 캐싱된 토큰(cached-token) 수를 기록하고, hits / (hits + full)을 계산하여, 이 값이 ~70% 미만으로 떨어지면 경고를 보내는 방식입니다. 이 수치는 시작점일 뿐 법칙이 아닙니다 — 여러분의 트레이스에 맞춰 조정하십시오.
도구: 40줄의 코드, API 키 불필요, 읽기 전용
cache_break.py는 하나의 JSONL 트레이스를 읽습니다. 각 라인은 prefix를 가진 하나의 에이전트 단계(step)로 구성됩니다. 여기서 prefix란 캐싱될 것으로 기대하는 명명된 세그먼트들(system, tool_defs, memory)의 순서가 있는 리스트입니다. 이 도구는 네 가지 결정론적인 작업을 수행합니다.
- 접두사 안정성 해시 (Prefix-stability hash). 각 세그먼트를 정규화(sorted keys를 사용한
json.dumps)하고 sha256 해싱합니다. 1단계(step-1)의 접두사를 기준점(baseline)으로 고정합니다. - 중단 지점 국지화 (Break-point localization). 모든 단계에 대해 세그먼트 해시를 기준점과 비교합니다. 처음으로 달라지는 세그먼트가 중단 지점(break point)입니다. 변경된 바이트 이후의 모든 내용은 새로운 것으로 다시 비용이 청구되기 때문에, 그 지점부터 아래로는 캐시 미스(cache miss)가 발생합니다.
- 히트율 계산 (Hit-rate compute). 중단 지점 위의 토큰은 캐싱된 것(cached)으로 간주하며, 중단 지점과 그 아래의 모든 것은 새로운 것(fresh)으로 간주합니다.
hit-rate = cached / (cached + fresh)를 모든 단계에 대해 합산합니다. tiktokeno200k_base를 사용하며, tiktoken이 없는 경우 len/4를 대체값으로 사용합니다. - 임계값 검사 (Gate). 전체 히트율을
--min-hit-rate(기본값 0.70)와 비교합니다. 이보다 낮거나 캐시 중단(cache-break)이 감지되면 → exit 1을 반환합니다. 이는 실행 전 단계의 게이트입니다. 여러분의 캐시를 뚫고 지나가 버리는 프롬프트 순서를 그대로 배포하지 마십시오.
내장된 하나의 정직한 규칙: 출력값에 source: estimated from prefix stability라고 표시됩니다. 만약 여러분의 트레이스(trace)에 실제 제공업체의 usage 필드(Anthropic/OpenAI는 캐시된 토큰 수를 반환함)가 포함되어 있다면, 대신 그것을 사용하여 measured라고 표시될 것입니다. 저는 추정치를 측정값인 것처럼 꾸미지 않습니다.
#!/usr/bin/env python3
"""cache_break.py - JSONL 트레이스에서 프롬프트 캐시 히트율(cache-hit-rate)을 측정하고 캐시 중단(cache-break) 지점을 식별합니다."""
import json, hashlib, sys
...
키(key)도, 네트워크도, 디스크에 기록되는 것도 없습니다. pip install tiktoken을 실행하고, 트레이스를 지정한 뒤, 종료 코드(exit code)를 읽으십시오. 만약 tiktoken이 설치되어 있지 않으면 len/4 방식으로 대체되며 헤더에 그 사실을 명시합니다 (실제 BPE와 약 ±15% 차이 발생). 저는 정밀도를 속이느니 차라리 주의 사항을 출력하겠습니다.
실제 실행
두 가지 피스처(fixtures)가 함께 제공됩니다. 둘 다 합성된 5단계 코딩 세션이며(개인 데이터 없음), 동일한 payments-svc 작업과 동일한 세 개의 세그먼트 접두사(prefix)인 system, tool_defs, memory를 사용합니다.
**trace_clean.jsonl**은 모든 단계에서 해당 접두사를 바이트 단위로 동일하게 유지합니다. 오직 사용자 꼬리(user tail) 부분만 변경되며, 사용자 꼬리는 캐시된 접두사의 일부가 아닙니다. 실제 출력:
cache_break | fixtures/trace_clean.jsonl | tokenizer: tiktoken o200k_base (exact) | source: estimated from prefix stability
------------------------------------------------------------------------------
steps=5 prefix_segments=3 (system, tool_defs, memory)
...
히트율(hit-rate) 100%, 중단 없음, 종료 코드 0. 정상(Green)입니다. 전체 접두사가 매 단계 캐시를 타고 이동하며, 유효 가격은 최저 수준인 $0.30/M에 머뭅니다.
**trace_broken.jsonl**은 _정확히 동일한 내용_이지만, 한 가지 변경 사항이 있습니다. 3단계부터 시스템 세그먼트에 실시간 시계인 now=2026-06-21T08:14:03Z가 포함되어, 매 단계마다 값이 달라집니다. 이것이 유일한 수정 사항입니다. 실제 출력:
cache_break | fixtures/trace_broken.jsonl | tokenizer: tiktoken o200k_base (exact) | source: estimated from prefix stability
------------------------------------------------------------------------------
steps=5 prefix_segments=3 (system, tool_defs, memory)
...
40% hit-rate, exit 1. 그리고 이는 정확히 범인을 가리킵니다: segment 0, system, step 3에서 처음 발견됨. step 1–2는 정상적으로 캐싱되었습니다. 하지만 step 3부터는 타임스탬프(timestamp)가 접두사(prefix)의 맨 최상단에 위치하게 되므로, 시스템 블록(system block)뿐만 아니라 그 아래에 있는 tool_defs와 memory에 대해서도 캐시 미스(cache miss)가 발생합니다. 이 두 가지는 전혀 변하지 않았음에도 말이죠. 이것이 프롬프트 캐시 중단(cache-break)의 잔혹한 점입니다: 상단에서의 손상이 그 아래의 모든 것을 무효화합니다. 이 접두사(prefix)의 혼합 가격(blended price)은 $0.30에서 $1.92/M로 치솟았으며, 개발자가 도움을 주려고 추가한 단 하나의 필드 때문에 이 고정값(fixture)에서 6.4배의 급증이 발생했습니다.
이것이 타임스탬프 때문이며 다른 이유는 없음을 증명하는 두 숫자를 보십시오. 동일한 payments-svc 콘텐츠, 동일한 도구(tool) 목록, 동일한 메모리(memory)입니다. 캐시된 토큰(cached tokens)은 335에서 134로 감소했고, 새로운 토큰(fresh tokens)은 0에서 201로 증가했습니다. 입력값의 유일한 차이점(delta)은 시계(clock)뿐이었습니다.
사람들이 논쟁하는 설계 결정(design decision)이기 때문에 보여줄 가치가 있는 실행 결과가 하나 더 있습니다. 중단(break)은 단순히 낮은 비율(low rate)을 의미하는 것이 아니라, *그 자체로 하나의 게이트 조건(gate condition)*입니다. 따라서 임계값(threshold)을 0.30까지 완전히 완화하더라도, 깨진 트레이스(broken trace)는 여전히 실패합니다:
$ python3 cache_break.py fixtures/trace_broken.jsonl --min-hit-rate 0.30
cache_hit_rate : 40.00% (threshold 30%)
cache_break : TRUE at segment 0 'system', first seen step 3
...
40%는 30% 기준을 통과하지만, 중단(break)은 여전히 exit 1을 발생시킵니다. 저는 의도적으로 그렇게 설계했습니다. 접두사 중단(prefix-break)이 감지되었다는 것은 측정 가능한 어딘가에서 돈이 새고 있다는 것을 의미하며, "평균은 여전히 괜찮다"라는 논리는 바로 그 돈이 한 달 동안 계속 새어나가게 만드는 전형적인 사고방식이기 때문입니다. 이에 대해 제 의견에 동의하지 않으셔도 됩니다. 이것은 실제적인 설계 트레이드오프(tradeoff)이지, 법칙은 아니니까요.
잘못된 입력(Bad input)은 세 번째 종료 코드(exit code)입니다. 형식이 잘못된 JSONL 라인은 크래시(crash)를 일으키거나 잘못된 통과(false pass)를 하는 대신 exit 2를 반환합니다:
$ python3 cache_break.py fixtures/trace_bad.jsonl
cache_break | BAD INPUT: Expecting ',' delimiter: line 2 column 1 (char 82)
[exit 2]
두 번의 실제 실행 모두 재현 가능합니다. 저는 shasum -a 256을 사용하여 연속된 두 번의 정상 실행(clean runs)과 연속된 두 번의 오류 실행(broken runs)을 해싱했습니다. 각 쌍은 바이트 단위로 일치했습니다 (3608e4d5…는 정상, fb72c110…은 오류). 이는 결정론적(Deterministic)인 현상이며, 단 한 번의 우연이 아닙니다.
내 실행에서는 6.4배, 한계치에서는 10배 — 솔직한 버전
캐시 중단(cache breaks)에 대해 "10배"라는 표현이 떠도는 것을 보게 될 것입니다. 그 수치가 어디서 나왔는지, 그리고 제가 실제로 얻은 결과는 무엇인지 말씀드리겠습니다. 10배는 단위(unit) 격차입니다. Anthropic이 발표한 캐시된 읽기(cached-read) 대비 새로운 입력(fresh-input)의 비율은 대략 $0.30 대 $3.00로, 캐시에서 새로운 입력으로 전환되는 토큰에 대해 약 10배의 차이가 납니다 (Anthropic docs). 이는 그들의 공개된 수치이지, 제 수치가 아닙니다.
이 테스트 장치의 접두사(prefix)에 대해 도구가 계산한 값은 6.4배 — $0.30/M에서 $1.92/M로 — 였습니다. 왜냐하면 접두사 토큰의 전부가 아닌 60%만이 새로운 입력으로 전환되었기 때문입니다. 만약 장기 실행되는 에이전트(agent)의 첫 번째 단계에서 세그먼트 0에 중단이 발생한다면(가장 흔한 사례인 동적 타임스탬프 케이스), 모든 접두사 토큰이 새로운 입력으로 다시 과금되며 10배에 근접하게 됩니다. 따라서 10배는 공개 가격 기준의 한계치(ceiling)이며, 6.4배는 제 실행에서 얻은, 더 작지만 정직하게 라벨링된 실제 수치입니다. 저는 제가 보여드릴 수 없는 10배보다, 제가 보여드릴 수 있는 6.4배를 여러분이 더 신뢰하기를 바랍니다.
이는 컨텍스트 세금 (context tax)과 형태는 유사하지만, 누수(leak)의 성격이 다릅니다. 컨텍스트 세금의 경우, 대화 내용이 계속 늘어나기 때문에 매 단계마다 _전체 대화 내용(transcript)_에 대해 다시 과금됩니다. 이는 계측기(meter)와 같으며 방어 기제가 아닙니다. 반면 여기서는 캐시를 통해 접두사가 거의 무료여야 하는데, 단 1바이트의 변화가 조용히 그 무료 상태를 해제해 버립니다. 축은 다르고 수치도 다르지만, 교훈은 같습니다. 직접 측정하십시오. 기능이 제 역할을 했다고 가정하지 마십시오.
이것이 어디에 해당하는지
이 도구가 모니터링하는 접두사(prefix) — 즉, 길고 안정적인 system + 도구 정의(tool definitions) + 메모리 블록 — 는 제가 your MCP server's token tax에서 원가(raw cost)를 측정할 때 사용했던 것과 동일합니다. Token-tax는 "이 접두사의 크기가 얼마나 큰가"를 묻는다면, cache-break는 "당신이 실제로 캐시된 가격을 지불하고 있는가, 아니면 조용히 새로운(fresh) 가격을 지불하고 있는가"를 묻습니다. 동일한 트레이스(trace)에서 두 가지를 모두 실행하면 접두사의 크기와 캐시 히트 여부라는 두 가지 측면을 모두 다룰 수 있습니다.
또한 이는 pre-execution gate와 동일한 철학을 따릅니다. 즉, 잘못된 프롬프트 순서(prompt ordering)를 배포하기 전에 잡아내어, 청구서가 나온 후가 아니라 빌드 단계에서 실패하게 만드는 것입니다. Cache-break는 결정론적(deterministic)이기 때문에 완벽한 CI 게이트(gate)가 됩니다. 즉, 동일한 트레이스에 대해 매번 동일한 판정을 내립니다. 만약 구조적 점검 대신 런타임 지출 제동 장치를 원한다면, 그것은 sliding-window spend guard입니다. 이는 접두사의 안정성을 감사하는 대신, 일정 기간(window) 동안의 누적 비용을 제한합니다.
이것이 아닌 것 (과장하지 않기 위해)
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기