프롬프트 엔지니어링 기초 (7) - Operations: caching, 비용, 버전 관리
prompt 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이면 캐시가 깨진 것입니다. 프롬프트 전체 크기는 세 값을 더한 값입니다.
캐시가 깨지는 이유
캐시는 앞부분 일치로 동작합니다. 앞에서 한 글자만 달라져도 그 뒤는 전부 무효가 됩니다. 읽히는 순서는 tools → system → messages입니다.
| 원인 | 증상 |
|---|---|
system에 현재 시각이나 요청 ID를 넣음 | 매 호출이 다른 프롬프트가 되어 캐시가 절대 안 걸림 |
사용자 이름이나 세션 ID를 system에 끼워 넣음 | 사용자마다 다른 캐시가 생겨 공유되지 않음 |
조건에 따라 system 문단을 넣었다 뺐다 함 | 조합마다 다른 캐시가 생김 |
| tool 목록이 호출마다 달라지거나 순서가 바뀜 | tools가 맨 앞이라 전체가 무효 |
| 모델을 바꿈 | 캐시는 모델별로 따로 존재 |
json.dumps를 sort_keys 없이 씀 | 직렬화 순서가 흔들려 바이트가 달라짐 |
고치는 방향은 하나입니다. 고정된 것을 앞에, 변하는 것을 뒤에 둡니다. 현재 날짜처럼 매번 바뀌는 값은 system이 아니라 messages의 사용자 메시지에 넣습니다.
언제 이득인가
캐시 읽기는 기본 입력 단가의 약 0.1배, 캐시 쓰기는 1.25배(5분 유지 기준)입니다. 그래서 같은 앞부분을 두 번 이상 쓰면 이득이고, 한 번만 쓰면 손해입니다.
캐시가 걸리는 최소 길이도 모델마다 다릅니다.
| 모델 | 최소 캐시 가능 길이 |
|---|---|
| Claude Opus 5 | 512 토큰 |
| Claude Sonnet 5, Opus 4.8 | 1024 토큰 |
| Claude Haiku 4.5 | 4096 토큰 |
이보다 짧으면 cache_control을 붙여도 조용히 캐시되지 않습니다. 오류가 아니라 cache_creation_input_tokens: 0으로 나타납니다. 비용을 아끼려고 Haiku로 바꿨는데 캐시가 안 걸리는 상황이 여기서 나옵니다.
기본 유지 시간은 5분이고, {"type": "ephemeral", "ttl": "1h"}로 한 시간까지 늘릴 수 있습니다. 대신 쓰기 비용이 2배가 되므로 요청이 드문드문 들어오는 경우에만 유리합니다.
비용을 줄이는 순서
- effort를 내립니다. 분류나 추출을 기본값
high로 돌리고 있다면low로 내리는 것만으로 출력 토큰이 크게 줄어듭니다(5편) - 캐시를 겁니다. 고정된 앞부분이 512 토큰을 넘으면 붙일 값이 있습니다
- 모델을 내립니다. 6편의 채점 스크립트를
claude-haiku-4-5로 한 번 돌려보고 정확도가 유지되면 그대로 씁니다. 감이 아니라 점수로 정합니다 - 묶어 보냅니다. 즉시 응답이 필요 없는 대량 작업은 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 전달 시 400 | Claude 5 계열에서 제거됨 | effort와 프롬프트 제약으로 대체 (1편) |
budget_tokens 전달 시 400 | adaptive thinking으로 대체됨 | output_config의 effort 사용 (5편) |
| assistant prefill 시 400 | 4.6 이후 지원 종료 | json_schema 또는 tool use (4편) |
| JSON 파싱 실패가 가끔 발생 | 프롬프트로 부탁만 함 | output_config.format으로 강제 |
| 라벨 문자열이 흔들림 | 자유 텍스트 출력 | 스키마의 enum |
| 캐시가 안 걸림 | 앞부분에 변하는 값이 있음 | 고정 부분을 앞으로, 최소 길이 확인 |
| 프롬프트를 고쳤는데 다른 곳이 나빠짐 | 입력 하나만 보고 판단 | TEST 셋 전체 채점 (6편) |
시리즈를 마치며
1편의 출발점은 “요청과 응답이 어떤 모양인가”였습니다. 그 뒤로 한 일은 모델에게 맡겨두던 결정을 하나씩 명시적으로 가져오는 것이었습니다. 2편에서 지시를, 3편에서 예시를, 4편에서 출력 형식을, 5편에서 추론 깊이를 정했고, 6편에서 그 결정들이 실제로 나은지 채점했습니다.
남은 한 가지는 모델이 모르는 내용을 답하게 만드는 일입니다. 프롬프트를 아무리 다듬어도 사내 문서나 어제 바뀐 정책은 모델 안에 없습니다. 그 문서를 찾아 프롬프트에 넣어주는 구조가 RAG 기초 시리즈이고, 그 시리즈의 프롬프트 설계는 여기서 익힌 것을 그대로 씁니다.
여러 모델 호출과 도구를 엮어 애플리케이션으로 조립하는 쪽은 LangChain 기초와 LangGraph 기초로 이어집니다.