Cursor와 같은 코딩 에이전트가 KV 상태 재사용을 통해 입력 비용을 절감하는 방식과 캐시가 깨지는 이유
요약
Cursor와 같은 코딩 에이전트가 프롬프트 캐싱을 통해 입력 비용을 절감하는 원리를 설명합니다. 트랜스포머 모델의 KV 상태(KV states)를 재사용하여 방대한 컨텍스트를 효율적으로 처리하는 방식을 다룹니다.
핵심 포인트
- 프롬프트 캐싱은 트랜스포머의 프리필 단계에서 KV 상태를 저장하여 재사용함
- 에이전트 도구는 정적 콘텐츠를 앞에, 가변적 콘텐츠를 뒤에 배치하여 캐시 효율을 높임
- 캐싱은 입력 토큰에만 적용되며 출력 생성 속도에는 영향을 주지 않음
- 방대한 컨텍스트를 사용하는 코딩 에이전트에게 입력 비용 절감은 필수적임
내 Cursor 청구서가 왜 이럴까
Cursor의 사용량 대시보드를 살펴보던 중, 이전에는 한 번도 주의 깊게 보지 않았던 지표를 발견했습니다: Cache Read (캐시 읽기).
Date Type Model Tokens
May 2, 06:50 PM Included auto 898K → Cache Read
May 2, 06:49 PM Included auto 1.4M → Cache Write
...
해당 세션의 입력 토큰(input tokens) 중 거의 99%가 캐시에서 제공되었습니다. 이를 계기로 프롬프트 캐싱 (Prompt Caching)이 실제로 무엇인지, 그리고 왜 특히 에이전트형 코딩 도구에 있어 그토록 중요한지에 대해 깊이 파고들게 되었습니다. 제가 찾아낸 내용은 다음과 같습니다.
프롬프트 캐싱 (Prompt Caching)이란 정확히 무엇인가
에이전트형 코딩 도구는 매 호출마다 엄청난 양의 컨텍스트 (context)를 전달합니다. 시스템 지침 (system instructions), 워크스페이스 규칙 (workspace rules), 파일 내용, 대화 기록 등 모든 것이 포함됩니다. LLM (Large Language Models)은 기본적으로 상태가 없는 (stateless) 특성을 가지므로, 단순하게 생각하면 모든 내용이 매 요청마다 처음부터 다시 처리됩니다.
프롬프트 캐싱은 이 과정을 단축합니다. 트랜스포머 (transformer)의 프리필 (prefill) 단계에서 발생하는 중간 계산 값, 구체적으로는 Key/Value 상태 (KV states)를 저장합니다. 따라서 이후의 요청이 동일한 앞부분 토큰 시퀀스를 공유할 때, 모델은 이를 다시 계산하는 대신 저장된 상태를 재사용합니다.
한 가지 중요한 주의 사항이 있습니다: 이는 입력 토큰 (input tokens)에만 적용됩니다. 출력 생성 (output generation)은 여전히 토큰 단위로 순차적으로 이루어지며, 캐싱을 통한 속도 향상의 이점을 얻지 못합니다.
페이로드 (Payload)가 실제로 구축되는 방식
코딩 에이전트는 단순히 사용자의 질문을 모델에 던지는 것이 아닙니다. 이들은 거대한 복합 프롬프트를 조립하며, 결정적으로 가장 정적인(static) 콘텐츠를 앞에, 가장 가변적인(volatile) 콘텐츠를 뒤에 배치하여 구조화합니다:
<system_instructions> 당신은 전문 코딩 어시스턴트입니다... </system_instructions>
<project_rules> 함수형 React 컴포넌트를 사용하세요... </project_rules>
<file_context_1> ... </file_context_1>
...
해당 시퀀스가 모델에 도달하면, 트랜스포머 (Transformer)는 이를 토큰화하고 어텐션 (Attention) 계산을 시작합니다. 모든 토큰에 대해 모델은 키 (Key, K)와 값 (Value, V)을 도출하는데, 이는 본질적으로 해당 토큰이 그 이전의 모든 다른 토큰과 어떻게 연관되는지를 나타내는 지도와 같습니다. 이 부분이 비용이 많이 드는 구간입니다. 어텐션 계산은 시퀀스 길이의 제곱에 비례하여 증가하므로, 큰 컨텍스트 윈도우 (Context Window, 많은 파일 및 긴 히스토리)를 사용하는 경우 프리필 (Prefill) 비용이 불균형적으로 높아집니다.
모델은 응답을 생성할 때 사용자의 원문 텍스트를 다시 참조하는 것이 아니라, 이렇게 계산된 KV 행렬 (KV Matrices)을 참조합니다. 아티팩트 캐싱 (Artifact Caching)이 실제로 보존하는 것이 바로 이것입니다.
제공업체들은 일반적으로 출력 토큰보다 입력 토큰에 대해 더 적은 비용을 부과하지만, 에이전트 기반 코딩 도구들은 매우 방대한 입력 페이로드 (Input Payloads)를 밀어넣기 때문에 입력 측의 비용을 절감하는 것은 여전히 매우 가치 있는 일입니다.
첫 번째 턴 vs. 그 이후의 모든 턴
대략적인 흐름은 다음과 같습니다:
첫 번째 턴 (Cold Start):
- 프롬프트 구성: 시스템 지침 (System Instructions) + 프로젝트 규칙 (Project Rules) + 파일 컨텍스트 (File Context) + 사용자의 첫 번째 메시지.
- 아직 캐시가 존재하지 않음 → 전체 프리필 (Full Prefill) 수행. 모델이 모든 토큰에 대해 KV 상태를 계산합니다 (비용이 많이 드는 $O(n^2)$ 패스).
- 해당 KV 상태들이 캐시 저장소에 기록됩니다.
- 생성이 정상적으로 진행됩니다.
두 번째 턴...N번째 턴 (Warm):
- 새로운 프롬프트 구성: 동일한 시스템 지침 + 동일한 파일 + 이전 대화 + 사용자의 새로운 메시지.
- 시스템 + 컨텍스트에 대한 프리픽스 캐시 히트 (Prefix Cache Hit) 여부를 확인합니다.
- 히트 (Hit): 변경되지 않은 프리픽스(Prefix)에 대해 캐시된 KV 상태를 읽어오고, 증가된 대화 턴 + 사용자의 최신 입력에 대해서만 새로운 KV 상태를 계산합니다.
- 캐시된 KV 상태와 새로운 KV 상태를 연결(Concatenate)한 후, 생성을 진행합니다.
따라서 매 메시지마다 전체 $O(n^2)$ 프리필을 다시 실행하는 대신, 한계적인(Marginal) 새로운 토큰들만 새로 처리됩니다. 이것이 핵심 기술이며, 긴 코딩 세션이 턴이 거듭될수록 비례해서 느려지거나 비싸지지 않는 이유입니다.
현재 대부분의 주요 제공업체들은 이러한 최적화(optimization)를 내부적으로 암묵적으로 처리하고 있습니다. 한 제공업체가 이를 어떻게 명시적으로 노출하는지 확인하고 싶다면, 컨텍스트 캐싱 (context caching)에 관한 Google Vertex AI 문서를 주요 출처로 참고하는 것이 좋습니다.
캐시를 깨뜨리는 요소
캐싱은 전적으로 **접두사 안정성 (prefix stability)**에 의존합니다. 토큰이 캐시된 내용과 달라지는 순간, 그 지점 이후의 모든 다운스트림(downstream) 데이터는 다시 계산되어야 합니다. 실제로 캐시 적중률(hit rate)을 떨어뜨리는 요인은 다음과 같습니다:
- 동적 주입 (Dynamic injections) — 타임스탬프나 기타 변경되는 값이 프롬프트 상단 근처에 삽입되면, 매 요청마다 접두사(prefix)가 무효화됩니다.
- 컨텍스트 재정렬 (Context reordering) — 파일이나 컨텍스트 블록이 로드되는 순서를 섞어버리면 접두사 매칭(prefix matching)이 완전히 불가능해집니다.
- 상위 파일 수정 (Upstream file edits) — 프롬프트 구조의 앞부분에 위치한 파일을 수정하면, 해당 파일뿐만 아니라 시퀀스 상에서 그 뒤에 오는 모든 내용에 대한 캐시가 무효화됩니다.
대부분의 시스템은 정확한 토큰-접두사 일치(exact token-prefix matches)에 의존하지만, 일부 제공업체(예: 위에 링크된 Google의 문서)는 더 명시적인 캐시 핸들(cache handles)이나 부분 재사용(partial-reuse) 메커니즘을 제공하기도 합니다.
혼란스러웠던 부분: 왜 추가(appending)는 캐시를 깨뜨리지 않는가
이 부분은 처음에는 모순적으로 보였습니다. 어텐션(attention)이 모든 토큰을 다른 모든 토큰과 연결한다면, 프롬프트의 끝에 새로운 메시지를 추가하는 것이 역방향으로 파동을 일으켜 이전 토큰들(시스템 지침, 파일 컨텍스트)에 대한 어텐션 방식까지 바꿔놓아야 하지 않을까요?
만약 모델이 양방향으로 어텐션을 수행한다면 그럴 것입니다. 하지만 현대의 생성형 LLM은 디코더 전용 (decoder-only) 모델이며 **인과적 (마스킹된) 셀프 어텐션 (causal (masked) self-attention)**을 사용합니다. 즉, 토큰은 오직 자신보다 앞에 나온 토큰들에만 어텐션을 수행할 수 있으며, 결코 그 뒤의 토큰에는 어텐션을 수행할 수 없습니다.
그러한 단 하나의 구조적 세부 사항이 캐싱(caching)을 가능하게 만드는 핵심입니다. 우리는 취약하고 양방향적인 전역 어텐션 행렬(bidirectional global attention matrix)을 캐싱하는 것이 아니라, **접두사(prefix)의 KV 텐서(KV tensors)**를 캐싱하는 것입니다. 초기 컨텍스트에 대한 해당 K와 V 상태는 한 번 계산되면 그 뒤에 무엇을 추가하든 결코 변하지 않습니다. 새로운 토큰들은 단지 자신만의 쿼리(Queries)를 생성하고, 그 이전에 있는 모든 것들의 이미 캐싱된 키(Keys)에 대해 어텐션(attend)을 수행할 뿐입니다.
다음 단계
프롬프트 캐싱(Prompt caching)은 "변하지 않은 것을 다시 계산하지 마라"는 문제를 해결합니다. 하지만 이는 "애초에 컨텍스트 윈도우(context window)에 무엇을 넣을 것인가"라는 별개의 문제는 해결하지 못합니다. 코딩 에이전트들은 모든 파일을 프롬프트에 밀어 넣고 캐시 히트(cache hit)를 기대하는 대신, 코드베이스에 대한 RAG(Retrieval-Augmented Generation) 방식의 임베딩(embeddings)을 활용하여 관련 있는 코드 조각만을 선택적으로 가져오는 검색(retrieval)에 점점 더 의존하고 있습니다.
이것이 제가 다음에 깊이 파고들 목록입니다. 코딩 도구들이 임베딩을 통해 코드베이스의 쿼리 가능한 뷰(queryable views)를 어떻게 구축하는지, 그리고 그것이 비용 방정식에 어떤 영향을 미치는지에 대해서 말이죠.
KV 캐싱(KV caching)과 어텐션(attention) 뒤에 숨겨진 트랜스포머 아키텍처(transformer-architecture)에 대한 더 깊은 배경 지식을 원하신다면, 3Blue1Brown의 "Large Language Models explained briefly"가 전문 용어가 적은 진정으로 훌륭한 입문서입니다.
원문은 Susheem's Jottings에 게시되었습니다 — dev.to 독자들을 위해 이곳에 교차 게시합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기