목차
- Claude 컨텍스트 윈도우의 실제 한계
- 첫 번째 시도: 단순 슬라이딩 윈도우 (그리고 실패)
- Claude 컨텍스트 윈도우 메모리 관리의 핵심: 요약 압축 시스템
- 토큰 카운팅과 압축 타이밍
- 계층적 메모리 아키텍처 설계
- Claude 컨텍스트 윈도우 메모리 관리와 Prompt Caching
- RAG 연동으로 장기 메모리 확장하기
- 실전 트러블슈팅: 자주 만나는 문제들
- 컨텍스트 윈도우 메모리 관리 전략별 성능 비교
- 프로덕션 적용 시 고려할 것들
- 다음 단계: MCP와 외부 메모리 연동
# 이 코드가 뭘 하는지 한번 보자
conversation_history = []
for turn in range(200):
user_msg = get_user_input()
conversation_history.append({"role": "user", "content": user_msg})
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=4096,
messages=conversation_history # 이게 계속 커진다
)
conversation_history.append({"role": "assistant", "content": response.content[0].text})
# 200턴째... 토큰이 얼마일까?
이 코드의 문제가 뭔지 바로 보이면, 이미 Claude 컨텍스트 윈도우 메모리 관리로 한번은 삽질해본 거다. conversation_history를 그대로 쌓으면 50턴쯤에서 200K 토큰을 넘기기 시작한다. API가 400 에러를 뱉고, 요금은 요금대로 나간다.
Claude 컨텍스트 윈도우 메모리 관리 문제를 처음 만난 건 챗봇 프로젝트에서였다. 사용자가 30분 넘게 대화하면 응답이 느려지더니, 어느 순간 prompt is too long 에러가 터졌다. 그때부터 메모리 시스템을 직접 설계하기 시작했다. 단순해 보이는 문제인데, 파고들수록 깊더라.
Claude 컨텍스트 윈도우의 실제 한계
Claude 모델별로 컨텍스트 윈도우 크기가 다르다. 2025년 기준으로 정리하면 이렇다.
| 모델 | 컨텍스트 윈도우 | 최대 출력 토큰 | 실사용 가능 입력 |
|---|---|---|---|
| Claude 3.5 Sonnet | 200K | 8,192 | ~191K |
| Claude 3.5 Haiku | 200K | 8,192 | ~191K |
| Claude Sonnet 4 | 200K | 16,384 | ~183K |
숫자만 보면 200K 토큰이 엄청 많아 보인다. 근데 실제로 써보면 얘기가 다르다. 시스템 프롬프트에 2~3K 토큰, 출력에 4~8K 토큰을 빼면 실제로 대화 히스토리에 쓸 수 있는 건 180K 정도다.
토큰이 실제로 얼마나 빨리 차는가
한국어는 영어보다 토큰 효율이 나쁘다. 같은 내용을 한국어로 쓰면 영어 대비 1.5~2배 토큰을 먹는다. 한국어 대화 한 턴이 평균 300~500토큰이라고 치면, 사용자+어시스턴트 합쳐서 턴당 1,000토큰 정도다.
import anthropic
client = anthropic.Anthropic()
# 토큰 수를 직접 세보자
def count_tokens(messages, system=""):
response = client.messages.count_tokens(
model="claude-sonnet-4-20250514",
system=system,
messages=messages
)
return response.input_tokens
# 실험: 빈 대화부터 시작해서 토큰 증가량 확인
messages = []
for i in range(10):
messages.append({"role": "user", "content": f"이전 대화를 참고해서 {i+1}번째 질문에 답해줘. 파이썬 비동기 처리에서 주의할 점이 뭐야?"})
messages.append({"role": "assistant", "content": f"파이썬 비동기 처리에서 주의할 점은... (긴 응답 {i+1})"})
token_count = count_tokens(messages)
print(f"10턴 후 토큰: {token_count}")
직접 세보면 10턴에 3,000~5,000토큰 정도 나온다. 별거 아닌 것 같지만, 코드 리뷰 같은 대화에서는 한 턴에 2,000토큰 넘게 들어간다. 50턴이면 100K를 훌쩍 넘는다.
컨텍스트 윈도우를 꽉 채우면 응답 품질이 떨어진다. Anthropic 문서에서도 전체 윈도우의 70~80% 이하로 유지하길 권장한다. 160K 이상 채우면 초반 대화 내용을 잘 기억 못 하는 현상이 생긴다.
가장 먼저 떠오른 건 슬라이딩 윈도우였다. 최근 N개 메시지만 유지하고 오래된 건 버리는 거다.
class SlidingWindowMemory:
def __init__(self, max_turns=30):
self.max_turns = max_turns
self.messages = []
def add(self, role, content):
self.messages.append({"role": role, "content": content})
# 최근 max_turns * 2개만 유지 (user + assistant 쌍)
if len(self.messages) > self.max_turns * 2:
self.messages = self.messages[-(self.max_turns * 2):]
def get_messages(self):
return self.messages
간단하고 빠르다. 근데 치명적인 문제가 있다. 20턴 전에 사용자가 "내 이름은 김철수야, 백엔드 개발자야"라고 했는데, 슬라이딩 윈도우가 그걸 날려버리면? "아까 내가 뭐 한다고 했지?"라는 질문에 답을 못 한다.
실제로 이 방식으로 배포했다가 사용자 피드백이 안 좋았다. "봇이 기억력이 없다", "계속 같은 말 반복한다". 당연하다. 대화 앞부분을 통째로 잘라내니까.
왜 단순 슬라이딩으로는 부족한가
사람의 대화에는 두 종류 정보가 있다. 휘발성 정보(방금 한 질문의 맥락)와 지속성 정보(사용자 이름, 프로젝트 맥락, 이전 결정사항). 슬라이딩 윈도우는 둘을 구분 못 한다. 오래됐다고 다 버리면 지속성 정보가 날아간다.
Claude 컨텍스트 윈도우 메모리 관리의 핵심: 요약 압축 시스템
삽질 끝에 정착한 방식이 요약 기반 메모리다. 핵심 아이디어는 간단하다: 오래된 대화를 Claude한테 요약시키고, 그 요약을 시스템 프롬프트에 넣는 거다.
import anthropic
from typing import Optional
class SummarizationMemory:
def __init__(self, client: anthropic.Anthropic, max_tokens: int = 50000):
self.client = client
self.max_tokens = max_tokens # 이 이상이면 요약 트리거
self.messages = []
self.summary: Optional[str] = None
def add(self, role: str, content: str):
self.messages.append({"role": role, "content": content})
def _estimate_tokens(self) -> int:
"""대략적 토큰 추정. 정확하진 않지만 API 호출 줄이려고."""
total_chars = sum(len(m["content"]) for m in self.messages)
return total_chars // 2 # 한국어 기준 대략 2자당 1토큰
async def maybe_compress(self):
if self._estimate_tokens() < self.max_tokens:
return
# 앞쪽 절반을 요약 대상으로
split_point = len(self.messages) // 2
to_summarize = self.messages[:split_point]
to_keep = self.messages[split_point:]
summary_prompt = self._build_summary_prompt(to_summarize)
response = self.client.messages.create(
model="claude-haiku-3-5-20241022", # 요약은 저렴한 모델로
max_tokens=2048,
messages=[{"role": "user", "content": summary_prompt}]
)
new_summary = response.content[0].text
# 기존 요약이 있으면 합치기
if self.summary:
self.summary = f"{self.summary}\n\n[추가 요약]\n{new_summary}"
else:
self.summary = new_summary
self.messages = to_keep
def _build_summary_prompt(self, messages: list) -> str:
conversation = "\n".join(
f"{'사용자' if m['role'] == 'user' else '어시스턴트'}: {m['content']}"
for m in messages
)
return f"""아래 대화를 요약해줘. 다음 규칙을 지켜:
1. 사용자가 언급한 개인 정보(이름, 직업, 프로젝트명 등) 반드시 유지
2. 주요 결정사항과 합의된 내용 유지
3. 기술적 맥락(사용 중인 기술 스택, 에러 상황 등) 유지
4. 일상적인 인사, 중복 질문은 생략
5. 500자 이내로
대화:
{conversation}"""
def get_system_prompt(self, base_prompt: str) -> str:
if self.summary:
return f"""{base_prompt}
[이전 대화 요약]
{self.summary}"""
return base_prompt
def get_messages(self) -> list:
return self.messages
이 방식의 포인트가 몇 가지 있다.
요약 작업에 Claude Sonnet을 쓰면 비용이 2배로 뛴다. Haiku로 요약하면 비용이 1/10 수준이고, 요약 품질도 충분하다. 요약에 필요한 건 이해력이지 창의성이 아니다.
토큰 카운팅과 압축 타이밍
요약을 언제 트리거하느냐도 중요한 설계 포인트다. 너무 자주 하면 비용이 늘고, 너무 늦으면 컨텍스트가 넘친다.
Anthropic Token Counting API 활용
위 코드에서 _estimate_tokens()를 대략적으로 구현했는데, 정확한 카운팅이 필요하면 Anthropic의 Token Counting API를 쓰면 된다.
def get_exact_token_count(self) -> int:
"""정확한 토큰 수. API 호출이 발생하므로 매 턴마다 쓰진 않는다."""
if not self.messages:
return 0
response = self.client.messages.count_tokens(
model="claude-sonnet-4-20250514",
system=self.get_system_prompt(""),
messages=self.messages
)
return response.input_tokens
다만 이것도 매 턴마다 호출하면 API 비용이 늘어난다. 내가 쓰는 전략은 이렇다:
- 평소에는 문자 수 기반 추정 (
총 문자 수 / 2) - 추정값이 임계치의 80%를 넘으면 그때 정확한 카운팅
- 정확한 값이 임계치를 넘으면 요약 트리거
async def should_compress(self) -> bool:
estimate = self._estimate_tokens()
# 임계치의 80% 이하면 패스
if estimate < self.max_tokens * 0.8:
return False
# 80% 넘으면 정확히 세본다
exact = self.get_exact_token_count()
return exact >= self.max_tokens
솔직히 문자 수 기반 추정은 정확도가 70% 정도밖에 안 된다. 코드가 많은 대화는 토큰이 적게 나오고, 한국어 텍스트가 많으면 많이 나온다. 근데 정확한 타이밍보다 비용 절감이 더 중요해서 이렇게 쓰고 있다.
계층적 메모리 아키텍처 설계
실전에서는 요약만으로 부족하다. 대화가 수백 턴 넘어가면 요약의 요약이 필요해지고, 그러다 보면 초기 맥락이 뭉개진다. 그래서 메모리를 계층으로 나눈다.
graph TD
A[사용자 메시지] --> B[작업 메모리<br/>최근 10~20턴]
B --> C{토큰 임계치<br/>초과?}
C -->|No| D[그대로 유지]
C -->|Yes| E[요약 엔진<br/>Claude Haiku]
E --> F[단기 요약<br/>시스템 프롬프트에 삽입]
E --> G[엔티티 추출<br/>사용자 정보, 결정사항]
G --> H[장기 메모리<br/>벡터 DB 또는 KV 저장소]
H --> I[검색으로 필요할 때 꺼내기]
작업 메모리 (Working Memory)
최근 대화. 슬라이딩 윈도우로 관리한다. 여기가 메인이고, 대부분의 응답은 여기만으로 충분하다.
단기 요약 (Short-term Summary)
작업 메모리에서 밀려난 대화의 요약. 시스템 프롬프트에 들어간다. 보통 1,000~2,000토큰 수준으로 유지한다.
장기 메모리 (Long-term Memory)
여기가 좀 까다롭다. 대화에서 추출한 핵심 엔티티(사용자 이름, 선호 기술 스택, 프로젝트명 등)를 별도로 저장한다. 벡터 DB를 쓸 수도 있고, 간단하게 JSON 파일로 할 수도 있다.
import json
from datetime import datetime
class EntityMemory:
def __init__(self, storage_path: str = "memory.json"):
self.storage_path = storage_path
self.entities = self._load()
def _load(self) -> dict:
try:
with open(self.storage_path, "r") as f:
return json.load(f)
except FileNotFoundError:
return {"user_info": {}, "decisions": [], "context": {}}
def save(self):
with open(self.storage_path, "w") as f:
json.dump(self.entities, f, ensure_ascii=False, indent=2)
def update_user_info(self, key: str, value: str):
self.entities["user_info"][key] = {
"value": value,
"updated_at": datetime.now().isoformat()
}
self.save()
def add_decision(self, decision: str):
self.entities["decisions"].append({
"content": decision,
"created_at": datetime.now().isoformat()
})
# 최근 20개만 유지
self.entities["decisions"] = self.entities["decisions"][-20:]
self.save()
def to_prompt(self) -> str:
parts = []
if self.entities["user_info"]:
info = ", ".join(
f"{k}: {v['value']}"
for k, v in self.entities["user_info"].items()
)
parts.append(f"[사용자 정보] {info}")
if self.entities["decisions"]:
recent = self.entities["decisions"][-5:]
decisions = "\n".join(f"- {d['content']}" for d in recent)
parts.append(f"[최근 결정사항]\n{decisions}")
return "\n\n".join(parts)
개인적으로 이 구조가 제일 깔끔하다. 벡터 DB까지 갈 필요 없는 프로젝트가 대부분이고, JSON 파일 하나로 충분한 경우가 많다.
매번 수동으로 엔티티를 넣을 수는 없다. Claude한테 대화를 주고 “여기서 기억할 만한 정보를 JSON으로 뽑아줘”라고 하면 된다. 이것도 Haiku로 충분하다.
Anthropic의 Prompt Caching 기능을 메모리 시스템과 같이 쓰면 비용을 크게 줄일 수 있다. 사실 이걸 모르고 한동안 비용을 많이 썼다.
Prompt Caching이 뭐냐면, 시스템 프롬프트나 대화 앞부분이 여러 요청에서 동일하면 캐싱해서 토큰 비용을 90% 줄여주는 기능이다.
from anthropic import Anthropic
client = Anthropic()
# 캐싱을 활용한 메모리 시스템
def create_cached_request(system_prompt: str, summary: str, recent_messages: list):
system_blocks = [
{
"type": "text",
"text": system_prompt,
"cache_control": {"type": "ephemeral"} # 이 부분이 캐싱됨
}
]
# 요약도 캐싱 대상. 요약은 자주 안 바뀌니까.
if summary:
system_blocks.append({
"type": "text",
"text": f"\n[이전 대화 요약]\n{summary}",
"cache_control": {"type": "ephemeral"}
})
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=4096,
system=system_blocks,
messages=recent_messages
)
return response
캐싱의 효과가 꽤 크다. 시스템 프롬프트 + 요약이 3,000토큰이라고 하면, 캐시 히트 시 이 부분의 비용이 1/10로 줄어든다. 장기 대화에서 턴마다 절약되니까 누적하면 상당하다.
캐싱과 메모리 압축의 타이밍 문제
근데 여기서 주의할 게 있다. 요약을 업데이트하면 캐시가 무효화된다. 그래서 요약 갱신 주기를 너무 짧게 잡으면 캐싱 효과가 없어진다.
내 경험상 최적 지점은 이렇다:
- 시스템 프롬프트: 거의 안 바뀜 → 캐싱 효과 극대
- 대화 요약: 20~30턴마다 갱신 → 적당한 캐싱 효과
- 엔티티 메모리: 변경 시에만 갱신 → 캐싱에 영향 적음
Prompt Caching은 캐싱할 블록이 최소 1,024토큰(Claude Sonnet 기준) 이상이어야 작동한다. 시스템 프롬프트가 짧으면 캐싱 대상이 안 될 수 있다. 시스템 프롬프트에 충분한 지시사항을 넣는 게 오히려 비용 절감에 도움이 된다.
대화가 수천 턴을 넘어가거나, 여러 세션에 걸쳐 맥락을 유지해야 하면 요약만으로는 한계가 온다. 이때 RAG(Retrieval-Augmented Generation)를 붙인다.

전체 대화를 벡터 DB에 저장하고, 현재 질문과 관련된 과거 대화만 검색해서 컨텍스트에 넣는 방식이다.
# chromadb를 쓴 예시. pip install chromadb
import chromadb
class VectorMemory:
def __init__(self, collection_name: str = "conversations"):
self.chroma = chromadb.PersistentClient(path="./chroma_db")
self.collection = self.chroma.get_or_create_collection(
name=collection_name,
metadata={"hf:space": "default"}
)
self.turn_id = 0
def store_turn(self, user_msg: str, assistant_msg: str, metadata: dict = None):
"""대화 턴을 벡터 DB에 저장"""
self.turn_id += 1
combined = f"사용자: {user_msg}\n어시스턴트: {assistant_msg}"
self.collection.add(
documents=[combined],
ids=[f"turn_{self.turn_id}"],
metadatas=[metadata or {"turn": self.turn_id}]
)
def search_relevant(self, query: str, n_results: int = 5) -> list[str]:
"""현재 질문과 관련된 과거 대화를 검색"""
results = self.collection.query(
query_texts=[query],
n_results=n_results
)
return results["documents"][0] if results["documents"] else []
이걸 메인 메모리 시스템에 통합하면 이렇게 된다:
class HybridMemory:
def __init__(self, client: anthropic.Anthropic):
self.client = client
self.sliding = SlidingWindowMemory(max_turns=15)
self.summarizer = SummarizationMemory(client, max_tokens=40000)
self.entity = EntityMemory()
self.vector = VectorMemory()
def build_context(self, current_query: str, base_system: str) -> tuple[str, list]:
# 1. 벡터 검색으로 관련 과거 대화 찾기
relevant_history = self.vector.search_relevant(current_query, n_results=3)
# 2. 시스템 프롬프트 조합
system_parts = [base_system]
entity_prompt = self.entity.to_prompt()
if entity_prompt:
system_parts.append(entity_prompt)
if self.summarizer.summary:
system_parts.append(f"[대화 요약]\n{self.summarizer.summary}")
if relevant_history:
history_text = "\n---\n".join(relevant_history)
system_parts.append(f"[관련 과거 대화]\n{history_text}")
system_prompt = "\n\n".join(system_parts)
# 3. 최근 메시지는 슬라이딩 윈도우에서
messages = self.sliding.get_messages()
return system_prompt, messages
솔직히 이건 좀 불편하다. ChromaDB 설치하고 관리하는 것도 일이고, 검색 품질이 항상 좋은 것도 아니다. 사용자가 "아까 그거"라고 하면 벡터 검색이 "아까 그거"를 제대로 못 찾는 경우가 있다. 임베딩의 한계다.
그래도 수백 턴 이상의 장기 대화에서는 요약+벡터 검색 조합이 가장 나은 방법이다. 다른 대안을 못 찾았다.
실전 트러블슈팅: 자주 만나는 문제들
Claude 컨텍스트 윈도우 메모리 관리를 실제로 구현하면 예상 못 한 문제가 꽤 나온다.
요약이 핵심을 놓치는 경우
제일 짜증났던 건 요약 모델이 기술적 디테일을 빠뜨리는 거였다. 사용자가 "PostgreSQL 15에서 jsonb_path_query 쓰고 있어"라고 했는데, 요약에서 "데이터베이스를 사용 중"으로 뭉개버린다.
해결법: 요약 프롬프트에 도메인 특화 지시를 넣는다.
TECH_SUMMARY_PROMPT = """아래 대화를 요약해줘.
반드시 유지할 정보:
- 구체적 기술명과 버전 (예: PostgreSQL 15, React 18)
- 에러 메시지 원문
- 사용자가 선택한 방법/도구
- 코드 스니펫의 핵심 로직 (전체 복사 불필요, 핵심만)
- 파일 경로, 환경 설정값
생략해도 되는 정보:
- 인사말, 감사 표현
- 이미 해결된 문제의 상세 과정 (결론만 유지)
- 중복된 설명
500자 이내로 요약:
{conversation}"""
이렇게 하니까 요약 품질이 확 올랐다.
messages 배열의 role 순서 문제
Claude API는 messages 배열에서 user와 assistant가 번갈아 와야 한다. 요약 후 메시지를 재조합할 때 이 순서가 깨지면 400 에러가 난다.
# 이렇게 되면 에러
messages = [
{"role": "user", "content": "질문1"},
{"role": "user", "content": "질문2"}, # user가 연속으로 오면 안 됨
{"role": "assistant", "content": "답변"}
]
요약 후 메시지를 잘라낼 때 항상 user로 시작하는지 확인해야 한다.
def sanitize_messages(messages: list) -> list:
"""messages 배열의 role 순서를 보정"""
if not messages:
return messages
# user로 시작해야 함
while messages and messages[0]["role"] != "user":
messages = messages[1:]
# 연속 role 제거
sanitized = [messages[0]]
for msg in messages[1:]:
if msg["role"] != sanitized[-1]["role"]:
sanitized.append(msg)
return sanitized
이것 때문에 2시간 날렸다. 에러 메시지가 messages: roles must alternate between "user" and "assistant"인데, 요약 직후에만 발생해서 원인 찾기가 어려웠다.
컨텍스트가 거의 찬 상태에서 요청을 보내면, Claude가 응답을 중간에 끊는 경우가 있다. `stop_reason`이 `end_turn`이 아니라 `max_tokens`이면 출력이 잘린 거다. 메모리 압축 로직에서 이 경우를 처리해야 한다.

지금까지 다룬 전략들을 정리하면 이렇다.
| 전략 | 구현 난이도 | 토큰 효율 | 맥락 보존 | 비용 | 적합한 경우 |
|---|---|---|---|---|---|
| 슬라이딩 윈도우 | 쉬움 | 낮음 | 최근만 | 높음 | 짧은 대화, 프로토타입 |
| 요약 압축 | 중간 | 높음 | 중간 | 중간 | 일반적인 챗봇 |
| 엔티티 추출 | 중간 | 높음 | 좋음 | 중간 | 개인화 서비스 |
| 하이브리드(요약+RAG) | 어려움 | 매우 높음 | 매우 좋음 | 낮음 | 장기 대화, 복잡한 태스크 |
비용 열에서 슬라이딩 윈도우가 "높음"인 이유가 의아할 수 있다. 요약 없이 최근 메시지를 그대로 보내면, 매 요청마다 보내는 토큰 수가 계속 높은 상태로 유지되기 때문이다. 요약을 쓰면 전체 토큰 수 자체가 줄어서 장기적으로는 더 싸다.
개인적으로 대부분의 프로젝트에서 "요약 압축 + 엔티티 추출" 조합이면 충분하다. RAG까지 붙이는 건 대화가 수백 턴 이상 가는 경우에만 의미 있다.
프로덕션 적용 시 고려할 것들

실제 서비스에 메모리 시스템을 넣을 때 빠뜨리기 쉬운 것들이 있다.
동시성과 세션 격리
여러 사용자가 동시에 대화하면 메모리가 섞이면 안 된다. 당연한 얘기인데, 싱글턴으로 메모리를 구현하면 이 문제가 생�다.
from collections import defaultdict
class SessionManager:
def __init__(self, client: anthropic.Anthropic):
self.client = client
self.sessions: dict[str, HybridMemory] = {}
def get_or_create(self, session_id: str) -> HybridMemory:
if session_id not in self.sessions:
self.sessions[session_id] = HybridMemory(self.client)
return self.sessions[session_id]
def cleanup_old_sessions(self, max_idle_minutes: int = 30):
"""오래된 세션 정리. 메모리 누수 방지."""
# 실제로는 마지막 활동 시간을 추적해서 정리
pass
비용 모니터링
Claude 컨텍스트 윈도우 메모리 관리에서 놓치기 쉬운 게 비용이다. 요약 API 호출 비용, 메인 대화 비용, 벡터 DB 운영 비용을 합치면 턴당 비용이 생각보다 높아질 수 있다.
class CostTracker:
def __init__(self):
self.total_input_tokens = 0
self.total_output_tokens = 0
self.summary_calls = 0
def track(self, response):
self.total_input_tokens += response.usage.input_tokens
self.total_output_tokens += response.usage.output_tokens
def estimate_cost(self, model: str = "claude-sonnet-4-20250514") -> float:
"""대략적 비용 추정 (USD)"""
# 2025년 기준 Sonnet 가격
input_cost = self.total_input_tokens * 3 / 1_000_000
output_cost = self.total_output_tokens * 15 / 1_000_000
return input_cost + output_cost
비용 추적을 처음부터 넣어야 한다. 나중에 넣으려면 귀찮아서 안 하게 된다. 운영 중에 "이번 달 Claude 비용이 왜 이렇게 나왔지?" 하고 나서야 찾게 되는데, 그때는 이미 늦다.
에러 핸들링
요약 API 호출이 실패하면 어떻게 할 건지도 정해야 한다. 요약 실패 시 그냥 슬라이딩 윈도우로 폴백하는 게 가장 안전하다.
async def safe_compress(self):
try:
await self.summarizer.maybe_compress()
except anthropic.APIError as e:
# 요약 실패 시 슬라이딩 윈도우로 폴백
if len(self.sliding.messages) > 40:
self.sliding.messages = self.sliding.messages[-30:]
print(f"요약 실패, 폴백: {e}")
메모리 시스템 테스트할 때 실제 Claude API를 매번 호출하면 비용이 나간다. 요약 로직은 mock 응답으로 단위 테스트하고, 통합 테스트만 실제 API로 돌리는 게 낫다. `unittest.mock.patch`로 `client.messages.create`를 모킹하면 된다.
메모리 시스템이 어느 정도 잡혔으면 다음은 MCP(Model Context Protocol)와의 연동이다. MCP 서버로 외부 데이터베이스나 파일 시스템을 직접 연결하면, 컨텍스트 윈도우 안에 모든 걸 담을 필요가 없어진다. 필요한 정보를 그때그때 가져오는 구조로 바꿀 수 있다.
그리고 Anthropic의 Agent SDK를 쓰면 멀티 에이전트 구조에서 메모리를 공유하는 것도 가능하다. 에이전트 A가 수집한 맥락을 에이전트 B가 참조하는 식인데, 이건 아직 안정화가 안 된 영역이라 프로덕션에서 쓰려면 좀 기다려야 할 것 같다.
프롬프트 엔지니어링 쪽에서도 할 게 남아있다. 시스템 프롬프트에 메모리를 어떤 포맷으로 넣느냐에 따라 Claude의 활용 능력이 달라진다. XML 태그로 구조화해서 넣는 방식이 체감상 가장 안정적인데, 이것도 별도로 정리할 만한 주제다.
Claude 컨텍스트 윈도우 메모리 관리, 처음엔 단순한 문제인 줄 알았다. 대화 길어지면 자르면 되지, 싶었는데 실제로는 뭘 자르고 뭘 남길지가 핵심이다. 요약 + 엔티티 추출 + 적절한 캐싱, 이 세 가지를 조합하면 200K 토큰으로도 꽤 긴 대화를 커버하게 된다.
관련 글
- Claude API 오케스트레이션 패턴 5가지: Python 워크플로우 자동화 실전 구현 – Claude API로 워크플로우를 자동화할 때 어떤 오케스트레이션 패턴을 써야 할까? 프롬프트 체이닝, 라우팅, 병렬화, 오케스트레이터-워…
- Claude 시스템 프롬프트 설계 패턴 7가지: 역할 지정부터 출력 포맷 제어까지 – Claude API를 쓸 때 시스템 프롬프트 하나가 응답 품질을 완전히 바꾼다. 실무에서 반복 검증한 7가지 설계 패턴을 코드 예제와 함께…
- Claude API 토큰 비용 70% 절감한 3가지 실전 전략: 프롬프트 캐싱·배치 API·컨텍스트 압축 – 월 300만 원 나오던 Claude API 비용을 프롬프트 캐싱, 배치 API, 컨텍스트 압축 세 가지 조합으로 90만 원대까지 줄인 과정…