목차
- 왜 메모리가 필요한가?
- 수동으로 대화 기록 관리하기
- 토큰 카운팅 (tiktoken)
- ConversationBufferMemory (langchain_classic)
- InMemoryChatMessageHistory (langchain_core)
- RunnableWithMessageHistory — 세션 기반 자동 관리
- Window Memory — 최근 N개 턴만 유지
- Summary Memory — 오래된 대화 요약으로 압축
- SummaryChatbot — 요약 메모리 통합 챗봇
- 자주 나오는 실수 / 주의사항
- [보충] 메모리 전략 비교
1. 왜 메모리가 필요한가?
LLM(대규모 언어 모델)은 기본적으로 무상태(stateless) 입니다. 각 invoke() 호출은 완전히 독립된 요청이므로, 이전 대화 내용을 전혀 기억하지 못합니다.
1
2
3
4
5
| llm.invoke('내 이름은 abc입니다.').content
# → '안녕하세요, abc님! 어떻게 도와드릴까요?'
llm.invoke('내 이름은 뭔가요?').content
# → '죄송하지만, 당신의 이름은 알 수 없습니다...'
|
▶ 문제: 두 번째 호출 때는 첫 번째 호출의 정보가 없기 때문에 이름을 모릅니다.
해결책: 대화 기록을 직접 전달
messages 리스트에 대화 히스토리를 누적해서 LLM에 함께 전달해야 합니다.
1
2
3
4
5
6
7
| [SystemMessage] 당신은 친절한 AI 어시스턴트입니다
↓
[HumanMessage] 내 이름은 abc입니다
↓
[AIMessage] 반가워요, abc님!
↓
[HumanMessage] 내 이름은 뭔가요? ← 이 시점에 위의 히스토리가 함께 전달됨
|
메모리 전략 3가지
1
2
3
4
5
6
7
| ┌─────────────────────────────────────────────────────┐
│ 메모리 전략 │
│ │
│ 1. Buffer : 모든 대화 저장 (무제한) │
│ 2. Window : 최근 K개 턴만 유지 (슬라이딩 윈도우) │
│ 3. Summarize : 오래된 대화를 요약으로 압축 │
└─────────────────────────────────────────────────────┘
|
2. 수동으로 대화 기록 관리하기
messages 리스트에 SystemMessage / HumanMessage / AIMessage를 순서대로 추가합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| from langchain_core.messages import HumanMessage, SystemMessage, AIMessage
messages = [
SystemMessage(content='당신은 친절한 AI 어시스턴트입니다'),
HumanMessage(content='내 이름은 abc입니다. 반가워요')
]
response1 = llm.invoke(messages)
print(response1.content)
# → 반가워요, abc님! 어떻게 도와드릴까요?
# AI 응답을 기록에 추가하고, 다음 질문도 추가
messages.append(AIMessage(content=response1.content))
messages.append(HumanMessage(content='내 이름은 뭔가요?'))
response2 = llm.invoke(messages)
print(response2.content)
# → 당신의 이름은 abc입니다! 다른 질문이 있으신가요?
|
▶ 포인트: AIMessage를 직접 messages에 추가해야 다음 턴에서 이전 맥락을 기억합니다.
3. 토큰 카운팅 (tiktoken)
대화가 길어질수록 토큰 수가 증가해 비용과 속도에 영향을 줍니다. tiktoken으로 현재 대화의 토큰 수를 측정할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
| import tiktoken
enc = tiktoken.encoding_for_model('gpt-4o-mini')
def count_tokens(messages):
total = 0
for msg in messages:
total += len(enc.encode(msg.content))
total += 4 # 메시지 구분자 토큰 (OpenAI 규격)
return total
|
실제 측정 예시:
| 턴 수 | 누적 토큰 |
|---|
| 1턴 (교환 1회) | 50 |
| 2턴 (교환 2회) | 82 |
▶ 주의: 대화가 쌓일수록 토큰이 선형 증가합니다. 100턴이면 수천 토큰 → 메모리 전략 필수.
4. ConversationBufferMemory (langchain_classic)
Deprecated 경고 발생 (현재는 langchain_core의 InMemoryChatMessageHistory 권장)
기본 사용법
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| from langchain_classic.memory import ConversationBufferMemory
memory = ConversationBufferMemory(return_messages=True) # True: 메시지 객체 반환
memory.save_context(
{'input': '안녕하세요, 저는 abc입니다'},
{'output': '안녕하세요, abc님! 만나서 반갑습니다'}
)
memory.save_context(
{'input': '오늘 날씨가 좋네요!'},
{'output': '네, 정말 화창한 날씨입니다'}
)
history = memory.load_memory_variables({})
|
return_messages 옵션 차이
return_messages | 반환 형식 |
|---|
True | [HumanMessage(...), AIMessage(...), ...] — 메시지 객체 리스트 |
False | 'Human: ...\nAI: ...' — 하나의 문자열 |
1
2
3
4
5
6
7
8
9
10
| # return_messages=True 결과
{'history': [
HumanMessage(content='안녕하세요, 저는 abc입니다'),
AIMessage(content='안녕하세요, abc님! 만나서 반갑습니다'),
HumanMessage(content='오늘 날씨가 좋네요!'),
AIMessage(content='네, 정말 화창한 날씨입니다')
]}
# return_messages=False 결과
{'history': 'Human: 안녕하세요, 저는 abc입니다\nAI: 안녕하세요, abc님! 만나서 반갑습니다\n...'}
|
▶ 실무 팁: 체인(Chain)에 주입할 때는 return_messages=True(메시지 리스트)가 더 범용적입니다.
5. InMemoryChatMessageHistory (langchain_core)
langchain_core에서 제공하는 현대적인 방식. Deprecated 경고 없음.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| from langchain_core.chat_history import InMemoryChatMessageHistory
chat_history = InMemoryChatMessageHistory()
# 메시지 추가
chat_history.add_user_message('안녕하세요, 저는 abc입니다')
chat_history.add_ai_message('안녕하세요, abc님! 만나서 반갑습니다')
chat_history.add_user_message('오늘 날씨가 좋네요')
chat_history.add_ai_message('네, 정말 화창한 날씨입니다')
# 메시지 조회
for msg in chat_history.messages:
print(msg.type, msg.content)
# human 안녕하세요, 저는 abc입니다
# ai 안녕하세요, abc님! 만나서 반갑습니다
# ...
|
.messages 직접 조작
1
2
3
4
5
6
7
8
9
10
| # 리스트처럼 직접 append 가능
chat_history.messages.append(HumanMessage(content='내 이름이 뭔가요?'))
# 또는 스프레드(*)로 기존 히스토리에 새 메시지 추가
messages = [
*chat_history.messages,
HumanMessage(content='내 이름이 뭔가요?')
]
llm.invoke(messages)
# → '당신의 이름은 "abc"입니다.'
|
▶ 포인트: *chat_history.messages로 언패킹하면 히스토리를 건드리지 않고 임시 메시지 추가가 가능합니다.
6. RunnableWithMessageHistory — 세션 기반 자동 관리
수동으로 messages에 추가하는 번거로움 없이, 세션 ID 기반으로 히스토리를 자동 관리합니다.
구조 흐름
1
2
3
4
5
6
7
8
9
10
| 사용자 입력 (input)
↓
ChatPromptTemplate
[SystemMessage: 역할 설정]
[MessagesPlaceholder: 히스토리 자동 삽입]
[HumanMessage: 사용자 입력]
↓
LLM
↓
AI 응답 → 자동으로 store[session_id]에 저장
|
구현 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_core.chat_history import InMemoryChatMessageHistory
# 1. 프롬프트 템플릿 (MessagesPlaceholder가 히스토리 자리)
prompt = ChatPromptTemplate.from_messages([
('system', '당신은 친절한 AI 어시스턴트입니다'),
MessagesPlaceholder(variable_name='history'), # ← 히스토리가 여기에 자동 삽입
('human', '{input}')
])
chain = prompt | llm
# 2. 세션 저장소 (세션 ID → InMemoryChatMessageHistory)
store = {}
def get_session_history(session_id):
if session_id not in store:
store[session_id] = InMemoryChatMessageHistory()
return store[session_id]
# 3. 히스토리 자동 관리 래퍼 생성
chain_with_history = RunnableWithMessageHistory(
chain,
get_session_history,
input_messages_key='input', # chain의 입력 키
history_messages_key='history' # MessagesPlaceholder의 variable_name
)
|
실행 방법
1
2
3
4
5
6
7
8
9
10
11
| # session_id로 어떤 사용자의 히스토리를 쓸지 지정
config = {'configurable': {'session_id': 'user_001'}}
r1 = chain_with_history.invoke({'input': '안녕하세요, 제 이름은 abc입니다'}, config=config)
# → '안녕하세요, abc님! 어떻게 도와드릴까요?'
r2 = chain_with_history.invoke({'input': '내 이름이 뭐라고 했죠?'}, config=config)
# → '당신의 이름은 abc입니다. 맞나요?'
r3 = chain_with_history.invoke({'input': '오늘 뭐하면 좋을까요?'}, config=config)
# → '오늘 할 수 있는 다양한 아이디어가 있습니다! ...'
|
실전 예제 — 전자제품 판매원 챗봇
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| prompt = ChatPromptTemplate.from_messages([
('system', '당신은 친절한 전자제품 판매원입니다'),
MessagesPlaceholder(variable_name='history'),
('human', '{input}')
])
chain = prompt | llm
store = {}
def get_session_history(session_id):
if session_id not in store:
store[session_id] = InMemoryChatMessageHistory()
return store[session_id]
chain_with_history = RunnableWithMessageHistory(
chain, get_session_history,
input_messages_key='input',
history_messages_key='history'
)
config = {'configurable': {'session_id': 'user_001'}}
r1 = chain_with_history.invoke({'input': '안녕하세요, 노트북을 사려는데 어떤게 있나요?'}, config=config)
r2 = chain_with_history.invoke({'input': '가장 인기 있는 제품의 가격이 어떻게 되나요?'}, config=config)
r3 = chain_with_history.invoke({'input': '좋아요, 2번째 제품으로 구입하겠습니다.'}, config=config)
# → AI가 "2번째 제품"이 무엇인지 히스토리를 통해 파악하여 답변
|
대화 기록 확인:
1
2
3
4
5
6
7
8
9
10
| for msg in store['user_001'].messages:
prefix = 'customer' if msg.type == 'human' else 'sales person'
print(f"[{prefix}] - {msg.content[:30]}")
# [customer] - 안녕하세요, 노트북을 사려는데 어떤게 있나요?
# [sales person] - 안녕하세요! 노트북을 구매하시려는군요...
# [customer] - 가장 인기 있는 제품의 가격이 어떻게 되나요?
# [sales person] - 인기 있는 노트북 제품들의 가격은...
# [customer] - 좋아요, 2번째 제품으로 구입하겠습니다.
# [sales person] - 삼성 노트북 9을 선택해 주셨군요!...
|
7. Window Memory — 최근 N개 턴만 유지
대화가 길어지면 오래된 내용은 버리고 최근 K턴만 메모리에 유지합니다.
방법 1: ConversationBufferWindowMemory
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| from langchain_classic.memory import ConversationBufferWindowMemory
window_memory = ConversationBufferWindowMemory(k=2, return_messages=True)
# k=2 → 최근 2턴(Human+AI 쌍 2개)만 유지
window_memory.save_context({'input': '첫번째, 내 이름은 abc입니다'}, {'output': '안녕하세요, abc님'})
window_memory.save_context({'input': '두번째, 나는 학생입니다'}, {'output': '학생이시군요! 멋지십니다'})
window_memory.save_context({'input': '세번째, 나는 파이썬을 좋아합니다'}, {'output': '파이썬은 정말 재미있죠!'})
window_memory.load_memory_variables({})
# → 첫번째 대화는 사라지고, 두번째 + 세번째만 남음
# {'history': [
# HumanMessage('두번째, 나는 학생입니다'),
# AIMessage('학생이시군요! 멋지십니다'),
# HumanMessage('세번째, 나는 파이썬을 좋아합니다'),
# AIMessage('파이썬은 정말 재미있죠!')
# ]}
|
방법 2: trim_messages (langchain_core)
1
2
3
4
5
6
7
8
9
10
11
| from langchain_core.messages import trim_messages
trimmer = trim_messages(
max_tokens=4, # 최대 4개 메시지까지만 유지
strategy='last', # 최신 것부터 선택
token_counter=len, # 토큰 카운터 (여기서는 메시지 개수 기준)
start_on='human' # Human 메시지로 시작 (쌍 맞추기)
)
trimmed = trimmer.invoke(messages)
# → 최근 4개 메시지만 반환 (두번째, 세번째 대화만 남음)
|
방법 3: 수동 슬라이싱
1
2
3
4
5
6
7
8
| messages = []
K = 2 # 유지할 턴 수
def add_turn(user_input, ai_output):
global messages
messages.append(HumanMessage(content=user_input))
messages.append(AIMessage(content=ai_output))
messages = messages[-(K * 2):] # 최근 K턴(메시지 K*2개)만 유지
|
실행 결과:
1
2
3
| 턴 1 추가 후: [Human(첫번째), AI(첫번째)] → 2개
턴 2 추가 후: [Human(첫번째), AI(첫번째), Human(두번째), AI(두번째)] → 4개 (K*2=4)
턴 3 추가 후: [Human(두번째), AI(두번째), Human(세번째), AI(세번째)] → 4개 (첫번째 밀려남)
|
▶ 포인트: messages[-(K*2):]로 슬라이싱하면 K턴 윈도우를 간단하게 구현 가능.
8. Summary Memory — 오래된 대화 요약으로 압축
오래된 대화를 LLM이 요약하여 압축 보관. 디테일 일부 유지 + 토큰 절약.
방법 1: ConversationSummaryMemory (langchain_classic)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| from langchain_classic.memory import ConversationSummaryMemory
summary_memory = ConversationSummaryMemory(
llm=ChatOpenAI(model='gpt-4o-mini'),
return_messages=False
)
conversations = [
('첫번째, 내 이름은 abc입니다', '안녕하세요, abc님'),
('두번째, 나는 학생입니다', '학생이시군요! 멋지십니다'),
('세번째, 나는 파이썬을 좋아합니다', '파이썬은 정말 재미있죠!')
]
for user_msg, ai_msg in conversations:
summary_memory.save_context({'input': user_msg}, {'output': ai_msg})
result = summary_memory.load_memory_variables({})
print(result['history'])
# → 'The human introduces themselves as abc. The AI greets abc.
# The human then states that they are a student, and the AI praises them...
# The human adds that they like Python, to which the AI responds that Python is indeed very fun.'
|
▶ 주의: ConversationSummaryMemory는 영어로 요약하는 경향이 있습니다. 한국어 강제 지정이 필요하면 커스텀 구현을 권장합니다.
방법 2: 커스텀 SummaryMemory 클래스
summary_interval 턴마다 자동으로 요약을 수행하는 커스텀 메모리 클래스입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| class SummaryMemory:
def __init__(self, summary_interval=3):
self.summary = '' # 누적 요약 텍스트
self.recent_messages = [] # 아직 요약되지 않은 최근 메시지들
self.summary_interval = summary_interval # 몇 턴마다 요약할지
self.turn_count = 0
def _summarize(self, text):
"""기존 요약 + 새 대화를 합쳐서 LLM으로 재요약"""
response = llm.invoke([
SystemMessage(content='주어진 대화 내용을 핵심만 간결하게 요약하세요. 한국어로 작성하세요'),
HumanMessage(content=f'기존 요약:\n{self.summary}\n\n새 대화:\n{text}')
])
return response.content
def add_exchange(self, user_msg, ai_msg):
"""새 대화 턴 추가. summary_interval 도달 시 자동 요약"""
self.recent_messages.append(f'사용자 : {user_msg}')
self.recent_messages.append(f'AI : {ai_msg}')
self.turn_count += 1
if self.turn_count % self.summary_interval == 0:
conversation_text = '\n'.join(self.recent_messages)
self.summary = self._summarize(conversation_text) # 요약 업데이트
self.recent_messages = [] # 최근 메시지 초기화
print(f' 요약 완료 턴 {self.turn_count} 에서 요약')
def get_context(self):
"""LLM에 전달할 컨텍스트 문자열 반환"""
parts = []
if self.summary:
parts.append(f'[이전 대화 요약] {self.summary}')
if self.recent_messages:
parts.append(f'[최근 대화]\n' + '\n'.join(self.recent_messages))
return '\n\n'.join(parts)
|
동작 원리
1
2
3
4
5
6
7
8
9
10
11
12
| 대화 1턴: recent_messages에 추가
대화 2턴: recent_messages에 추가 ← summary_interval=2 도달!
→ recent_messages를 LLM으로 요약 → self.summary 업데이트
→ recent_messages = [] 초기화
대화 3턴: recent_messages에 추가
get_context() 호출 시:
[이전 대화 요약] 사용자가 이름과 학생임을 소개하였고, AI는 인사를 하고...
[최근 대화]
사용자 : 세번째, 나는 파이썬을 좋아합니다
AI : 파이썬은 정말 재미있죠!
|
실사용 예시:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| smem = SummaryMemory(summary_interval=2)
conversations = [
('첫번째, 내 이름은 abc입니다', '안녕하세요, abc님'),
('두번째, 나는 학생입니다', '학생이시군요! 멋지십니다'), # ← 2턴: 요약 실행
('세번째, 나는 파이썬을 좋아합니다', '파이썬은 정말 재미있죠!')
]
for user_msg, ai_msg in conversations:
smem.add_exchange(user_msg, ai_msg)
# → '요약 완료 턴 2 에서 요약' 출력
smem.get_context()
# → '[이전 대화 요약] 사용자가 이름과 학생임을 소개하였고...\n\n[최근 대화]\n사용자: 세번째...'
|
9. SummaryChatbot — 요약 메모리 통합 챗봇
SummaryMemory를 활용한 실전 챗봇 클래스입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| class SummaryChatbot:
def __init__(self, system_prompt='당신은 도움이 되는 AI 어시스턴트입니다.', summary_interval=3):
self.system_prompt = system_prompt
self.memory = SummaryMemory(summary_interval=summary_interval)
def chat(self, user_input):
# 1. 현재 메모리 컨텍스트 가져오기
context = self.memory.get_context()
# 2. 메시지 구성
messages = [SystemMessage(content=self.system_prompt)]
if context:
messages.append(SystemMessage(content=f'대화 맥락 : \n{context}'))
messages.append(HumanMessage(content=user_input))
# 3. LLM 호출
response = llm.invoke(messages)
ai_response = response.content
# 4. 메모리에 저장
self.memory.add_exchange(user_input, ai_response)
return ai_response
|
실전 예제 — IT 커리어 상담사
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| bot = SummaryChatbot(
system_prompt='당신은 IT 커리어 상담사입니다',
summary_interval=2
)
questions = [
"안녕하세요, 백엔드 개발자 3년차인데 고민이 있습니다",
"AI/ML 분야로 전환을 고려 중인데 어떤 준비가 필요할까요?",
"현재 Python은 잘하는데 수학과 통계 기초가 부족합니다",
"온라인 강의와 학교 중 어떤 것이 효과적일까요?"
]
for q in questions:
print(f'[user] {q}')
answer = bot.chat(q)
print(f'[상담사] {answer}')
|
실행 출력:
1
2
3
4
5
6
7
8
9
| [user] 안녕하세요, 백엔드 개발자 3년차인데 고민이 있습니다
[상담사] 안녕하세요! 백엔드 개발자로 3년 차라니 멋지네요...
[user] AI/ML 분야로 전환을 고려 중인데 어떤 준비가 필요할까요?
요약 완료 턴 2 에서 요약 ← summary_interval=2 도달, 자동 요약
[상담사] AI/ML 분야로의 전환을 고려하고 계시군요. 다음은 준비에 도움이 될 단계입니다:
1. 기초 수학 및 통계학 이해 (선형 대수, 미적분학, 확률/통계)
2. Python 심화 학습
3. ML 기본 개념 학습 ...
|
챗봇 클래스 데이터 흐름
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| user_input
↓
memory.get_context()
├─ [이전 대화 요약] ... (summary가 있을 경우)
└─ [최근 대화] ... (recent_messages가 있을 경우)
↓
messages 구성:
[SystemMessage: 역할]
[SystemMessage: 맥락] ← 있을 경우만
[HumanMessage: 질문]
↓
llm.invoke(messages)
↓
AI 응답
↓
memory.add_exchange(user_input, ai_response)
→ turn_count 증가
→ summary_interval 도달 시 자동 요약
|
10. 자주 나오는 실수 / 주의사항
❌ AIMessage를 빠뜨리는 실수
1
2
3
4
5
6
7
8
9
10
| # 잘못된 예
messages.append(HumanMessage('내 이름은 abc입니다'))
response = llm.invoke(messages)
messages.append(HumanMessage('내 이름은 뭔가요?')) # ← AIMessage 누락!
# 올바른 예
messages.append(HumanMessage('내 이름은 abc입니다'))
response = llm.invoke(messages)
messages.append(AIMessage(response.content)) # ← AI 응답도 반드시 추가
messages.append(HumanMessage('내 이름은 뭔가요?'))
|
❌ session_id 없이 RunnableWithMessageHistory 호출
1
2
3
4
5
6
| # 잘못된 예 (config 없음)
chain_with_history.invoke({'input': '안녕'})
# 올바른 예
config = {'configurable': {'session_id': 'user_001'}}
chain_with_history.invoke({'input': '안녕'}, config=config)
|
❌ MessagesPlaceholder의 variable_name과 history_messages_key 불일치
1
2
3
4
5
6
7
8
9
10
11
12
| # 잘못된 예
MessagesPlaceholder(variable_name='chat_history') # 'chat_history'로 설정
RunnableWithMessageHistory(
chain,
get_session_history,
history_messages_key='history' # 'history'로 다름 → 오류!
)
# 올바른 예: 이름 일치
MessagesPlaceholder(variable_name='history')
RunnableWithMessageHistory(..., history_messages_key='history')
|
⚠️ ConversationBufferMemory는 Deprecated
langchain_classic.memory 모듈의 클래스들은 LangChain v0.2+ 에서 deprecated.- 신규 프로젝트에서는
langchain_core.chat_history.InMemoryChatMessageHistory 사용 권장.
⚠️ SummaryMemory의 summary_interval 선택
summary_interval이 너무 작으면 LLM 요약 호출이 잦아져 비용 증가.summary_interval이 너무 크면 메모리 절감 효과가 없음.- 일반적으로 5~10 턴 권장.
11. [보충] 메모리 전략 비교
| 전략 | 특징 | 토큰 비용 | 맥락 유실 | 추가 LLM 호출 |
|---|
| Buffer | 전체 대화 저장 | 매우 높음 | 없음 | 없음 |
| Window (k턴) | 최근 k턴만 유지 | 낮음 | 오래된 것 유실 | 없음 |
| Summary | 오래된 것 요약 압축 | 중간 | 일부 유실 | 있음 (요약용) |
LangGraph와의 관계
1
2
3
| LangChain Chain : 단방향 파이프라인. memory를 내부에 자동 관리.
LangGraph : 상태(State) 기반 그래프. 개발자가 state를 직접 설계.
더 복잡한 multi-turn, multi-agent 시나리오에 적합.
|
요약 메모리 활용 시 컨텍스트 구조
1
2
3
4
5
6
7
8
9
10
11
12
13
| ┌──────────────────────────────────────────────┐
│ SystemMessage: 역할 설정 │
├──────────────────────────────────────────────┤
│ SystemMessage: 대화 맥락 │
│ [이전 대화 요약] 요약된 내용... │
│ [최근 대화] │
│ 사용자: 최근 질문1 │
│ AI: 최근 답변1 │
│ 사용자: 최근 질문2 │
│ AI: 최근 답변2 │
├──────────────────────────────────────────────┤
│ HumanMessage: 현재 질문 │
└──────────────────────────────────────────────┘
|
끝