Claude Code에서 프롬프트 캐싱을 효과적으로 최적화하는 방법을 소개한다. 프롬프트 구조 설계부터 도구 활용, 컴팩션(compaction) 단계적 적용까지 실전 노하우를 담았다.
엔지니어링 세계에서 "캐시가 모든 것을 지배한다"는 말이 있는데, 에이전트의 세계에서도 이 법칙은 그대로 통한다.
Claude Code처럼 오랜 시간 동안 실행되는 에이전트 제품이 현실적으로 운용 가능한 이유는 프롬프트 캐싱(prompt caching) 덕분이다. 이전 왕복 요청의 연산 결과를 재사용함으로써 지연 시간과 비용을 대폭 줄일 수 있다.
Claude Code는 프롬프트 캐싱을 중심으로 전체 하네스를 설계한다. 프롬프트 캐시 적중률이 높으면 비용이 낮아지고, 구독 플랜의 요청 한도를 더 여유롭게 설정할 수 있다. 그래서 우리는 캐시 적중률에 알림을 걸어두고, 수치가 너무 낮아지면 장애(SEV)로 선언한다.
이 글에서는 프롬프트 캐싱을 대규모로 최적화하면서 깨달은 교훈들을 소개한다. 직관과 어긋나는 내용도 적지 않다.

프롬프트 캐싱은 접두사 매칭(prefix matching) 방식으로 동작한다. API는 요청의 처음부터 각 cache_control 중단점(breakpoint)까지의 내용을 캐시한다. 즉, 내용을 배치하는 순서가 매우 중요하다. 최대한 많은 요청이 동일한 접두사를 공유하도록 설계해야 한다.
가장 좋은 방법은 정적인 내용을 앞에, 동적인 내용을 뒤에 배치하는 것이다. Claude Code에서는 이렇게 구성한다:
이 구조 덕분에 여러 세션이 캐시 적중을 최대한 공유할 수 있다.
다만 이 접근 방식은 생각보다 깨지기 쉽다. 우리도 여러 이유로 이 순서를 망가뜨린 적이 있다. 정적 시스템 프롬프트에 상세한 타임스탬프를 넣거나, 도구 정의 순서를 비결정적으로 뒤섞거나, 도구의 파라미터를 수정(예: Agent 도구가 호출할 수 있는 에이전트 목록 변경)한 것이 그 예다.
시간이 바뀌거나 사용자가 파일을 수정하는 등, 프롬프트에 넣어둔 정보가 오래된 경우가 생길 수 있다. 이때 프롬프트를 직접 수정하고 싶은 마음이 들겠지만, 그렇게 하면 캐시 미스가 발생해 사용자 비용이 크게 늘어날 수 있다.
대신, 해당 정보를 에이전트의 다음 턴 메시지로 전달할 수 있는지 먼저 고려해보자. Claude Code에서는 다음 사용자 메시지나 도구 결과에 태그를 추가해 모델에 업데이트된 정보를 전달한다. 이 방식이 캐시를 유지하는 데 효과적이다.
프롬프트 캐시는 모델마다 독립적으로 존재하기 때문에, 모델 전환이 캐싱 비용에 미치는 영향이 직관과 상당히 다를 수 있다.
예를 들어, Opus와 대화를 10만 토큰 이어온 상황에서 간단한 질문 하나를 Haiku에 던지려 한다고 해보자. 이 경우 Haiku로 전환하는 것이 Opus에게 그냥 묻는 것보다 오히려 더 비쌀 수 있다. Haiku용 프롬프트 캐시를 처음부터 다시 쌓아야 하기 때문이다.
모델을 전환해야 한다면 서브에이전트(subagent)를 활용하는 것이 가장 좋다. 앞의 예를 이어서 설명하면, Opus에게 다른 모델에게 넘길 "인수인계" 메시지를 작성하도록 지시하는 서브에이전트를 실행하는 방식이다. 실제로 우리는 Haiku를 사용하는 Claude Code의 탐색(Explore) 에이전트에서 이 방식을 자주 사용한다.
대화 중간에 도구 세트를 변경하는 것은 프롬프트 캐싱을 망가뜨리는 가장 흔한 원인 중 하나다. "지금 당장 필요한 도구만 모델에게 제공해야 한다"는 생각은 직관적으로 맞아 보이지만, 도구는 캐시된 접두사의 일부이기 때문에 도구를 추가하거나 제거하는 순간 전체 대화에 대한 캐시가 무효화된다.
캐시를 고려해 설계한 플랜 모드
플랜 모드(Plan Mode)는 캐싱 제약을 고려해 기능을 설계한 좋은 예시다. 직관적으로 생각하면 사용자가 플랜 모드에 진입할 때 읽기 전용 도구만 남기고 나머지를 제거하는 방식이 맞아 보이지만, 그렇게 하면 캐시가 깨진다.
대신 우리는 항상 모든 도구를 요청에 포함해두고, EnterPlanMode와 ExitPlanMode를 도구로 만들었다. 사용자가 플랜 모드를 켜면 에이전트는 현재 플랜 모드임을 설명하는 시스템 메시지를 받는다. 내용은 코드베이스를 탐색하고, 파일을 수정하지 말고, 계획이 완료되면 ExitPlanMode를 호출하라는 것이다. 도구 정의 자체는 절대 바뀌지 않는다.
여기에는 부가적인 이점도 있다. EnterPlanMode가 모델이 직접 호출할 수 있는 도구이기 때문에, 어려운 문제를 감지했을 때 에이전트가 캐시를 깨지 않고 스스로 플랜 모드로 진입할 수 있다.
도구를 제거하는 대신 도구 검색으로 지연 로딩하라
같은 원칙이 도구 검색 도구(tool search tool)에도 적용된다. Claude Code에는 수십 개의 MCP 도구가 로드될 수 있는데, 모든 도구를 매 요청마다 포함하면 비용이 크게 늘어난다. 그렇다고 대화 중간에 도구를 제거하면 캐시가 깨진다.
우리가 찾은 해법은 defer_loading이다. 도구를 아예 제거하는 대신, 모델이 필요할 때 도구 검색을 통해 찾아 쓸 수 있는 경량 스텁(stub)을 전달한다. 스텁에는 도구 이름과 defer_loading: true만 담겨 있으며, 전체 도구 스키마는 모델이 실제로 선택할 때만 로드된다. 동일한 스텁이 항상 같은 순서로 존재하기 때문에, 캐시된 접두사는 안정적으로 유지된다.
API에서도 도구 검색 도구를 활용해 이 과정을 간소화할 수 있다.

컴팩션(Compaction)은 컨텍스트 윈도우가 가득 찼을 때 실행된다. 지금까지의 대화를 요약하고, 그 요약을 바탕으로 새 세션을 이어간다.
컴팩션과 프롬프트 캐싱의 상호작용은 실수하기 쉬운 지점이다. 대화를 컴팩션하려면 모델이 요약을 작성할 수 있도록 전체 대화를 전송해야 한다. 가장 단순한 방법은 별도의 API 호출을 만들고, "이걸 요약해줘" 같은 별개의 시스템 프롬프트를 사용하고, 도구를 붙이지 않는 것이다. 하지만 바로 여기에 비용 함정이 있다. 프롬프트 캐싱은 요청의 접두사가 이미 캐시된 내용과 첫 번째 토큰부터 바이트 단위로 일치할 때만 적용된다. 기존 대화는 특정 시스템 프롬프트와 도구 세트를 기반으로 캐시되어 있는데, 요약 호출은 다른 시스템 프롬프트를 사용하고 도구도 없으니 첫 토큰부터 접두사가 달라진다. 결국 전송하는 대화 전체에 대해 캐시 없이 전체 입력 요금을 내게 된다. 대화가 길수록(즉, 컴팩션이 더 필요한 상황일수록) 이 한 번의 호출 비용이 눈덩이처럼 불어난다.
해법: 캐시 안전 포킹(cache-safe forking)
컴팩션을 실행할 때 우리는 상위 대화와 완전히 동일한 시스템 프롬프트, 사용자 컨텍스트, 시스템 컨텍스트, 도구 정의를 사용한다. 상위 대화의 메시지를 앞에 붙이고, 컴팩션 프롬프트를 마지막 사용자 메시지로 추가하는 방식이다.
API 입장에서 이 요청은 상위 대화의 마지막 요청과 거의 동일하게 보인다. 접두사, 도구, 대화 내역이 모두 같기 때문에 캐시된 접두사가 재사용된다. 새로 처리해야 할 토큰은 컴팩션 프롬프트뿐이다.
다만 이 방식을 위해서는 "컴팩션 버퍼"를 미리 확보해두어야 한다. 컴팩션 메시지와 요약 출력 토큰을 담을 공간을 컨텍스트 윈도우 안에 남겨둬야 하기 때문이다.
컴팩션은 까다롭지만, 다행히 이 모든 것을 직접 배울 필요는 없다. Claude Code에서 얻은 노하우를 바탕으로 우리는 컴팩션을 API에 직접 내장했으니, 누구든 자신의 애플리케이션에 이 패턴을 바로 적용할 수 있다.
에이전트를 개발하면서 프롬프트 캐싱을 최적화할 때 유용하게 쓴 패턴들을 정리한다.
Claude Code는 처음부터 프롬프트 캐싱을 중심으로 설계되었다. 에이전트를 직접 개발한다면, 같은 방식으로 접근하기를 권한다.
지금 바로 시작하기 — Claude Code를 사용해보자.
이 글은 Claude Code 팀의 테크니컬 스태프 Thariq Shihipar가 작성했습니다.