포스트

프롬프트 엔지니어링 기초 (7) - Operations: caching, 비용, 버전 관리

prompt caching으로 반복되는 앞부분의 비용을 줄이는 방법과 캐시가 깨지는 원인, 프롬프트를 코드처럼 관리하는 방식과 시리즈 전체의 함정을 정리합니다.

프롬프트 엔지니어링 기초 (7) - Operations: caching, 비용, 버전 관리

프롬프트 엔지니어링 기초 시리즈의 마지막 편입니다. 전체 목차는 0편에 있습니다.

같은 앞부분을 매번 다시 보낸다

6편의 채점 스크립트는 문의 8건에 대해 같은 system 프롬프트를 8번 보냅니다. 실제 서비스라면 사용자 수만큼 보냅니다. 규칙과 예시가 쌓여 system이 2000 토큰쯤 되면, 매 호출마다 그 2000 토큰의 입력 비용을 처음부터 다시 냅니다.

prompt caching은 이 반복되는 앞부분을 서버에 남겨두고 재사용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
message = client.messages.create(
    model="claude-opus-5",
    max_tokens=2048,
    system=[
        {
            "type": "text",
            "text": LONG_SYSTEM_PROMPT,
            "cache_control": {"type": "ephemeral"},
        }
    ],
    messages=[{"role": "user", "content": inquiry}],
)

system을 문자열이 아니라 block 목록으로 바꾸고 마지막 block에 cache_control을 붙였습니다. 첫 호출은 캐시를 쓰고, 이후 호출은 그 지점까지를 읽어옵니다.

캐시가 걸렸는지 확인

붙였다고 되는 것이 아니라 확인해야 합니다. usage에 값이 들어옵니다.

1
2
3
print(message.usage.cache_creation_input_tokens)   # 이번에 캐시에 쓴 토큰
print(message.usage.cache_read_input_tokens)       # 이번에 캐시에서 읽은 토큰
print(message.usage.input_tokens)                  # 캐시를 못 탄 나머지

같은 프롬프트로 두 번째 호출을 했는데 cache_read_input_tokens가 0이면 캐시가 깨진 것입니다. 프롬프트 전체 크기는 세 값을 더한 값입니다.

캐시가 깨지는 이유

캐시는 앞부분 일치로 동작합니다. 앞에서 한 글자만 달라져도 그 뒤는 전부 무효가 됩니다. 읽히는 순서는 toolssystemmessages입니다.

원인증상
system에 현재 시각이나 요청 ID를 넣음매 호출이 다른 프롬프트가 되어 캐시가 절대 안 걸림
사용자 이름이나 세션 ID를 system에 끼워 넣음사용자마다 다른 캐시가 생겨 공유되지 않음
조건에 따라 system 문단을 넣었다 뺐다 함조합마다 다른 캐시가 생김
tool 목록이 호출마다 달라지거나 순서가 바뀜tools가 맨 앞이라 전체가 무효
모델을 바꿈캐시는 모델별로 따로 존재
json.dumpssort_keys 없이 씀직렬화 순서가 흔들려 바이트가 달라짐

고치는 방향은 하나입니다. 고정된 것을 앞에, 변하는 것을 뒤에 둡니다. 현재 날짜처럼 매번 바뀌는 값은 system이 아니라 messages의 사용자 메시지에 넣습니다.

언제 이득인가

캐시 읽기는 기본 입력 단가의 약 0.1배, 캐시 쓰기는 1.25배(5분 유지 기준)입니다. 그래서 같은 앞부분을 두 번 이상 쓰면 이득이고, 한 번만 쓰면 손해입니다.

캐시가 걸리는 최소 길이도 모델마다 다릅니다.

모델최소 캐시 가능 길이
Claude Opus 5512 토큰
Claude Sonnet 5, Opus 4.81024 토큰
Claude Haiku 4.54096 토큰

이보다 짧으면 cache_control을 붙여도 조용히 캐시되지 않습니다. 오류가 아니라 cache_creation_input_tokens: 0으로 나타납니다. 비용을 아끼려고 Haiku로 바꿨는데 캐시가 안 걸리는 상황이 여기서 나옵니다.

기본 유지 시간은 5분이고, {"type": "ephemeral", "ttl": "1h"}로 한 시간까지 늘릴 수 있습니다. 대신 쓰기 비용이 2배가 되므로 요청이 드문드문 들어오는 경우에만 유리합니다.

비용을 줄이는 순서

  1. effort를 내립니다. 분류나 추출을 기본값 high로 돌리고 있다면 low로 내리는 것만으로 출력 토큰이 크게 줄어듭니다(5편)
  2. 캐시를 겁니다. 고정된 앞부분이 512 토큰을 넘으면 붙일 값이 있습니다
  3. 모델을 내립니다. 6편의 채점 스크립트를 claude-haiku-4-5로 한 번 돌려보고 정확도가 유지되면 그대로 씁니다. 감이 아니라 점수로 정합니다
  4. 묶어 보냅니다. 즉시 응답이 필요 없는 대량 작업은 Batches API(client.messages.batches.create)로 보내고 나중에 결과를 회수합니다

순서가 중요합니다. 모델을 먼저 내리면 품질이 떨어지고, 그 상태에서 프롬프트를 고치느라 시간을 씁니다.

프롬프트를 코드처럼 다루기

프롬프트가 문자열 리터럴로 코드 여기저기 흩어져 있으면 무엇이 언제 바뀌었는지 추적할 수 없습니다.

1
2
3
4
5
6
7
8
prompts/
├── classify.txt          # 프롬프트 본문만
├── summarize.txt
└── judge.txt
evals/
├── data.py               # DEV / TEST 데이터
└── evaluate.py           # 채점 harness
runs.csv                  # 실행 기록
  • 프롬프트 본문은 파일로 분리해 코드와 함께 커밋합니다. 리뷰에서 git diff로 문장 변경이 보입니다
  • 프롬프트를 고치는 커밋에는 6편의 점수를 함께 남깁니다
  • 사용자 입력을 프롬프트에 f-string으로 이어 붙이지 말고 XML 태그로 구획합니다(2편)

자주 밟는 함정 정리

증상원인해결
응답이 빈 문자열thinking이 max_tokens를 다 씀max_tokens를 넉넉히 주고 stop_reason 확인 (1편)
content[0].text에서 예외첫 block이 thinking텍스트 block만 골라내는 헬퍼 사용
temperature 전달 시 400Claude 5 계열에서 제거됨effort와 프롬프트 제약으로 대체 (1편)
budget_tokens 전달 시 400adaptive thinking으로 대체됨output_configeffort 사용 (5편)
assistant prefill 시 4004.6 이후 지원 종료json_schema 또는 tool use (4편)
JSON 파싱 실패가 가끔 발생프롬프트로 부탁만 함output_config.format으로 강제
라벨 문자열이 흔들림자유 텍스트 출력스키마의 enum
캐시가 안 걸림앞부분에 변하는 값이 있음고정 부분을 앞으로, 최소 길이 확인
프롬프트를 고쳤는데 다른 곳이 나빠짐입력 하나만 보고 판단TEST 셋 전체 채점 (6편)

시리즈를 마치며

1편의 출발점은 “요청과 응답이 어떤 모양인가”였습니다. 그 뒤로 한 일은 모델에게 맡겨두던 결정을 하나씩 명시적으로 가져오는 것이었습니다. 2편에서 지시를, 3편에서 예시를, 4편에서 출력 형식을, 5편에서 추론 깊이를 정했고, 6편에서 그 결정들이 실제로 나은지 채점했습니다.

남은 한 가지는 모델이 모르는 내용을 답하게 만드는 일입니다. 프롬프트를 아무리 다듬어도 사내 문서나 어제 바뀐 정책은 모델 안에 없습니다. 그 문서를 찾아 프롬프트에 넣어주는 구조가 RAG 기초 시리즈이고, 그 시리즈의 프롬프트 설계는 여기서 익힌 것을 그대로 씁니다.

여러 모델 호출과 도구를 엮어 애플리케이션으로 조립하는 쪽은 LangChain 기초LangGraph 기초로 이어집니다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.