LLM&RAG

vLLM OpenAI API 서버와 랭체인(LangChain) 연동하여 RAG 구축하기

이수진의 블로그 2024. 11. 2. 10:20
반응형
728x170

포스팅 개요

본 포스팅은 대규모 언어 모델(Large Language Models, LLM)을 쉽고 빠르게 배포 및 서빙할 수 있는 vLLM 글의 3번째 글(vLLM Langchain Tutorial)로, OpenAI API 서버로 배포된 vLLM과 랭체인(Langchain) 라이브러리를 연동해 RAG를 간단하게 구현하는 방법과 예제(example)를 알아보는 포스팅입니다.

이전 글들에서 vLLM이란 무엇이고, 사용법, 설치 방법, API로 배포하는 방법 등을 정리해두었으니 vLLM에 익숙하지 않으신 분들은 이전 글들을 참고 부탁드립니다.

 

vLLM과 관련된 포스팅은 아래와 같습니다.

 

이번 포스팅을 작성하면서 참고한 문서 및 자료는 아래와 같습니다.

 

 

OpenAI | 🦜️🔗 LangChain

You are currently on a page documenting the use of OpenAI text completion models. The latest and most popular OpenAI models are chat completion models.

python.langchain.com


포스팅 본문

이번 포스팅은 LLM 모델을 vLLM 라이브러리를 활용해 OpenAI API 형태로 배포한 후 랭체인(Langchain)과 연동하여 RAG를 구성하는 예제(example)를 정리한 포스팅입니다. 본 포스팅의 순서는 아래와 같이 진행됩니다.

 

1. 필요한 라이브러리 가져오기(import)

2. vLLM의 API를 활용한 간단한 랭체인 예시

3. RAG 구성 및 구현

 

1. 라이브러리 가져오기

먼저, 랭체인(LangChain)을 활용하기 때문에 랭체인과 관련된 라이브러리들을 로드합니다. 저는 RAG를 구성할 때 FAISS를 이용해서 벡터 검색(vector search)를 진행할 것이라서 faiss를 import 했습니다. 또한, 임베딩도 허깅페이스(HuggingFace) 모델을 활용하기에 허깅페이스 임베딩을 가져왔습니다. 그리고 RAG에서 사용되는 데이터는 제 블로그 글을 활용하려고 해서 WebBaseLoader로 데이터를 가져올 것입니다. 나머지는 프롬프트 템플릿(Prompt Template)과 텍스트를 분할(Split)할 때 사용하는 재귀적 문자 분할(RecursiveCharacterTextSplitter) 등을 import했습니다. 

 

from langchain_community.vectorstores.faiss import FAISS
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.document_loaders import WebBaseLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
from langchain.prompts import PromptTemplate
from langchain.chains import LLMChain
from langchain_openai  import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage

openai_api_key = "EMPTY"
openai_api_base = "http://localhost:8007/v1"
model_path = "/llm_models"
model_code = "llama-3-Korean-Bllossom-8B"

 

뿐만 아니라, 이전 글에서 설정하고 테스트 해봤던 vLLM의 OpenAI API 서버와 통신하기 위해서 openai_api_key는 empty로 설정하고 url과 사용하는 모델들을 설정하였습니다. 이때, LLM으로 사용하는 모델은 Korean-Bllossom-8B 모델을 감사히 사용했습니다. 

 

2. vLLM의 API를 활용한 간단한 랭체인 연동 예시

이제 본격적으로 랭체인 기반으로 vLLM의 OpenAI API 서버와 연동해서 LLM의 결과를 가져와보겠습니다. 

가장 간단한 예시로는 랭체인에서 제공해주는 시스템 메세지(SystemMessage)와 휴먼메세지(HumanMessage)를 이용한 통신 방법이 있는데요. 아래 코드와 같이 적용할 수 있을겁니다.

llm = ChatOpenAI(
    model=f"{model_path}/{model_code}",
    openai_api_key=openai_api_key,
    openai_api_base=openai_api_base,
    max_tokens=850,
    temperature=0,
)

messages = [
    SystemMessage(
        content="당신은 유능한 AI 어시스턴트 입니다. 사용자의 질문에 대해 친절하게 답변해주세요."
    ),
    HumanMessage(
        content="안녕하세요. 저는 이수진입니다."
    ),
]
msg = llm.invoke(messages)
print(msg.content)
print(msg.response_metadata['token_usage'])

 

시스템 메세지에는 AI의 시스템 메세지를 적용하면 되고, 휴먼 메세지에는 질문 등 원하는 질의를 넣어주시면 됩니다. 이렇게 구성된 llm을 llm.invoke로 통신하게 되면 vLLM에 올라가있는 llm 모델(저는 Korean-Bllossom-8B 모델)과 통신하여 답을 얻을 수 있습니다. 

 

또 다른 랭체인의 간단한 예시는 바로 프롬프트 템플릿(PromptTemplate)을 활용한 코드일 것입니다. 이때, 템플릿을 함께 제공해서 from_template에 셋팅해준 뒤 원하는 질의를 제공하여 LLM과 통신할 수 있는데요. 

바로 아래 코드와 같은 형태로 동작이 될 것입니다.

template = """Question: {question}
Answer: """

prompt = PromptTemplate.from_template(template)
llm_chain = LLMChain(prompt=prompt, llm=llm)


question = "안녕하세요. 저는 이수진입니다. 당신은 누구인가요? 저와 대화할 수 있나요?"
print(llm_chain.invoke(question))

 

반응형

 

이때 저는 LLMChain을 활용해 prompt에는 셋팅한 프롬프트를 넣어주고 llm은 vLLM을 통신하는 llm 정보를 넣어주었습니다.

질의로 저는 이수진입니다. 당신은 누구인가요? 대화할 수 있나요?라고 제공했을 때 LLM이 적절한 답을 제공하는 것을 확인할 수 있습니다.

 

자, 이렇게 간단하게 랭체인(Lanchain)을 이용해서 vLLM의 OpenAI API 서버와 통신하는 방법을 알아봤습니다. 

여기서 한 스텝 더 나아가서 RAG를 구성해서 vLLM에 올려져 있는 LLM 모델이 결과를 제대로 제공해주는지 결과를 확인해보겠습니다. 

 

3. RAG 구성 및 구현 예시

저는 RAG 구성을 위해서 임베딩 모델(embedding model)은 bge-m3 모델을 활용했습니다. 이 모델은 허깅페이스에서 제공해주는 모델이기 때문에 랭체인에서 제공해주는 허깅페이스 임베딩(HuggingFaceEmbeddings) 클래스를 사용하여 모델을 준비합니다. 

또한, RAG에 사용되는 데이터는 제 블로그 668번 vLLM 사용법 블로그 글을 활용합니다. 이를 위해서 랭체인의 웹 문서 로더(WebBaseLoader)를 활용해서 게시글 데이터를 가져왔습니다. 이를 RecursiveCharacterTextSplitter로 텍스트를 분할한 뒤 FAISS를 사용해 임베딩화 하여 retriever로 사용합니다. 

이 과정을 담은 코드는 다음과 같습니다.

model_path = "/llm_models"
embed_model_name = 'bge-m3'
encode_kwargs = {"normalize_embeddings": True}
model_kwargs = {
            "device": "cuda:0"
        }

embeddings = HuggingFaceEmbeddings(
                    model_name=f'{model_path}/{embed_model_name}',
                    model_kwargs=model_kwargs,
                    encode_kwargs=encode_kwargs,
                )



loader = WebBaseLoader("https://lsjsj92.tistory.com/668")
# loader.requests_kwargs = {'verify':False}  #  [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed 와 같은 에러가 날 경우 임시방편
data = loader.load()
text_splitter = RecursiveCharacterTextSplitter(chunk_size=350, chunk_overlap=50)
texts = text_splitter.split_documents(data)
db = FAISS.from_documents(texts, embeddings)
db_retriever = db.as_retriever(search_kwargs={"k": 2})

 

이제 vLLM과 통신해서 답을 가져오기 위해 템플릿을 구성합니다. 구성한 템플릿은 간단하게 랭체인의 PromptTemplate을 사용하며 {ref}라는 곳에 검색기(retriever)가 가져온 데이터를 넣어서 답변을 해주는 형태로 프롬프트 템플릿을 구성했습니다. 

template = """당신은 유능한 AI 어시스턴트 입니다. [Ref] 문서를 참고하여, 사용자의 질문에 대해 친절하게 답변해주세요. 문서에 없는 내용은 말하지 말아주시고, 문서에 내용이 없다면 내용이 없다고 말해주세요.
Question: {question}
[Ref]
{ref}
Answer: """

prompt = PromptTemplate(input_variables=["question", "ref"], template=template)
llm_chain = LLMChain(prompt=prompt, llm=llm)

print(prompt)
llm_chain

 

300x250

이렇게 구성한 템플릿은 위 사진과 같습니다. 유능한 AI 어시스턴트이며 [Ref]의 문서를 참고해서 사용자의 질문(Question 영역)에 따라 답변을 수행하도록 구성해두었습니다. 

 

자! 이제 간단하게 RAG를 사용하기 위한 준비가 완료되었습니다. 이제 질의를 던져서 vLLM의 답변을 확인하면 되는데요. 랭체인의 LCEL으로 확인하기 전에 먼저, 검색(retriever)을 해보고 거기에서 나온 답변을 LLM에게 전달하는 과정 하나하나를 살펴보면서 답변이 vLLM이 답변을 잘 해주는지 확인해보겠습니다.

question = "vllm이란 무엇인가요?"
docs = db_retriever.invoke(question)
llm_result = llm_chain.invoke({"question": question, "ref": docs})['text']
llm_result

 

위 코드를 실행하면 먼저, db_retriever로 "vLLM이란 무엇인가요?"라는 질문에 적합한 문서를 찾아줍니다. 이때 문서는 WebBaseLoader로 가져온 블로그 글의 내용입니다. 그리고 찾아준 문서를 llm_chain의 invoke에 인자 값으로 넣어줘서 vLLM 모델이 제공해주는 결과를 확인할 수 있습니다. vLLM이란 무엇인가요?라는 질문에 vLLM은 대규모 언어모델(LLM)의 추론, 서빙을 쉽고 빠르게 도와주는 라이브러리라는 답변을 잘 찾아주어 제공해주었습니다. 

 

그럼 마지막으로 랭체인의 LCEL(LangChain Expression Language) 문법으로 실행해보겠습니다. 프롬프트나 모델 구성 방법은 똑같으며 chain 형식으로 묶은 것만 차이점이 있습니다. 이때, 사용자의 Question은 RunnablePassThrough로 넘어가도록 설정하였고 ref는 db_retriever를 사용하도록 했습니다.

template = """당신은 유능한 AI 어시스턴트 입니다. [Ref] 문서를 참고하여, 사용자의 질문에 대해 친절하게 답변해주세요. 문서에 없는 내용은 말하지 말아주시고, 문서에 내용이 없다면 내용이 없다고 말해주세요.
Question: {question}
[Ref]
{ref}
Answer: """

prompt = PromptTemplate(input_variables=["question", "ref"], template=template)

llm = ChatOpenAI(
    model=f"{model_path}/{model_code}",
    openai_api_key="EMPTY",
    openai_api_base=openai_api_base,
    max_tokens=850,
    temperature=0,
)

chain = (
    {"ref": db_retriever, "question": RunnablePassthrough()}
    | prompt
    | llm
    | StrOutputParser()
)

question = "vllm이란 무엇인가요?"
res = chain.invoke(question)
print(res)

 

똑같이 vLLM이란 무엇인가요?라는 질문에 vLLM에 대한 설명을 블로그 글에서 잘 찾아내어 설명하는 것을 확인할 수 있습니다.


마무리

이번 포스팅은 vLLM을 OpenAI API 서버로 서빙(serving)하여 랭체인(LangChain)과 연동하는 예제를 살펴본 포스팅입니다. 3번에 걸쳐서 vLLM Langchain 튜토리얼(Tutorial) 예제를 살펴보았는데요. 부디 도움되시길 바랍니다.

긴 글 읽어주셔서 감사합니다.

반응형
그리드형