Anthropic SDK로 툴 콜링 붙은 멀티턴 에이전트를 돌리고 있습니다. API가 stateless라서 매 요청마다 system + 지금까지의 messages 전체를 다시 실어 보내야 하는 건 이해했는데, 문제는 대화가 10턴, 20턴 넘어가면 입력 토큰이 계속 누적돼서 뒤쪽 턴 한 번 호출하는 비용이 앞쪽의 몇 배가 됩니다.
system 프롬프트가 좀 큰 편이에요. 툴 정의랑 few-shot 예시, 그리고 회사 룰 같은 게 들어가서 고정으로 꽤 됩니다. 이걸 매 턴 풀로 다시 계산해서 과금되는 것 같은데, 히스토리를 통째로 요약해서 줄여버리면 맥락이 깨질까 봐 겁납니다.
usage를 찍어보면 input_tokens가 턴마다 쭉쭉 올라가는 게 보이는데, 이 고정 프리픽스 부분을 매번 풀로 안 물게 하는 방법이 있을까요? 아니면 그냥 감수해야 하나요?
댓글 4
방법은 안정적인 부분의 마지막 블록에 cache_control ephemeral을 붙이는 거예요. 렌더 순서가 tools 다음 system 다음 messages라서, 마지막 system 블록에 브레이크포인트를 걸면 툴 정의랑 system이 통째로 캐시로 잡힙니다. 대화 히스토리는 가장 최근에 추가된 턴의 마지막 블록에도 하나 걸어두면 다음 요청이 그 앞부분 전체를 캐시로 재사용해요.
확인은 응답의 usage에서 cache_read_input_tokens 값이 0보다 큰지 보면 됩니다. 계속 0이면 프리픽스 어딘가가 매번 바뀌고 있다는 뜻이에요. system 프롬프트에 현재시각이나 요청 UUID 같은 걸 끼워 넣고 있으면 그 뒤가 통째로 캐시 무효화됩니다. 그거부터 빼세요.
요약(compaction)은 컨텍스트 창 한계에 실제로 근접했을 때 얹는 거지, 비용 문제의 1차 해법은 캐싱입니다.
그러면 캐시된 히스토리 프리픽스는 그대로 살아있고, 새 지시는 그 뒤에 추가되는 거라 캐시가 안 깨집니다. 모드 전환, 갱신된 컨텍스트 같은 걸 이 채널로 넣으면 돼요. (이건 최신 Opus에서 지원됩니다. 안 되는 모델이면 유저 턴 안에 시스템 리마인더 형태로 넣는 걸로 폴백하고요.)
원칙은 하나예요. 안 바뀌는 건 앞에, 자주 바뀌는 건 브레이크포인트 뒤에. 그 선만 지키면 캐시 히트율이 유지됩니다.