[AI] 10. 맥락에 맞는 응답 얻기.

늘 공부하는 괴짜·2025년 3월 31일
0

AI : Langchain (RAG)

목록 보기
10/38
post-thumbnail

지난번에는 주피터 노트북 작업물을 한데 모아놓고 돌려보았다.
그런데 내가 아는 chatgpt 는 채팅하면서 이전 질문을 기억하여 응답을 줬는데 내가 만든건 1회성 질문과 응답일 뿐이었다. 이번에는 맥락을 이해하는 AI를 만들어 보겠다.

모델은 llama3.1:8b 로 변경하였다.

1. 전체 소스

소스 구석구석에 주석을 달아보았다.

import os
from dotenv import load_dotenv  # 환경 변수 로드를 위한 라이브러리
from langchain_community.embeddings import OllamaEmbeddings  # Ollama 모델을 통한 임베딩 생성
from langchain_pinecone import PineconeVectorStore  # Pinecone 벡터 스토어
from langchain_community.chat_models import ChatOllama  # Ollama 기반의 채팅 모델
from langchain_core.output_parsers import StrOutputParser  # 출력 파싱을 위한 도구
from langchain_core.prompts import ChatPromptTemplate  # 채팅 프롬프트 템플릿을 위한 도구
from pprint import pprint  # 출력값을 깔끔하게 출력하기 위한 라이브러리
from langchain.chains import create_history_aware_retriever  # 채팅 기록을 고려한 검색 체인
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder  # 메시지와 시스템 프롬프트를 위한 도구
from langchain.chains import create_retrieval_chain  # 검색 기반 체인 생성 도구
from langchain.chains.combine_documents import create_stuff_documents_chain  # 문서 결합 체인 생성 도구
from langchain_core.runnables.history import RunnableWithMessageHistory  # 메시지 기록을 관리할 수 있는 실행 가능 객체
from langchain_core.chat_history import BaseChatMessageHistory  # 기본 채팅 기록 클래스
from langchain_community.chat_message_histories import ChatMessageHistory  # 채팅 기록 관리 클래스

# 세션별 채팅 기록을 저장할 딕셔너리 초기화
store = {}

# Pinecone API 키를 환경 변수에서 가져옴
pinecone_api_key = os.environ.get("PINECONE_API_KEY")

# Pinecone 인덱스 이름 지정
pinecone_index_name = "index-4096"


# 세션별 채팅 기록을 관리하는 함수
def getSessionHistory(session_id: str) -> BaseChatMessageHistory:
    # 주어진 세션 ID에 해당하는 채팅 기록이 없으면 새로 생성
    if session_id not in store:
        store[session_id] = ChatMessageHistory()  # 새로운 채팅 기록 객체 생성
    return store[session_id]  # 세션에 해당하는 채팅 기록 반환


# .env 파일에서 환경 변수를 로드하는 함수 정의
def setLoadDotEnv():
    load_dotenv()  # 환경 변수 로드

# Ollama 임베딩 모델을 반환하는 함수
def getEmbedding():
    # Llama3.1 모델을 사용하여 텍스트를 임베딩으로 변환
    embeddings = OllamaEmbeddings(
        model="llama3.1:8b"  # 사용할 모델명
    )
    return embeddings  # 생성된 임베딩 객체 반환


# LLM(대형 언어 모델)을 생성하는 함수
def getLlm():
    # Llama3.1 모델을 사용하여 ChatOllama 객체 생성
    llm = ChatOllama(
        model="llama3.1:8b"  # 사용할 모델 지정
    )  
    return llm  # 생성된 LLM 반환

# Pinecone 인덱스에서 벡터 검색을 위한 retriever 생성 함수
def getRetriever():
    # 기존 Pinecone 인덱스를 사용하여 벡터 스토어를 불러옴
    database = PineconeVectorStore.from_existing_index(
        embedding=getEmbedding(),  # 벡터 임베딩 생성 함수 전달
        index_name=pinecone_index_name  # 사용할 Pinecone 인덱스 이름
    )

    # 벡터 스토어를 검색할 수 있는 retriever로 변환, 검색할 항목의 수는 4개로 제한
    retriever = database.as_retriever(search_kwargs={"k": 4})
    return retriever  # 검색 기능을 제공하는 retriever 반환


# 사용자의 질문을 사전에 맞게 변형하는 체인 생성 함수
def getDictionaryChain():
    # 사용자 사전 정의 (현재는 빈 리스트로 설정)
    dictionary = []

    # 사용자의 질문을 사전에 맞게 변형하도록 요청하는 프롬프트 설정
    prompt = ChatPromptTemplate.from_template(f"""
        사용자의 질문을 보고, 우리의 사전을 참고해서 질문을 변경해 주세요.
        만약 변경할 필요가 없다고 판단된다면, 사용자의 질문을 변경하지 않아도 됩니다.
        사전 : {dictionary}
        
        질문 : {{question}}
    """)

    # 프롬프트를 LLM에 전달하고 그 결과를 파싱하는 체인 구성
    dictionary_chain = prompt | getLlm() | StrOutputParser()

    # 변형된 질문을 생성하는 체인 반환
    return dictionary_chain


# RAG (Retrieval-Augmented Generation) 체인을 생성하는 함수
def getRagChain():
    """
        * 채팅 기록과 최신 질문을 고려하여, 맥락을 알 수 있는 독립형 질문을 공식화하는 시스템 프롬프트를 생성합니다.
        채팅 기록 없이도 이해할 수 있는 독립형 질문을 만들고, 만약 질문에 대한 맥락을 알고 있다면, 이를 바탕으로 
        질문을 재구성합니다. (즉, 질문을 바꿔서 더 명확하게 만듭니다.)
    """
    # 시스템 프롬프트 정의: 채팅 기록과 최신 사용자 질문을 바탕으로 독립적인 질문을 생성
    context_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.
    """
    # 1. 시스템 프롬프트와 채팅 기록을 결합하여 새로운 프롬프트를 생성
    context_prompt = ChatPromptTemplate.from_messages(
        [
            ("system", context_system_prompt),
            MessagesPlaceholder("chat_history"),  # 채팅 기록 자리 표시자
            ("human", "{input}"),  # 인간의 질문 입력 자리 표시자
        ]
    )

    """
        * 실제 질문을 처리하는 프롬프트를 정의합니다.
        질문에 대해 답변을 할 때 검색된 문맥을 사용하며, 답을 모르더라도 "모른다"고 답하게 됩니다. 
        답변은 간결하게 최대 3개의 문장으로 제공됩니다.
    """
    # 실제 질문에 대한 답변을 하기 위한 프롬프트 설정
    qa_system_prompt = """
        You are an assistant for question-answering tasks. \
        Use the following pieces of retrieved context to answer the question. \
        If you don't know the answer, just say that you don't know. \
        Use three sentences maximum and keep the answer concise.\

        {context}  # 검색된 문맥을 기반으로 한 질문에 대한 답변
    """
    # 2. 생성된 QA 시스템 프롬프트를 기반으로 질문-답변 체인 생성
    qa_prompt = ChatPromptTemplate.from_messages(
        [
            ("system", qa_system_prompt),
            MessagesPlaceholder("chat_history"),  # 채팅 기록 자리 표시자
            ("human", "{input}"),  # 사용자 질문 입력 자리 표시자
        ]
    )

    # 3. 질문-답변 체인 생성
    qa_chain = create_stuff_documents_chain(getLlm(), qa_prompt)

    # 4. LLM, retriever 및 프롬프트를 결합하여 history-aware retriever 생성
    history_aware_retriever = create_history_aware_retriever(
        getLlm(), getRetriever(), context_prompt
    )

    # 5. 검색 및 질문-답변 체인을 결합하여 RAG 체인 생성
    rag_chain = create_retrieval_chain(history_aware_retriever, qa_chain)

    # 6. 메시지 기록을 관리하는 실행 가능 객체로 래핑
    conversational_rag_chain = RunnableWithMessageHistory(
        rag_chain,
        getSessionHistory,  # 세션별 채팅 기록을 가져오는 함수
        input_messages_key="input",  # 입력 메시지 키
        history_messages_key="chat_history",  # 채팅 기록 키
        output_messages_key="answer",  # 출력 메시지 키
    )

    # 채팅 기록을 관리하고, RAG 체인을 사용하는 실행 가능한 객체 반환
    return conversational_rag_chain



# AI 응답을 처리하는 함수
def getAiResponse(query: str):

    # 1. 두 체인을 결합하여 새로운 실행 가능한 체인 생성
    new_rag_chain = {"input": getDictionaryChain()} | getRagChain()

    # 2. 결합된 체인을 실행하여 사용자 질문에 대한 AI 메시지 생성
    ai_message = new_rag_chain.invoke(
        {"question": query},  # 사용자 질문을 입력
        {
            "configurable": {"session_id": "exoluse"}  # 세션 ID 설정
        }
    )

    # 3. 생성된 AI 메시지 중 답변 부분만 출력
    pprint(ai_message["answer"])


# .env 파일에서 환경 변수를 로드
setLoadDotEnv()

# 사용자로부터 계속해서 질문을 입력받고 AI 응답을 처리
for i in range(1, 99999): 
    inp = input("질문을 입력하세요 => ")
    getAiResponse(inp)  # 입력된 질문에 대해 AI 응답 처리

2. 테스트

(llm-app) exoluse@exoluseui-MacBookAir dev % python llm.py
질문을 입력하세요 => My name is exoluse.
/Users/exoluse/Desktop/dev/llm.py:58: LangChainDeprecationWarning: The class `ChatOllama` was deprecated in LangChain 0.3.1 and will be removed in 1.0.0. An updated version of the class exists in the :class:`~langchain-ollama package and should be used instead. To use it run `pip install -U :class:`~langchain-ollama` and import as `from :class:`~langchain_ollama import ChatOllama``.
  llm = ChatOllama(
/Users/exoluse/Desktop/dev/llm.py:31: LangChainDeprecationWarning: The class `OllamaEmbeddings` was deprecated in LangChain 0.3.1 and will be removed in 1.0.0. An updated version of the class exists in the :class:`~langchain-ollama package and should be used instead. To use it run `pip install -U :class:`~langchain-ollama` and import as `from :class:`~langchain_ollama import OllamaEmbeddings``.
  embeddings = OllamaEmbeddings(
('현재 사용자의 질문은 "My name is exoluse."로 남아 있습니다. 이 이름은 사전에서 포함되어 있지 않습니다. 사용자의 '
 '이름을 변경하지 못할 수있다.')
질문을 입력하세요 => Who am I?
('사용자의 질문은 "당신이谁인가?"로 변경되었습니다. 이 새로운 질문은 사전에서 포함되어 있지 않습니다. 사용자가 자신의 이름을 '
 '"exoluse"라고 말한 것으로 추정됩니다.')
질문을 입력하세요 => My favorite food is meat      
('사용자의-question이 변하지 않고 "My name is exoluse."로 남아 있습니다. 사용자의 새로운 질문 '
 '"당신이ใคร인가?"는 원래 내용으로 유지됩니다. 현재 사용자의 질문은 "My favorite food is meat"입니다.')
질문을 입력하세요 => I am 999 years old
('사용자의 질문이 बदल되었습니다. "My name is exoluse."는 남아 있지만 새로운 질문은 "당신이ใคร인가? 99세"으로 '
 '변경되었습니다. 새로운 질문은 사전에서 포함되어 있지 않습니다.')
질문을 입력하세요 => What is my favorite food?
('사용자의 질문이 변경되지 않습니다. 사용자의 질문은 "My name is exoluse."로 남아 있습니다. 새로운 질문 "What is '
 'my favorite food?"은 원래 내용으로 유지됩니다.')
질문을 입력하세요 =>  How old am I? 
('사용자의 질문이 변경되었습니다. "My name is exoluse."는 남아 있지만 새로운 질문은 "How old am I?"를 '
 '포함하는 사용자와의 대화로 changes되었습니다.')
질문을 입력하세요 => 

Finally

아직 갈길이 겁나 멀다... 다음 번에는 좀더 좋은 모델로 시도해 봐야겠다.

profile
인공지능이라는 옷을 입었습니다. 뭔가 멋지면서도 잘 맞습니다.

0개의 댓글