프롬프트 캐싱은 prefix 매치입니다. 요청 앞부분이 직전 호출과 바이트 단위로 같아야 적중합니다. GEODE의 캐싱 설계는 이 한 문장에서 다 나옵니다. 변하지 않는 것을 앞에, 변하는 것을 뒤에 두고, 턴마다 바뀌는 조각이 prefix를 다시 키잉하지 못하게 막습니다.
static/dynamic 경계
시스템 프롬프트는 core/agent/system_prompt.py의PROMPT_CACHE_BOUNDARY마커(<dynamic_context> 여는 태그)로 두 쪽이 납니다. 마커 앞은 턴 사이 불변(베이스 스캐폴드, 스타일 가이드, 기본 ON identity), 마커 뒤는 턴마다 변합니다(model card, 날짜, 메모리 레이어, 사용자 컨텍스트).
Anthropic 어댑터(core/llm/providers/anthropic.py)가 이 마커에서 시스템 문자열을 갈라 static 블록에cache_control: {"type": "ephemeral"}을 붙입니다. dynamic 쪽은 캐시 없이 나갑니다. static이 비어 있으면(audit 모드에서 레이어를 벗긴 경우) 빈 텍스트 블록에 cache_control을 붙이는 400 오류를 피해 dynamic 쪽을 단일 캐시 블록으로 승격합니다.
롤링 메시지 breakpoint
Anthropic은 요청당 cache_control breakpoint를 4개까지 허용합니다. 시스템 블록이 1-2개를 쓰고, 나머지는apply_messages_cache_control이 대화 이력의 마지막 메시지들에 붙입니다. 몇 개를 붙일지(0-3)는 cache-policy SoT(core/llm/cache_policy.py)가 정하고 기본값은 3입니다. breakpoint가 많을수록 긴 멀티턴 루프의 적중률이 오르지만, 캐시된 블록마다 적중 여부와 무관하게 쓰기 오버헤드가 붙습니다. 짧은 작업이라면 낮추는 쪽이 맞습니다.
비용 산식은 단가표 기준으로 cache write가 input의 1.25배, cache read가 input의 0.1배입니다 (core/llm/pricing_loader.py).
system reminder는 append 전용
멀티턴 강화용 <system-reminder> 블록 (core/agent/system_injection.py)에는 캐시 계약이 걸려 있습니다. 이전 설계는 reminder를 messages[0]에 넣고 라운드마다 다시 썼습니다. 메시지는 시스템 블록 뒤에 렌더되므로 이력 prefix 전체가 매 라운드 다시 키잉되고, 롤링 breakpoint는 한 번도 적중하지 못했습니다. 현재 계약은 두 가지입니다.
append_system_reminder는 새 리스트를 반환합니다. 호출자의 이력 리스트는 그대로이므로 reminder가 저장된 대화 컨텍스트에 stale한 중간 prefix 바이트로 남지 않습니다.- 라운드 인덱스와 날짜 같은 턴별 변량은 마지막 안정 이력 블록 뒤에 붙습니다. 매 라운드 캐시에서 빠지는 것은 reminder 자신뿐입니다.
가드 테스트는tests/core/agent/test_system_injection.py의 TestCacheContract입니다.
실패 모드
| 증상 | 원인 | 해법 |
|---|---|---|
| cache_read가 늘 0 | prefix가 매 호출 변함. static 영역에 턴별 값이 새어 들어간 경우 | 턴마다 변하는 값은 경계 마커 뒤로 옮깁니다. |
| 짧은 작업의 비용 증가 | breakpoint 쓰기 오버헤드가 적중 이득을 초과 | cache-policy SoT에서 messages_breakpoints를 낮춥니다. |
| 400: empty text block에 cache_control | 빈 static에 breakpoint를 붙이려는 시도 | 어댑터가 dynamic 승격으로 처리합니다. 직접 어댑터를 다룰 때만 해당합니다. |
다음
- 시스템 프롬프트 모드. static 영역에 무엇이 실리는지.
- 프롬프트 해싱. static 템플릿의 drift 가드.
- 비용 모니터링. 캐시 적중이 보이는 곳.