Chat History 추가와 Streaming 구현

2025. 8. 17. 14:49·RAG 을 활용하여 LLM 만들어보기

이번에는 create_history_aware_retriever 을 활용해서 chat history 를 받을 수 있도록 구조를 수정해보려고 한다.

 

최종 코드는 최하단에 추가해둘 예정이며, 전체적인 흐름은 아래와 같다.

  • user_question 으로 사용자에게 질문 받기
  • get_dictionary_chain() 및 invoke 로 정규화된 질문  normalized_q 생성
  • RunnableWithMessageHistory(rag_chain) 과
    get_session_history() 으로 세션별 대화 히스토리 자동 관리
  • create_stuff_documents_chain 를 통해 context 문서와 질문을 통해 LLM 답변을 생성
  • LLM답변 중 pick("answer").stream(..) 으 로 streamlit 에 스트리밍 출력.

1. user_question 으로 사용자에게 질문 받기

나는 streamlit 을 구성하는 부분과 LLM을 호출하는 부분을 분리해서 만들었다.
먼저 streamlit 을 통해서 user_question 을 받아온 뒤에 get_dictionary_chain() 으로 질문을 정규화한다.

chat.py
import streamlit as st
from llm_with_History import get_ai_message

st.set_page_config(page_title="Chat Application", page_icon="📈")

st.title("소득세 챗봇")
st.caption("소득세 관련 질문을 해보세요!")

if "messages" not in st.session_state:
    st.session_state.messages = []

# 세션에 저장된 메시지를 반복하고 각 메시지 역할에 따라 메시지 표시
print(f"before: {st.session_state.messages}")
for message in st.session_state.messages:
    with st.chat_message(message["role"]):
        st.write(message["content"])


if user_question := st.chat_input("소득세와 관련된 궁금한 내용들을 말씀해주세요!"):
    # 채팅이 입력되었을 때 상단에 메시지가 노출되도록 함.
    # "user", "assistant", "ai", "human" 가 있는데 그중 user 을 사용함.
    with st.chat_message("user"):
        st.write(user_question)

    # 채팅 메시지를 Session State에 저장
    st.session_state.messages.append({"role": "user", "content": user_question})


    # 로딩중인 부분을 보여주기
    with st.spinner("AI 응답을 생성하는 중..."):
        ai_message = get_ai_message(user_question)

        # with st.chat_message("ai"):
        #     st.write(ai_message)
        #     st.session_state.messages.append({"role": "ai", "content": ai_message})
        with st.chat_message("ai"):
            final_text = st.write_stream(ai_message)
            # 채팅 메시지를 Session State에 저장
    st.session_state.messages.append({"role": "ai", "content": final_text})

2. get_dictionary_chain() 및 invoke 를 통해 정규화된 질문을 normalized_q 변수에 입력하기

llm_with_History.py
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain.chains import create_history_aware_retriever, create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain_upstage import UpstageEmbeddings, ChatUpstage
from langchain_pinecone import PineconeVectorStore

# 자동 히스토리 관리용
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
from dotenv import load_dotenv
load_dotenv() 

def get_llm():
    llm = ChatUpstage()
    return llm


def get_dictionary_chain():
    llm = get_llm()
    dictionary = ["사람을 나타내는 표현은 모두 거주자로 변경해주세요"]

    prompt = ChatPromptTemplate.from_template(
        f"""
        사용자의 질문을 보고, 우리의 사전을 참고해서 사용자의 질문을 변경해주세요.
        변경할 필요가 없다고 판단되면, 질문 원문을 그대로 반환하세요.

        추가로 초과 금액은 단순하게 사용자가 제시한 금액에서 기준 금액을 뺀 값으로 계산해주세요.

        사전: {dictionary}
        질문: {{question}}
        """
        )

    return prompt | llm | StrOutputParser()


def get_ai_message(user_question: str, session_id: str = "abc123"):
    """
    - 사전 변환 → 히스토리 인지형 RAG 스트리밍
    - 문자열 제너레이터를 반환 (Streamlit의 st.write_stream에 바로 전달 가능)
    """
    # 1) 질문 정규화
    normalized_q = get_dictionary_chain().invoke({"question": user_question})

3. RunnableWithMessageHistory(rag_chain) 과 get_session_history() 으로 히스토리 저장 및 입력

4. create_stuff_documents_chain 를 통해 context 문서와 질문을 통해 LLM 답변을 생성

get_qa_chain() 에서 정규화된 질문을 히스토리 인지형 RAG 파이프라인을 사용하여 조립하는데
흐름은 아래와 같다.

  • get_history_retrieval() 함수를 통해 create_history_aware_retriever 으로
    채팅 기록과 새 질문을 입력받아 단독 질문으로 재작성하고 문서 검색을 수행한다.
  • answer_prompt 를 통해 최종 답변 프롬프트를 생성한다.
  • create_stuff_documents_chain() 으로 문서를 결합한다.
  • create_retriever_chain() 으로 최종 RAG 체인을 조립한다.
  • RunnableWithMessageHistory 를 활용하여 채팅 메시지 기록을 관리한다.
  • 이후 rag_with_history 에 최종 RAG 와 get_session_history 를 활용하여 결과를 받아옴.
llm_with_History.py
# 세션 히스토리 저장소
store = {}


def get_llm():
    llm = ChatUpstage()
    return llm

def get_session_history(session_id: str) -> BaseChatMessageHistory:
    if session_id not in store:
        store[session_id] = ChatMessageHistory()
    return store[session_id]

def get_dictionary_chain():
    llm = get_llm()
    dictionary = ["사람을 나타내는 표현은 모두 거주자로 변경해주세요"]

    prompt = ChatPromptTemplate.from_template(
        f"""
        사용자의 질문을 보고, 우리의 사전을 참고해서 사용자의 질문을 변경해주세요.
        변경할 필요가 없다고 판단되면, 질문 원문을 그대로 반환하세요.

        추가로 초과 금액은 단순하게 사용자가 제시한 금액에서 기준 금액을 뺀 값으로 계산해주세요.

        사전: {dictionary}
        질문: {{question}}
        """
        )
    return prompt | llm | StrOutputParser()


def get_history_retriever():
    llm = get_llm()
    retriever = get_retriever()
    
    contextualize_q_system_prompt = (
        "Given a chat history and the latest user question "
        "which might reference context in the chat history, "
        "formulate a standalone question which can be understood "
        "without the chat history. Do NOT answer the question, "
        "just reformulate it if needed and otherwise return it as is."
    )

    contextualize_q_prompt = ChatPromptTemplate.from_messages(
        [
            ("system", contextualize_q_system_prompt),
            MessagesPlaceholder("chat_history"),
            ("human", "{input}"),
        ]
    )
    
    history_aware_retriever = create_history_aware_retriever(
        llm, retriever, contextualize_q_prompt
    )
    return history_aware_retriever


def get_qa_chain():
    llm = get_llm()
    history_aware_retriever = get_history_retriever()

    # 2) 최종 답변 프롬프트 (stuff) – {context} + {input}
    answer_prompt = ChatPromptTemplate.from_messages(
        [
            (
                "system",
                "너는 소득세 도우미야. 아래 컨텍스트만 사용해 간결하고 정확히 답해.\n"
                "<context>\n{context}\n</context>",
            ),
            MessagesPlaceholder("chat_history"),
            ("user", "{input}"),
        ]
    )
    combine_docs_chain = create_stuff_documents_chain(llm=llm, prompt=answer_prompt)

    # 3) 최종 RAG 체인  (⚠️ 인자명: combine_docs_chain)
    rag_chain = create_retrieval_chain(
        retriever=history_aware_retriever,
        combine_docs_chain=combine_docs_chain,
    )
    return rag_chain


def get_ai_message(user_question: str, session_id: str = "abc123"):
    """
    - 사전 변환 → 히스토리 인지형 RAG 스트리밍
    - 문자열 제너레이터를 반환 (Streamlit의 st.write_stream에 바로 전달 가능)
    """
    # 1) 질문 정규화
    normalized_q = get_dictionary_chain().invoke({"question": user_question})

    # 2) 체인 + 자동 히스토리 래핑
    rag_chain = get_qa_chain()
    rag_with_history = RunnableWithMessageHistory(
        rag_chain,
        get_session_history,
        input_messages_key="input",
        history_messages_key="chat_history",
        output_messages_key="answer",
    )

5. LLM답변 중 pick("answer").stream(..) 으 로 streamlit 에 스트리밍 출력.

마지막으로 이렇게 완성한 값 중 "answer"을 pick 하여 stream 으로 streamlit 에 retrurn 한다.

llm_with_History.py
def get_ai_message(user_question: str, session_id: str = "abc123"):
    """
    - 사전 변환 → 히스토리 인지형 RAG 스트리밍
    - 문자열 제너레이터를 반환 (Streamlit의 st.write_stream에 바로 전달 가능)
    """
    # 1) 질문 정규화
    normalized_q = get_dictionary_chain().invoke({"question": user_question})

    # 2) 체인 + 자동 히스토리 래핑
    rag_chain = get_qa_chain()
    rag_with_history = RunnableWithMessageHistory(
        rag_chain,
        get_session_history,
        input_messages_key="input",
        history_messages_key="chat_history",
        output_messages_key="answer",
    )

    # 3) 'answer'만 스트리밍
    return rag_with_history.pick("answer").stream(
        {"input": normalized_q},
        config={"configurable": {"session_id": session_id}},
    )

 

아래와 같이 두번째 질문에서 이전의 대화 내용을 참고하여 1억 직장인의 소득세에 대해서 답변하는 것을 볼 수 있다.

 

6. 최종 코드

chat.py

더보기
import streamlit as st
from llm_with_History import get_ai_message

st.set_page_config(page_title="Chat Application", page_icon="📈")

st.title("소득세 챗봇")
st.caption("소득세 관련 질문을 해보세요!")

if "messages" not in st.session_state:
    st.session_state.messages = []

# 세션에 저장된 메시지를 반복하고 각 메시지 역할에 따라 메시지 표시
print(f"before: {st.session_state.messages}")
for message in st.session_state.messages:
    with st.chat_message(message["role"]):
        st.write(message["content"])


if user_question := st.chat_input("소득세와 관련된 궁금한 내용들을 말씀해주세요!"):
    # 채팅이 입력되었을 때 상단에 메시지가 노출되도록 함.
    # "user", "assistant", "ai", "human" 가 있는데 그중 user 을 사용함.
    with st.chat_message("user"):
        st.write(user_question)

    # 채팅 메시지를 Session State에 저장
    st.session_state.messages.append({"role": "user", "content": user_question})


    # 로딩중인 부분을 보여주기
    with st.spinner("AI 응답을 생성하는 중..."):
        ai_message = get_ai_message(user_question)

        # with st.chat_message("ai"):
        #     st.write(ai_message)
        #     st.session_state.messages.append({"role": "ai", "content": ai_message})
        with st.chat_message("ai"):
            final_text = st.write_stream(ai_message)
            # 채팅 메시지를 Session State에 저장
    st.session_state.messages.append({"role": "ai", "content": final_text})

 

llm_with_History.py

더보기
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain.chains import create_history_aware_retriever, create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain_upstage import UpstageEmbeddings, ChatUpstage
from langchain_pinecone import PineconeVectorStore

# (선택) 자동 히스토리 관리용
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
from dotenv import load_dotenv

load_dotenv() 
# 세션 히스토리 저장소
store = {}


def get_session_history(session_id: str) -> BaseChatMessageHistory:
    if session_id not in store:
        store[session_id] = ChatMessageHistory()
    return store[session_id]


def get_retriever():
    embeddings = UpstageEmbeddings(model="solar-embedding-1-large")
    index_name = "tax-index-markdown"
    database = PineconeVectorStore.from_existing_index(
        index_name=index_name,
        embedding=embeddings,
    )
    # 필요 시 k 조정
    retriever = database.as_retriever(search_kwargs={"k": 1})
    return retriever


def get_llm():
    llm = ChatUpstage()
    return llm


def get_dictionary_chain():
    llm = get_llm()
    dictionary = ["사람을 나타내는 표현은 모두 거주자로 변경해주세요"]

    prompt = ChatPromptTemplate.from_template(
        f"""
        사용자의 질문을 보고, 우리의 사전을 참고해서 사용자의 질문을 변경해주세요.
        변경할 필요가 없다고 판단되면, 질문 원문을 그대로 반환하세요.

        추가로 초과 금액은 단순하게 사용자가 제시한 금액에서 기준 금액을 뺀 값으로 계산해주세요.

        사전: {dictionary}
        질문: {{question}}
        """
        )

    return prompt | llm | StrOutputParser()

def get_history_retriever():
    llm = get_llm()
    retriever = get_retriever()
    
    contextualize_q_system_prompt = (
        "Given a chat history and the latest user question "
        "which might reference context in the chat history, "
        "formulate a standalone question which can be understood "
        "without the chat history. Do NOT answer the question, "
        "just reformulate it if needed and otherwise return it as is."
    )

    contextualize_q_prompt = ChatPromptTemplate.from_messages(
        [
            ("system", contextualize_q_system_prompt),
            MessagesPlaceholder("chat_history"),
            ("human", "{input}"),
        ]
    )
    
    history_aware_retriever = create_history_aware_retriever(
        llm, retriever, contextualize_q_prompt
    )
    return history_aware_retriever


def get_qa_chain():
    llm = get_llm()
    history_aware_retriever = get_history_retriever()

    # 2) 최종 답변 프롬프트 (stuff) – {context} + {input}
    answer_prompt = ChatPromptTemplate.from_messages(
        [
            (
                "system",
                "너는 소득세 도우미야. 아래 컨텍스트만 사용해 간결하고 정확히 답해.\n"
                "<context>\n{context}\n</context>",
            ),
            MessagesPlaceholder("chat_history"),
            ("user", "{input}"),
        ]
    )
    combine_docs_chain = create_stuff_documents_chain(llm=llm, prompt=answer_prompt)

    # 3) 최종 RAG 체인  (⚠️ 인자명: combine_docs_chain)
    rag_chain = create_retrieval_chain(
        retriever=history_aware_retriever,
        combine_docs_chain=combine_docs_chain,
    )
    return rag_chain


def get_ai_message(user_question: str, session_id: str = "abc123"):
    """
    - 사전 변환 → 히스토리 인지형 RAG 스트리밍
    - 문자열 제너레이터를 반환 (Streamlit의 st.write_stream에 바로 전달 가능)
    """
    # 1) 질문 정규화
    normalized_q = get_dictionary_chain().invoke({"question": user_question})

    # 2) 체인 + 자동 히스토리 래핑
    rag_chain = get_qa_chain()
    rag_with_history = RunnableWithMessageHistory(
        rag_chain,                            # 실제 실행할 체인 (여기서는 RAG 체인)
        get_session_history,                  # 세션별 히스토리 불러오는 함수
        input_messages_key="input",           # 입력 프롬프트에서 사용자가 친 질문이 담기는 키
        history_messages_key="chat_history",  # 대화 히스토리(대화 로그)를 넘길 때 쓰는 키
        output_messages_key="answer",         # 체인 실행 후 나오는 답변이 저장되는 키
    )


    # 3) 'answer'만 스트리밍
    return rag_with_history.pick("answer").stream(
        {"input": normalized_q},
        config={"configurable": {"session_id": session_id}},
    )

'RAG 을 활용하여 LLM 만들어보기' 카테고리의 다른 글

streamlit cloud 를 활용하여 서비스 배포하기  (0) 2025.08.20
Few Shot 을 활용한 답변 정확도 향상과 포맷 수정.  (0) 2025.08.18
Streamit 으로 chatbot 만들기  (2) 2025.08.09
Retrieval 효율 개선을 위한 데이터 전처리  (2) 2025.08.04
Pinecone 과 LangChain을 활용한 Vector Database 변경  (2) 2025.07.30
'RAG 을 활용하여 LLM 만들어보기' 카테고리의 다른 글
  • streamlit cloud 를 활용하여 서비스 배포하기
  • Few Shot 을 활용한 답변 정확도 향상과 포맷 수정.
  • Streamit 으로 chatbot 만들기
  • Retrieval 효율 개선을 위한 데이터 전처리
몽자비루
몽자비루
코딩공부 정리용 블로그입니다.
  • 몽자비루
    공부하는 블로그
    몽자비루
  • 전체
    오늘
    어제
    • 분류 전체보기 (195)
      • python (30)
        • python_selenium (16)
        • python_pygame (3)
      • appium (0)
      • 쿠버네티스 (60)
        • linux (8)
        • shell programming (8)
        • docker (18)
        • cka (23)
      • postman&API (16)
      • QA성장하기 (33)
        • 개발자에서 아키텍트로 스터디 (6)
        • 소프트웨어 공학 이해도 높이기 (6)
        • 테스팅 전문 지식 쌓기 (18)
        • 제4회 QA conference (3)
      • 에러일기 (1)
      • Server&load (35)
        • AWS (27)
        • load test (5)
        • CI CD (3)
        • Jmeter (0)
      • RAG 을 활용하여 LLM 만들어보기 (12)
      • git&github (7)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    vi에디터
    리눅스
    앱공존성
    qa
    로스트아크api
    포스트맨
    linux
    QAKOREA
    LOSTARK
    qa conference
    k8s
    로스트아크
    python
    개발자에서아키텍트로
    cka
    application log
    애플리케이션로그
    네트워크 테스트
    테스트스크립트
    e2c
    qa 컨퍼런스
    .cpu
    사드웨어리소스
    도커
    API
    스터디
    공존성테스트
    테스트 결과보고서
    postman
    쿠버네티스
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
몽자비루
Chat History 추가와 Streaming 구현
상단으로

티스토리툴바