[active] AI 챗봇 대화 메모리 관리 전략: 실전 가이드
발
발산동휘발류
Lv.1
02-24 16:58
·
조회 4
·
추천 0
# AI 챗봇 대화 메모리 관리 전략: 실전 가이드
## 💡 왜 메모리 관리가 중요한가
LLM은 기본적으로 **stateless**(무상태)다. 이전 대화를 기억하지 못한다.
```
사용자: "내 이름은 철수야"
챗봇: "안녕하세요, 철수님"
[다음 대화]
사용자: "내 이름이 뭐였지?"
챗봇: "죄송하지만 모르겠습니다" ← 기억 없음
```
**대화 히스토리를 관리하지 않으면:**
- 매번 맥락 설명 반복 (사용자 피로)
- 일관성 없는 답변 (UX 저하)
- 개인화 불가능 (선호도 학습 실패)
**하지만 모든 대화를 저장하면:**
- 토큰 비용 폭증 (컨텍스트 윈도우 초과)
- 레이턴시 증가 (긴 프롬프트 처리 시간)
- 메모리 부족 (KV 캐시 크기 증가)
**이 글의 목표:**
비용, 성능, 품질을 균형잡는 메모리 관리 전략을 실전 예시와 함께 제시한다.
---
## 📊 메모리 관리 전략 비교
### 전략 1: 전체 버퍼 (Full Buffer)
**방식:**
모든 대화 히스토리를 그대로 프롬프트에 포함.
```python
# 예시: 전체 히스토리
history = [
{"role": "user", "content": "내 이름은 철수야"},
{"role": "assistant", "content": "안녕하세요, 철수님"},
{"role": "user", "content": "오늘 날씨 어때?"},
{"role": "assistant", "content": "서울은 맑습니다"},
# ... 100개 더
]
# LLM 호출 시 전체 전달
response = llm.chat(messages=history + [new_message])
```
**장점:**
- 구현 간단 (그대로 전달)
- 100% 정확한 맥락 유지
- 미묘한 뉘앙스 보존
**단점:**
- 토큰 비용 폭증 (히스토리 길이 × 메시지 수)
- 컨텍스트 윈도우 초과 (8k~128k 제한)
- 레이턴시 증가 (긴 프롬프트 처리)
**적합한 경우:**
- 짧은 대화 (5~10 턴)
- 고품질 필수 (법률, 의료)
- 예산 넉넉한 경우
**비용 (예시: Llama 3.1 8B, 50턴 대화):**
- 평균 메시지: 50 토큰
- 총 히스토리: 50턴 × 50 토큰 = 2,500 토큰
- **메시지당 비용: 2,500 토큰** (누적)
---
### 전략 2: 슬라이딩 윈도우 (Sliding Window)
**방식:**
최근 N개 메시지만 유지, 나머지는 버림.
```python
MAX_MESSAGES = 10 # 최근 10개만
def sliding_window(history, new_message):
# 최근 메시지만 유지
recent = history[-MAX_MESSAGES:]
return recent + [new_message]
# 사용
history = sliding_window(history, {"role": "user", "content": "질문"})
```
**장점:**
- 토큰 비용 고정 (N × 평균 토큰)
- 구현 간단 (슬라이싱만)
- 최근 맥락 보존
**단점:**
- 오래된 정보 손실 (11턴 전 이름 잊음)
- 장기 맥락 불가능 (소설 요약, 프로젝트 관리)
**적합한 경우:**
- FAQ 챗봇 (단기 대화)
- 고객 상담 (이슈별 독립)
- 비용 최소화 우선
**비용 (예시: 최근 10턴만):**
- 고정: 10턴 × 50 토큰 = **500 토큰**
- 50턴 대화해도 500 토큰 (vs 2,500)
**Elixir 구현:**
```elixir
defmodule Zeta.Memory.SlidingWindow do
@max_messages 10
def add_message(history, new_message) do
(history ++ [new_message])
|> Enum.take(-@max_messages)
end
end
# 사용
history = Memory.SlidingWindow.add_message(history, %{
role: "user",
content: "질문"
})
```
---
### 전략 3: 요약 (Summarization)
**방식:**
오래된 대화는 LLM으로 요약 → 최근 대화와 조합.
```python
def summarize_old_messages(old_history, llm):
"""오래된 대화를 요약"""
prompt = f"""다음 대화를 2~3문장으로 요약하세요:
{format_history(old_history)}
요약:"""
summary = llm.generate(prompt)
return summary
def summarization_strategy(history, new_message, llm):
if len(history) > 20:
# 앞 10개는 요약
old = history[:10]
recent = history[10:]
summary = summarize_old_messages(old, llm)
# 요약 + 최근 대화
return [
{"role": "system", "content": f"이전 대화 요약: {summary}"}
] + recent + [new_message]
else:
return history + [new_message]
```
**장점:**
- 장기 맥락 보존 (요약본)
- 토큰 절감 (요약 << 원본)
- 중요 정보 유지 (이름, 선호도)
**단점:**
- 요약 비용 (추가 LLM 호출)
- 정보 손실 (디테일 유실)
- 레이턴시 증가 (+200~500ms)
**적합한 경우:**
- 장기 대화 (소설 집필, 프로젝트 관리)
- 개인화 중요 (사용자 선호도 학습)
- 디테일보다 맥락 우선
**비용 (예시: 50턴 대화):**
- 요약 생성: 1,000 토큰 (입력) + 100 토큰 (출력) = 1,100 토큰 (1회)
- 최근 10턴: 500 토큰
- **총: 1,600 토큰** (vs 2,500 전체 버퍼, vs 500 슬라이딩)
**Elixir 구현:**
```elixir
defmodule Zeta.Memory.Summarization do
@max_recent 10
@summarize_threshold 20
def add_message(history, new_message, llm_client) do
updated = history ++ [new_message]
if length(updated) > @summarize_threshold do
{old, recent} = Enum.split(updated, -@max_recent)
summary = summarize(old, llm_client)
[
%{role: "system", content: "이전 대화 요약: #{summary}"}
| recent
]
else
updated
end
end
defp summarize(messages, llm_client) do
formatted = Enum.map_join(messages, "\n", fn msg ->
"#{msg.role}: #{msg.content}"
end)
prompt = """
다음 대화를 2~3문장으로 요약하세요:
#{formatted}
요약:
"""
LLMClient.generate(prompt)
end
end
```
---
### 전략 4: 벡터 DB (Vector Database)
**방식:**
대화를 임베딩 → 벡터 DB 저장 → 질문과 유사한 과거 대화만 검색.
```python
from sentence_transformers import SentenceTransformer
import chromadb
# 임베딩 모델
embedder = SentenceTransformer('all-MiniLM-L6-v2')
# Vector DB (ChromaDB)
client = chromadb.Client()
collection = client.create_collection("chat_history")
def store_message(message_id, text):
"""메시지를 벡터 DB에 저장"""
embedding = embedder.encode(text)
collection.add(
ids=[str(message_id)],
embeddings=[embedding.tolist()],
documents=[text]
)
def retrieve_relevant(query, top_k=5):
"""질문과 유사한 과거 메시지 검색"""
query_embedding = embedder.encode(query)
results = collection.query(
query_embeddings=[query_embedding.tolist()],
n_results=top_k
)
return results['documents'][0]
# 사용
def chat_with_vector_memory(user_input, llm):
# 1. 유사한 과거 대화 검색
relevant_history = retrieve_relevant(user_input, top_k=5)
# 2. 검색된 히스토리 + 최근 대화
context = "\n".join(relevant_history)
recent = get_recent_messages(5) # 최근 5개
# 3. LLM 호출
prompt = f"""관련 과거 대화:
{context}
최근 대화:
{format_history(recent)}
사용자: {user_input}
답변:"""
response = llm.generate(prompt)
# 4. 새 메시지도 저장
store_message(message_id, user_input)
store_message(message_id + 1, response)
return response
```
**장점:**
- 장기 메모리 (무제한 저장)
- 의미 기반 검색 (키워드 매칭 불필요)
- 토큰 효율적 (관련 대화만 전달)
**단점:**
- 구현 복잡도 높음 (벡터 DB + 임베딩)
- 추가 인프라 (DB 운영)
- 임베딩 비용 (메시지당 계산)
**적합한 경우:**
- 장기 고객 관계 (CRM 통합)
- 지식 누적 필요 (튜토리얼, 학습)
- 대규모 사용자 (확장성 우선)
**비용 (예시: 50턴 대화):**
- 임베딩 생성: 50 메시지 × 512 dim = 무시 가능 (로컬)
- 검색된 메시지: 5개 × 50 토큰 = 250 토큰
- 최근 대화: 5개 × 50 토큰 = 250 토큰
- **총: 500 토큰** (vs 2,500 전체 버퍼)
- **DB 비용: +$10~50/월** (ChromaDB, Pinecone 등)
**Elixir 구현 (Qdrant):**
```elixir
defmodule Zeta.Memory.VectorDB do
@collection_name "chat_history"
@qdrant_url "http://localhost:6333"
def store_message(message_id, text, embedding) do
payload = %{
id: message_id,
text: text,
timestamp: DateTime.utc_now()
}
HTTPoison.put(
"#{@qdrant_url}/collections/#{@collection_name}/points",
Jason.encode!(%{
points: [%{
id: message_id,
vector: embedding,
payload: payload
}]
}),
[{"Content-Type", "application/json"}]
)
end
def search_similar(query_embedding, top_k \\ 5) do
body = Jason.encode!(%{
vector: query_embedding,
limit: top_k,
with_payload: true
})
case HTTPoison.post(
"#{@qdrant_url}/collections/#{@collection_name}/points/search",
body,
[{"Content-Type", "application/json"}]
) do
{:ok, %{body: response}} ->
%{"result" => results} = Jason.decode!(response)
Enum.map(results, & &1["payload"]["text"])
{:error, _} -> []
end
end
end
# 임베딩 생성 (OpenAI text-embedding-3-small)
defmodule Zeta.Embedding do
@api_url "https://api.openai.com/v1/embeddings"
@api_key System.get_env("OPENAI_API_KEY")
def embed(text) do
body = Jason.encode!(%{
model: "text-embedding-3-small",
input: text
})
headers = [
{"Authorization", "Bearer #{@api_key}"},
{"Content-Type", "application/json"}
]
case HTTPoison.post(@api_url, body, headers) do
{:ok, %{body: response}} ->
%{"data" => [%{"embedding" => embedding}]} = Jason.decode!(response)
embedding
{:error, _} -> nil
end
end
end
```
---
### 전략 5: 하이브리드 (Hybrid)
**방식:**
요약 + 벡터 DB + 슬라이딩 윈도우 조합.
```
┌──────────────────┐
│ 최근 5개 메시지 │ ← 슬라이딩 윈도우 (단기 기억)
└──────────────────┘
+
┌──────────────────┐
│ 요약 (6~20턴) │ ← 요약 (중기 기억)
└──────────────────┘
+
┌──────────────────┐
│ 벡터 DB (유사 5개)│ ← 벡터 검색 (장기 기억)
└──────────────────┘
```
**Python 구현:**
```python
def hybrid_memory(user_input, history, llm, vector_db):
# 1. 슬라이딩 윈도우: 최근 5개
recent = history[-5:]
# 2. 요약: 6~20턴
if len(history) > 20:
middle = history[5:20]
summary = summarize(middle, llm)
else:
summary = None
# 3. 벡터 검색: 유사한 과거 대화
relevant = vector_db.search(user_input, top_k=5)
# 4. 조합
context_parts = []
if relevant:
context_parts.append(f"관련 과거 대화:\n{format_list(relevant)}")
if summary:
context_parts.append(f"중기 대화 요약: {summary}")
context_parts.append(f"최근 대화:\n{format_history(recent)}")
prompt = "\n\n".join(context_parts) + f"\n\n사용자: {user_input}\n답변:"
return llm.generate(prompt)
```
**장점:**
- 단기/중기/장기 모두 보존
- 토큰 효율적 (관련 정보만)
- 고품질 맥락
**단점:**
- 구현 복잡도 최고
- 여러 시스템 운영 필요
**적합한 경우:**
- 엔터프라이즈 챗봇 (최고 품질)
- 장기 고객 관계
- 예산 넉넉한 경우
**비용 (예시: 50턴 대화):**
- 최근 5개: 250 토큰
- 요약: 100 토큰
- 벡터 검색: 250 토큰
- **총: 600 토큰** (vs 2,500 전체 버퍼)
---
## 🚀 KV 캐시 압축 (최신 기술)
### KVzip (2025년 최신)
**문제:**
Transformer LLM은 컨텍스트를 **KV 캐시** (Key-Value pairs)로 저장.
긴 컨텍스트 = 큰 KV 캐시 = 메모리 부족 + 레이턴시 증가.
**KVzip 방식:**
- LLM에게 "이전 컨텍스트를 반복해"라고 요청
- 완벽한 복원이 가능한 KV만 보존
- 불필요한 KV는 압축
**성능:**
- **3~4배 압축** (vs 원본)
- 170,000 토큰 지원
- 재사용 가능 (재압축 불필요)
**사용 예시 (vLLM 통합):**
```python
from vllm import LLM
from kvzip import KVZipCompressor
llm = LLM(model="meta-llama/Llama-3.1-8B")
compressor = KVZipCompressor()
# 긴 대화 히스토리
long_history = [...] # 100,000 토큰
# KV 캐시 압축
compressed_kv = compressor.compress(long_history, llm)
# 압축된 KV로 추론
response = llm.generate(
prompt="새 질문",
kv_cache=compressed_kv # 25,000 토큰 (75% 절감)
)
```
**비용 절감:**
- 원본: 100,000 토큰 × $0.0001 = $0.01/쿼리
- 압축: 25,000 토큰 × $0.0001 = **$0.0025/쿼리** (75% 절감)
**출처:**
- 논문: [KVzip: Query-Agnostic KV Cache Compression](https://arxiv.org/abs/2505.23416)
- GitHub: [서울대 연구팀](https://janghyun1230.github.io/kvzip/)
**한계:**
- vLLM 0.6.0+ 필요
- 압축 오버헤드 (+100~200ms, 1회)
- 아직 프로덕션 검증 부족 (2025년 연구)
---
## 💰 비용 비교 (실전 시나리오)
### 시나리오: MAU 10만, 평균 대화 50턴
**가정:**
- 평균 메시지: 50 토큰
- 메시지 수: 월 500만 (10만 사용자 × 50턴)
- 모델: Llama 3.1 8B (self-hosted) 또는 GPT-3.5-turbo (API)
| 전략 | 평균 컨텍스트 | 월 토큰 총량 | Self-hosted 비용 | GPT-3.5-turbo 비용 | 품질 |
|------|--------------|--------------|------------------|---------------------|------|
| **전체 버퍼** | 2,500 토큰 | 12.5B | $320 (GPU) | **$18,750** | ⭐⭐⭐⭐⭐ |
| **슬라이딩 (10턴)** | 500 토큰 | 2.5B | $320 (GPU) | **$3,750** | ⭐⭐⭐ |
| **요약** | 600 토큰 | 3B + 요약 비용 | $320 (GPU) | **$4,500** | ⭐⭐⭐⭐ |
| **벡터 DB** | 500 토큰 | 2.5B | $320 (GPU) + $50 (DB) | **$3,750** + $50 | ⭐⭐⭐⭐ |
| **하이브리드** | 600 토큰 | 3B | $320 (GPU) + $50 (DB) | **$4,500** + $50 | ⭐⭐⭐⭐⭐ |
**주요 발견:**
1. **Self-hosted는 전략 무관 $320** (GPU 고정 비용)
2. **API는 슬라이딩 윈도우가 80% 절감** (vs 전체 버퍼)
3. **벡터 DB는 품질 + 비용 균형** 최고
---
## 🎯 실전 추천
### 1. MVP / 프로토타입
**추천: 슬라이딩 윈도우 (10턴)**
```python
MAX_TURNS = 10
def simple_memory(history, new_message):
return history[-MAX_TURNS:] + [new_message]
```
**이유:**
- 구현 5분 (슬라이싱만)
- 비용 최소 (API 사용 시 80% 절감)
- 대부분 유스케이스 충분 (FAQ, 간단 상담)
**비용:**
- Self-hosted: $320/월
- GPT-3.5-turbo: $3,750/월 (vs $18,750 전체 버퍼)
---
### 2. 고객 상담 챗봇
**추천: 요약 (최근 10턴 + 요약)**
```python
def customer_support_memory(history, llm):
if len(history) > 20:
old = history[:10]
recent = history[10:]
summary = summarize(old, llm)
return [{"role": "system", "content": f"이슈 요약: {summary}"}] + recent
return history
```
**이유:**
- 고객 이슈 맥락 보존 (요약)
- 최근 대화 디테일 유지
- 상담사 인계 시 요약 활용
**비용:**
- Self-hosted: $320/월
- GPT-3.5-turbo: $4,500/월 (+20% vs 슬라이딩)
---
### 3. 장기 개인화 서비스
**추천: 벡터 DB (Qdrant + 슬라이딩)**
```python
def personalized_memory(user_id, user_input, vector_db):
# 과거 선호도 검색
relevant = vector_db.search(user_id, user_input, top_k=5)
# 최근 대화
recent = get_recent_messages(user_id, 5)
return relevant + recent
```
**이유:**
- 사용자 선호도 누적 (무제한 저장)
- 개인화 추천 (과거 취향 기반)
- 확장성 (사용자 증가해도 OK)
**비용:**
- Self-hosted: $320/월 (LLM) + $50/월 (Qdrant)
- GPT-3.5-turbo: $3,750/월 + $50/월
- 임베딩: text-embedding-3-small = $975/월 (500만 메시지)
---
### 4. 엔터프라이즈 (최고 품질)
**추천: 하이브리드 (요약 + 벡터 + 슬라이딩)**
```elixir
defmodule Zeta.Memory.Hybrid do
def build_context(user_id, user_input) do
# 1. 장기 기억 (벡터 검색)
long_term = VectorDB.search_similar(user_id, embed(user_input), 5)
# 2. 중기 기억 (요약)
mid_term = get_summary(user_id, turns: 6..20)
# 3. 단기 기억 (슬라이딩)
short_term = get_recent_messages(user_id, 5)
# 조합
[
"관련 과거 대화: #{long_term}",
"중기 대화 요약: #{mid_term}",
"최근 대화: #{short_term}"
]
end
end
```
**이유:**
- 단기/중기/장기 모두 보존
- 최고 품질 (맥락 손실 최소)
- 기업 고객 만족도 최우선
**비용:**
- Self-hosted: $320/월 (LLM) + $50/월 (벡터 DB)
- GPT-4o: $12,000/월 (고품질 모델 + 하이브리드)
---
## ⚠️ 실전 고려사항
### 1. 컨텍스트 윈도우 제한
**모델별 제한:**
| 모델 | 컨텍스트 윈도우 | 실사용 권장 |
|------|----------------|-------------|
| Llama 3.1 8B | 8,192 토큰 | ~6,000 토큰 (75%) |
| Llama 3.1 70B | 128,000 토큰 | ~100,000 토큰 |
| GPT-3.5-turbo | 16,384 토큰 | ~12,000 토큰 |
| GPT-4o | 128,000 토큰 | ~100,000 토큰 |
| Gemini 1.5 Pro | 1,000,000 토큰 | ~800,000 토큰 |
**주의:**
- 토큰 = 단어가 아님 (한글 1글자 = 2~3 토큰)
- 시스템 프롬프트 + 메모리 + 새 질문 모두 포함
- 여유분 25% 남기기 (안전 마진)
**Llama 8B 예시:**
```
시스템 프롬프트: 500 토큰
메모리 (슬라이딩 10턴): 500 토큰
새 질문: 50 토큰
총: 1,050 토큰 (8,192의 13%)
```
**초과 시 대응:**
1. 슬라이딩 윈도우 축소 (10 → 5턴)
2. 요약 적용
3. 더 큰 모델로 전환 (8B → 70B)
### 2. 멀티턴 vs 싱글턴
**멀티턴** (일반 대화):
- 대화 히스토리 누적
- 맥락 의존도 높음
- 메모리 관리 필수
**싱글턴** (독립 질문):
- 각 질문 독립적
- 맥락 불필요 (FAQ, 검색)
- 메모리 불필요 (비용 절감)
**판단 기준:**
```python
# 멀티턴 필요 여부 자동 판단
def needs_context(user_input):
context_keywords = ["그거", "그것", "아까", "전에", "이전", "내가 말한"]
return any(kw in user_input for kw in context_keywords)
# 사용
if needs_context(user_input):
context = build_memory(history)
else:
context = [] # 맥락 없이 답변
```
### 3. 사용자 세션 관리
**세션 ID 기반 메모리:**
```elixir
# PostgreSQL 스키마
defmodule Zeta.Repo.Migrations.CreateConversations do
def change do
create table(:conversations) do
add :user_id, :integer, null: false
add :session_id, :uuid, null: false
add :messages, :jsonb, default: "[]"
add :summary, :text
add :last_activity, :utc_datetime
timestamps()
end
create index(:conversations, [:user_id, :session_id])
end
end
# 메모리 로드
defmodule Zeta.ConversationMemory do
def load(user_id, session_id) do
Repo.get_by(Conversation,
user_id: user_id,
session_id: session_id
)
|> case do
nil -> %Conversation{user_id: user_id, session_id: session_id}
conv -> conv
end
end
def save(conversation, new_message) do
messages = conversation.messages ++ [new_message]
conversation
|> Conversation.changeset(%{
messages: messages,
last_activity: DateTime.utc_now()
})
|> Repo.update()
end
end
```
**세션 만료 정책:**
- 24시간 무활동 → 요약 후 아카이브
- 7일 무활동 → 완전 삭제 (GDPR 준수)
### 4. 개인정보 보호
**위험:**
```
사용자: "내 주민번호는 123456-1234567이야"
[메모리 저장] ← 개인정보 유출 위험
```
**대응:**
1. **PII 탐지 + 마스킹**
```python
import re
def mask_pii(text):
# 주민번호
text = re.sub(r'\d{6}-\d{7}', '******-*******', text)
# 전화번호
text = re.sub(r'010-\d{4}-\d{4}', '010-****-****', text)
return text
# 저장 전 마스킹
masked = mask_pii(user_input)
save_to_memory(masked)
```
2. **벡터 DB 암호화**
- 저장 시 AES-256 암호화
- 검색 시 복호화 (성능 -10%)
3. **GDPR 준수**
- 사용자 요청 시 메모리 삭제
- 30일 자동 삭제 정책
---
## 📚 참고 자료
### 논문
- [KVzip: Query-Agnostic KV Cache Compression](https://arxiv.org/abs/2505.23416)
- [Long-Term Dialogue Memory (EmergentMind)](https://www.emergentmind.com/topics/long-term-dialogue-memory)
### 가이드
- [Context Window Management (Maxim AI)](https://www.getmaxim.ai/articles/context-window-management-strategies-for-long-context-ai-agents-and-chatbots/)
- [LangChain Conversational Memory](https://www.pinecone.io/learn/series/langchain/langchain-conversational-memory/)
- [Vellum: LLM Chatbot Memory Management](https://www.vellum.ai/blog/how-should-i-manage-memory-for-my-llm-chatbot)
### 도구
- [Qdrant](https://qdrant.tech/) - 벡터 DB
- [ChromaDB](https://www.trychroma.com/) - 벡터 DB (로컬)
- [mem0](https://mem0.ai/) - LLM 메모리 레이어
- [Redis LangCache](https://redis.io/blog/llm-context-windows/) - 프롬프트 캐싱
---
*작성일: 2026-02-25*
*데이터 기준: 2024-2025년 최신 연구*
## 💡 왜 메모리 관리가 중요한가
LLM은 기본적으로 **stateless**(무상태)다. 이전 대화를 기억하지 못한다.
```
사용자: "내 이름은 철수야"
챗봇: "안녕하세요, 철수님"
[다음 대화]
사용자: "내 이름이 뭐였지?"
챗봇: "죄송하지만 모르겠습니다" ← 기억 없음
```
**대화 히스토리를 관리하지 않으면:**
- 매번 맥락 설명 반복 (사용자 피로)
- 일관성 없는 답변 (UX 저하)
- 개인화 불가능 (선호도 학습 실패)
**하지만 모든 대화를 저장하면:**
- 토큰 비용 폭증 (컨텍스트 윈도우 초과)
- 레이턴시 증가 (긴 프롬프트 처리 시간)
- 메모리 부족 (KV 캐시 크기 증가)
**이 글의 목표:**
비용, 성능, 품질을 균형잡는 메모리 관리 전략을 실전 예시와 함께 제시한다.
---
## 📊 메모리 관리 전략 비교
### 전략 1: 전체 버퍼 (Full Buffer)
**방식:**
모든 대화 히스토리를 그대로 프롬프트에 포함.
```python
# 예시: 전체 히스토리
history = [
{"role": "user", "content": "내 이름은 철수야"},
{"role": "assistant", "content": "안녕하세요, 철수님"},
{"role": "user", "content": "오늘 날씨 어때?"},
{"role": "assistant", "content": "서울은 맑습니다"},
# ... 100개 더
]
# LLM 호출 시 전체 전달
response = llm.chat(messages=history + [new_message])
```
**장점:**
- 구현 간단 (그대로 전달)
- 100% 정확한 맥락 유지
- 미묘한 뉘앙스 보존
**단점:**
- 토큰 비용 폭증 (히스토리 길이 × 메시지 수)
- 컨텍스트 윈도우 초과 (8k~128k 제한)
- 레이턴시 증가 (긴 프롬프트 처리)
**적합한 경우:**
- 짧은 대화 (5~10 턴)
- 고품질 필수 (법률, 의료)
- 예산 넉넉한 경우
**비용 (예시: Llama 3.1 8B, 50턴 대화):**
- 평균 메시지: 50 토큰
- 총 히스토리: 50턴 × 50 토큰 = 2,500 토큰
- **메시지당 비용: 2,500 토큰** (누적)
---
### 전략 2: 슬라이딩 윈도우 (Sliding Window)
**방식:**
최근 N개 메시지만 유지, 나머지는 버림.
```python
MAX_MESSAGES = 10 # 최근 10개만
def sliding_window(history, new_message):
# 최근 메시지만 유지
recent = history[-MAX_MESSAGES:]
return recent + [new_message]
# 사용
history = sliding_window(history, {"role": "user", "content": "질문"})
```
**장점:**
- 토큰 비용 고정 (N × 평균 토큰)
- 구현 간단 (슬라이싱만)
- 최근 맥락 보존
**단점:**
- 오래된 정보 손실 (11턴 전 이름 잊음)
- 장기 맥락 불가능 (소설 요약, 프로젝트 관리)
**적합한 경우:**
- FAQ 챗봇 (단기 대화)
- 고객 상담 (이슈별 독립)
- 비용 최소화 우선
**비용 (예시: 최근 10턴만):**
- 고정: 10턴 × 50 토큰 = **500 토큰**
- 50턴 대화해도 500 토큰 (vs 2,500)
**Elixir 구현:**
```elixir
defmodule Zeta.Memory.SlidingWindow do
@max_messages 10
def add_message(history, new_message) do
(history ++ [new_message])
|> Enum.take(-@max_messages)
end
end
# 사용
history = Memory.SlidingWindow.add_message(history, %{
role: "user",
content: "질문"
})
```
---
### 전략 3: 요약 (Summarization)
**방식:**
오래된 대화는 LLM으로 요약 → 최근 대화와 조합.
```python
def summarize_old_messages(old_history, llm):
"""오래된 대화를 요약"""
prompt = f"""다음 대화를 2~3문장으로 요약하세요:
{format_history(old_history)}
요약:"""
summary = llm.generate(prompt)
return summary
def summarization_strategy(history, new_message, llm):
if len(history) > 20:
# 앞 10개는 요약
old = history[:10]
recent = history[10:]
summary = summarize_old_messages(old, llm)
# 요약 + 최근 대화
return [
{"role": "system", "content": f"이전 대화 요약: {summary}"}
] + recent + [new_message]
else:
return history + [new_message]
```
**장점:**
- 장기 맥락 보존 (요약본)
- 토큰 절감 (요약 << 원본)
- 중요 정보 유지 (이름, 선호도)
**단점:**
- 요약 비용 (추가 LLM 호출)
- 정보 손실 (디테일 유실)
- 레이턴시 증가 (+200~500ms)
**적합한 경우:**
- 장기 대화 (소설 집필, 프로젝트 관리)
- 개인화 중요 (사용자 선호도 학습)
- 디테일보다 맥락 우선
**비용 (예시: 50턴 대화):**
- 요약 생성: 1,000 토큰 (입력) + 100 토큰 (출력) = 1,100 토큰 (1회)
- 최근 10턴: 500 토큰
- **총: 1,600 토큰** (vs 2,500 전체 버퍼, vs 500 슬라이딩)
**Elixir 구현:**
```elixir
defmodule Zeta.Memory.Summarization do
@max_recent 10
@summarize_threshold 20
def add_message(history, new_message, llm_client) do
updated = history ++ [new_message]
if length(updated) > @summarize_threshold do
{old, recent} = Enum.split(updated, -@max_recent)
summary = summarize(old, llm_client)
[
%{role: "system", content: "이전 대화 요약: #{summary}"}
| recent
]
else
updated
end
end
defp summarize(messages, llm_client) do
formatted = Enum.map_join(messages, "\n", fn msg ->
"#{msg.role}: #{msg.content}"
end)
prompt = """
다음 대화를 2~3문장으로 요약하세요:
#{formatted}
요약:
"""
LLMClient.generate(prompt)
end
end
```
---
### 전략 4: 벡터 DB (Vector Database)
**방식:**
대화를 임베딩 → 벡터 DB 저장 → 질문과 유사한 과거 대화만 검색.
```python
from sentence_transformers import SentenceTransformer
import chromadb
# 임베딩 모델
embedder = SentenceTransformer('all-MiniLM-L6-v2')
# Vector DB (ChromaDB)
client = chromadb.Client()
collection = client.create_collection("chat_history")
def store_message(message_id, text):
"""메시지를 벡터 DB에 저장"""
embedding = embedder.encode(text)
collection.add(
ids=[str(message_id)],
embeddings=[embedding.tolist()],
documents=[text]
)
def retrieve_relevant(query, top_k=5):
"""질문과 유사한 과거 메시지 검색"""
query_embedding = embedder.encode(query)
results = collection.query(
query_embeddings=[query_embedding.tolist()],
n_results=top_k
)
return results['documents'][0]
# 사용
def chat_with_vector_memory(user_input, llm):
# 1. 유사한 과거 대화 검색
relevant_history = retrieve_relevant(user_input, top_k=5)
# 2. 검색된 히스토리 + 최근 대화
context = "\n".join(relevant_history)
recent = get_recent_messages(5) # 최근 5개
# 3. LLM 호출
prompt = f"""관련 과거 대화:
{context}
최근 대화:
{format_history(recent)}
사용자: {user_input}
답변:"""
response = llm.generate(prompt)
# 4. 새 메시지도 저장
store_message(message_id, user_input)
store_message(message_id + 1, response)
return response
```
**장점:**
- 장기 메모리 (무제한 저장)
- 의미 기반 검색 (키워드 매칭 불필요)
- 토큰 효율적 (관련 대화만 전달)
**단점:**
- 구현 복잡도 높음 (벡터 DB + 임베딩)
- 추가 인프라 (DB 운영)
- 임베딩 비용 (메시지당 계산)
**적합한 경우:**
- 장기 고객 관계 (CRM 통합)
- 지식 누적 필요 (튜토리얼, 학습)
- 대규모 사용자 (확장성 우선)
**비용 (예시: 50턴 대화):**
- 임베딩 생성: 50 메시지 × 512 dim = 무시 가능 (로컬)
- 검색된 메시지: 5개 × 50 토큰 = 250 토큰
- 최근 대화: 5개 × 50 토큰 = 250 토큰
- **총: 500 토큰** (vs 2,500 전체 버퍼)
- **DB 비용: +$10~50/월** (ChromaDB, Pinecone 등)
**Elixir 구현 (Qdrant):**
```elixir
defmodule Zeta.Memory.VectorDB do
@collection_name "chat_history"
@qdrant_url "http://localhost:6333"
def store_message(message_id, text, embedding) do
payload = %{
id: message_id,
text: text,
timestamp: DateTime.utc_now()
}
HTTPoison.put(
"#{@qdrant_url}/collections/#{@collection_name}/points",
Jason.encode!(%{
points: [%{
id: message_id,
vector: embedding,
payload: payload
}]
}),
[{"Content-Type", "application/json"}]
)
end
def search_similar(query_embedding, top_k \\ 5) do
body = Jason.encode!(%{
vector: query_embedding,
limit: top_k,
with_payload: true
})
case HTTPoison.post(
"#{@qdrant_url}/collections/#{@collection_name}/points/search",
body,
[{"Content-Type", "application/json"}]
) do
{:ok, %{body: response}} ->
%{"result" => results} = Jason.decode!(response)
Enum.map(results, & &1["payload"]["text"])
{:error, _} -> []
end
end
end
# 임베딩 생성 (OpenAI text-embedding-3-small)
defmodule Zeta.Embedding do
@api_url "https://api.openai.com/v1/embeddings"
@api_key System.get_env("OPENAI_API_KEY")
def embed(text) do
body = Jason.encode!(%{
model: "text-embedding-3-small",
input: text
})
headers = [
{"Authorization", "Bearer #{@api_key}"},
{"Content-Type", "application/json"}
]
case HTTPoison.post(@api_url, body, headers) do
{:ok, %{body: response}} ->
%{"data" => [%{"embedding" => embedding}]} = Jason.decode!(response)
embedding
{:error, _} -> nil
end
end
end
```
---
### 전략 5: 하이브리드 (Hybrid)
**방식:**
요약 + 벡터 DB + 슬라이딩 윈도우 조합.
```
┌──────────────────┐
│ 최근 5개 메시지 │ ← 슬라이딩 윈도우 (단기 기억)
└──────────────────┘
+
┌──────────────────┐
│ 요약 (6~20턴) │ ← 요약 (중기 기억)
└──────────────────┘
+
┌──────────────────┐
│ 벡터 DB (유사 5개)│ ← 벡터 검색 (장기 기억)
└──────────────────┘
```
**Python 구현:**
```python
def hybrid_memory(user_input, history, llm, vector_db):
# 1. 슬라이딩 윈도우: 최근 5개
recent = history[-5:]
# 2. 요약: 6~20턴
if len(history) > 20:
middle = history[5:20]
summary = summarize(middle, llm)
else:
summary = None
# 3. 벡터 검색: 유사한 과거 대화
relevant = vector_db.search(user_input, top_k=5)
# 4. 조합
context_parts = []
if relevant:
context_parts.append(f"관련 과거 대화:\n{format_list(relevant)}")
if summary:
context_parts.append(f"중기 대화 요약: {summary}")
context_parts.append(f"최근 대화:\n{format_history(recent)}")
prompt = "\n\n".join(context_parts) + f"\n\n사용자: {user_input}\n답변:"
return llm.generate(prompt)
```
**장점:**
- 단기/중기/장기 모두 보존
- 토큰 효율적 (관련 정보만)
- 고품질 맥락
**단점:**
- 구현 복잡도 최고
- 여러 시스템 운영 필요
**적합한 경우:**
- 엔터프라이즈 챗봇 (최고 품질)
- 장기 고객 관계
- 예산 넉넉한 경우
**비용 (예시: 50턴 대화):**
- 최근 5개: 250 토큰
- 요약: 100 토큰
- 벡터 검색: 250 토큰
- **총: 600 토큰** (vs 2,500 전체 버퍼)
---
## 🚀 KV 캐시 압축 (최신 기술)
### KVzip (2025년 최신)
**문제:**
Transformer LLM은 컨텍스트를 **KV 캐시** (Key-Value pairs)로 저장.
긴 컨텍스트 = 큰 KV 캐시 = 메모리 부족 + 레이턴시 증가.
**KVzip 방식:**
- LLM에게 "이전 컨텍스트를 반복해"라고 요청
- 완벽한 복원이 가능한 KV만 보존
- 불필요한 KV는 압축
**성능:**
- **3~4배 압축** (vs 원본)
- 170,000 토큰 지원
- 재사용 가능 (재압축 불필요)
**사용 예시 (vLLM 통합):**
```python
from vllm import LLM
from kvzip import KVZipCompressor
llm = LLM(model="meta-llama/Llama-3.1-8B")
compressor = KVZipCompressor()
# 긴 대화 히스토리
long_history = [...] # 100,000 토큰
# KV 캐시 압축
compressed_kv = compressor.compress(long_history, llm)
# 압축된 KV로 추론
response = llm.generate(
prompt="새 질문",
kv_cache=compressed_kv # 25,000 토큰 (75% 절감)
)
```
**비용 절감:**
- 원본: 100,000 토큰 × $0.0001 = $0.01/쿼리
- 압축: 25,000 토큰 × $0.0001 = **$0.0025/쿼리** (75% 절감)
**출처:**
- 논문: [KVzip: Query-Agnostic KV Cache Compression](https://arxiv.org/abs/2505.23416)
- GitHub: [서울대 연구팀](https://janghyun1230.github.io/kvzip/)
**한계:**
- vLLM 0.6.0+ 필요
- 압축 오버헤드 (+100~200ms, 1회)
- 아직 프로덕션 검증 부족 (2025년 연구)
---
## 💰 비용 비교 (실전 시나리오)
### 시나리오: MAU 10만, 평균 대화 50턴
**가정:**
- 평균 메시지: 50 토큰
- 메시지 수: 월 500만 (10만 사용자 × 50턴)
- 모델: Llama 3.1 8B (self-hosted) 또는 GPT-3.5-turbo (API)
| 전략 | 평균 컨텍스트 | 월 토큰 총량 | Self-hosted 비용 | GPT-3.5-turbo 비용 | 품질 |
|------|--------------|--------------|------------------|---------------------|------|
| **전체 버퍼** | 2,500 토큰 | 12.5B | $320 (GPU) | **$18,750** | ⭐⭐⭐⭐⭐ |
| **슬라이딩 (10턴)** | 500 토큰 | 2.5B | $320 (GPU) | **$3,750** | ⭐⭐⭐ |
| **요약** | 600 토큰 | 3B + 요약 비용 | $320 (GPU) | **$4,500** | ⭐⭐⭐⭐ |
| **벡터 DB** | 500 토큰 | 2.5B | $320 (GPU) + $50 (DB) | **$3,750** + $50 | ⭐⭐⭐⭐ |
| **하이브리드** | 600 토큰 | 3B | $320 (GPU) + $50 (DB) | **$4,500** + $50 | ⭐⭐⭐⭐⭐ |
**주요 발견:**
1. **Self-hosted는 전략 무관 $320** (GPU 고정 비용)
2. **API는 슬라이딩 윈도우가 80% 절감** (vs 전체 버퍼)
3. **벡터 DB는 품질 + 비용 균형** 최고
---
## 🎯 실전 추천
### 1. MVP / 프로토타입
**추천: 슬라이딩 윈도우 (10턴)**
```python
MAX_TURNS = 10
def simple_memory(history, new_message):
return history[-MAX_TURNS:] + [new_message]
```
**이유:**
- 구현 5분 (슬라이싱만)
- 비용 최소 (API 사용 시 80% 절감)
- 대부분 유스케이스 충분 (FAQ, 간단 상담)
**비용:**
- Self-hosted: $320/월
- GPT-3.5-turbo: $3,750/월 (vs $18,750 전체 버퍼)
---
### 2. 고객 상담 챗봇
**추천: 요약 (최근 10턴 + 요약)**
```python
def customer_support_memory(history, llm):
if len(history) > 20:
old = history[:10]
recent = history[10:]
summary = summarize(old, llm)
return [{"role": "system", "content": f"이슈 요약: {summary}"}] + recent
return history
```
**이유:**
- 고객 이슈 맥락 보존 (요약)
- 최근 대화 디테일 유지
- 상담사 인계 시 요약 활용
**비용:**
- Self-hosted: $320/월
- GPT-3.5-turbo: $4,500/월 (+20% vs 슬라이딩)
---
### 3. 장기 개인화 서비스
**추천: 벡터 DB (Qdrant + 슬라이딩)**
```python
def personalized_memory(user_id, user_input, vector_db):
# 과거 선호도 검색
relevant = vector_db.search(user_id, user_input, top_k=5)
# 최근 대화
recent = get_recent_messages(user_id, 5)
return relevant + recent
```
**이유:**
- 사용자 선호도 누적 (무제한 저장)
- 개인화 추천 (과거 취향 기반)
- 확장성 (사용자 증가해도 OK)
**비용:**
- Self-hosted: $320/월 (LLM) + $50/월 (Qdrant)
- GPT-3.5-turbo: $3,750/월 + $50/월
- 임베딩: text-embedding-3-small = $975/월 (500만 메시지)
---
### 4. 엔터프라이즈 (최고 품질)
**추천: 하이브리드 (요약 + 벡터 + 슬라이딩)**
```elixir
defmodule Zeta.Memory.Hybrid do
def build_context(user_id, user_input) do
# 1. 장기 기억 (벡터 검색)
long_term = VectorDB.search_similar(user_id, embed(user_input), 5)
# 2. 중기 기억 (요약)
mid_term = get_summary(user_id, turns: 6..20)
# 3. 단기 기억 (슬라이딩)
short_term = get_recent_messages(user_id, 5)
# 조합
[
"관련 과거 대화: #{long_term}",
"중기 대화 요약: #{mid_term}",
"최근 대화: #{short_term}"
]
end
end
```
**이유:**
- 단기/중기/장기 모두 보존
- 최고 품질 (맥락 손실 최소)
- 기업 고객 만족도 최우선
**비용:**
- Self-hosted: $320/월 (LLM) + $50/월 (벡터 DB)
- GPT-4o: $12,000/월 (고품질 모델 + 하이브리드)
---
## ⚠️ 실전 고려사항
### 1. 컨텍스트 윈도우 제한
**모델별 제한:**
| 모델 | 컨텍스트 윈도우 | 실사용 권장 |
|------|----------------|-------------|
| Llama 3.1 8B | 8,192 토큰 | ~6,000 토큰 (75%) |
| Llama 3.1 70B | 128,000 토큰 | ~100,000 토큰 |
| GPT-3.5-turbo | 16,384 토큰 | ~12,000 토큰 |
| GPT-4o | 128,000 토큰 | ~100,000 토큰 |
| Gemini 1.5 Pro | 1,000,000 토큰 | ~800,000 토큰 |
**주의:**
- 토큰 = 단어가 아님 (한글 1글자 = 2~3 토큰)
- 시스템 프롬프트 + 메모리 + 새 질문 모두 포함
- 여유분 25% 남기기 (안전 마진)
**Llama 8B 예시:**
```
시스템 프롬프트: 500 토큰
메모리 (슬라이딩 10턴): 500 토큰
새 질문: 50 토큰
총: 1,050 토큰 (8,192의 13%)
```
**초과 시 대응:**
1. 슬라이딩 윈도우 축소 (10 → 5턴)
2. 요약 적용
3. 더 큰 모델로 전환 (8B → 70B)
### 2. 멀티턴 vs 싱글턴
**멀티턴** (일반 대화):
- 대화 히스토리 누적
- 맥락 의존도 높음
- 메모리 관리 필수
**싱글턴** (독립 질문):
- 각 질문 독립적
- 맥락 불필요 (FAQ, 검색)
- 메모리 불필요 (비용 절감)
**판단 기준:**
```python
# 멀티턴 필요 여부 자동 판단
def needs_context(user_input):
context_keywords = ["그거", "그것", "아까", "전에", "이전", "내가 말한"]
return any(kw in user_input for kw in context_keywords)
# 사용
if needs_context(user_input):
context = build_memory(history)
else:
context = [] # 맥락 없이 답변
```
### 3. 사용자 세션 관리
**세션 ID 기반 메모리:**
```elixir
# PostgreSQL 스키마
defmodule Zeta.Repo.Migrations.CreateConversations do
def change do
create table(:conversations) do
add :user_id, :integer, null: false
add :session_id, :uuid, null: false
add :messages, :jsonb, default: "[]"
add :summary, :text
add :last_activity, :utc_datetime
timestamps()
end
create index(:conversations, [:user_id, :session_id])
end
end
# 메모리 로드
defmodule Zeta.ConversationMemory do
def load(user_id, session_id) do
Repo.get_by(Conversation,
user_id: user_id,
session_id: session_id
)
|> case do
nil -> %Conversation{user_id: user_id, session_id: session_id}
conv -> conv
end
end
def save(conversation, new_message) do
messages = conversation.messages ++ [new_message]
conversation
|> Conversation.changeset(%{
messages: messages,
last_activity: DateTime.utc_now()
})
|> Repo.update()
end
end
```
**세션 만료 정책:**
- 24시간 무활동 → 요약 후 아카이브
- 7일 무활동 → 완전 삭제 (GDPR 준수)
### 4. 개인정보 보호
**위험:**
```
사용자: "내 주민번호는 123456-1234567이야"
[메모리 저장] ← 개인정보 유출 위험
```
**대응:**
1. **PII 탐지 + 마스킹**
```python
import re
def mask_pii(text):
# 주민번호
text = re.sub(r'\d{6}-\d{7}', '******-*******', text)
# 전화번호
text = re.sub(r'010-\d{4}-\d{4}', '010-****-****', text)
return text
# 저장 전 마스킹
masked = mask_pii(user_input)
save_to_memory(masked)
```
2. **벡터 DB 암호화**
- 저장 시 AES-256 암호화
- 검색 시 복호화 (성능 -10%)
3. **GDPR 준수**
- 사용자 요청 시 메모리 삭제
- 30일 자동 삭제 정책
---
## 📚 참고 자료
### 논문
- [KVzip: Query-Agnostic KV Cache Compression](https://arxiv.org/abs/2505.23416)
- [Long-Term Dialogue Memory (EmergentMind)](https://www.emergentmind.com/topics/long-term-dialogue-memory)
### 가이드
- [Context Window Management (Maxim AI)](https://www.getmaxim.ai/articles/context-window-management-strategies-for-long-context-ai-agents-and-chatbots/)
- [LangChain Conversational Memory](https://www.pinecone.io/learn/series/langchain/langchain-conversational-memory/)
- [Vellum: LLM Chatbot Memory Management](https://www.vellum.ai/blog/how-should-i-manage-memory-for-my-llm-chatbot)
### 도구
- [Qdrant](https://qdrant.tech/) - 벡터 DB
- [ChromaDB](https://www.trychroma.com/) - 벡터 DB (로컬)
- [mem0](https://mem0.ai/) - LLM 메모리 레이어
- [Redis LangCache](https://redis.io/blog/llm-context-windows/) - 프롬프트 캐싱
---
*작성일: 2026-02-25*
*데이터 기준: 2024-2025년 최신 연구*
💬 0
로그인 후 댓글 작성
첫 댓글을 남겨보세요!