4장. 대화형 챗봇과 메모리 전략
손님마다 기록 상자 나누기
여행사에는 손님마다 기록철이 따로 있고, 철 겉면에는 회원번호가 적혀 있습니다. 손님이 오면 회원번호로 철을 찾아 펼치고, 상담이 끝나면 오늘의 장을 끼워 다시 꽂습니다. 처음 온 손님에게는 새 철을 만듭니다. LangChain 에서 이 철이 InMemoryChatMessageHistory, 회원번호가 세션 id, 철을 찾아 펼치고 다시 꽂는 비서가 RunnableWithMessageHistory 입니다.
네 상자는 bot.invoke(...) 한 번에 일어나는 일입니다. 우리가 세션 id 를 함께 넘기면, 그 id 의 기록 상자를 꺼내 프롬프트의 기록 자리에 넣고, 체인을 실행한 뒤, 질문과 답을 그 상자에 덧붙입니다. 앞 절의 chat() 함수가 손으로 하던 일을 부품 하나가 해 줍니다. 여기서 회원번호가 세션 id, 기록철이 InMemoryChatMessageHistory, 철을 다루는 비서가 RunnableWithMessageHistory 입니다. 비유와 다른 점은, 이 철은 컴퓨터 메모리에만 있어서 프로그램을 끄면 모두 사라진다는 것입니다.
RunnableWithMessageHistory 에 넘기는 것
| 인자 | 뜻 | 이 장의 값 |
|---|---|---|
| 첫째 인자 | 감쌀 체인 | prompt | model |
get_session_history | 세션 id 를 받아 기록 상자를 돌려주는 함수 | get_session_history |
input_messages_key | 입력 사전에서 새 질문이 든 열쇠 | "question" |
history_messages_key | 프롬프트의 기록 자리 이름 | "history" |
config={"configurable": {"session_id": …}} | 부를 때 넘기는 세션 id. configurable 은 "설정할 수 있는"이라는 뜻 | "kim", "lee" |
짧은 예시
가짜 모델로 손님 두 명(kim, lee)의 대화를 나눠 담습니다.
import warnings
from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.language_models.fake_chat_models import FakeListChatModel
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables.history import RunnableWithMessageHistory
# 두 부품이 띄우는 "사용 중단 예정" 경고(아래 주의할 점)를 읽었으면 이 줄로 숨긴다
warnings.filterwarnings("ignore", category=DeprecationWarning)
store = {} # {세션 id: 기록 상자}
def get_session_history(session_id):
# 처음 보는 세션이면 빈 기록 상자를 새로 만든다
if session_id not in store:
store[session_id] = InMemoryChatMessageHistory()
return store[session_id]
prompt = ChatPromptTemplate.from_messages([
("system", "너는 여행 비서다."), MessagesPlaceholder("history"), ("human", "{question}"),
])
model = FakeListChatModel(responses=["며칠인가요?", "어디로 가시나요?", "2박 3일 좋습니다."])
bot = RunnableWithMessageHistory(
prompt | model, # 감쌀 체인
get_session_history, # 세션 id → 기록 상자
input_messages_key="question", # 새 질문이 든 열쇠
history_messages_key="history", # 프롬프트의 기록 자리 이름
)
for sid, q in [("kim", "제주도 가요"), ("lee", "여행 가고 싶어요"), ("kim", "2박 3일이요")]:
reply = bot.invoke({"question": q}, config={"configurable": {"session_id": sid}})
print(sid, "→", reply.content)
# 세션마다 기록 상자에 쌓인 메시지 수
print({sid: len(h.messages) for sid, h in store.items()})kim → 며칠인가요?
lee → 어디로 가시나요?
kim → 2박 3일 좋습니다.
{'kim': 4, 'lee': 2}kim 은 두 번 물어 질문·답 두 쌍(메시지 4개)이, lee 는 한 번 물어 한 쌍(2개)이 쌓였습니다. 두 손님의 기록이 섞이지 않았습니다. 기록 상자에는 시스템 지시가 들어가지 않습니다. 시스템 지시는 프롬프트 틀에 있어서 요청마다 새로 붙기 때문입니다.
코드가 의미하는 것
이 코드는 회원번호로 기록철을 찾아 펴고 다시 꽂는 비서를 부품으로 세운 것입니다. 철 보관함과 철을 찾는 함수를 두고, 체인을 비서로 감싸고, 두 손님과 번갈아 이야기합니다.
| 코드 | 하는 일 | 챗봇에서 뜻하는 것 |
|---|---|---|
store = {} 와 get_session_history | 세션 id 마다 기록 상자를 찾거나 만든다 | 손님별 기록철 보관함입니다. 처음 온 손님에게만 새 철을 만듭니다 |
RunnableWithMessageHistory(prompt | model, …) | 체인을 기록 다루는 부품으로 감싼다 | 앞 절의 chat() 이 손으로 하던 꺼내기와 덧붙이기를 대신합니다 |
input_messages_key, history_messages_key | 새 질문의 열쇠와 기록 자리 이름을 알려 준다 | 무엇을 기록에 덧붙이고 기록을 어디에 넣을지 정하는 이름표입니다 |
config={"configurable": {"session_id": sid}} | 부를 때 어느 손님인지 넘긴다 | 웹에서는 로그인한 사람의 번호처럼 손님마다 다른 값이 들어갈 자리입니다 |
결과에서 읽을 것은 두 가지입니다.
- 번갈아 물어도 섞이지 않았다.
{'kim': 4, 'lee': 2}로 손님마다 자기 대화만 쌓였습니다. - 답의 내용은 기록과 상관없다. 가짜 모델은 목록의 답을 차례로 돌려주므로 "2박 3일 좋습니다."가
kim에게 간 것은 적어 둔 순서 때문입니다. 이 예시에서 볼 것은 답이 아니라 기록의 수입니다.
주의할 점
- 지금 판의 langchain-core(1.7)에서 이 두 부품은 "사용 중단 예정(deprecated)"으로 표시되어 있어서, 처음 쓸 때 경고 글이 나옵니다.
RunnableWithMessageHistory는 1.3.3 판부터,InMemoryChatMessageHistory는 1.6.4 판부터이고, 둘 다 2.0 판에서 빠질 예정이라고 적혀 있습니다. 경고는 LangGraph 의 저장 기능(persistence)을 대신 쓰라고 권합니다. 이 과정은 1.x 판에서 그대로 돌아가고 원리를 보기 쉬운 이 두 부품으로 배우고, 실습 코드에서는 경고를 읽은 뒤warnings.filterwarnings로 숨깁니다. 판을 올릴 때는 공식 문서의 단기 기억(short-term memory) 안내를 확인합니다. - 세션 id 는 손님을 가리는 열쇠입니다. 웹에서는 로그인한 사람의 번호나 브라우저마다 만든 값으로 정합니다. 남의 세션 id 로 부르면 남의 대화가 보이므로 아무나 정할 수 있게 두지 않습니다.
- 기록을 오래 두어야 하면 메모리 대신 파일이나 데이터베이스에 저장합니다. 실습 3에서는 기록을 json 파일로 남겨 봅니다.