byteforce

CPN 한국어 자습서 · 러닝패스 2 / 4 — Building with the Claude API

Claude의 기능

프롬프트 캐싱 실전

Prompt caching in action

개념과 규칙을 코드로 옮깁니다. chat() 함수가 toolssystem을 기본으로 캐시하도록 고치고, 응답의 usage 필드에서 쓰기(cache_creation_input_tokens)와 읽기(cache_read_input_tokens)가 어떻게 바뀌는지 직접 확인합니다.

전체 내레이션영상 나레이션 한국어 번역 (전체)

Stephen Grider · Anthropic 기술 스태프

이제 프롬프트 캐싱을 직접 손에 익힐 차례입니다. 003 캐싱이라는 새 노트북을 만들었습니다. 강의에 첨부돼 있습니다. 안에는 6K 토큰짜리 프롬프트 섹션이 있는데, 이건 system 프롬프트로 쓸 겁니다. 그리고 tool 스키마 섹션이 따로 있고, 여러 tool 스키마가 들어 있어 다 합치면 약 1.7K 토큰입니다.

헬퍼 함수 셀에 있는 chat 함수를 고칠 겁니다. chat 함수가 tool 스키마와 system 프롬프트를 기본으로 항상 캐시하도록 만들겠습니다. chat 함수 안에 to-do를 두 개 적어 뒀습니다. 첫째, tools 리스트가 들어오면 항상 그 리스트를 캐시한다. 둘째, system 프롬프트가 들어오면 그것도 캐시한다. 한 요청 안에 여러 캐시 breakpoint를 둘 수 있다는 걸 기억하세요. system과 tools를 둘 다 넘기면 breakpoint를 두 개 설정하게 됩니다.

먼저 tools 리스트 캐싱부터. 마지막 tool 스키마에 cache_control 필드를 더해야 합니다. 그냥 tools[-1]에 cache_control을 ephemeral로 직접 넣을 수도 있고 그러면 작동은 합니다. 하지만 좋은 코딩은 아닙니다. 그렇게 하면 원본 tool 스키마 자체가 바뀝니다. 나중에 애플리케이션에서 tool 순서를 바꾸기로 하면, 의도치 않게 여러 곳에 breakpoint가 생길 수 있습니다.

그래서 조금 더 나은 방법은, 먼저 tools 리스트를 복사하고, 그 안의 마지막 tool 스키마를 클론한 뒤 거기에 cache_control을 붙이는 겁니다. tools_clone = tools.copy()로 리스트 전체를 복사하고, 마지막 tool을 복사한 뒤 cache_control ephemeral을 더하고, tools_clone[-1]에 그 사본을 덮어쓰고, params의 tools에 tools_clone을 넣습니다. 다시 말하지만 이 복사 로직이 꼭 필요한 건 아닙니다. 그저 나중에 tool 리스트를 바꿀 때를 대비한 좋은 습관입니다.

두 번째 to-do. system 프롬프트가 들어오면 거기에도 항상 breakpoint를 설정합니다. system을 리스트로 바꾸고, 안에 text 블록을 넣습니다. type은 text, text는 받은 system, 그리고 cache_control type ephemeral. 그게 전부입니다. 셀을 실행합니다.

맨 아래로 내려가 테스트해 봅니다. 이미 tool 리스트와 아주 큰 system 프롬프트 code_prompt가 정의돼 있습니다. 먼저 아무것도 안 넘기고 그냥 1+1 메시지를 처리하는 데 쓰인 토큰만 봅니다. 실행하면 usage 필드가 보이는데, 입력 14, 출력 11 정도입니다.

이제 tools 리스트를 더해 실행하면 usage가 완전히 달라집니다. cache_creation_input_tokens가 1700이 됩니다. Claude가 스키마를 캐시하라는 걸 보고 약 1700 토큰을 캐시에 기록한 겁니다. 곧바로 아무것도 바꾸지 않고 후속 요청을 보내면, 이번엔 캐시에서 일정 토큰을 읽어 옵니다. cache_read가 1700. 스키마를 캐시에 저장했다가 다시 꺼내 온 겁니다.

여기서 user 메시지를 바꿔 봅니다. 끝의 물음표를 지우고 다시 실행해도 여전히 캐시에서 읽습니다. 캐싱 순서가 tools, 그다음 system, 그다음 messages이기 때문입니다. 메시지는 맨 뒤라, 그것만 바뀌어도 앞쪽 tools 캐시는 그대로 적중합니다.

하지만 tools를 조금이라도 바꾸면 캐시가 무효가 됩니다. 첫 tool의 설명에서 ads의 s를 지워 봅니다. 이제 tool 스키마가 바뀌었으니, 모든 tool에 걸어 둔 breakpoint가 더는 적용되지 않습니다. 맨 아래 셀을 다시 돌리면 usage가 갱신됩니다. 다시 cache write입니다. 더는 읽지 않고 쓰기로 돌아갑니다. Claude 입장에선 완전히 다른 tool 리스트를 받은 셈이니까요.

이제 system 프롬프트를 더해 봅니다. chat 함수에 system code_prompt를 넣습니다. 순서는 tools, system, messages. tools는 완전히 그대로 두고 system만 바꿨으니, 부분 캐시 읽기와 쓰기가 동시에 나오리라 예상됩니다. 읽기는 같은 tools를 쓰기 때문이고, 쓰기는 새 system breakpoint를 올리기 때문입니다. 실행하면 cache read 1700, cache write 6.3입니다.

tools와 마찬가지로 system 프롬프트도 끝의 builder 한 단어를 지우는 식으로 바꾸면, Claude 입장에선 완전히 다른 system 프롬프트입니다. 그러면 system에 쌓아 둔 캐시를 전부 잃습니다. 다시 보내면 또 cache read 약 1.7, 그리고 새 system을 다시 쓰느라 또 6.3입니다.

이게 프롬프트 캐싱입니다. 동일한 콘텐츠, 즉 같은 메시지, 같은 tool 스키마, 같은 system 프롬프트를 반복해서 보낼 때 아주 자주 쓰게 됩니다.

이 장에서 배우는 것What you'll learn

약 7분
1

chat()tools·system을 기본으로 캐시하도록 수정

2

tools 캐시 = tools.copy()마지막 스키마만 클론cache_control

3

system 캐시 = 롱폼(리스트+text 블록)+cache_control

4

usage: 쓰기 cache_creation_input_tokens vs 읽기 cache_read_input_tokens

5

순서 tools->system->messages — 메시지만 바뀌면 여전히 읽기

6

tool/system 변경 = 무효(다시 쓰기), system 추가 = 부분 적중

먼저 짚고 갈 용어
cache_creation_input_tokens
캐시에 기록한 토큰 수(=쓰기). 첫 요청·무효 후 나타난다.
cache_read_input_tokens
캐시에서 읽어 온 토큰 수(=읽기, 적중). 비용·지연이 절감된다.
tools.copy() · 클론
원본 보호. 리스트를 복사하고 마지막 tool만 사본으로 바꿔 cache_control을 붙인다.
부분 적중
tools는 그대로(읽기) + system은 새로(쓰기)가 한 요청에서 동시에 일어나는 것.

chat()에 캐싱을 심는다

Cache tools & system by default

노트북 003에는 6K 토큰 system 프롬프트와 1.7K 토큰 tool 스키마가 준비돼 있습니다. chat() 함수가 이 둘을 기본으로 캐시하도록 고칩니다. 한 요청에 breakpoint를 두 개 둘 수 있으므로, tools와 system을 각각 한 곳씩 잡습니다. 모델은 코스 내내 쓰던 claude-sonnet-4-5입니다.

003_caching.ipynb · 클라이언트 · model
from dotenv import load_dotenv
from anthropic import Anthropic

load_dotenv()
client = Anthropic()
model = "claude-sonnet-4-5"
003_caching.ipynb · chat() — tools·system 캐시
def chat(messages, system=None, ..., tools=None, ...):
    params = {"model": model, "max_tokens": 4000,
              "messages": messages, ...}

    if tools:
        # 마지막 tool 스키마에만 cache_control
        # 원본 보호 위해 리스트 복사 + 마지막 항목만 클론
        tools_clone = tools.copy()
        tool_copy = tools_clone[-1].copy()
        tool_copy["cache_control"] = {"type": "ephemeral"}
        tools_clone[-1] = tool_copy
        params["tools"] = tools_clone

    if system:
        # system 을 롱폼(리스트+text 블록) + cache_control
        params["system"] = [{
            "type": "text",
            "text": system,
            "cache_control": {"type": "ephemeral"},
        }]

    return client.messages.create(**params)

tools는 마지막 스키마에만 cache_control을 붙입니다. 원본을 그대로 두려고 tools.copy()로 리스트를 복사한 뒤 마지막 항목만 클론해 거기에만 필드를 답니다. system은 문자열 단축형으로는 cache_control을 못 붙이므로 롱폼(리스트+text 블록)으로 바꿉니다.

쓰기와 읽기를 눈으로

Creation vs read

맨 아래에서 테스트합니다. 먼저 아무것도 안 넘기면 캐시는 관여하지 않습니다 — cache_*는 0입니다.

맨 아래 테스트 셀 · 1) 캐시 없음
# 6K 토큰 system 프롬프트 + 1.7K 토큰 tool 스키마 4개
tools = [db_query_schema, add_duration_to_datetime_schema,
         set_reminder_schema, get_current_datetime_schema]

messages = []
add_user_message(messages, "what's 1+1")

# 1) 아무것도 안 넘김 — 캐시 관여 없음
chat(messages)
출력 · usage(캐시 미관여)
Usage(
  input_tokens=14,
  output_tokens=11,
  cache_creation_input_tokens=0,
  cache_read_input_tokens=0,
)

tools를 넘겨 첫 요청을 보내면 cache_creation_input_tokens가 약 1700 — 스키마를 캐시에 기록(쓰기)합니다. 바로 같은 요청을 다시 보내면 이번엔 cache_read_input_tokens가 약 1700 — 캐시에서 읽어(적중) 옵니다.

2) 첫 요청=쓰기 -> 3) 재요청=읽기
# 2) tools 를 넘겨 첫 요청 — 캐시에 "쓰기"
chat(messages, tools=tools).usage
# cache_creation_input_tokens 약 1700  (tool 스키마를 캐시에 기록)

# 3) 동일 요청을 즉시 다시 — 캐시에서 "읽기"
chat(messages, tools=tools).usage
# cache_read_input_tokens 약 1700   (적중)

무엇이 캐시를 깨는가

Order & invalidation

캐싱 순서는 tools -> system -> messages입니다. 뒤쪽(messages)만 바뀌면 앞쪽 tools 캐시는 그대로 적중합니다. 하지만 tool 스키마를 한 글자라도 바꾸면 무효가 되어 다시 쓰기로 돌아갑니다.

4) 메시지만 변경=읽기 유지 / tool 변경=다시 쓰기
# 4) user 메시지만 변경 (예: 끝의 ? 삭제)
#    순서가 tools -> system -> messages 이므로
#    뒤쪽 messages 만 바뀌어도 tools 캐시는 여전히 적중
add_user_message(messages, "what's 1+1")   # ? 없이
chat(messages, tools=tools).usage
# 여전히 cache_read_input_tokens 약 1700

# 그러나 tool 스키마를 한 글자라도 바꾸면 -> 무효 -> 다시 쓰기
# (예: 첫 tool 설명의 "adds" -> "add")
chat(messages, tools=tools).usage
# cache_creation_input_tokens 약 1700  (다시 쓰기)

system을 추가하면 어떻게 될까요? tools는 동일하니 읽기, system은 새 breakpoint라 쓰기 — 한 요청에서 둘 다 나타나는 부분 적중입니다.

5) system 추가 = tools 읽기 + system 쓰기(부분 적중)
# 5) system 프롬프트 추가 (tools 는 그대로 유지)
#    tools 는 동일 -> 읽기, system 은 새 breakpoint -> 쓰기 (부분 적중)
chat(messages, tools=tools, system=code_prompt).usage
# cache_read_input_tokens     약 1700  (tools 적중)
# cache_creation_input_tokens 약 6300  (system 새로 기록)
usage 시뮬 · 시나리오를 눌러 cache_creation(쓰기) vs cache_read(읽기) 변화를 보세요
cache_creation (쓰기) cache_read (읽기) input / output

막대는 토큰 규모를 비교용으로 단순화한 것입니다. 핵심은 어느 필드가 채워지는가 — 쓰기인지 읽기인지, 그리고 부분 적중이 어떻게 둘 다 나타나는지입니다.

언제 쓰나

같은 콘텐츠를 반복해서 보낼 때 — 동일한 메시지, 동일한 tool 스키마, 동일한 system 프롬프트 — 프롬프트 캐싱이 가장 빛납니다. system·tools는 잘 안 바뀌므로 캐시 명당입니다.

정리 & 점검

Recap & check
핵심 정리
  • tools 캐시 = tools.copy()마지막 스키마만 클론cache_control: ephemeral.
  • system 캐시 = 롱폼(리스트+{type:text, text, cache_control}). 문자열 단축형으론 불가.
  • 첫 요청 = cache_creation_input_tokens(쓰기), 동일 재요청 = cache_read_input_tokens(읽기).
  • 순서 tools->system->messages. 메시지만 바뀌면 읽기 유지, tool/system 변경은 무효(다시 쓰기), system 추가는 부분 적중.

Q1tool 스키마를 캐시할 때 tools.copy() 후 마지막만 클론하는 이유는?

Q2tools를 캐시한 뒤 user 메시지의 물음표만 지우고 다시 보내면?

Q3tools는 그대로 두고 system만 새로 추가하면 usage는?

MEMBER SESSION REQUIRED · REGISTRATION IS FREE

여기부터는 등록한 분에게 열립니다.

전 코스는 계속 무료입니다. 등록하면 이 코스의 남은 76개 레슨을 끝까지 읽을 수 있습니다.

등록하고 이어서 읽기

이미 등록하셨다면 그때 쓰신 이메일을 넣어 주세요.