프롬프트 캐싱은 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).
대화 이력은 실제 turn만 append
AgenticLoop는 라운드 번호나 날짜를 합성 user message로 만들지 않습니다. 날짜와 runtime rule은 이미 core/agent/system_prompt.py의 동적 시스템 영역에 있고, 한 실행 안에서 대화 이력에는 실제 user, assistant, tool turn만 뒤에 붙습니다. 따라서 다음 요청의 메시지열은 이전 요청의 메시지열을 정확한 prefix로 보존합니다.
- 턴마다 바뀌는 시스템 정보는 메시지 이력에 중복 주입하지 않습니다.
- 컨텍스트 정리는 공유 이력을 직접 갱신하고, 그 뒤 adapter request를 조립합니다.
이 계약은 연속 AgenticLoop._call_llm 요청의 실제 AdapterCallRequest.messages prefix를 비교하는 회귀 테스트로 고정합니다.
실패 모드
| 증상 | 원인 | 해법 |
|---|---|---|
| cache_read가 늘 0 | prefix가 매 호출 변함. static 영역에 턴별 값이 새어 들어간 경우 | 턴마다 변하는 값은 경계 마커 뒤로 옮깁니다. |
| 짧은 작업의 비용 증가 | breakpoint 쓰기 오버헤드가 적중 이득을 초과 | cache-policy SoT에서 messages_breakpoints를 낮춥니다. |
| 400: empty text block에 cache_control | 빈 static에 breakpoint를 붙이려는 시도 | 어댑터가 dynamic 승격으로 처리합니다. 직접 어댑터를 다룰 때만 해당합니다. |
다음
- 시스템 프롬프트 모드. static 영역에 무엇이 실리는지.
- 프롬프트 해싱. static 템플릿의 drift 가드.
- 비용 모니터링. 캐시 적중이 보이는 곳.