랭체인으로 챗봇을 만들면 첫 번째 벽에 부딪힙니다. '아까 제 이름이 다비드라고 했잖아요'라고 하면 AI가 '무슨 이름이요?'라고 답하는 것이죠. LLM 자체는 매 호출이 독립적이라 과거를 기억하지 못합니다. 대화 내용을 직접 저장해 두고, 새 질문을 보낼 때마다 함께 담아 보내면 이 문제가 해결됩니다.
왜 기억을 못 할까: LLM의 무상태(stateless) 특성
LLM은 전화를 끊으면 모든 것을 잊어버리는 사람과 같습니다. 다음 전화를 걸면 초면인 상태입니다. 챗봇에 기억을 주려면 우리가 대화 기록을 저장하고, 새 질문을 보낼 때 이전 대화 내역을 함께 첨부해야 합니다. 랭체인의 RunnableWithMessageHistory가 이 반복 작업을 자동화해 줍니다.
- 기록장(ChatMessageHistory): 대화 내역을 저장하는 객체. 인메모리·Redis·DB 등 교체 가능
- 세션 ID(session_id): 사용자나 대화방을 구분하는 고유 식별자. 같은 ID면 같은 기록장 사용
- 플레이스홀더(MessagesPlaceholder): 프롬프트에서 이전 대화가 펼쳐질 빈 슬롯
코드로 구현: 기억하는 챗봇 만들기
InMemoryChatMessageHistory + RunnableWithMessageHistory
from langchain.chat_models import init_chat_model
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.output_parsers import StrOutputParser
from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
# 세션 ID를 키로, 기록장 객체를 값으로 담는 딕셔너리
store = {}
def get_session_history(session_id: str) -> InMemoryChatMessageHistory:
# 처음 방문하면 새 기록장을, 이미 있으면 기존 것을 돌려줍니다
if session_id not in store:
store[session_id] = InMemoryChatMessageHistory()
return store[session_id]
# MessagesPlaceholder = 이전 대화가 끼워질 프롬프트의 빈 슬롯
prompt = ChatPromptTemplate.from_messages([
('system', '너는 친절한 한국어 AI 어시스턴트야.'),
MessagesPlaceholder(variable_name='history'),
('human', '{input}'),
])
chain = prompt | init_chat_model('openai:gpt-4o-mini') | StrOutputParser()
# 체인에 메모리 레이어를 씌웁니다
chain_with_memory = RunnableWithMessageHistory(
chain,
get_session_history, # 세션 ID -> 기록장 반환 함수
input_messages_key='input', # 사용자 입력의 딕셔너리 키
history_messages_key='history', # 프롬프트의 history 슬롯 이름
)
실제 대화: 이름을 기억하는지 확인
같은 session_id로 연속 대화하기
# config에 session_id를 담아 호출합니다
config = {'configurable': {'session_id': 'user-alice'}}
# 첫 번째 대화: 이름을 알려줍니다
reply1 = chain_with_memory.invoke(
{'input': '안녕, 나는 앨리스야. 파이썬 공부 중이야.'},
config=config,
)
print('AI:', reply1)
# 두 번째 대화: 이름을 기억하는지 확인합니다
reply2 = chain_with_memory.invoke(
{'input': '내 이름이 뭔지 기억해?'},
config=config,
)
print('AI:', reply2) # 앨리스 라고 답해야 합니다
# 쌓인 메시지 기록 출력
for msg in store['user-alice'].messages:
print(f'{msg.type}: {msg.content[:40]}')
session_id가 같으면 대화가 이어지고, 다른 session_id를 주면 완전히 새 대화가 시작됩니다. 사용자 A와 사용자 B의 대화 기록이 완전히 분리됩니다.
대화가 길어지면: 토큰 비용을 줄이는 전략
대화가 수십 번 이어지면 기록이 너무 길어져 토큰 비용이 폭발합니다. 두 가지 전략으로 대응하세요. 첫째, 슬라이딩 윈도우: 최근 N개 메시지만 유지하고 오래된 것은 버립니다. 둘째, 요약 압축: LLM으로 오래된 대화를 한 단락으로 압축한 뒤 요약문 하나로 교체합니다. 실서비스에서는 두 방법을 조합해 씁니다.
| 저장소 종류 | 패키지 | 특징 |
|---|---|---|
| InMemoryChatMessageHistory | langchain-core | 개발·테스트용, 재시작 시 초기화 |
| RedisChatMessageHistory | langchain-community | 빠른 영구 저장, 실서비스 적합 |
| PostgresChatMessageHistory | langchain-community | 기존 PostgreSQL DB와 통합 가능 |
session_id는 사용자 인증 후 받는 user_id 또는 대화방 UUID처럼 고유한 값을 쓰세요. get_session_history 함수 내부만 바꾸면 저장소를 교체할 수 있으므로, 개발 단계에서 인메모리로 시작하고 운영 단계에서 Redis로 교체하는 전략이 깔끔합니다.
InMemoryChatMessageHistory는 서버를 재시작하면 모든 기록이 사라집니다. 운영 환경에서는 반드시 Redis나 DB 기반 저장소로 교체하세요. 대화 기록에 민감한 개인정보가 포함될 수 있으므로 저장 시 암호화와 보관 기간 정책도 함께 설계하세요.