본문 바로가기
Project/명지대학교-입학관리팀챗봇-MARU_EGG

입학관리팀 RAG 방식 챗봇 - 1차 테스트 및 보완할 점들 기록

by 지식을 쌓는 개구리 2024. 7. 6.

 

프로젝트 세팅 및 1차 테스트 코드 완성

장고 프로젝트를 생성하고 기본 세팅을 모두 완료하고 llm 모델이 참고할 문서가 될 데이터 db도 정의하고

어느정도 진행을 하였다.

 

기본적으로 rag는 두가지의 주요 단계를 가지게 된다. 바로 문서 검색과 생성이다.

나는 코드를 아래와 같은 단계로 진행했다.

1. HTML 파일을 드래그앤 드롭해서 입학모집요강 데이터를 추출해 적당한 크기로 쪼개에 db에 저장한다.

2. 사용자가 질문을 하면 해당 질문과 가장 관련성 높은 문서를 유사도 계산해서 검색한다.

3. 검색된 문서를 기반으로 해서 OpenAI의 GPT-3.5 모델을 사용해 답변 생성을 한다.

 

사용한 프레임워크, 라이브러리는 다음과 같다.

- DJango : 프레임워크

- BeautifulSoup : HTML 파일에서 데이터 추출

- Scikit-learn: TfidfVectorizer와 cosine_similarity를 사용하여 유사도 측정

- OpenAI API: GPT-3.5를 이용하여 질문에 대한 답변 생성

 



코드를 좀더 상세히 설명해보겠다.

 

1. HTML 파일 데이터베이스에 저장

HTML 파일에서 데이터를 추출하여 데이터베이스에 저장하는 작업을 했다.

-> 이를 위해 BeautifulSoup을 사용하여 HTML 파일을 파싱하고 표 데이터를 추출했다.

각 데이터를 최대 1000자로 분할하여 Document 모델에 저장했다. -> db는 효율성을 생각했을때 장고

내부 장착된 sqlite를 쓰기로 했다.

@csrf_exempt
def upload_html(request):
    if request.method == "POST" and request.FILES.get("html_file"):
        html_file = request.FILES["html_file"]

        with tempfile.NamedTemporaryFile(delete=False, suffix='.html') as temp_file:
            temp_file.write(html_file.read())
            temp_file_path = temp_file.name

        try:
            with open(temp_file_path, 'r', encoding='utf-8') as file:
                soup = BeautifulSoup(file, 'html.parser')

            tables_text = ""
            tables = soup.find_all('table')
            for table in tables:
                rows = table.find_all('tr')
                for row in rows:
                    cells = row.find_all(['td', 'th'])
                    row_text = "\t".join(cell.get_text(strip=True) for cell in cells)
                    tables_text += row_text + "\n"
                tables_text += "\n"

            title = html_file.name
            parts = split_text(tables_text, max_length=1000)
            for i, part in enumerate(parts):
                Document.objects.create(title=title, content=part, part=i+1)
                logger.debug(f"Document part {i+1} created with content: {part[:100]}...")

        finally:
            os.remove(temp_file_path)

        return render(request, "maruegg/upload_html_file.html", {"message": "Document uploaded and parsed successfully"})

    return HttpResponse("Invalid request", status=400)

 

 

 

2. 질문과 관련성 높은 문서 검색

질문이 들어오면, 데이터베이스에 저장된 문서 중에서 질문과 가장 관련성이 높은 문서를 검색한다.

-> TfidfVectorizer cosine_similarity를 사용했다.

def get_relevant_documents(question, max_docs=5):
    documents = Document.objects.all()
    if not documents.exists():
        return ""

    doc_contents = [doc.content for doc in documents]
    vectorizer = TfidfVectorizer().fit_transform([question] + doc_contents)
    vectors = vectorizer.toarray()

    cosine_matrix = cosine_similarity(vectors)
    scores = cosine_matrix[0][1:]
    
    top_indices = scores.argsort()[-max_docs:][::-1]
    relevant_docs = [doc_contents[i] for i in top_indices]
    context = "\n\n".join(relevant_docs)
    
    return context

 

3. OpenAI GPT-3.5를 통한 답변 생성

검색된 문서를 기반으로 GPT-3.5 모델을 사용하여 질문에 대한 답변을 생성했다.

여기서 OpenAI API를 사용했는데 추후 로컬 llm모델로 바뀔 수도...

개발 단계기에.. -> 응답 생성 시간을 측정하여 로깅했다. 테스트 목적

@csrf_exempt
def ask_question_api(request):
    question = None
    answer = None

    if request.method == "POST":
        question = request.POST.get("question")
        
        logger.debug(f"Searching for question: {question}")
        
        start_time = time.time()
        context = get_relevant_documents(question)
        end_time = time.time()
        retrieval_time = end_time - start_time
        logger.debug(f"Document retrieval took {retrieval_time:.2f} seconds")

        if not context:
            logger.debug("No relevant documents found.")
            return HttpResponse("No relevant documents found.")
        
        logger.debug(f"Context for question: {context[:100]}...")
        
        start_time = time.time()
        
        response = client.chat.completions.create(
            model="gpt-3.5-turbo",
            messages=[
                {"role": "system", "content": "You are a helpful assistant."},
                {"role": "user", "content": f"Context: {context}\n\nQ: {question}\nA:"}
            ]
        )
        end_time = time.time()
        completion_time = end_time - start_time
        logger.debug(f"OpenAI completion took {completion_time:.2f} seconds")
        
        answer = response.choices[0].message.content.strip()
        
    return render(request, "maruegg/ask_question.html", {"question": question, "answer": answer})

 

 

결론 및 앞으로 해야할 일

 

보는 것처럼 결과는 잘 나온다..? 걱정했던 표의 데이터도 잘 인식을 한다는 것..

일단 1차적으로 테스트겸 코드를 작성해보았다. rag방식으로 llm 챗봇을 open ai api로 만드는 것은 처음인데

생각보다 가벼워서 그런지 다루기도 쉬웠고 속도도 빨라서 좋았다.

html 데이터 파싱해서 db에 저장하는 것이나 유사도 측정을 통해서 관련성 높은 문서를 검색해 api를 통해

답변 생성을 하는 과정도 직접 개발해보면서 이해도를 높일 수 있었다.

 

그런데 아직 부족하다.. 모집요강 기반 정확한 답변을 리턴해야하는데 생각보다 부족하게 답변을 하는경우도

너무 많고 딴소리 할 때도 있다..

-> 데이터 파싱, 처리, 또 유사도 계산에서 아직 많이 부족한 것 같다.

이 부분은 앞으로 추가 진행을 하여 여러 라이브러리, 코드 수정을 거쳐 리팩토링을 진행해야할 것 같다.