Data Navigator

RAG(Retrieval Augmented Generation) 기반 GPT 챗봇 만들기 본문

Python

RAG(Retrieval Augmented Generation) 기반 GPT 챗봇 만들기

코딩하고분석하는돌스 2024. 11. 12. 15:52

RAG(Retrieval Augmented Generation) 기반 GPT 챗봇 만들기 

 

 

 

1. RAG(Retrieval-Augmented Generation): 검색 증강 생성이란?

RAG는 미리 지정한 텍스트를 데이터베이스로 준비해 두었다가 사용자가 입력하면 그 입력 내용과 연관성이 높은 텍스트를 데이터베이스에서 검색해 프롬프트에 추가해 보다 정확한 답변을 할 수 있게 하는 기법

 

* 질문에 더 정확하고 풍부한 답변을 주기 위해 정보 검색과 답변 생성을 결합한 기술
* 정보 검색 단계: 사용자가 질문을 하면, 외부 데이터베이스나 문서에서 관련 정보 검색
* 답변 생성 단계: 찾은 정보를 바탕으로 AI 모델이 답변 생성

 

2. 임베딩(Embedding)이란?

컴퓨터는 문자를 인식하지 못하므로 컴퓨터가 이해할 수 있도록 문자를 숫자 벡터로 변환해주는 작업

 

 

3. 호텔 고객 응대 매뉴얼에 기반한 RAG  GPT 챗봇 만들기

본 예제는 "OpenAI API와 파이썬으로 나만의 챗 GPT 만들기" 의 것을 약간 변경하였음.

 

예제 실행을 위해서는 가상환경 설정 및 라이브러리 설치, openai api key가 필요하므로 이전 글을 참고해서 설치하기 바란다.

 

 

https://datanavigator.tistory.com/91 

 

OpenAI API 를 이용한 챗봇 만들기01 - OpenAI 계정등록하고 API key 발급 후 테스트 하기

OpenAI API 를 이용한 ChatGPT 챗봇 만들기01- OpenAI 계정등록하고 API key 발급 후 테스트 하기 -   1. google에서 openai api를 검색한다.혹은 https://openai.com/index/openai-api/ 로 접속 2. sighup을 누르고 회원가입

datanavigator.tistory.com

 

 

1)  고객 응대 매뉴얼 데이터 만들기

예제로 사용할 데이터는 아래의 것을 data.txt로 저장 후 사용한다.

1. 손님 맞이
손님이 호텔에 도착하면 친절한 미소와 함께 예의 바르고 활기찬 인사말을 건네는 것이 좋다. '어서 오세요' 또는 '어서 오세요' 등 상황에 맞는 표현을 사용해야 한다. 고객의 이름을 알고 있는 경우, 개인화된 인사말을 통해 고객의 만족도를 높일 수 있다.

2. 체크인과 체크아웃
체크인 시간은 오후 3시, 체크아웃 시간은 오전 11시이다. 일찍 체크인하거나 늦게 체크아웃을 원하는 고객에 대해서는 객실의 공실 상황을 확인하여 가능한 한 대응해 주어야 한다. 만약 그것이 어렵다면, 짐을 일시적으로 보관할 수 있는 서비스를 제안한다.

3. Wi-Fi 및 주차장 안내
모든 객실에 무료 와이파이가 제공된다. 연결 방법과 비밀번호를 확실히 설명해 줄 수 있도록 하자. 또한, 180대의 무료 주차장이 마련되어 있다. 주차장의 위치, 이용 방법, 개방 시간 등을 정확하게 안내할 수 있도록 한다.

4. 배리어 프리 대응
유니버설 룸의 배치와 시설, 특징을 이해하고 필요한 경우 고객에게 설명할 수 있도록 한다. 휠체어를 이용하는 고객이 있을 경우, 관내의 장애인 편의시설에 대해 안내하고 필요한 경우 도움을 줄 수 있도록 한다.

5. 반려동물 대응
반려동물을 동반한 고객에게는 정중하게, 그러나 분명하게 반려동물을 동반할 수 없음을 알려주어야 한다. 이때 인근의 반려동물 동반 가능 호텔을 소개하여 고객의 불편을 덜어주어야 한다. 인근의 반려동물 호텔 정보를 항상 최신 상태로 유지해야 한다.

6. 룸 서비스
오후 11시까지 룸서비스가 제공된다. 룸서비스 메뉴의 내용을 숙지하여 고객의 문의에 적절히 대응할 수 있도록 한다. 또한, 음식에 대한 알레르기 정보나 특별한 식단 제한에 대응할 수 있도록 주방과의 협력도 중요하다.

7. 금연 정책 및 흡연실 안내
모든 객실은 금연입니다. 그러나 흡연자 고객의 요구를 충족시키기 위해 1층에 흡연실을 마련한다. 이 정보를 명확하게 전달하고, 흡연실 위치와 이용 시간을 고객에게 안내해 주어야 한다.

8. 취소 정책
취소 수수료는 전날까지 연락 시 숙박 요금의 30%, 당일 취소 시 50%, 연락 없이 취소할 경우 100%를 부과한다. 이 정책은 모든 예약에 적용되며, 예약 시 고객에게 이 사실을 명확히 알려야 한다.

9. 결제 방법
체크아웃 시 프런트에서 현금, 신용카드, 직불카드로 결제한다. 또한 인터넷 예약을 이용하는 고객은 예약 시 카드 결제를 선택할 수 있다. 다양한 결제 방법을 제공하여 투숙객의 편의를 도모하는 것이 좋다.

10. 항상 존중을 실천한다.
고객 한 사람 한 사람을 존중하는 태도로 대하자. 고객에 대한 예의, 배려, 전문성은 호텔의 품질을 결정짓는 중요한 요소이다. 고객이 편안하게 지낼 수 있도록 최선을 다하는 것을 잊지 말아야 한다.

 

 

2) 학습데이터 로딩 후 CSV  형식으로 변형하기

(1) with open 문을 이용해서 txt 파일을 읽어온다.

import os
import pandas as pd

with open("../ch4/data.txt", "r", encoding="utf-8") as f:
    data = f.read()
print(data)

 

 

 

jupyter notebook에서 print문 없이 변수를 출력하면 이스케이프문자가 보인다. 1.손님 맞이 같은 제목과 손님이 호텔에 도착하면.. 의 사이는 \n 로 구분되고 각 항목은 \n\n으로 구분되는 것을 볼 수 있다. 

python의 문자열 함수 split()을 사용해서 항목별로 나누고 다시 각 항목의 제목과 내용을 분리해 데이터 프레임으로 만든다.

 

(2) split 함수로 항목 분리 후 리스트화

data = data.split("\n\n")
data

 

(3) 리스트화 된 데이터를 반복문과 딕셔너리 함수를 이용해서 데이터프레임으로 만든다.

text2df ={}
for content in data:
    temp = content.split("\n")
    print(len(temp))
    text2df.setdefault("title",[]).append(temp[0])
    text2df.setdefault("content",[]).append(temp[1:]) # 마지막 항목이 2개로 분리 되기 때문에 슬라이싱으로 추가함
df = pd.DataFrame(text2df)
df

 

 

(4) content의 내용이 list 형태로 들어있으므로 join 함수를 이용해 리스트 요소를 1개의 문자열로 합친다.

df['content'] = df['content'].apply(lambda x: " ".join(x))
df

 

3) 텍스트 임베딩(embedding)

content의 내용을 gpt에게 알려주기 위해서 문자를 숫자로 임베딩한다. 

최근 GPT에서 받아들일 수 있는 token수가 gpt-4o-mini 모델  기준 128000 이므로 상당히 긴 내용도 한 번에 임베딩이 가능하다.

토큰은 1문자나 단어가 아니므로 토큰을 계산해 주는 모듈을 설치하면 좋다. 

tiktoken이라는 모듈을 설치하고 토큰 수를 계산한 후 openai의 api를 이용해 content의 내용을 임베딩하자.

 

(1) tiktoken을 설치하고 content의 token을 계산해 최대값 + 100을 max_token 변수에 저장한다.

max_token을 구하는 이유는 필요이상으로 긴 답변이 출력되지 않도록 제한하고 불필요한 비용 발생도 줄일 수 있기 때문이다.

!pip install tiktoken
import tiktoken
from openai import OpenAI
from typing import List

client = OpenAI()

# 임베딩 매개변수 설정
embedding_model = "text-embedding-3-small"
embedding_encoding = "cl100k_base"

tokenizer = tiktoken.get_encoding(embedding_encoding)
df['n_tokens'] = df.content.apply(lambda x: len(tokenizer.encode(x)))
df

 

(2) max_token 구하기

max_token = df['n_tokens'].max() + 100
print(max_token)

 

(3)  content열의 텍스트를 embedding

#content열의 텍스트를 embedding하고 csv로 저장하는 함수
def get_embedding(text, model):
    text = text.replace("\n", " ")
    return client.embeddings.create(input=[text], model=model).data[0].embedding
    
# content열의 텍스트를 embedding하고 csv로 저장
df['embeddings'] = df['content'].apply(lambda x: get_embedding(x, model=embedding_model))
df

 

(4) content, n_tokens, embeddings 컬럼만 추려서 csv로 저장

df = df[['content', 'n_tokens', 'embeddings']]
df

df.to_csv('embeddings.csv')

 

 

4)  csv로 저장한 데이터를 기반으로 대답하는 RAG 기반 GPT 만들기

데이터 준비가 끝났으므로 이제 데이터에 기반해서 대답하는 GPT를 만들어보자

질문입력 - 질문 임베딩 - csv 데이터에서 임베딩된 질문과 유사도 비교후 유사한 값이 있으면 그 내용을 기반으로 GPT가 대답하도록 한다.

 

(1) 질문 임베딩과 RAG 임베딩 목록 사이의 거리 반환 함수 작성

입력받은 질문을 임베딩 값과 CSV에 저장되어있는 호텔 응대 매뉴얼 임베딩 값의 유사도를 비교하고 거리를 산출해 반환한다.

def distances_from_embeddings(
    query_embedding: List[float],
    embeddings: List[List[float]],
    distance_metric="cosine",
    ) -> List[List]:
    """
    질문 임베딩과 RAG 임베딩 목록 사이의 거리 반환
    """
    
    distance_metrics = {
        "cosine": spatial.distance.cosine,
        "L1": spatial.distance.cityblock,
        "L2": spatial.distance.euclidean,
        "Linf": spatial.distance.chebyshev,
    }
    distances = [distance_metrics[distance_metric](query_embedding, embedding) for embedding in embeddings]
    
    return distances

 

(2) 질문을 임베딩 하고 함수 (1) 번을 실행해 거리 값을 구한 후 유사한 내용으로 질문과 csv의 데이터를 합쳐서 GPT에게 퓨샷으로 전달할 텍스트를 반환한다.

def create_context(question, df, max_len=1800):
    """
    질문과 학습 데이터를 비교하여 유사도를 구하는 함수
    """
    
    # 질문을 벡터화
    q_embeddings = client.embeddings.create(input=[question], model='text-embedding-3-small').data[0].embedding
    
    # 질문과 RAG 비교 후 코사인 유사도 산출 후 distance 컬럼에 저장
    df['distances'] = distances_from_embeddings(q_embeddings, df['embeddings'].apply(eval).apply(np.array).values, distance_metric='cosine')
    
    # 컨텍스트를 저장하기 위한 리스트
    returns = []
    # 컨텍스트의 현재 길이
    cur_len = 0
    
    # 학습 데이터를 유사도 순으로 정렬하고 토큰 개수 한도까지 컨텍스트에 추가
    for _, row, in df.sort_values('distances', ascending=True).iterrows():
        # 텍스트 길이를 현재 길이에 더하기
        cur_len += row['n_tokens'] + 4
        
        # 텍스트가 너무 길면 종료
        if cur_len > max_len:
            break
        
        # 컨텍스트 목록에 텍스트 추가하기
        returns.append(row['content'])
    
    # 컨텍스트를 결합해 반환
    return "\n\n###\n\n".join(returns)

 

(3) 질문과 csv 자료를 합쳐서 만든 context를 gpt에서 퓨샷으로 학습시킨 후 질문에 답하도록 하는 함수 작성

def answer_question(question, conversation_history, max_token):
    """
    문맥에 따라 질문에 답하는 기능
    """
    
    # RAG 데이터 불러오기
    df = pd.read_csv('embeddings.csv')
    
    # 질문과 RAG 데이터를 비교해 컨텍스트 생성
    context = create_context(question, df, max_len=200)
    
    # 프롬프트 생성하고 대화 기록에 추가하기
    prompt = f"당신은 어느 호텔 직원입니다. 문맥에 따라 고객의 질문에 정중하게 대답해 주세요. 컨텍스트가 질문에 대답할 수 없는 경우 \
                '모르겠습니다'라고 대답하세요.\n\n컨텍스트: {context}\n\n---\n\n질문: {question}\n답변:"
    conversation_history.append({"role" : "user", "content" : prompt})
    
    try:
        # 챗GPT 에서 답변 생성
        response = client.chat.completions.create(
            model = "gpt-4o-mini",
            messages=conversation_history,
            temperature=1,
            max_tokens=max_token
        )
        
        # 챗GPT에서 답변 반환
        return response.choices[0].message.content.strip()
    except Exception as e:
        # 오류시 빈 문자열 반환
        print(e)
        return ""

 

5) 위의 함수를 실행해 질문에 따른 답변을 생성 및 출력하고 exit를 입력하면 실행을 종료하도록 한다. 

print("질문을 입력하세요", end="\n\n")

conversation_history = []

while True:
    user_input = input()
    
    if user_input == "exit":
        break
        
    conversation_history.append({'role' : 'user', 'content' : user_input})
    answer = answer_question(user_input, conversation_history, int(max_token))
    
    print("\nChatGPT:", answer, end="\n\n")
    conversation_history.append({"role": "assistant", "content": answer})

 

 

참고: 오기와라 유이, 후루카와 쇼이치, 최용 역, OpenAI API와 파이썬으로 나만의 챗GPT 만들기, 위키북스, 2024.