Files
Liti-agent-Development/Caching_설계/Claude_캐싱설계.md
T

7.5 KiB

질문

그렇다면, “작업 1의 결과물 파일을 KV-caching처럼 저장해 두고, 작업 2에서 추가 토큰 없이 동일 Claude 모델이 그것을 hit하여 쓰게 설계할 수 있는가?”

답변

결론부터: "완전한 0 토큰"은 불가능하지만, 구조적으로 거의 동일하게 설계 가능합니다


왜 완전한 KV-cache hit가 안 되는가

진짜 KV-cache (Transformer 내부)
─────────────────────────────────
[Task 1 실행] → KV matrix 메모리에 상주
[Task 2 실행] → 동일 KV matrix를 attention에서 그대로 hit → 0 토큰

Anthropic API의 Prompt Caching
─────────────────────────────────
[Task 1 실행] → 서버에 캐시 저장 (5분 TTL)
[Task 2 실행] → cache_read_input_tokens 발생 (기본 가격의 10%)
                → "추가 토큰 없음"이 아니라 "90% 할인된 토큰"

API는 Stateless이기 때문에, Task 1의 출력이 Task 2의 입력이 되는 순간 반드시 토큰이 발생합니다. 단, 캐시를 제대로 설계하면 90% 절감된 비용으로 "hit에 가까운" 동작을 구현할 수 있습니다.


설계 원리: 대화 히스토리 캐시 체인

Task 1의 출력을 Task 2의 **캐시된 프리픽스(prefix)**에 포함시키는 것이 핵심입니다.

캐시 프리픽스 구조:
┌─────────────────────────────────────────────────────┐
│  System [file_content] ← cache_control (캐시 앵커 1) │
│  User: "Task 1 수행하라"                              │
│  Assistant: [Task 1 출력]                            │ ← 이 전체가
│  User: "Task 2 수행하라" ← cache_control (캐시 앵커 2)│ ← 캐시됨
└─────────────────────────────────────────────────────┘
                                ↑
                     Task 2 실행 시 이 prefix 전체를 hit

구현 코드

import anthropic

client = anthropic.Anthropic()

with open("input_document.md", "r", encoding="utf-8") as f:
    file_content = f.read()

# 변하지 않는 시스템 + 파일 (캐시 앵커 1)
CACHED_SYSTEM = [
    {
        "type": "text",
        "text": "당신은 한국 민사소송 전문 AI입니다. 최종 결과물만 출력하세요.",
    },
    {
        "type": "text",
        "text": file_content,
        "cache_control": {"type": "ephemeral"}  # ← 앵커 1
    }
]

# ──────────────────────────────────────────────
# Task 1 실행
# ──────────────────────────────────────────────
print("=== Task 1 실행 ===")
response1 = client.messages.create(
    model="claude-opus-4-6",
    max_tokens=4000,
    system=CACHED_SYSTEM,
    messages=[
        {
            "role": "user",
            "content": "이 사건의 법적 쟁점을 추출하세요."
        }
    ]
)

task1_output = response1.content[0].text
u1 = response1.usage
print(f"  캐시 생성: {u1.cache_creation_input_tokens} | 캐시 읽기: {u1.cache_read_input_tokens} | 일반 입력: {u1.input_tokens}")

# ──────────────────────────────────────────────
# Task 2 실행 - Task 1 출력을 캐시 프리픽스에 포함
# ──────────────────────────────────────────────
print("\n=== Task 2 실행 ===")
response2 = client.messages.create(
    model="claude-opus-4-6",
    max_tokens=6000,
    system=CACHED_SYSTEM,          # ← 앵커 1 재사용 (hit)
    messages=[
        # Task 1의 전체 대화를 히스토리로 포함
        {
            "role": "user",
            "content": "이 사건의 법적 쟁점을 추출하세요."
        },
        {
            "role": "assistant",
            "content": task1_output   # ← Task 1 출력이 히스토리에 포함
        },
        # Task 2 지시 + 캐시 앵커 2
        {
            "role": "user",
            "content": [
                {
                    "type": "text",
                    "text": "위 법적 쟁점을 바탕으로 소장을 작성하세요.",
                    "cache_control": {"type": "ephemeral"}  # ← 앵커 2
                }
            ]
        }
    ]
)

task2_output = response2.content[0].text
u2 = response2.usage
print(f"  캐시 생성: {u2.cache_creation_input_tokens} | 캐시 읽기: {u2.cache_read_input_tokens} | 일반 입력: {u2.input_tokens}")

# ──────────────────────────────────────────────
# Task 3가 있다면 - Task 1+2 모두 캐시로 hit
# ──────────────────────────────────────────────
print("\n=== Task 3 실행 ===")
response3 = client.messages.create(
    model="claude-opus-4-6",
    max_tokens=2000,
    system=CACHED_SYSTEM,
    messages=[
        {"role": "user",    "content": "이 사건의 법적 쟁점을 추출하세요."},
        {"role": "assistant","content": task1_output},
        {
            "role": "user",
            "content": [{"type": "text", "text": "위 법적 쟁점을 바탕으로 소장을 작성하세요.",
                         "cache_control": {"type": "ephemeral"}}]
        },
        {"role": "assistant","content": task2_output},
        # Task 3 신규 지시만 새 토큰
        {
            "role": "user",
            "content": "소장의 청구취지 부분만 요약하세요."
        }
    ]
)

u3 = response3.usage
print(f"  캐시 생성: {u3.cache_creation_input_tokens} | 캐시 읽기: {u3.cache_read_input_tokens} | 일반 입력: {u3.input_tokens}")

각 Task별 토큰 흐름

Task 1 호출:
  [file_content 10,000 토큰] → cache_creation (1회 비용 발생, 이후 캐시)
  [user 지시 20 토큰]        → 일반 input

Task 2 호출 (5분 이내):
  [file_content 10,000 토큰] → cache_read ✓ (10% 비용)
  [Task1 user 20 토큰]       → cache_read ✓ (prefix 일치)
  [Task1 output 1,000 토큰]  → cache_read ✓ (prefix에 포함)
  [Task2 지시 30 토큰]       → cache_creation (앵커 2 생성)

Task 3 호출 (5분 이내):
  [위 전체 ~11,050 토큰]     → cache_read ✓
  [Task2 output 2,000 토큰]  → cache_read ✓ (prefix에 포함)
  [Task3 지시 20 토큰]       → 일반 input (신규 토큰)

Law-aid Agent 파이프라인 적용 시 핵심 제약

조건 이유
5분 내 다음 Task 호출 TTL 만료 시 캐시 miss → 전체 재과금
prefix가 완전히 동일해야 함 앞 메시지 하나라도 다르면 cache miss
동일 모델 사용 claude-opus-4-6 ↔ sonnet 간 캐시 공유 불가
캐시 앵커는 최소 1,024 토큰 이상 미만이면 캐시 대상에서 제외됨