지난번에는 주피터 노트북 작업물을 한데 모아놓고 돌려보았다.
그런데 내가 아는 chatgpt 는 채팅하면서 이전 질문을 기억하여 응답을 줬는데 내가 만든건 1회성 질문과 응답일 뿐이었다. 이번에는 맥락을 이해하는 AI를 만들어 보겠다.
모델은 llama3.1:8b 로 변경하였다.
소스 구석구석에 주석을 달아보았다.
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 응답 처리
(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되었습니다.')
질문을 입력하세요 =>
아직 갈길이 겁나 멀다... 다음 번에는 좀더 좋은 모델로 시도해 봐야겠다.