
에이전트에서의 프롬프트 캐싱 (Prompt Caching)
요약
코딩 에이전트 운영 시 발생하는 비용과 지연 시간을 줄이기 위한 프롬프트 캐싱의 원리와 중요성을 다룹니다. KV 캐시의 작동 방식과 에이전트 설계 시 캐시 효율성을 고려해야 하는 이유를 설명합니다.
핵심 포인트
- 에이전트의 반복적인 컨텍스트 입력은 프롬프트 캐싱을 통해 비용과 지연 시간을 절감할 수 있음
- KV 캐시는 트랜스포머의 프리필 단계에서 계산된 키(Key)와 값(Value)을 저장하여 재계산을 방지함
- 도구 정의 변경이나 모델 전환은 캐시 효율을 떨어뜨려 전체 재현(full replay)을 유발할 수 있음
- 캐시 동작은 단순 최적화를 넘어 도구 및 세션 설계 전반에 영향을 미치는 핵심 요소임
에이전트에서의 프롬프트 캐싱 (Prompt Caching)
대규모 언어 모델 (LLM)은 종종 함수처럼 생각되곤 합니다. 즉, 텍스트를 입력하면 텍스트를 받는 식입니다. 이는 유용한 추상화이지만, 코딩 에이전트 (coding agent)를 실행할 때 가장 중요한 부분 중 하나를 간과하고 있습니다. 바로 입력값의 대부분이 지난번과 동일하다는 점입니다. 다시 말해, 우리는 주로 기존 내용에 새로운 내용을 추가(append)합니다.
코딩 에이전트는 시스템 프롬프트 (system prompt), 도구 정의 (tool definitions), 프로젝트 지침 (project instructions), 대화 기록 (conversation history), 도구 호출 (tool calls), 그리고 도구 결과 (tool results)를 모델에 보냅니다. 다음 턴 (turn)에서 에이전트는 이 중 거의 모든 것을 다시 보내며, 여기에 소량의 새로운 내용이 추가됩니다. 세션이 수만 또는 수십만 토큰으로 커지면, 매 턴마다 전체 프롬프트를 다시 계산하는 것은 느리고 비용이 많이 듭니다.
프롬프트 캐싱 (Prompt caching)은 이를 어느 정도 경제적으로 만들어 주지만, 동시에 상당히 취약하기도 합니다. 변경된 도구 정의, 모델 전환 (model switch), 또는 제공자 (provider)의 라우팅 결정은 저렴한 증분 요청 (incremental request)이 될 것으로 예상했던 것을 컨텍스트 (context)의 전체 재현 (full replay)으로 바꿔버릴 수 있습니다.
따라서 코딩 에이전트에게 캐시 동작은 단순한 구현 세부 사항이나 최적화 문제가 아닙니다. 이는 지연 시간 (latency), 비용 (cost), 도구 설계 (tool design), 세션 설계 (session design), 그리고 심지어 어떤 제품 기능이 제공되어야 하는지에까지 영향을 미칩니다.
KV 캐시 (KV Cache)에 포함되는 내용
트랜스포머 (transformer)는 크게 두 단계로 프롬프트를 처리합니다. 프리필 (prefill) 단계 동안에는 입력 토큰을 읽고 이에 대한 어텐션 상태 (attention state)를 계산합니다. 디코드 (decode) 단계 동안에는 새로운 토큰을 하나씩 생성합니다.
각 어텐션 레이어 (attention layer)에서, 처리된 모든 토큰은 키 (key)와 값 (value)을 생성합니다. 이것들은 해시 테이블 (hash table)에서의 키-값 조회 (key–value lookup)와는 조금 다릅니다. 둘 다 숫자 배열이며, 대개 부동 소수점 (floats) 또는 더 낮은 정밀도의 양자화된 값 (quantized values)입니다. 새로운 토큰을 처리할 때, 모델은 해당 토큰의 **쿼리 (query)**를 이전의 **키 (keys)**들과 비교하여 각 이전 토큰이 얼마나 관련이 있는지 결정합니다. 그런 다음 이 관련성 점수들을 사용하여 대응하는 **값 (values)**들의 가중치 혼합 (weighted mixture)을 형성합니다. 그런 의미에서, 키는 모델이 대조하는 대상이며, 값은 모델이 검색하는 정보입니다 (단, 이 조회는 사전 조회처럼 "단일 정확한 일치를 반환"하는 것이 아니라 퍼지 (fuzzy)한 방식입니다).
이러한 키와 값들은 다음 생성된 토큰이 이전 토큰들을 다시 계산하지 않고도 그 이전의 모든 것에 어텐션 (attend)할 수 있도록 유지됩니다. 이렇게 유지되는 상태가 바로 **KV 캐시 (KV cache)**입니다.
개념적으로, 요청은 다음과 같은 형태를 띱니다:
request 1:
[system][tools][user][assistant][tool result][user]
<--------------------- prefill -------------------->
...
실제 표현 (representations)은 더 복잡하고, 모델마다 다르며, "상당히" 큽니다. 중요한 속성은 이것들이 특정 토큰 접두사 (token prefix)에 대응한다는 점입니다. 의미는 같지만 토큰화 (tokenize) 방식이 다른 두 프롬프트는 KV 캐시를 공유하지 않습니다. 중간에 토큰 하나가 바뀌면, 그 토큰 이후의 모든 것은 다른 연속 (continuation)이 됩니다.
프롬프트 캐싱 (Prompt caching)은 이 상태의 수명을 단일 생성 단계를 넘어 확장합니다. 코딩 에이전트 (coding agent)로부터 오는 다음 API 요청이 동일한 토큰들로 시작될 때, 추론 시스템 (inference system)은 일치하는 접두사에 대해 저장된 작업을 재사용하고 새로운 접미사 (suffix)에 대해서만 프리필 (prefill)을 수행할 수 있습니다. 지금까지는 이론적인 내용입니다.
캐시가 저장되는 위치
캐시가 작동하려면 어딘가에 저장되어야 하며, 주소 지정 (addressable)이 가능해야 합니다. 추론 시스템이 KV 캐시를 이후의 요청에서 사용할 수 있도록 만드는 방식에는 크게 두 가지가 있습니다.
더 간단한 접근 방식은 **세션 어피니티 (session affinity)**입니다. 이는 KV 캐시를 계산한 GPU 위 또는 그 근처에 유지하고, 다음 요청을 동일한 워커 (worker)로 다시 라우팅하는 방식으로 작동합니다. 세션 ID (session ID) 또는 프롬프트 캐시 키 (prompt-cache key)가 사소한 라우팅 힌트가 되므로, 페이로드 (payload)를 들여다볼 필요 없이 HTTP 로드 밸런서 (load balancer) 수준에서 이 문제를 처리할 수도 있습니다.
request(session-42) --> router --> worker 7 --> GPU 7 KV cache
next(session-42) --> router --> worker 7 --> GPU 7 KV cache
이 방식은 매우 큰 캐시를 네트워크를 통해 이동시키는 것을 방지합니다. 제대로 작동할 때는 빠르지만, 스케줄링 (scheduling)을 제약합니다. 선택된 워커가 과부하 상태가 되거나, 재시작되거나, 해당 항목을 제거 (evict)할 수 있습니다. 또한 라우터가 단일 세션의 캐시를 보존하는 것보다 플릿 (fleet)의 균형을 맞추는 것이 더 중요하다고 판단할 수도 있습니다. 하지만 추가적인 인프라나 하드웨어 배포를 거의 하지 않고도 작동하기 때문에 매우 매력적인 솔루션입니다.
다른 접근 방식은 **캐시를 분산 (distribute the cache)**하는 것입니다. KV 블록 (KV blocks)을 다른 메모리 계층에 저장하거나 여러 워커에서 사용할 수 있도록 하여, 요청이 하나의 GPU에 너무 밀접하게 결합되지 않도록 합니다.
+--------------------+
request --> scheduler -->| worker 3 / GPU 3 |
| +--------------------+
...
이는 스케줄링 유연성과 복구 능력을 향상시키지만, KV 블록을 이동, 인덱싱 및 유지하는 것 자체가 시스템적인 문제입니다. 구현 방식에 따라 GPU 메모리, 호스트 메모리 (host memory), 로컬 스토리지 (local storage), 원격 스토리지 (remote storage), 접두사 인식 라우팅 (prefix-aware routing) 및 제거 정책 (eviction policies)을 다양한 방식으로 혼합하여 사용합니다.
KV 캐시의 규모를 비교해 보자면, 크기가 클 수는 있지만 어떤 면에서는 예상보다 작을 수 있습니다. 다양한 트릭을 사용하면 긴 대화의 경우에도 KV 캐시의 크기를 몇 기가바이트 수준으로 줄일 수 있습니다.
캐시와 접두사 (Caches and Prefixes)
Pi 세션은 리스트가 아니라 트리 (tree)입니다. /tree
활성 대화를 이전 시점으로 되돌려 다른 브랜치(branch)를 따라 계속 진행할 수 있습니다. 되감기(rewind)를 수행하면 세션 파일에서 삭제하지 않고도 활성 접미사(suffix)를 버릴 수 있습니다. 새로운 브랜치는 이전 컨텍스트의 대부분을 공유할 수도, 일부만 공유할 수도, 혹은 사실상 전혀 공유하지 않을 수도 있습니다. 이러한 설계는 Pi에만 국한된 것이 아니며, 꽤 많은 코딩 에이전트들이 개념적으로 유사한 방식을 가지고 있습니다. 세션을 트리(tree)로 표현하지 않더라도, 에이전트가 어떤 형태로든 되감기 기능을 갖는 것은 드문 일이 아닙니다.
+-- E -- F 다른 브랜치
|
세션 S: 루트(root) -- A -- B -- C -- D 현재 브랜치
...
세 개의 브랜치 모두 동일한 Pi 세션 ID를 가질 수 있습니다. 라우터(router)의 관점에서는 이들이 하나의 세션이지만, 프롬프트 캐시(prompt cache)의 관점에서는 접두사(prefix)가 부분적으로만 겹치는 세 개의 토큰 시퀀스(token sequences)입니다.
만약 캐시가 재사용 가능한 접두사 블록(prefix blocks)을 유지한다면, D에서 F로 점프하더라도 root -> C를 여전히 재사용할 수 있습니다. 만약 캐시가 가장 빈번하게 사용되는 연속 부분(hottest continuation)만 유지하거나, 공유된 블록이 제거(evicted)되었거나, 혹은 요청이 다른 곳으로 라우팅된다면 히트율(hit rate)은 훨씬 낮아질 수 있습니다. A에서 시작하더라도 Z로 점프하는 경우에는 시스템 프롬프트(system prompt)와 초기 도구 정의(tool definitions)만 보존될 수도 있습니다. 여기서 정확한 캐시 관리 동작은 제공업체(provider)에 따라 크게 달라집니다.
반대의 경우도 발생할 수 있습니다. /fork 명령이나 새로운 세션은 대량의 동일한 컨텍스트를 그대로 가져가면서도 새로운 세션 ID를 생성할 수 있습니다. 세션 키(session key)별로 캐시를 격리하는 라우팅 시스템은 이러한 유용한 중첩(overlap)을 감지하지 못할 수도 있습니다.
재사용 가능한 접두사가 어떤 작업을 캐싱할 수 있는지를 결정합니다. 세션 식별(session identity)은 단지 인프라가 일치할 가능성이 높은 콘텐츠를 찾는 데 도움을 줄 뿐입니다. 어떤 시스템에서는 라우팅 키(routing key)가 캐시 관리에 결정적이지만, 다른 시스템에서는 단순히 최적화 요소일 뿐입니다.
명시적 vs 자동 접두사 캐싱 (Explicit vs Automatic Prefix Caching)
제공업체 API는 크게 두 가지 스타일로 캐싱을 노출합니다.
Anthropic의 전통적인 인터페이스는 명시적인 cache_control을 사용합니다.
클라이언트는 시스템 프롬프트 (system prompt), 도구 정의 (tool definitions), 또는 최신 캐싱 가능한 대화 내용과 같이 요청의 안정적인 부분 뒤에 경계 지점 (points)을 표시합니다. 그러면 서버는 해당 지점에서 끝나는 접두사 (prefix)를 쓰거나 조회할 수 있습니다. 경계는 명시적이지만, 재사용을 위해서는 그 이전의 내용이 일치해야 합니다. 캐시 지점이 명시적인 만큼 가격 책정도 명시적입니다. 캐시 쓰기 (cache writes)에 대해 비용을 지불하며, 캐시 유지 기간을 직접 선택할 수 있고 이에 따라 가격대가 달라집니다.
다른 API들은 자동 접두사 캐싱 (automatic prefix caching)을 사용합니다. 클라이언트는 정상적으로 요청을 보내고, 제공업체는 클라이언트가 설정한 중단점 (breakpoints) 없이도 재사용 가능한 접두사를 찾아냅니다. 프롬프트 캐시 키 (prompt-cache key)나 세션 헤더 (session header)가 라우팅이나 그룹화 성능을 향상시킬 수는 있지만, 서로 다른 접두사를 동일하게 만들어주지는 않습니다.
도구 구성 (Tool Loadouts)이 캐시를 망치는 이유
도구 정의 (tool definitions)는 보통 대화 이전에 나타나며, 내부적으로 시스템 프롬프트에 "접혀 (folded)" 들어갑니다. 도구의 이름, 설명, JSON 스키마 (JSON schemas)는 다른 텍스트와 마찬가지로 모델의 입력값입니다. 도구를 하나 추가하거나, 하나를 제거하거나, 스키마를 변경하거나, 심지어 도구들을 다른 순서로 직렬화 (serializing)하는 것만으로도 첫 번째 불일치 지점이 프롬프트의 시작 부분 근처로 이동할 수 있습니다.
turn 1: [system][read][write][bash][conversation...........]
turn 2: [system][read][write][bash][deploy][conversation...]
|
...
이는 플러그인 시스템과 MCP 스타일의 도구 카탈로그 (tool catalogs)에서 흔히 발생하는 당혹스러운 상황입니다. 도구가 관련성이 생길 때만 로드하는 것이 초기에는 더 적은 스키마를 전송하므로 효율적으로 보일 수 있습니다. 하지만 대부분의 모델에서는 새로 확장된 도구 구성 (loadout)이 그 뒤에 따르는 캐싱된 대화를 무효화합니다. 몇 개의 도구 스키마 토큰을 아끼려다 수만 개의 대화 토큰을 다시 처리하게 될 수 있습니다.
일부 최신 모델 API는 **가산적 도구 로딩 (additive tool loading)**을 지원합니다. 도구가 원래의 도구 목록에 삽입되는 대신, 트랜스크립트 (transcript) 내부의 특정 도구 결과 시점에서 사용 가능해질 수 있습니다. 이 경우 이전의 접두사는 변경되지 않은 채 유지됩니다.
[system][initial tools][conversation][new tool][next turn]
<--------- 캐싱된 접두사 (cached prefix) ----------->
Pi는 현재 네이티브 지연 도구 (native deferred-tool) 메커니즘을 갖춘 모델들에 대해 이 기능을 지원합니다. 확장 프로그램(extension)이 setActiveTools()를 통해 순수하게 추가적인(additive) 변경을 수행할 때, Pi는 도구 결과(tool result)에 추가된 이름들을 기록합니다. 지원되는 Anthropic 모델의 경우 지연 정의 (deferred definitions)와 tool_reference를 사용하며, 지원되는 OpenAI 모델의 경우 그에 상응하는 도구 검색 항목 (tool-search items)을 방출합니다. 다른 모델들은 안전한 폴백 (fallback) 방식을 사용합니다. Pi는 다음 요청 시 전체 활성 도구 목록을 전송하며, 이는 기능적으로는 작동하지만 프롬프트 캐시 (prompt cache)를 삭제할 수 있습니다.
여기서 추가적인 (additive) 이라는 단어가 중요합니다. 도구를 제거하거나, 한 구성(loadout)을 다른 것으로 교체하거나, 프롬프트 스니펫 (prompt snippets)을 변경하는 것은 여전히 이전 입력을 변경하기 때문입니다. 시스템 프롬프트 (system prompt)를 재구축하거나, 도구 순서를 섞거나, 타임스탬프를 주입하거나, 매 턴마다 활성 도구를 변경하는 확장 프로그램은 실수로 전체 세션의 캐싱을 무력화할 수 있습니다.
확장성 (Extensibility)이란 Pi가 모든 확장 프로그램을 대신하여 캐시 안정성을 보장할 수 없음을 의미합니다. 우리는 캐시 친화적인 메커니즘을 제공할 수 있지만, 확장 프로그램이 이를 반드시 사용해야 하며, 우리가 관찰한 바에 따르면 많은 확장 프로그램에서 캐시 효율성은 뒷전인 경우가 많습니다. 이는 부분적으로, 고정 구독료를 지불할 때는 캐시 미스 (cache miss)와 관련된 비용이 그리 명확하게 느껴지지 않기 때문입니다.
중단 (Interruptions) 및 TTL
일부 중요한 프롬프트 캐시는 기본 수명이 짧습니다. Anthropic의 기본 5분 캐시는 일반적인 코딩 활동보다 짧기 때문에 특히 중요합니다. 만약 Fable을 사용하다가 커피를 마시러 갔다가 10분 후에 돌아온다면, 단 한 번의 "안녕 (say hi)" 메시지가 예상보다 더 많은 비용을 발생시킬 것입니다.
그 이유는 사용자는 코딩 세션을 지속적으로 활성화된 상태로 생각할 수 있지만, 추론 제공자 (inference provider)는 일련의 격리된 요청으로 보기 때문입니다:
모델 요청 (model request) --> 7분 동안 테스트 실행 --> 모델 요청 (model request)
이 구간에는 캐시 트래픽이 없음
긴 빌드 과정, 테스트 스위트 (test suite), 점심시간, 회의, 또는 단순히 diff를 검토하기 위해 잠시 멈추는 시간은 캐시 유지 시간을 초과할 수 있습니다. 다음 요청에 동일한 프롬프트가 포함되어 있더라도, 저장된 KV 상태(KV state)가 사라졌다면 해당 접두사(prefix)는 다시 입력(input) 비용으로 청구됩니다.
현재 Pi는 Anthropic의 구독 서비스에서 허용되는 하네스 (harness)가 아니기 때문에, 우리는 Anthropic이 API 사용자에게 권장하는 기본값인 5분을 따르고 있습니다. 하지만 Claude Code의 코드베이스를 살펴보면, 그들의 자체 구독 사용자들을 위해 캐시 타임아웃 (cache timeout)을 1시간으로 늘리고 있다는 것을 알 수 있습니다. 그러나 API 토큰 가격을 지불해야 하는 경우, 이로 인해 증가하는 비용은 종종 그만한 가치가 없습니다.
하지만 이를 선택적으로 적용할 수 있습니다. Anthropic과 같은 일부 제공업체는 더 긴 유지 관리 제어 (retention controls) 기능을 노출합니다. 지원되는 직접 API의 경우, Pi 사용자들은 이를 요청하기 위해 PI_CACHE_RETENTION=long을 설정할 수 있습니다. 다만 이는 여전히 요청일 뿐입니다. Pi는 게이트웨이 (gateway)가 항목을 유지하도록 강제하거나, 메모리 압박 (memory pressure) 상황에서 제거 (eviction)를 방지하거나, 모델 요청이 이루어지지 않는 동안 캐시를 계속 활성화 상태로 유지하도록 할 수는 없습니다.
캐시 미스 (Cache Miss)의 대가
제공업체들은 일반적으로 캐시되지 않은 입력 (uncached input), 캐시 쓰기 (cache writes), 그리고 캐시 읽기 (cache reads)의 가격을 다르게 책정합니다. 캐시 읽기는 비용이 많이 드는 프리필 (prefill) 작업이 이미 완료되었기 때문에 흔히 할인된 가격이 적용됩니다. 캐시 쓰기는 제공업체가 나중에 사용할 수 있도록 상태를 유지하겠다고 약속하는 것이므로 프리미엄이 붙을 수 있습니다.
위의 Fable 예시처럼 100,000 토큰의 히스토리(history)가 있고 그 뒤에 짧은 새로운 요청이 이어지는 코딩 세션을 상상해 보십시오. 캐시가 작동할 때는 그 히스토리의 거의 대부분이 더 낮은 캐시 읽기 가격으로 청구됩니다. 오직 소량의 새로운 내용만이 일반 입력 가격으로 처리되고 잠재적으로 캐시에 기록될 필요가 있습니다.
캐시 미스 (cache miss)가 발생하면, 제공업체는 100,000 토큰의 히스토리 전체를 일반 입력 가격으로 다시 처리해야 합니다. 또한 해당 히스토리를 다시 캐시에 쓰는 비용을 청구할 수도 있습니다. 이것이 바로 continue와 같은 짧은 요청이
AI 자동 생성 콘텐츠
본 콘텐츠는 Lobste.rs AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기