190 lines
7.5 KiB
Markdown
190 lines
7.5 KiB
Markdown
### 질문
|
|
|
|
> 그렇다면, “작업 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
|
|
```
|
|
|
|
------
|
|
|
|
### 구현 코드
|
|
|
|
```python
|
|
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 토큰 이상** | 미만이면 캐시 대상에서 제외됨 |
|
|
|