이 글은 누구를 위한 것인가
- 긴 대화가 이어지면서 초반 내용을 LLM이 잊어버리는 문제를 겪는 팀
- 100페이지 PDF를 LLM에 넣으려는 개발자
- 컨텍스트 한계로 인한 에러를 처리해야 하는 엔지니어
들어가며
Claude의 컨텍스트 윈도우는 200K 토큰이지만, 비용은 토큰 수에 비례한다. 100K 토큰을 매 요청마다 보내면 하루 API 비용이 수백만 원이 된다. 스마트한 컨텍스트 관리가 필요하다.
이 글은 bluefoxdev.kr의 LLM 컨텍스트 최적화 를 참고하여 작성했습니다.
1. 컨텍스트 관리 전략
[컨텍스트 관리 방법 비교]
슬라이딩 윈도우:
최근 N개 메시지만 유지
장점: 단순, 최신 컨텍스트 보장
단점: 초반 중요 정보 손실
적합: 짧은 태스크형 대화
요약 압축:
오래된 메시지를 LLM으로 요약
장점: 핵심 정보 보존
단점: 요약 비용, 세부 정보 손실
적합: 장기 상담, 프로젝트 대화
중요도 기반 선택:
중요 메시지 태그 → 우선 보존
장점: 유연한 정보 선택
단점: 중요도 판단 로직 필요
RAG 하이브리드:
대화 기록 → 벡터 DB 저장
현재 질문과 관련된 과거 메시지 검색
장점: 무제한 기억, 관련 정보만 로드
단점: 구현 복잡도
[토큰 추정 (대략)]
한국어: 1글자 ≈ 1-2 토큰
영어: 1단어 ≈ 1.3 토큰
코드: 1줄 ≈ 5-10 토큰
Claude 200K ≈ 약 15만 한국어 글자
2. 대화 기록 관리 구현
import anthropic
from dataclasses import dataclass, field
client = anthropic.Anthropic()
@dataclass
class Message:
role: str
content: str
token_count: int = 0
importance: float = 1.0 # 0.0 (낮음) ~ 1.0 (높음)
is_summary: bool = False
class ConversationManager:
"""컨텍스트 윈도우 관리"""
def __init__(
self,
max_tokens: int = 100_000,
summary_trigger_ratio: float = 0.8,
):
self.max_tokens = max_tokens
self.summary_trigger = int(max_tokens * summary_trigger_ratio)
self.messages: list[Message] = []
self.system_tokens = 0
def count_tokens(self, text: str) -> int:
return len(text) // 2 # 간단 추정 (실제: tiktoken 또는 API 호출)
def total_tokens(self) -> int:
return sum(m.token_count for m in self.messages) + self.system_tokens
def add_message(self, role: str, content: str, importance: float = 1.0):
msg = Message(
role=role,
content=content,
token_count=self.count_tokens(content),
importance=importance,
)
self.messages.append(msg)
if self.total_tokens() > self.summary_trigger:
self._compress()
def _compress(self):
"""오래된 메시지 압축"""
if len(self.messages) < 4:
return
# 압축할 메시지: 앞 절반 중 낮은 중요도 메시지
cutoff = len(self.messages) // 2
to_compress = self.messages[:cutoff]
summary_text = self._summarize_messages(to_compress)
summary_msg = Message(
role="assistant",
content=f"[이전 대화 요약]: {summary_text}",
token_count=self.count_tokens(summary_text),
importance=1.0,
is_summary=True,
)
self.messages = [summary_msg] + self.messages[cutoff:]
def _summarize_messages(self, messages: list[Message]) -> str:
"""LLM으로 메시지 요약"""
history = "\n".join(
f"{m.role}: {m.content[:200]}" for m in messages
)
response = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=500,
messages=[{
"role": "user",
"content": f"""다음 대화를 3-5문장으로 요약하세요. 중요한 결정, 정보, 컨텍스트를 보존하세요.
{history}"""
}]
)
return response.content[0].text
def get_messages_for_api(self) -> list[dict]:
"""API 전송용 메시지 리스트"""
return [{"role": m.role, "content": m.content} for m in self.messages]
def sliding_window(self, n_recent: int = 20) -> list[dict]:
"""슬라이딩 윈도우: 요약 + 최근 N개"""
summaries = [m for m in self.messages if m.is_summary]
recent = [m for m in self.messages if not m.is_summary][-n_recent:]
return [{"role": m.role, "content": m.content} for m in summaries + recent]
def chunk_document(text: str, chunk_size: int = 2000, overlap: int = 200) -> list[str]:
"""긴 문서를 오버랩 청크로 분할"""
chunks = []
start = 0
while start < len(text):
end = start + chunk_size
chunk = text[start:end]
# 문장 경계에서 자르기
if end < len(text):
last_period = chunk.rfind(".")
if last_period > chunk_size * 0.7:
chunk = chunk[:last_period + 1]
end = start + last_period + 1
chunks.append(chunk)
start = end - overlap
return chunks
async def process_long_document(
document: str,
question: str,
max_chunks: int = 5,
) -> str:
"""긴 문서 처리: 관련 청크만 선택하여 질문 답변"""
chunks = chunk_document(document)
# 관련도 스코어링 (간단한 키워드 매칭)
question_words = set(question.lower().split())
scored_chunks = []
for i, chunk in enumerate(chunks):
chunk_words = set(chunk.lower().split())
overlap = len(question_words & chunk_words)
scored_chunks.append((overlap, i, chunk))
# 상위 K개 청크 선택
top_chunks = sorted(scored_chunks, reverse=True)[:max_chunks]
top_chunks = sorted(top_chunks, key=lambda x: x[1]) # 원래 순서 복원
context = "\n\n---\n\n".join(chunk for _, _, chunk in top_chunks)
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1000,
messages=[{
"role": "user",
"content": f"""다음 문서 내용을 바탕으로 질문에 답하세요.
문서:
{context}
질문: {question}"""
}]
)
return response.content[0].text
마무리
컨텍스트 관리의 핵심은 "무엇을 버릴 것인가"다. 상담 봇은 최근 10턴 + 요약으로 충분하다. 문서 Q&A는 전체를 보내지 말고 관련 청크만 선택하라. 프롬프트 캐싱으로 시스템 프롬프트 토큰 비용을 90% 줄이면 남은 예산을 사용자 컨텍스트에 쓸 수 있다.