728x90
반응형
어제(26일차)까지는 VectorStoreRetrieverMemory로 의미 기반 회상을 만들고, LCEL 파이프라인에 메모리를 “붙이는 법”을 살폈죠.
오늘은 그 연장선으로, 실전 멀티턴 대화가 가능한 Streamlit 챗봇을 완성합니다. 핵심 포인트는 두 가지입니다.
- LCEL + RunnableWithMessageHistory로 대화 히스토리를 자동 주입
- 세션 ID별로 채팅 기록을 분리해, 탭처럼 독립 대화를 유지
아래 코드는 어제 구조에 멀티턴(대화 히스토리)만 추가한 버전입니다. 전체 코드는 질문 아래에 첨부해 주신 것을 기준으로 설명합니다.
무엇이 달라졌나 (한눈 개요)
- LangChain 메모리 축:
- ChatMessageHistory (실제 메시지 스토리지)
- RunnableWithMessageHistory (LCEL 체인에 히스토리 주입기)
- 프롬프트 축:
- ChatPromptTemplate + MessagesPlaceholder("chat_history")
- → 히스토리가 자동으로 system/human/ai 메시지로 들어감
- UI/세션 축:
- session_id를 사이드바에서 입력 → 같은 ID끼리 히스토리 공유
- st.session_state['store']에 세션별 ChatMessageHistory를 보관
- 대화 초기화 버튼으로 화면 메시지(렌더링용) 리셋
핵심 흐름 이해하기
1) 히스토리 저장소: get_session_history()
def get_session_history(session_ids):
if session_ids not in st.session_state['store']:
st.session_state['store'][session_ids] = ChatMessageHistory()
return st.session_state['store'][session_ids]
- 세션 ID를 키로 ChatMessageHistory 인스턴스를 관리합니다.
- 같은 세션 ID로 들어오는 호출은 같은 히스토리를 공유해 “멀티턴”이 됩니다.
2) 프롬프트에 히스토리 꽂기: MessagesPlaceholder
prompt = ChatPromptTemplate.from_messages(
[
("system", "당신은 Question-Answering 챗봇입니다. ..."),
MessagesPlaceholder(variable_name="chat_history"),
("human", "#Question:\\n{question}"),
]
)
- variable_name="chat_history"는 고정 관례로 쓰는 걸 추천합니다.
- 이후 LCEL이 실행될 때, 이 자리에 과거 대화(human/ai message 쌍)가 자동 삽입됩니다.
3) LCEL + 히스토리 주입: RunnableWithMessageHistory
chain = prompt | llm | StrOutputParser()
chain_with_history = RunnableWithMessageHistory(
chain,
get_session_history,
input_messages_key="question",
history_messages_key="chat_history",
)
- input_messages_key="question": 입력 딕셔너리에서 사용자 메시지를 가져올 키
- history_messages_key="chat_history": 프롬프트의 MessagesPlaceholder 키와 매칭
- 실행 시 config={"configurable": {"session_id": session_id}}로 세션 선택
4) 스트리밍 응답 & 화면 메시지
response = chain.stream(
{"question": user_input},
config={"configurable": {"session_id": session_id}},
)
with st.chat_message('assistant'):
container = st.empty()
ai_answer = ''
for token in response:
ai_answer += token
container.markdown(ai_answer)
- LCEL의 .stream() 결과를 토큰 단위로 화면에 뿌려 자연스러운 출력
- 완료 후 add_message()로 화면용 로그를 세션 상태에 따로 저장(렌더링에 사용)
전체 처리 순서 (요약 다이어그램)
- 사용자 입력 (st.chat_input)
- 세션 ID로 히스토리 객체 획득 (get_session_history)
- LCEL 체인에 RunnableWithMessageHistory가 히스토리 주입
- 프롬프트(system + chat_history + human) → LLM → 스트리밍 출력
- UI용 배열(st.session_state['messages'])에 최종 turn 저장(화면 재렌더 시 사용)
실전 확장 아이디어
- RAG 접목: retriever를 history 앞/뒤에 붙여 “지식 + 대화”를 함께 컨텍스트로 전달
- 요약형 메모리: 대화가 길어질 때 ConversationSummaryBufferMemory로 히스토리 압축
- 세션 관리 UI: 최근 세션 목록/삭제/복원 기능 추가
- 보안/프라이버시: 민감 발화(PII) 마스킹 후 히스토리에 저장
마무리
오늘의 멀티턴 챗봇은, LCEL 파이프라인에 Memory를 정석적으로 주입하는 좋은 예시입니다.
RunnableWithMessageHistory + MessagesPlaceholder 조합만 기억하면, 어떤 체인에도 대화 맥락을 자연스럽게 끼워 넣을 수 있습니다.
전체 코드는 다음 링크에서 확인할 수 있습니다.
계속해서 발전해 나가는 감자가 되자..!

728x90
반응형