728x90
반응형

어제(26일차)까지는 VectorStoreRetrieverMemory로 의미 기반 회상을 만들고, LCEL 파이프라인에 메모리를 “붙이는 법”을 살폈죠.

오늘은 그 연장선으로, 실전 멀티턴 대화가 가능한 Streamlit 챗봇을 완성합니다. 핵심 포인트는 두 가지입니다.

  1. LCEL + RunnableWithMessageHistory로 대화 히스토리를 자동 주입
  2. 세션 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()로 화면용 로그를 세션 상태에 따로 저장(렌더링에 사용)

전체 처리 순서 (요약 다이어그램)

  1. 사용자 입력 (st.chat_input)
  2. 세션 ID로 히스토리 객체 획득 (get_session_history)
  3. LCEL 체인에 RunnableWithMessageHistory가 히스토리 주입
  4. 프롬프트(system + chat_history + human) → LLM스트리밍 출력
  5. UI용 배열(st.session_state['messages'])에 최종 turn 저장(화면 재렌더 시 사용)

실전 확장 아이디어

  • RAG 접목: retriever를 history 앞/뒤에 붙여 “지식 + 대화”를 함께 컨텍스트로 전달
  • 요약형 메모리: 대화가 길어질 때 ConversationSummaryBufferMemory로 히스토리 압축
  • 세션 관리 UI: 최근 세션 목록/삭제/복원 기능 추가
  • 보안/프라이버시: 민감 발화(PII) 마스킹 후 히스토리에 저장

마무리

오늘의 멀티턴 챗봇은, LCEL 파이프라인에 Memory를 정석적으로 주입하는 좋은 예시입니다.

RunnableWithMessageHistory + MessagesPlaceholder 조합만 기억하면, 어떤 체인에도 대화 맥락을 자연스럽게 끼워 넣을 수 있습니다.

전체 코드는 다음 링크에서 확인할 수 있습니다.

 

계속해서 발전해 나가는 감자가 되자..!

728x90
반응형

+ Recent posts