Ssul's Blog

[ChatGPT] OpenAI임베딩 이용해서 RAG구현(langchain,vectordb) 본문

AI & ML/사용하기

[ChatGPT] OpenAI임베딩 이용해서 RAG구현(langchain,vectordb)

Ssul 2024. 2. 23. 15:37

RAG의 구동 개념이 궁금하면 우선 아래글 확인하시고,

https://issul.tistory.com/441

 

[ChatGPT] openai 임베딩 사용해서 RAG구현(생코딩,csv파일)

0. RAG란? Retrieval-Augmented Generation의 약자로 한글로 표현하면, 검색증강생성? 뭔가 알듯 모르는 단어 느낌? 간단하게 말하면, ai가 응답을 하기 전에, 질문받은 내용과 유사한 자료(텍스트 또는 문장

issul.tistory.com

 

이번 글에서는 Langchain과 벡터DB를 활용해서, RAG를 구현하는 실전 진행

 

#1. 기획(계획)

- RAG에 사용할 주요 저서를 pdf파일이 있다

- 해당 pdf 본문을 500개 단위로 끝어서 > openai임베딩을 하여 > 벡터DB에 저장한다.

- 사용자가 질문을 하면, 질문을 openai임베딩한다

- 임베딩된 사용자의 질문과 임베딩된 pdf본문을 비교한다 > 가장 유사한 문단 3개를 가져온다

- 임베딩된 3개 문단을 다시 문장으로 바꾼다 > 3개의 문단과 사용자의 질문을 AI한테 한다

- 응답을 받아 사용자에게 출력해준다 

 

#2. 필요한 라이브러리를 설치한다

pip install langchain openai chromadb tiktoken langchain-openai

 

 

[./database/embedding_chroma.py]

#3. pdf파일 본문을 쪼개서, 임베딩 하고, vectordb에 저장

- 주요 저서파일을 ./bookdata폴더 밑에 저장

(*맥 미리보기로 pdf파일의 표지나 목차를 삭제했는데, 이럴경우 글자가 깨져서 읽히니 참고)

 

load_dotenv()

openai.api_key = os.getenv('OPENAI_API_KEY')

# 디렉토리 설정
CUR_DIR = os.path.dirname(os.path.abspath(__file__))

CHROMA_PERSIST_DIR = os.path.join(CUR_DIR, 'chroma_persist')
CHROMA_COLLECTION_NAME= "ai-kwon-book"

- openai 임베딩을 사용하기 위해, openai셋팅

- vectordb가 저장될 폴더를 설정한다.

- vectordb의 collectionname을 설정한다. 일반적인 DB의 테이블 명이라 생각하면 됨

 

LOADER_DICT = {
    "pdf": PyPDFLoader,
    "txt": TextLoader,
    "md": UnstructuredMarkdownLoader,
    "ipynb": NotebookLoader,
}

loader = LOADER_DICT.get('pdf')
if loader is None:
    raise ValueError("Not supported file type")

# pdf 문서 불러오기
loader = DirectoryLoader('./bookdata', glob="*.pdf", loader_cls=loader)

docs = loader.load()

print('문서의 개수 :', len(docs))

- 문서의 종류에 따라, 가져올 Loader을 연결해주는 LOADER_DICT선언

- ./bookdata폴더에 있는 pdf파일을 모두 불러온다.

- docs에 pdf파일들을 읽은 data가 저장

 

# textsplitter로 문서를 쪼개기
text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=100)
texts = text_splitter.split_documents(docs)

print('문서의 페이지 수 :', len(texts))

- chunk사이즈 500으로, 100은 겹치게 해서 문서의 내용을 쪼갠다

- 쪼갠 문장모음을 texts에 저장

 

# 임베딩해서, 벡터db에 저장
vectordb = Chroma.from_documents(
    texts,
    OpenAIEmbeddings(),
    persist_directory=CHROMA_PERSIST_DIR,
    collection_name=CHROMA_COLLECTION_NAME,
)

- 임베딩을 진행하고,

- vectordb로 저장(vectordb 폴더, collection네임)

 

 

[./api.py]

#4. 고객이 질문을 받는다

@app.post("/chat")
def generate_chat(req: UserRequest):
    context = req.dict()
    user_input = context["user_message"]

    messages = create_prompt(user_input)
    #### 나중에 vectordb에서 가져온 검색값3개랑 질문 다시 보낼 예정

- 고객이 채팅창에 입력한다.

- 입력받은 메세지로, create_prompt함수를 호출한다.

 

[./chain.py]

#5. 고객의 질문을 기반으로 시스템 메세지를 만든다

def create_prompt(query):
    # 질문과 가장 관련있는 본문 3개를 가져옴
    result = query_vectordb(query, use_retriever=True)
    print(result[0].page_content)
    print(result[1].page_content)
    print(result[2].page_content)
    
    system_message = f"""
        You're a great counselor. The articles below are taken from my books that are relevant to your questions.
        Documents:
        doc1: """ + result[0].page_content + """
        doc2: """ + result[1].page_content + """
        doc3: """ + result[2].page_content + """
        Instead of just answering the question, please refer to this and answer it.
        Be sure to answer in Korean
    """

    user_content = f"""User question: "{str(query)}". """

    messages = [
        {"role": "system", "content": system_message},
        {"role": "user", "content": user_content},
    ]
    return messages

- 고객이 입력한 질문을 query_vectordb함수에 보내서, 총 3개의 관련도 높은 데이터를 가져온다.

- 가져온데이터 3개를 시스템 메세지에 참고하라고 넣어준다.

- 고객의 원래질문을 붙여넣어준다

- 완성된 최종 messages를 반환한다.

 

[./rag.py]

#6. 고객의 질문을 기반으로 vectordb에서 관련 본문 3개를 리턴한다

def query_vectordb(query: str, use_retriever: bool = False):
    from pprint import pprint

    vectordb = Chroma(
        persist_directory=CHROMA_PERSIST_DIR,
        embedding_function=OpenAIEmbeddings(),
        collection_name=CHROMA_COLLECTION_NAME,
    )

    if use_retriever:
        retriever = vectordb.as_retriever(search_kwargs={"k": 3})
        top_docs = retriever.get_relevant_documents(query)
    else:
        top_docs = vectordb.similarity_search(query, k=3)

    pprint(top_docs)
    return top_docs

- 앞에서 저장했던 chromadb를 불러온다.

- chromadb에 고객의 입력문장을 넣어서, 가장 관련성 높은 top3를 가져온다.

- use_retriever가 True면 retriever사용. False면 db직접사용(후자를 선호)

 

#7. 정리

- 참고할만한 문서/도서를 openai 임베딩으로 임베딩하여, vectordb에 저장(embedding_chroma.py)

- 입력받은 사용자의 문장을 저장된 vectordb데이터와 비교해서, 가장 관련도가 높은 top3본문 가져오기(rag.py)

- 가져온 본문을 가지고 > 시스템메세지 작성 > 사용자의 입력더해서 최종 messages 완성(chain.py)

- 완성된 messages를 가지고, AI에게 질문해서 좋은 응답 받아오기(api.py)

[최종 api.py코드]

load_dotenv()
openai.api_key = os.getenv('OPENAI_API_KEY')
client = OpenAI()

app = FastAPI()

class UserRequest(BaseModel):
    user_message: str

CUR_DIR = os.path.dirname(os.path.abspath(__file__))

@app.post("/chat")
def generate_chat(req: UserRequest):
    context = req.dict()
    user_input = context["user_message"]

    messages = create_prompt(user_input)
    response = client.chat.completions.create(
        model="gpt-3.5-turbo",
        messages=messages,
        temperature=0.4,
        max_tokens=500
    )
    print(response.choices[0].message.content)
    return response.choices[0].message.content