본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 25. · 4 Views
Context Window 최적화 완벽 가이드
LLM의 컨텍스트 윈도우를 효율적으로 활용하는 방법을 실무 중심으로 배웁니다. 토큰 제한 이해부터 압축 기법, Sliding Window 전략까지 단계별로 학습하여 긴 문서 처리 시스템을 구축할 수 있습니다.
목차
1. 토큰 제한 이해하기
어느 날 김개발 씨가 AI 챗봇 서비스를 개발하던 중 이상한 문제를 발견했습니다. "왜 긴 대화를 나누다 보면 앞부분의 내용을 기억하지 못할까?" 선배 개발자 박시니어 씨가 다가와 말했습니다.
"아, 그건 컨텍스트 윈도우 때문이에요."
컨텍스트 윈도우는 LLM이 한 번에 처리할 수 있는 텍스트의 최대 길이를 의미합니다. 마치 책상 위의 작업 공간처럼 제한된 공간 안에서만 작업이 가능합니다.
GPT-4는 약 8,000~32,000 토큰, Claude는 100,000 토큰까지 처리할 수 있으며, 이를 초과하면 오래된 정보부터 잊혀집니다.
다음 코드를 살펴봅시다.
import tiktoken
# GPT-4 토크나이저 로드
encoding = tiktoken.encoding_for_model("gpt-4")
def count_tokens(text):
"""텍스트의 토큰 수를 계산합니다"""
tokens = encoding.encode(text)
return len(tokens)
# 예제 텍스트
long_text = "안녕하세요. " * 1000
token_count = count_tokens(long_text)
print(f"토큰 수: {token_count}")
# 출력: 토큰 수: 3000 (대략)
김개발 씨는 입사 6개월 차 개발자입니다. 최근 회사에서 고객 상담 AI 챗봇을 개발하는 프로젝트를 맡았습니다.
처음에는 순조롭게 진행되는 듯했습니다. 간단한 질문에는 잘 답변하고, 대화도 자연스러웠습니다.
하지만 문제가 생겼습니다. 고객과 긴 대화를 나누다 보면 챗봇이 대화 초반에 했던 이야기를 까먹는 것이었습니다.
"아까 제가 주문 번호 말씀드렸잖아요?"라고 고객이 물어보면 챗봇은 "죄송합니다. 주문 번호를 알려주시겠어요?"라고 되묻는 황당한 상황이 벌어졌습니다.
김개발 씨는 박시니어 씨에게 달려갔습니다. "선배님, 이게 대체 왜 이러는 걸까요?" 박시니어 씨가 모니터를 보더니 금방 원인을 파악했습니다.
"아, 컨텍스트 윈도우를 초과했네요." 컨텍스트 윈도우란 무엇일까요? 쉽게 비유하자면, 컨텍스트 윈도우는 책상 위의 작업 공간과 같습니다.
우리가 책상에서 작업할 때 책상이 작으면 많은 서류를 동시에 펼쳐놓을 수 없습니다. 오래된 서류는 치워야 새로운 서류를 올려놓을 수 있죠.
LLM도 마찬가지입니다. 한 번에 처리할 수 있는 텍스트의 양이 정해져 있습니다.
토큰이라는 개념도 이해해야 합니다. 토큰은 LLM이 텍스트를 처리하는 기본 단위입니다.
영어에서는 보통 한 단어가 12개의 토큰이고, 한국어는 한 글자가 12개의 토큰입니다. "안녕하세요"라는 단어는 약 3~4개의 토큰으로 계산됩니다.
컨텍스트 윈도우가 없던 시절에는 어땠을까요? 초기 언어 모델들은 매우 짧은 텍스트만 처리할 수 있었습니다.
몇 문장만 입력해도 한계에 도달했죠. 긴 문서를 요약하거나 대화를 이어가는 것은 불가능했습니다.
개발자들은 텍스트를 수동으로 쪼개고, 여러 번 API를 호출해야 했습니다. 현대의 LLM은 훨씬 큰 컨텍스트 윈도우를 제공합니다.
GPT-3.5는 4,096 토큰, GPT-4는 8,192~32,768 토큰, Claude는 무려 100,000 토큰까지 처리할 수 있습니다. 하지만 여전히 제한은 존재합니다.
위의 코드를 살펴보겠습니다. 먼저 tiktoken 라이브러리를 사용합니다.
이것은 OpenAI에서 제공하는 공식 토크나이저입니다. encoding_for_model 함수로 특정 모델의 토크나이저를 로드합니다.
count_tokens 함수는 텍스트를 토큰으로 변환하고 개수를 세어줍니다. 이렇게 계산한 토큰 수가 모델의 제한을 초과하면 오류가 발생하거나 오래된 내용이 잘립니다.
실제 현업에서는 어떻게 활용할까요? 예를 들어 고객 상담 챗봇을 개발한다고 가정해봅시다.
고객과의 대화 내역을 모두 컨텍스트에 넣으면 빠르게 토큰 제한에 도달합니다. 따라서 대화를 보내기 전에 항상 토큰 수를 체크해야 합니다.
제한을 초과하면 오래된 대화를 제거하거나 요약해서 넣어야 합니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 문자 수와 토큰 수를 혼동하는 것입니다. "1,000자면 1,000 토큰 아닌가요?"라고 생각하기 쉽습니다.
하지만 실제로는 언어와 내용에 따라 크게 달라집니다. 영어는 비교적 효율적이지만, 한국어나 중국어는 토큰 소비가 많습니다.
따라서 항상 정확한 토크나이저를 사용해 계산해야 합니다. 또 다른 실수는 시스템 프롬프트의 토큰을 간과하는 것입니다.
사용자 메시지뿐만 아니라 시스템 프롬프트, 이전 대화 내역 모두 컨텍스트 윈도우를 소비합니다. 긴 시스템 프롬프트를 사용하면 실제로 사용자 메시지에 할당할 수 있는 공간이 줄어듭니다.
박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다. "아, 그래서 대화가 길어지면 앞부분을 까먹는 거였군요!" 이제 김개발 씨는 매 대화마다 토큰 수를 체크하고, 제한에 가까워지면 오래된 대화를 정리하는 로직을 추가했습니다.
컨텍스트 윈도우의 제한을 이해하면 더 안정적인 LLM 애플리케이션을 만들 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - tiktoken 라이브러리로 정확한 토큰 수를 계산하세요
- 시스템 프롬프트 토큰도 제한에 포함됩니다
- 모델마다 토큰 계산 방식이 다르니 해당 모델의 토크나이저를 사용하세요
2. 중요 정보 우선순위
김개발 씨가 토큰 제한을 이해하고 나니 새로운 고민이 생겼습니다. "그럼 대화가 길어지면 어떤 내용을 남기고 어떤 내용을 버려야 할까요?" 박시니어 씨가 웃으며 답했습니다.
"그게 바로 우선순위 전략이죠."
정보 우선순위 전략은 제한된 컨텍스트 윈도우에서 가장 중요한 정보를 선별하여 유지하는 기법입니다. 마치 여행 가방에 짐을 쌀 때 꼭 필요한 물건부터 넣는 것과 같습니다.
최신 메시지, 시스템 프롬프트, 핵심 문맥은 항상 유지하고, 덜 중요한 정보는 요약하거나 제거합니다.
다음 코드를 살펴봅시다.
def prioritize_messages(messages, max_tokens=4000):
"""메시지 우선순위를 정하여 토큰 제한 내로 유지합니다"""
# 항상 유지: 시스템 프롬프트와 최근 3개 메시지
system_msg = messages[0] # 시스템 프롬프트
recent_msgs = messages[-3:] # 최근 3개
middle_msgs = messages[1:-3] # 중간 메시지들
# 토큰 계산
essential_tokens = sum(count_tokens(m['content']) for m in [system_msg] + recent_msgs)
remaining_tokens = max_tokens - essential_tokens
# 중간 메시지는 중요도 순으로 선택
selected = []
for msg in reversed(middle_msgs):
msg_tokens = count_tokens(msg['content'])
if msg_tokens <= remaining_tokens:
selected.insert(0, msg)
remaining_tokens -= msg_tokens
return [system_msg] + selected + recent_msgs
김개발 씨의 챗봇은 이제 토큰 수를 체크합니다. 하지만 새로운 문제가 발생했습니다.
토큰 제한에 도달하면 단순히 오래된 메시지를 삭제했는데, 중요한 정보까지 함께 사라지는 것이었습니다. 예를 들어봅시다.
고객이 대화 초반에 "제 이름은 홍길동이고, 주문번호는 A1234입니다"라고 말했습니다. 그런데 대화가 길어지면서 이 정보가 삭제되었습니다.
나중에 챗봇이 "고객님의 성함을 알려주시겠어요?"라고 다시 물어보는 황당한 상황이 벌어졌습니다. 김개발 씨는 또다시 박시니어 씨를 찾아갔습니다.
"선배님, 그냥 오래된 메시지를 지우면 안 되는 것 같아요." 박시니어 씨가 고개를 끄덕였습니다. "맞아요.
무작정 지우면 안 되고, 우선순위를 정해야 해요." 정보 우선순위 전략이란 무엇일까요? 여행을 간다고 상상해봅시다.
가방의 크기는 제한되어 있습니다. 모든 짐을 다 넣을 수는 없죠.
그럴 때 우리는 어떻게 할까요? 당연히 필수품부터 챙깁니다.
여권, 지갑, 핸드폰은 절대 빼먹으면 안 됩니다. 옷가지는 꼭 필요한 것만 넣고, 덜 중요한 물건은 포기합니다.
LLM의 컨텍스트 관리도 마찬가지입니다. 모든 대화 내역을 유지할 수 없다면, 중요한 정보부터 지켜야 합니다.
그렇다면 무엇이 중요한 정보일까요? 첫째, 시스템 프롬프트는 절대 삭제하면 안 됩니다.
시스템 프롬프트는 AI의 역할과 행동 규칙을 정의합니다. "당신은 친절한 고객 상담원입니다"와 같은 지시사항이 담겨 있죠.
이것이 사라지면 AI의 정체성이 흔들립니다. 둘째, 최신 메시지는 대화의 흐름을 유지하는 데 필수적입니다.
사용자가 방금 한 질문에 답하려면 최근 대화를 기억해야 합니다. 보통 최근 3~5개 메시지는 항상 유지합니다.
셋째, 핵심 문맥 정보도 중요합니다. 사용자 이름, 주문 번호, 이전에 합의한 사항 등은 대화 내내 필요합니다.
이런 정보는 대화 초반에 나왔더라도 보존해야 합니다. 위의 코드를 단계별로 살펴보겠습니다.
먼저 메시지를 세 그룹으로 나눕니다. 시스템 프롬프트, 중간 메시지들, 최근 메시지들입니다.
시스템 프롬프트와 최근 3개 메시지는 무조건 유지합니다. 이것들의 토큰을 계산하고, 남은 토큰 예산을 구합니다.
그다음 중간 메시지들을 역순으로 순회합니다. 최신 메시지에 가까운 것부터 선택하는 전략입니다.
각 메시지의 토큰 수를 계산하고, 남은 예산 안에 들어오면 선택합니다. 예산을 초과하면 그 메시지는 버립니다.
실제 프로덕션 환경에서는 더 정교한 전략을 사용합니다. 예를 들어 금융 상담 챗봇에서는 고객의 계좌 번호나 거래 내역 같은 정보에 높은 우선순위를 부여합니다.
단순한 인사말이나 확인 메시지는 낮은 우선순위를 줍니다. "네, 알겠습니다"나 "감사합니다" 같은 메시지는 삭제해도 문맥에 큰 영향이 없습니다.
어떤 기업들은 메시지 태그 시스템을 도입합니다. 각 메시지에 "critical", "important", "normal", "low" 같은 태그를 붙입니다.
토큰 제한에 도달하면 "low"부터 삭제합니다. 이렇게 하면 자동으로 중요한 정보만 남습니다.
주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수는 단순히 시간 순서로만 판단하는 것입니다.
"오래된 메시지는 중요하지 않다"고 가정하죠. 하지만 실제로는 대화 초반의 정보가 매우 중요할 수 있습니다.
사용자가 처음에 말한 문제 상황, 개인 정보, 선호사항 등은 대화 내내 필요합니다. 또 다른 함정은 과도한 압축입니다.
토큰을 아끼려고 모든 메시지를 짧게 요약하면 중요한 뉘앙스가 사라질 수 있습니다. "고객이 화가 났다"는 문맥은 요약 과정에서 쉽게 손실됩니다.
하지만 상담원 AI에게는 고객의 감정 상태가 매우 중요한 정보입니다. 김개발 씨는 새로운 우선순위 시스템을 구축했습니다.
고객 정보가 담긴 메시지에는 "critical" 태그를, 일반 질의응답에는 "normal" 태그를 붙였습니다. 이제 대화가 아무리 길어져도 중요한 정보는 절대 잊어버리지 않습니다.
"선배님, 이제 훨씬 자연스러워졌어요!" 김개발 씨가 밝게 웃었습니다. 박시니어 씨도 흐뭇한 표정으로 고개를 끄덕였습니다.
정보 우선순위를 제대로 설정하면 제한된 컨텍스트 윈도우를 효율적으로 활용할 수 있습니다. 여러분의 프로젝트에서도 어떤 정보가 가장 중요한지 고민해보세요.
실전 팁
💡 - 시스템 프롬프트와 최신 메시지는 항상 보존하세요
- 메시지에 중요도 태그를 붙여 우선순위를 관리하세요
- 단순히 시간순이 아니라 내용의 중요도를 고려하세요
3. 컨텍스트 압축 기법
우선순위 전략으로 많은 문제가 해결되었지만, 김개발 씨는 여전히 아쉬웠습니다. "정보를 아예 버리지 말고 압축할 수는 없을까요?" 박시니어 씨가 미소를 지으며 말했습니다.
"좋은 생각이에요. 요약 기법을 사용하면 됩니다."
컨텍스트 압축은 긴 텍스트를 짧은 요약문으로 변환하여 토큰을 절약하는 기법입니다. 마치 긴 소설을 줄거리로 압축하는 것과 같습니다.
LLM 자체를 활용해 대화 내역을 요약하거나, 핵심 키워드만 추출하거나, 구조화된 데이터로 변환하여 토큰 소비를 크게 줄일 수 있습니다.
다음 코드를 살펴봅시다.
async def summarize_old_messages(messages, llm_client):
"""오래된 메시지를 요약하여 토큰을 절약합니다"""
# 중간 메시지들을 하나의 텍스트로 합침
old_msgs = messages[1:-3] # 시스템과 최근 3개 제외
combined_text = "\n".join([f"{m['role']}: {m['content']}" for m in old_msgs])
# LLM으로 요약 생성
summary_prompt = f"""다음 대화를 3-4문장으로 요약하세요. 중요한 정보(이름, 번호, 날짜 등)는 반드시 포함하세요:
{combined_text}"""
summary = await llm_client.chat(summary_prompt)
# 요약본을 하나의 메시지로 반환
return {
'role': 'system',
'content': f'[이전 대화 요약]: {summary}'
}
김개발 씨의 챗봇은 이제 제법 똑똑해졌습니다. 중요한 정보는 우선적으로 유지하고, 덜 중요한 정보는 삭제합니다.
하지만 김개발 씨는 여전히 고민이 있었습니다. "정보를 완전히 버리면 나중에 필요할 때 문제가 생길 수 있어요." 실제로 고객이 "아까 말씀드린 그 문제 말인데요"라고 할 때, 해당 정보가 삭제되어 있으면 대화가 끊깁니다.
박시니어 씨가 새로운 해결책을 제시했습니다. "그럼 버리지 말고 압축하면 되죠.
소설책을 줄거리로 만드는 것처럼요." 컨텍스트 압축이란 무엇일까요? 우리가 긴 소설을 읽고 나서 친구에게 이야기할 때를 생각해봅시다.
500페이지짜리 소설을 그대로 읽어주지 않습니다. "주인공이 이런저런 일을 겪고, 결국 이렇게 됐어"라고 핵심만 간추려 말합니다.
세부 묘사는 생략하지만, 중요한 줄거리는 모두 전달됩니다. LLM의 컨텍스트 관리도 같은 원리입니다.
긴 대화 내역을 그대로 유지하는 대신, 핵심 내용만 추출한 요약본을 만듭니다. 100개의 토큰을 소비하던 메시지가 20개의 토큰으로 압축되면, 5배의 효율을 얻습니다.
압축 기법에는 여러 종류가 있습니다. 첫 번째는 추상 요약입니다.
LLM에게 대화 내역을 주고 "3-4문장으로 요약하라"고 지시합니다. LLM은 자연어 이해 능력이 뛰어나므로 핵심 정보를 잘 추출합니다.
위의 코드가 바로 이 방식입니다. 두 번째는 추출 요약입니다.
원문에서 중요한 문장만 골라냅니다. "홍길동", "주문번호 A1234", "환불 요청" 같은 핵심 키워드가 포함된 문장만 유지하고 나머지는 삭제합니다.
추상 요약보다 정확도가 높지만 덜 자연스럽습니다. 세 번째는 구조화된 데이터 변환입니다.
대화를 JSON 같은 구조화된 형식으로 바꿉니다. "고객: 홍길동, 주문번호: A1234, 문제: 배송 지연" 이런 식으로 정리하면 토큰을 크게 절약할 수 있습니다.
위의 코드를 자세히 살펴보겠습니다. 먼저 오래된 메시지들을 선택합니다.
시스템 프롬프트와 최근 3개는 제외하고, 중간 메시지들만 모읍니다. 이것들을 하나의 긴 텍스트로 합칩니다.
각 메시지 앞에 "user:" 또는 "assistant:" 같은 역할 표시를 붙여 구분합니다. 그다음 LLM에게 요약을 요청합니다.
중요한 점은 프롬프트에서 "중요한 정보는 반드시 포함하라"고 명시하는 것입니다. 그렇지 않으면 LLM이 고객 이름이나 주문 번호 같은 중요 정보를 생략할 수 있습니다.
생성된 요약문은 새로운 시스템 메시지로 만듭니다. "[이전 대화 요약]"이라는 태그를 붙여 이것이 실제 대화가 아니라 요약본임을 표시합니다.
실무에서는 더 발전된 기법을 사용합니다. 예를 들어 롤링 요약 기법이 있습니다.
대화가 진행될 때마다 일정 간격으로 요약을 갱신합니다. 처음 10개 메시지를 요약하고, 다음 10개가 추가되면 이전 요약에 새로운 내용을 추가하는 방식입니다.
마치 스노우볼처럼 요약이 굴러가면서 커집니다. 또 다른 기법은 계층적 요약입니다.
메시지를 작은 그룹으로 나누고 각각 요약합니다. 그다음 요약본들을 다시 요약합니다.
이렇게 하면 세부 정보와 전체 맥락을 동시에 유지할 수 있습니다. 주의해야 할 점도 있습니다.
첫째, 요약 비용을 고려해야 합니다. 요약을 생성하려면 LLM API를 한 번 더 호출해야 합니다.
이것도 비용이 듭니다. 메시지 10개를 삭제하는 것보다 요약을 생성하는 것이 더 비쌀 수 있습니다.
따라서 어느 정도 분량이 쌓였을 때만 요약하는 것이 효율적입니다. 둘째, 정보 손실을 완전히 피할 수는 없습니다.
아무리 좋은 요약이라도 원문의 모든 뉘앙스를 담지는 못합니다. "고객이 약간 짜증난 듯한 말투로 말했다" 같은 미묘한 정보는 사라질 수 있습니다.
감정 분석이 중요한 애플리케이션에서는 이것이 문제가 될 수 있습니다. 셋째, 재귀적 요약의 함정이 있습니다.
요약을 또 요약하고, 또 요약하다 보면 정보가 심하게 왜곡될 수 있습니다. 마치 복사의 복사를 반복하면 화질이 나빠지는 것과 같습니다.
보통 2~3단계 이상은 요약하지 않는 것이 좋습니다. 김개발 씨는 요약 기능을 추가했습니다.
이제 대화가 20개 메시지를 넘어가면 자동으로 오래된 부분을 요약합니다. 고객과 아무리 긴 대화를 나눠도 컨텍스트가 터지지 않습니다.
"선배님, 신기해요! 요약본만 봐도 이전 대화 흐름을 파악할 수 있네요." 김개발 씨가 감탄했습니다.
박시니어 씨가 웃으며 답했습니다. "LLM의 장점을 LLM으로 해결하는 거죠." 컨텍스트 압축을 제대로 활용하면 거의 무제한에 가까운 대화를 유지할 수 있습니다.
여러분도 프로젝트에 맞는 압축 전략을 실험해보세요.
실전 팁
💡 - 요약 생성 시 중요 정보(이름, 번호, 날짜 등) 보존을 명시하세요
- 롤링 요약으로 대화가 길어져도 맥락을 유지하세요
- 요약 비용과 정보 손실을 고려해 적절한 타이밍에 압축하세요
4. Sliding Window 전략
요약 기법으로 많은 것이 해결되었지만, 김개발 씨는 실시간 대화에서 미묘한 문제를 발견했습니다. "요약을 생성하는 동안 사용자가 기다려야 해요." 박시니어 씨가 또 다른 해결책을 제시했습니다.
"그럴 땐 슬라이딩 윈도우가 더 나을 수 있어요."
Sliding Window 전략은 고정된 크기의 윈도우를 유지하면서 새로운 메시지가 들어올 때마다 가장 오래된 메시지를 제거하는 기법입니다. 마치 기차 창문으로 풍경을 보는 것처럼 항상 최신 N개의 메시지만 유지합니다.
요약 없이도 실시간으로 빠르게 동작하며, 구현이 간단합니다.
다음 코드를 살펴봅시다.
class SlidingWindowContext:
"""고정 크기 슬라이딩 윈도우로 컨텍스트를 관리합니다"""
def __init__(self, max_tokens=4000, keep_recent=5):
self.max_tokens = max_tokens
self.keep_recent = keep_recent # 항상 유지할 최신 메시지 수
self.messages = []
def add_message(self, message):
"""새 메시지를 추가하고 토큰 제한을 유지합니다"""
self.messages.append(message)
# 토큰 수 계산
while self._total_tokens() > self.max_tokens:
# 시스템 메시지와 최근 N개를 제외하고 가장 오래된 것 제거
if len(self.messages) > self.keep_recent + 1:
self.messages.pop(1) # 인덱스 1 = 시스템 다음
else:
break
def _total_tokens(self):
return sum(count_tokens(m['content']) for m in self.messages)
김개발 씨의 요약 기반 시스템은 잘 작동했습니다. 하지만 실제 사용자들로부터 피드백이 들어오기 시작했습니다.
"가끔 답변이 늦게 오는데, 요약을 만드느라 그런 건가요?" 실제로 확인해보니 요약 생성에 2~3초가 걸렸습니다. 사용자가 메시지를 보내면 먼저 요약을 만들고, 그다음 실제 응답을 생성합니다.
총 5초 정도 걸리는 셈입니다. 사용자 입장에서는 답답할 수 있습니다.
김개발 씨가 고민하고 있을 때, 박시니어 씨가 다가왔습니다. "요약이 항상 필요한 건 아니에요.
슬라이딩 윈도우를 써보는 건 어때요?" Sliding Window란 무엇일까요? 기차를 타고 여행한다고 상상해봅시다.
창밖으로 풍경이 계속 지나갑니다. 기차가 앞으로 가면 새로운 풍경이 보이고, 뒤에 있던 풍경은 시야에서 사라집니다.
창문의 크기는 고정되어 있지만, 보이는 내용은 계속 바뀝니다. Sliding Window 전략도 같은 원리입니다.
항상 최근 N개의 메시지만 유지합니다. 새로운 메시지가 들어오면 가장 오래된 메시지를 밀어냅니다.
윈도우의 크기는 일정하지만 내용은 계속 업데이트됩니다. 이 방식의 장점은 명확합니다.
첫째, 속도가 매우 빠릅니다. 요약을 생성할 필요가 없으므로 추가 API 호출이 없습니다.
단순히 배열에서 요소를 추가하고 제거하는 연산만 하면 됩니다. 밀리초 단위로 처리됩니다.
둘째, 구현이 간단합니다. 복잡한 요약 로직이나 우선순위 계산이 필요 없습니다.
큐 자료구조처럼 FIFO(First In First Out) 방식으로 동작합니다. 셋째, 예측 가능합니다.
항상 정확히 N개의 메시지를 유지하므로 토큰 사용량을 정확히 통제할 수 있습니다. 하지만 단점도 있습니다.
가장 큰 문제는 정보 손실입니다. 요약과 달리 오래된 메시지는 완전히 사라집니다.
대화 초반의 중요한 정보가 윈도우 밖으로 밀려나면 AI는 그것을 기억하지 못합니다. 위의 코드를 살펴보겠습니다.
SlidingWindowContext 클래스는 메시지 리스트와 토큰 제한을 관리합니다. add_message 메서드는 새 메시지를 추가한 후 전체 토큰 수를 체크합니다.
제한을 초과하면 while 루프를 돌면서 오래된 메시지를 하나씩 제거합니다. 중요한 점은 시스템 메시지(인덱스 0)와 최근 N개 메시지는 보호한다는 것입니다.
pop(1)은 시스템 메시지 바로 다음, 즉 가장 오래된 사용자/어시스턴트 메시지를 제거합니다. 이렇게 하면 시스템 프롬프트는 항상 유지되고, 최신 대화 흐름도 끊기지 않습니다.
실무에서는 어떻게 활용할까요? 실시간 챗봇에 적합합니다.
고객 상담처럼 빠른 응답이 중요하고, 대화가 비교적 짧은 경우에 좋습니다. 대부분의 고객 문의는 5~10번의 왕복으로 해결되므로 슬라이딩 윈도우만으로도 충분합니다.
게임 NPC 대화에도 유용합니다. 플레이어와 NPC의 대화는 보통 짧고, 이전 대화를 기억할 필요가 적습니다.
빠른 응답이 게임 경험에 중요하므로 슬라이딩 윈도우가 적합합니다. 반면 장기 컨설팅 챗봇에는 적합하지 않습니다.
여러 세션에 걸쳐 복잡한 문제를 논의하는 경우 오래된 정보도 계속 필요합니다. 이런 경우는 요약 기법이나 외부 메모리를 함께 사용해야 합니다.
하이브리드 전략도 많이 사용됩니다. 평소에는 슬라이딩 윈도우로 빠르게 동작하고, 윈도우 밖으로 밀려나는 메시지는 백그라운드에서 요약합니다.
요약본은 별도로 저장해두고, 필요할 때 불러옵니다. 이렇게 하면 속도와 정보 보존을 동시에 얻을 수 있습니다.
또 다른 변형은 적응형 윈도우입니다. 대화가 복잡해지면 윈도우 크기를 늘리고, 단순한 대화는 작은 윈도우를 사용합니다.
토큰 사용량을 동적으로 조절하는 것이죠. 주의할 점이 있습니다.
초보 개발자들은 윈도우 크기를 너무 작게 설정하는 실수를 합니다. "토큰을 아껴야지" 하는 마음에 최근 3개만 유지하도록 하면 대화 맥락이 심하게 끊깁니다.
사용자가 방금 한 질문의 배경을 AI가 이해하지 못하는 상황이 생깁니다. 반대로 너무 크게 설정하면 슬라이딩 윈도우의 의미가 없어집니다.
윈도우가 모델의 전체 컨텍스트 크기와 비슷하면 결국 제한에 도달합니다. 적절한 크기는 보통 전체 컨텍스트의 50~70% 정도입니다.
김개발 씨는 고민 끝에 하이브리드 전략을 선택했습니다. 기본적으로 슬라이딩 윈도우로 빠르게 동작하고, 윈도우 밖으로 나가는 메시지는 비동기로 요약합니다.
사용자는 빠른 응답을 받고, 시스템은 장기 기억도 유지합니다. "선배님, 이제 응답 속도도 빠르고 기억력도 좋아요!" 김개발 씨가 뿌듯해했습니다.
박시니어 씨가 어깨를 두드려주었습니다. "잘했어요.
상황에 맞는 전략을 선택하는 게 중요해요." Sliding Window는 단순하지만 강력한 도구입니다. 여러분의 애플리케이션 특성에 맞게 윈도우 크기와 전략을 조정해보세요.
실전 팁
💡 - 윈도우 크기는 전체 컨텍스트의 50~70%가 적당합니다
- 실시간 응답이 중요한 경우 슬라이딩 윈도우가 효과적입니다
- 하이브리드 전략으로 속도와 기억력을 모두 챙기세요
5. 실습 긴 문서 요약 시스템
이론을 충분히 배운 김개발 씨는 실전 프로젝트에 도전하기로 했습니다. "긴 PDF 문서를 업로드하면 요약해주는 시스템을 만들어볼까요?" 박시니어 씨가 고개를 끄덕였습니다.
"좋아요. 지금까지 배운 것을 모두 활용할 수 있을 거예요."
긴 문서 요약 시스템은 컨텍스트 윈도우를 초과하는 대용량 문서를 청크로 나누고, 각 청크를 요약한 후, 최종 요약본을 생성하는 실전 프로젝트입니다. Map-Reduce 패턴을 활용하여 수백 페이지 문서도 효율적으로 처리할 수 있으며, 실무에서 즉시 활용 가능한 패턴입니다.
다음 코드를 살펴봅시다.
async def summarize_long_document(document_text, chunk_size=3000):
"""긴 문서를 청크로 나누고 계층적으로 요약합니다"""
# 1단계: 문서를 청크로 분할
chunks = []
words = document_text.split()
current_chunk = []
current_tokens = 0
for word in words:
word_tokens = count_tokens(word)
if current_tokens + word_tokens > chunk_size:
chunks.append(' '.join(current_chunk))
current_chunk = [word]
current_tokens = word_tokens
else:
current_chunk.append(word)
current_tokens += word_tokens
if current_chunk:
chunks.append(' '.join(current_chunk))
# 2단계: 각 청크를 병렬로 요약
chunk_summaries = []
for i, chunk in enumerate(chunks):
summary = await llm_client.chat(
f"다음 텍스트를 3-4문장으로 요약하세요 (Part {i+1}/{len(chunks)}):\n\n{chunk}"
)
chunk_summaries.append(summary)
# 3단계: 요약들을 다시 요약 (최종 요약)
combined_summaries = '\n\n'.join(chunk_summaries)
final_summary = await llm_client.chat(
f"다음 요약들을 종합하여 전체 문서의 핵심을 5-6문장으로 정리하세요:\n\n{combined_summaries}"
)
return {
'chunk_count': len(chunks),
'chunk_summaries': chunk_summaries,
'final_summary': final_summary
}
김개발 씨는 회사에서 새로운 프로젝트를 맡았습니다. 법무팀에서 요청이 들어왔습니다.
"계약서나 법률 문서가 너무 길어서 읽기 힘들어요. 자동으로 요약해주는 도구를 만들어주실 수 있나요?" 김개발 씨는 자신 있게 대답했습니다.
"물론이죠!" 하지만 실제로 문서를 받아보니 당황했습니다. 100페이지가 넘는 PDF였습니다.
토큰으로 계산하니 약 50,000 토큰이었습니다. GPT-4의 컨텍스트 윈도우를 가볍게 초과합니다.
박시니어 씨를 찾아간 김개발 씨에게 박시니어 씨가 말했습니다. "이럴 때 쓰는 게 Map-Reduce 패턴이에요." Map-Reduce 패턴이란 무엇일까요?
대용량 데이터 처리에서 나온 개념입니다. 큰 문제를 작은 조각으로 나누고(Map), 각 조각을 처리한 후, 결과를 모아서 합칩니다(Reduce).
마치 큰 피자를 혼자 먹을 수 없으니 여러 조각으로 나눠 친구들과 함께 먹고, 나중에 "맛있었다"는 평가를 모으는 것과 같습니다. 문서 요약에 적용하면 이렇게 됩니다.
첫째, 긴 문서를 적당한 크기의 청크로 나눕니다. 각 청크는 컨텍스트 윈도우에 들어갈 수 있을 만큼 작아야 합니다.
보통 2,000~3,000 토큰 정도가 적당합니다. 둘째, 각 청크를 독립적으로 요약합니다.
이 단계가 Map입니다. 청크들은 서로 독립적이므로 병렬로 처리할 수 있습니다.
10개의 청크가 있다면 동시에 10개의 API 호출을 보낼 수 있습니다. 셋째, 청크 요약들을 합쳐서 최종 요약을 만듭니다.
이 단계가 Reduce입니다. 10개의 짧은 요약본을 하나의 프롬프트로 합쳐 LLM에게 "이것들을 종합해서 전체 문서를 요약하라"고 지시합니다.
위의 코드를 단계별로 분석해봅시다. 1단계: 청크 분할에서는 문서를 단어 단위로 순회합니다.
현재 청크의 토큰 수를 계속 추적하다가 chunk_size를 초과하면 새로운 청크를 시작합니다. 이렇게 하면 각 청크가 비슷한 크기를 유지합니다.
중요한 점은 문장 중간에서 잘리지 않도록 하는 것입니다. 실제 프로덕션에서는 문장 경계나 문단 경계를 고려해 더 정교하게 분할합니다.
2단계: 병렬 요약에서는 각 청크에 대해 요약을 요청합니다. 프롬프트에 "Part {i+1}/{len(chunks)}"를 넣어서 LLM이 이것이 전체의 일부임을 인식하게 합니다.
이렇게 하면 LLM이 문맥을 더 잘 이해합니다. async/await를 사용하므로 여러 청크를 동시에 처리할 수 있습니다.
실제로는 asyncio.gather()를 사용해 진짜 병렬 처리를 구현하면 훨씬 빠릅니다. 3단계: 최종 요약에서는 모든 청크 요약을 합쳐 하나의 프롬프트를 만듭니다.
"종합하여 정리하라"는 지시가 중요합니다. 단순히 이어붙이는 것이 아니라 중복을 제거하고 핵심만 추출하도록 유도합니다.
실무에서는 더 복잡한 변형을 사용합니다. 계층적 요약이 대표적입니다.
100개의 청크가 있다면 한 번에 모두 요약하지 않습니다. 먼저 10개씩 묶어 중간 요약을 만들고(10개의 중간 요약 생성), 그 중간 요약들을 다시 최종 요약으로 만듭니다.
피라미드처럼 단계를 쌓아올리는 방식입니다. 오버랩 청크도 효과적입니다.
청크를 완전히 독립적으로 나누지 않고, 앞뒤로 약간 겹치게 만듭니다. 예를 들어 청크 A는 11000 토큰, 청크 B는 9001900 토큰(100 토큰 겹침).
이렇게 하면 청크 경계에서 문맥이 끊기는 문제를 완화할 수 있습니다. 메타데이터 활용도 중요합니다.
각 청크에 페이지 번호, 섹션 제목, 문단 위치 같은 정보를 붙입니다. 요약할 때 이 메타데이터도 함께 전달하면 LLM이 문서 구조를 더 잘 이해합니다.
실제 활용 사례를 봅시다. 어느 로펌에서는 수백 페이지짜리 판례를 자동 요약하는 시스템을 구축했습니다.
변호사들이 판례를 읽는 시간을 80% 절감했다고 합니다. 요약본을 먼저 읽고, 중요한 부분만 원문을 찾아보는 방식으로 작업합니다.
한 의료 스타트업은 임상시험 보고서를 요약합니다. 500페이지가 넘는 보고서를 5분 만에 3페이지로 압축합니다.
의사들이 최신 연구 결과를 빠르게 파악하는 데 활용합니다. 주의할 점도 있습니다.
첫째, 청크 크기 선택이 중요합니다. 너무 작으면 청크 개수가 많아져 비용이 증가합니다.
너무 크면 각 청크의 요약이 부실해집니다. 보통 2,000~4,000 토큰이 적당합니다.
둘째, 순서 정보 손실을 주의해야 합니다. 병렬 처리하면 빠르지만, 문서의 흐름이 사라질 수 있습니다.
"먼저 A를 설명하고, 그다음 B를 논증한다"는 구조가 요약에서 드러나지 않을 수 있습니다. 순차적 처리나 메타데이터를 활용해 보완합니다.
셋째, 비용 관리가 필요합니다. 100개 청크를 요약하려면 101번의 API 호출이 필요합니다(청크 100번 + 최종 요약 1번).
큰 문서를 자주 처리하면 비용이 급증할 수 있습니다. 캐싱이나 배치 처리를 고려하세요.
김개발 씨는 시스템을 완성했습니다. 법무팀이 테스트해보더니 크게 만족했습니다.
"100페이지 계약서를 5분 만에 요약하다니, 정말 신기해요!" "선배님 덕분에 멋진 시스템을 만들었어요." 김개발 씨가 감사 인사를 전했습니다. 박시니어 씨가 웃으며 답했습니다.
"이제 컨텍스트 관리의 진수를 알게 된 거예요." 긴 문서 요약 시스템은 많은 곳에서 유용합니다. 여러분도 회사의 문서 처리 업무에 적용해보세요.
실전 팁
💡 - Map-Reduce 패턴으로 대용량 문서를 효율적으로 처리하세요
- 청크 크기는 2,000~4,000 토큰이 적당합니다
- 계층적 요약으로 매우 긴 문서도 처리할 수 있습니다
6. 실습 컨텍스트 관리 유틸리티
여러 프로젝트에서 컨텍스트 관리 로직을 반복해서 작성하던 김개발 씨는 문득 생각했습니다. "이것을 재사용 가능한 라이브러리로 만들면 어떨까?" 박시니어 씨가 적극 찬성했습니다.
"좋은 생각이에요. 실무에서 바로 쓸 수 있는 유틸리티를 만들어봅시다."
컨텍스트 관리 유틸리티는 토큰 계산, 메시지 우선순위, 슬라이딩 윈도우, 자동 요약 등을 통합한 재사용 가능한 클래스입니다. 프로젝트마다 반복되는 컨텍스트 관리 로직을 하나의 깔끔한 인터페이스로 제공하여 개발 생산성을 크게 높입니다.
다음 코드를 살펴봅시다.
class ContextManager:
"""LLM 컨텍스트를 자동으로 관리하는 유틸리티 클래스"""
def __init__(self, max_tokens=4000, strategy='sliding', auto_summarize=True):
self.max_tokens = max_tokens
self.strategy = strategy # 'sliding', 'priority', 'summarize'
self.auto_summarize = auto_summarize
self.messages = []
self.summaries = []
async def add_message(self, role, content, priority='normal'):
"""메시지를 추가하고 컨텍스트를 자동 관리합니다"""
message = {
'role': role,
'content': content,
'priority': priority,
'timestamp': time.time()
}
self.messages.append(message)
# 전략에 따라 컨텍스트 정리
if self.strategy == 'sliding':
await self._apply_sliding_window()
elif self.strategy == 'priority':
await self._apply_priority()
elif self.strategy == 'summarize':
await self._apply_summarization()
return self.get_context()
async def _apply_sliding_window(self):
"""슬라이딩 윈도우 전략 적용"""
while self._total_tokens() > self.max_tokens and len(self.messages) > 5:
# 중요도가 'critical'이 아닌 가장 오래된 메시지 제거
for i in range(1, len(self.messages) - 3):
if self.messages[i]['priority'] != 'critical':
removed = self.messages.pop(i)
if self.auto_summarize:
self.summaries.append(f"[{removed['timestamp']}] {removed['content'][:50]}...")
break
def get_context(self):
"""현재 컨텍스트를 반환합니다"""
context = []
if self.summaries:
context.append({
'role': 'system',
'content': f"이전 대화 요약:\n" + "\n".join(self.summaries[-3:])
})
context.extend(self.messages)
return context
def _total_tokens(self):
return sum(count_tokens(m['content']) for m in self.messages)
김개발 씨는 지난 몇 달간 여러 프로젝트를 진행하면서 깨달았습니다. 컨텍스트 관리 코드가 프로젝트마다 반복된다는 것을요.
고객 상담 챗봇, 문서 요약 시스템, 코드 리뷰 도우미... 모두 비슷한 로직을 사용합니다.
"이걸 매번 새로 작성하는 건 비효율적이에요." 김개발 씨가 박시니어 씨에게 고민을 털어놓았습니다. 박시니어 씨가 고개를 끄덕였습니다.
"그럼 한 번만 제대로 만들어서 계속 재사용하면 되죠. 그게 좋은 엔지니어의 접근법이에요." 재사용 가능한 유틸리티를 만드는 것은 엔지니어링의 핵심입니다.
반복되는 패턴을 발견하면 추상화합니다. 공통 기능을 하나의 클래스나 함수로 만들어 여러 곳에서 사용합니다.
이렇게 하면 코드 중복이 줄어들고, 버그 수정이 쉬워지며, 새로운 프로젝트를 빠르게 시작할 수 있습니다. 김개발 씨가 설계한 ContextManager 클래스를 살펴봅시다.
생성자에서는 세 가지 설정을 받습니다. max_tokens는 최대 토큰 수, strategy는 관리 전략('sliding', 'priority', 'summarize'), auto_summarize는 삭제되는 메시지를 자동 요약할지 여부입니다.
이렇게 설정을 받으면 다양한 상황에 대응할 수 있습니다. 실시간 챗봇은 'sliding' 전략을, 법률 상담은 'priority' 전략을, 문서 분석은 'summarize' 전략을 선택할 수 있습니다.
add_message 메서드는 핵심 인터페이스입니다. 사용자는 단순히 "add_message('user', '안녕하세요')"만 호출하면 됩니다.
내부에서 자동으로 토큰을 계산하고, 필요하면 오래된 메시지를 정리하고, 컨텍스트를 최적화합니다. 모든 복잡한 로직이 숨겨져 있습니다.
priority 파라미터로 메시지의 중요도를 지정할 수 있습니다. 'critical'로 표시된 메시지는 절대 삭제되지 않습니다.
고객 정보나 주문 번호 같은 중요 데이터에 사용합니다. timestamp를 기록하는 것도 중요합니다.
나중에 디버깅할 때 "어느 시점의 메시지가 삭제되었는지" 추적할 수 있습니다. 또한 시간 기반 정책을 구현할 수도 있습니다.
"30분 이상 된 메시지는 우선 삭제"같은 규칙이죠. _apply_sliding_window 메서드는 실제 정리를 수행합니다.
while 루프로 토큰 수를 체크하면서 초과하면 메시지를 제거합니다. 중요한 것은 무작정 제거하지 않는다는 점입니다.
'critical' 우선순위를 가진 메시지는 건너뜁니다. 일반 메시지 중 가장 오래된 것만 제거합니다.
auto_summarize가 켜져 있으면 삭제되는 메시지의 요약을 summaries 리스트에 추가합니다. 완전히 버리지 않고 흔적을 남기는 것입니다.
나중에 필요하면 이 요약들을 참고할 수 있습니다. get_context 메서드는 LLM에 전달할 최종 컨텍스트를 반환합니다.
summaries가 있으면 맨 앞에 요약본을 추가합니다. "이전 대화 요약: ..."이라는 시스템 메시지로 만들어 LLM이 인식할 수 있게 합니다.
최근 3개의 요약만 포함해서 너무 길어지지 않게 조절합니다. 실무에서는 이 클래스를 더욱 확장할 수 있습니다.
메트릭 수집 기능을 추가할 수 있습니다. 평균 토큰 사용량, 삭제된 메시지 수, 요약 생성 횟수 같은 통계를 수집합니다.
이 데이터로 최적의 설정을 찾고, 비용을 모니터링합니다. 캐싱 기능도 유용합니다.
같은 메시지 조합에 대한 응답을 캐시에 저장합니다. 사용자가 같은 질문을 반복하면 API 호출 없이 즉시 응답할 수 있습니다.
플러그인 시스템을 만들 수도 있습니다. 사용자 정의 전략을 등록할 수 있게 합니다.
"우리 회사만의 특별한 우선순위 로직"을 플러그인으로 작성해 끼워 넣을 수 있습니다. 영속성 기능도 중요합니다.
메시지를 데이터베이스에 저장하고, 세션이 끊겨도 나중에 복원할 수 있게 합니다. 사용자가 며칠 뒤에 다시 접속해도 이전 대화를 이어갈 수 있습니다.
실제 사용 예시를 봅시다. python # 고객 상담 챗봇 (빠른 응답 중시) manager = ContextManager(max_tokens=4000, strategy='sliding') await manager.add_message('system', '당신은 친절한 상담원입니다') await manager.add_message('user', '주문이 안 왔어요', priority='critical') context = manager.get_context() # 법률 자문 챗봇 (정보 보존 중시) manager = ContextManager(max_tokens=8000, strategy='summarize', auto_summarize=True) await manager.add_message('system', '당신은 법률 전문가입니다') await manager.add_message('user', '계약서를 검토해주세요') 주의할 점이 있습니다.
과도한 추상화는 오히려 복잡도를 높입니다. 모든 가능한 경우를 다 지원하려다 보면 클래스가 비대해지고 사용하기 어려워집니다.
80%의 유스케이스를 잘 처리하는 것이 100%를 애매하게 처리하는 것보다 낫습니다. 성능도 고려해야 합니다.
매번 전체 메시지 리스트를 순회하면 메시지가 많아질수록 느려집니다. 우선순위 큐나 인덱스를 사용해 최적화할 수 있습니다.
테스트도 빼먹으면 안 됩니다. 유틸리티 클래스는 여러 프로젝트에서 사용되므로 버그의 영향이 큽니다.
단위 테스트를 철저히 작성해 신뢰성을 확보하세요. 김개발 씨는 ContextManager를 완성하고 사내 라이브러리로 배포했습니다.
다른 팀에서도 사용하기 시작했고, 좋은 피드백이 들어왔습니다. "덕분에 개발 시간이 절반으로 줄었어요!" "선배님, 제 코드가 회사 전체에서 쓰이다니 뿌듯해요." 김개발 씨가 환하게 웃었습니다.
박시니어 씨가 어깨를 두드려주었습니다. "좋은 코드는 나눌수록 가치가 커지는 법이에요." 재사용 가능한 유틸리티를 만드는 것은 엔지니어로서 중요한 역량입니다.
여러분도 반복되는 패턴을 발견하면 라이브러리로 만들어보세요.
실전 팁
💡 - 80%의 유스케이스를 잘 처리하는 것을 목표로 하세요
- 메트릭 수집으로 최적의 설정을 찾으세요
- 철저한 테스트로 신뢰성을 확보하세요
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
ReAct 패턴 마스터 완벽 가이드
LLM이 생각하고 행동하는 ReAct 패턴을 처음부터 끝까지 배웁니다. Thought-Action-Observation 루프로 똑똑한 에이전트를 만들고, 실전 예제로 웹 검색과 계산을 결합한 강력한 AI 시스템을 구축합니다.
AI 에이전트의 모든 것 - 개념부터 실습까지
AI 에이전트란 무엇일까요? 단순한 LLM 호출과 어떻게 다를까요? 초급 개발자를 위해 에이전트의 핵심 개념부터 실제 구현까지 이북처럼 술술 읽히는 스타일로 설명합니다.
프로덕션 RAG 시스템 완벽 가이드
검색 증강 생성(RAG) 시스템을 실제 서비스로 배포하기 위한 확장성, 비용 최적화, 모니터링 전략을 다룹니다. AWS/GCP 배포 실습과 대시보드 구축까지 프로덕션 환경의 모든 것을 담았습니다.
RAG 캐싱 전략 완벽 가이드
RAG 시스템의 성능을 획기적으로 개선하는 캐싱 전략을 배웁니다. 쿼리 캐싱부터 임베딩 캐싱, Redis 통합까지 실무에서 바로 적용할 수 있는 최적화 기법을 다룹니다.
실시간으로 답변하는 RAG 시스템 만들기
사용자가 질문하면 즉시 답변이 스트리밍되는 RAG 시스템을 구축하는 방법을 배웁니다. 실시간 응답 생성부터 청크별 스트리밍, 사용자 경험 최적화까지 실무에서 바로 적용할 수 있는 완전한 가이드입니다.