끝난지는 일주일이 되었지만 llm에 대해서 좀 더 공부해보고 코드들도 좀더 보느라고 작성하는게 조금 늦었다.
일주일이란 시간동안 QA engine 혹은 Chatbot Project를 개발할 시간이 주어졌는데 초반에 QA engine을 고른 이유는 아무래도 챗봇이 좀 더 구현하는데 있어서 시간이 좀더 걸리고 Deadline동안 정말 기능적인 일부만 보여줄 수 있을거라 생각하고 Timeline 안에 맞추려면 QA engine이라는 좀더 system prompt에 focus를 두고 llm 이 어떤 workflow를 가지는지 배우면서 프로젝트를 가지고 가고 싶었다.
결과론적인 부분만 말하면 (초기 프로젝트였지만) llm의 workflow도 workflow대로, 팀장으로써 git 및 meeting log를 작성하면서여러 전체적으로 많은 부분을 배울 수 있었다고 생각한다.
1. Git :
- 솔직히 말해서 엄청 유연하게 git을 관리한건 아니지만 (중간중간에 merge나 log 기록 관리등 관리 해줘야 하는 부분을 좀 더 신경쓰지 못했다) 팀장으로써 전체적인 branch별로 PR을 받고 main.py에 각자 팀원들이 작업했던 branch에서 전체 작업을 merge 하는 역할을 하면서 굉장히 git에 대해서 많이 늘었던 기회였던 것 같다. 아무래도 옛날엔 팀원으로써 git push만 해봤던 상황이라 좀 git reset 등 여러 명령어를 더 쓰면서 git에 대한 이해가 더 좋아진 거 같다.
2. Notion의 활용도 및 log 공유:
- 학과에선 노션을 쓰진 않았고 프로젝트 할때마다 솔직히 pdf, docx 등으로 서로 정리해서 넘겨줬었는데 솔직히 조금 불편했던 경험이 있다. 협업하면서 노션이란 사이트는 당연히 필수라고는 알고 있었는데 막상 제대로 써보질 못했는데 각자 팀원들끼리 서로 애로사항이나 작업현황들을 meeting log로 팀장으로써 작성함으로써 좀 더 프로젝트의 진행상황 및 에러를 공유하기 쉬웠고 앞으로의 프로젝트 진행시에 좀더 원활하게 할 수 있을 거라고 생각한다.
3. Project Timeline 에 맞추는 연습 및 갑작스런 인원 부재시에 대처 :
- 솔직히 일주일이란 시간동안은 llm의 모든 모듈 및 chain을 전부 다 이해하기는 어렵다고 생각한다. 그래서 팀원들끼리 모듈별로 할 일을 나누되 전부 다 같이 공부하자는 식으로 짰지만 한 분이 개인 사정으로 프로젝트에 참여 못한다는 소식을 시작한지 3일차에 알게 되었다. 상당히 늦게 전달받아서 retriever 부분을 내가 더 맡아서 진행하였는데 솔직히 코드 자체는 길지 않아서 작업 자체는 오래 걸리지 않았으나 각자 조금씩 파트들을 회의를 통해 좀 더 가져가서 프로젝트 인원이 3명이더라도 잘 마무리 할 수 있었다고 생각한다.
4. llm에 직접호출하는 방식 및 chain으로 chat_log를 관리하는 형식 -> chain에 대한 이해도:
- 아무래도 모듈별로 파트를 나누다 보니 (ex. llm, vector_store, embedding, retrieve...) llm을 초기에 맡았던 나는 간단하게 llm만 부르는 것이 아닌 handler를 통해서 좀 더 코드를 작성했다.
class LLMHandler:
def __init__(self, api_key):
self.llm = ChatUpstage(api_key=api_key)
def ask_question_about_pdf(self, pdf_text, question):
messages = [
{"role": "system", "content": "You are a helpful assistant who knows the content of a PDF document."},
{"role": "user", "content": f"Here is some text from a PDF: {pdf_text[:2000]}"},
{"role": "user", "content": f"Based on the above context, please answer the following question: {question}"}
]
return messages
def call_llm(self, messages):
response = self.llm.invoke(messages)
return response.content # response 에서 text 가져옴
def get_response(self, context, question, chat_history=None):
if chat_history:
# 대화 히스토리가 있으면...
formatted_history = "\n".join([f"User: {entry['user']}\nLLM: {entry['llm']}" for entry in chat_history])
question = f"{formatted_history}\nUser: {question}"
#print(f"(디버그) 최종 질문 메시지: {question}") # 최종 질문 메시지
messages = self.ask_question_about_pdf(context, question)
#print(f"[디버그] LLM에 보낼 메시지 구조: {messages}") # LLM에 보낼 메시지 구조
response = self.call_llm(messages)
#print(f"[디버그] LLM으로부터 받은 응답: {response}") # LLM으로부터 받은 응답
return response
이 부분에서 다른 팀원분은 embedding을 진행해야하니 간단하게 아래와 같은 식으로 호출만 하고 system prompt를 다른 쪽에서 chat_log와 chain을 같이 처리하는 방식으로 진행을 하였다.
from langchain_upstage import ChatUpstage
class LlmModel():
def __init__(self, api_key) -> None:
self.api_key = api_key
def UpstageModel(self):
llm = ChatUpstage(api_key= self.api_key, model_name="solar-1-mini-chat")
return llm
retriever를 맡아서 작업하던 나는 팀원분이랑 merge 할때 이 리트리버에서 오류가 나는 것을 확인했고 하루동안 작업해본 결과 오류는 해결되지 않았고 시간적인 문제 떄문에 각자의 파트를 살릴려면 핸들러를 통해 작업한 llm에 직접 호출하는 방식과 chain을 활용해서 관리하는 방식 둘다 가져가기로 결정했다. 결국 이 과정에서 왜 chain을 이용한 관리가 편하고 효율적인지를 알게 되었고 전체적인 workflow를 더 잘 배울 수 있었다고 생각한다. (직접 llm에 호출하는 방식으로 작업하면서 chat_log도 따로 작업했어야했는데 상당히 코드가 늘어진다는 생각을 하게 되었다.)
import os
import json
class ChatLog:
def __init__(self, max_length=10):
self.chat_history = []
self.max_length = max_length
def add_to_log(self, user_input, llm_response):
self.chat_history.append({"user": user_input, "llm": llm_response})
# 대화 기록이 max_length>10 이면 log delete
if len(self.chat_history) > self.max_length:
self.chat_history.pop(0)
def get_log(self):
return self.chat_history
def clear_log(self):
self.chat_history = []
def save_log_to_file(self, filename):
try:
with open(filename, 'w', encoding='utf-8') as file:
json.dump(self.chat_history, file, ensure_ascii=False, indent=4)
except Exception as e:
print(f"저장 중 오류 발생: {e}")
def load_log_from_file(self, filename):
try:
if os.path.exists(filename):
with open(filename, 'r', encoding='utf-8') as file:
self.chat_history = json.load(file)
else:
print(f"파일 {filename}을 찾을 수 없습니다.")
except Exception as e:
print(f"불러오기 중 오류 발생: {e}")
이런식으로 따로 클래스를 안 만들더라도 (간편은 하겠지만) chain을 활용하면
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_community.chat_message_histories import ChatMessageHistory
# https://python.langchain.com/v0.1/docs/use_cases/question_answering/chat_history/#chain-with-chat-history
store = {}
temp_session_id = "abc123"
def get_session_history(session_id: str) -> BaseChatMessageHistory:
if session_id not in store:
store[session_id] = ChatMessageHistory()
return store[session_id]
class ChatLog():
def logger():
return store[temp_session_id]
class ConversationalRAGChain():
def __init__(self, rag_chain, input) -> None:
self.rag_chain = rag_chain
# self.get_session_history = get_session_history
self.input = input
def chain(self):
conversational_rag_chain = RunnableWithMessageHistory(
self.rag_chain,
get_session_history,
input_messages_key="input",
history_messages_key="chat_history",
output_messages_key="answer",
)
response = conversational_rag_chain.invoke(
{"input": self.input},
config={
"configurable": {"session_id": temp_session_id}
}, # constructs a key "abc123" in `store`.
)
return response
이런식으로 chain을 늘리면서 chat_history를 관리할 수 있게 된다.
vector_store, embedding은 어떤걸 선택하느냐에 따라 조금씩 달라지겠지만 우리 팀은 PyMuPDF로 pdf를 load, RecursiveCharacterTextSplitter로 text를 split, top_k (5): 5개 상위문서 및 simliary_search_by vector로 retrieve했다.
아래는 우리팀의 git project 주소이고 과정에서 첫 프로젝트였기 때문에 많이 미숙했지만 그래도 "협업"과 "배움"을 통해 이번 기회를 통해 많이 성장했다고 생각한다.
https://github.com/Upstage-AI-Lab-4/langchain-project-qa-engine-4
'AI' 카테고리의 다른 글
<8일차> Mlflow, FastApi, Bertmodel, Airflow ... (2) | 2024.09.25 |
---|---|
<7일차> ML 경진대회 : Regression Wrap up (5) | 2024.09.20 |
<5일차> Statistics (0) | 2024.08.23 |
<4일차> Git (0) | 2024.08.09 |
<3일차> 프로젝트 수행을 위한 이론 : python + langchain(및 LLM + RAG (0) | 2024.08.06 |