ML & DL

Langchain으로 LLM에 RAG(검색증강생성)을 적용해 보기

안모 2024. 2. 18. 14:16

참조 포스팅은 Langchain + Ollama + Chainlit을 이용하여 LLM 챗봇을 구성하는 것을 다루고 있다.

 

나도 한 번 따라서 만들어 봤는데, 구현과 에러 핸들링을 정리해 보았다. 자세한 건 포스팅 참고.

 

알아두면 좋은것

  • Langchain: 대규모 언어모델(LLM)을 이용한 애플리케이션을 개발하기 위한 프레임워크
  • Ollama: LLM을 실행, 생성 및 공유하는 오픈소스 프로젝트
  • RAG: LLM에 외부 지식소스를 제공하여 이를 바탕으로 답변을 생성하는 방법
  • Chainlit: 대화형 AI를 제공하기 위한 Python 패키지

필요한 pdf파일은 그냥 검색해서 나온 요리책 pdf를 이용했다.

 

load_data_vdb.py

from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import PyPDFLoader, DirectoryLoader
from langchain.document_loaders.pdf import PyPDFDirectoryLoader
from langchain_community.document_loaders import UnstructuredHTMLLoader, BSHTMLLoader
from langchain_community.vectorstores.chroma import Chroma
from langchain_community.embeddings import GPT4AllEmbeddings
from langchain_community.embeddings import OllamaEmbeddings  

import os

DATA_PATH="data/"
DB_PATH = "vectorstores/db/"

def create_vector_db():
    loader = PyPDFDirectoryLoader(DATA_PATH)
    documents = loader.load()
    print(f"Processed {len(documents)} pdf files")
    text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=50)
    texts=text_splitter.split_documents(documents)
    print(texts)
    vectorstore = Chroma.from_documents(documents=texts, embedding=GPT4AllEmbeddings(),persist_directory=DB_PATH)      
    vectorstore.persist()

if __name__=="__main__":
    create_vector_db()
  • load_data_vdb.py는 PDF파일을 임베딩하여 벡터DB에 저장한다. 해당 프로젝트의 data 디렉토리에 PDF파일을 저장하고 코드를 실행하면 vectorstores/db 디렉토리에 PDF파일에 대한 임베딩 정보가 저장된다.
  • langchain_community: langchain을 import하는 방식이 달라졌다. 그냥 langchain을 설치해서 import하면 에러가 발생하니 변경해줘야 한다.
  • Chroma: CromaDB를 이용할 때 vectorstores.chroma 처럼 vectorstores에 croma를 명시해줘야 Croma 클래스가 제대로 불러와진다.

RAG.py

from langchain import hub
from langchain_community.embeddings import GPT4AllEmbeddings
from langchain_community.vectorstores.chroma import Chroma
from langchain_community.llms.ollama import Ollama
from langchain.callbacks.manager import CallbackManager
from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler
import chainlit as cl
from langchain.chains import RetrievalQA,RetrievalQAWithSourcesChain

QA_CHAIN_PROMPT = hub.pull("rlm/rag-prompt-mistral")

def load_llm():
 llm = Ollama(
 model="dolphin2.2-mistral"
 verbose=True,
 callback_manager=CallbackManager([StreamingStdOutCallbackHandler()]),
 )
 return llm


def retrieval_qa_chain(llm,vectorstore):
 qa_chain = RetrievalQA.from_chain_type(
    llm,
    retriever=vectorstore.as_retriever(),
    chain_type_kwargs={"prompt": QA_CHAIN_PROMPT},
    return_source_documents=True,
)
 return qa_chain


def qa_bot(): 
 llm=load_llm() 
 DB_PATH = "vectorstores/db/"
 vectorstore = Chroma(persist_directory=DB_PATH, embedding_function=GPT4AllEmbeddings())

 qa = retrieval_qa_chain(llm,vectorstore)
 return qa 

@cl.on_chat_start
async def start():
 chain=qa_bot()
 msg=cl.Message(content="Firing up the research info bot...")
 await msg.send()
 msg.content= "Hi, welcome to research info bot. What is your query?"
 await msg.update()
 cl.user_session.set("chain",chain)

@cl.on_message
async def main(message):
 chain=cl.user_session.get("chain")
 cb = cl.AsyncLangchainCallbackHandler(
 stream_final_answer=True,
 answer_prefix_tokens=["FINAL", "ANSWER"]
 )
 cb.answer_reached=True
 res=await chain.ainvoke(message.content, callbacks=[cb])
 print(f"response: {res}")
 answer=res["result"]
 answer=answer.replace(".",".\n")
 sources=res["source_documents"]

 if sources:
  answer+=f"\nSources: "+str(str(sources))
 else:
  answer+=f"\nNo Sources found"

 await cl.Message(content=answer).send() 
  • RAG.py는 Ollama를 통해 서빙되는 LLM에 QA유닛으로 벡터DB에 저장된 임베딩 정보를 제공하며, 그 결과와 소스 참조 정보를 Chainlit을 통해 챗봇형태로 제공하는 방식이다.
  • Ollama: llms.ollama를 명시해줘야 Ollama 클래스가 제대로 불러와진다.
  • 참조 포스팅은 Ollama에 mistral모델을 이용했는데, 나는 더 훈련되고 조정된 모델을 기대하여 dolphin2.2-mistral을 이용했다.

 

 

제대로 동작한다. 어떤 치즈가 샐러드에 어울리냐는 질문을 하자 챗봇은 요리책 PDF내용을 사용해서 답변해준다. 동일한 질문도 완전히 같은 답변이 아니라 비슷한 맥락의 답변을 해주고 있고, 답변을 생성한 소스는 무엇인지도 알 수 있다.

 

그러면 제공된 데이터 소스에 없는 답변은 어떻게 대답할까?

 

일단 컨텍스트에 없다고 알려준다. 만약 하나를 정해야 한다면 호불호가 강한 블루치즈는 어울리지 않을것 같다고 답변한다. 이 답변이 데이터 소스를 이용해서 추론한 것인지 아니면 모델 생성 당시에 학습된 내용인지는 모르겠지만 의도대로 동작하는 것 같다. 

 

한국어로 파인튜닝된 모델도 비슷한 퀄리티가 나올까? 언제 한 번 시도 해봐야겠다.

 

참조: https://medium.aiplanet.com/implementing-rag-using-langchain-ollama-and-chainlit-on-windows-using-wsl-92d14472f15d