LangChain과 Ollama로 RAG를 추가한 챗봇 구현하기

yoohee.chung·2025년 1월 20일

LangChain + RAG 학습을 위해 예제 삼아 Ollama를 사용하여 RAG 적용된 Q&A 챗봇을 CLI로 구현해보았다. RAG가 무엇인지, RAG를 적용하면 LLM이 실제로 생성하는 답안이 어떻게 달라지는지 확인해보자!

개발 환경 준비

개발환경
OS: Ubuntu 22.04
Python 3.10
NVIDIA GPU + CUDA 12.4
다음의 명령어로 Miniconda를 설치해 conda 환경을 구성할 수 있도록 한다.

mkdir -p ~/miniconda3
wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O ~/miniconda3/miniconda.sh
bash ~/miniconda3/miniconda.sh -b -u -p ~/miniconda3
rm ~/miniconda3/miniconda.sh

Conda 환경에 필요한 패키지 설치하기

conda 환경을 생성하고 활성화한다. 필요한 패키지는 해당 가상 환경 내에 설치된다.

conda create --name langchain_env python=3.10
conda env list # 생성된 conda env 확인
conda activate langchain_env

langchain

langchain을 설치한다. 다양한 종류의 langchain 생태계의 라이브러리도 함께 설치해준다.

pip install langchain
pip install langchain_ollama
pip install langchain_text_splitters
pip install langchain_core
pip install langchain_community

Ollama

클라우드 환경의 Endpoint 대신 내 GPU에서 모델을 구동하기 위해 Ollama를 사용하였다.

curl -fsSL https://ollama.com/install.sh | sh
 
#GPU를 인식할 수 없다는 경고 메시지가 나올 경우 다음과 같이 lshw를 설치해준다.
sudo apt install lshw
 
#systemctl을 사용할 수 있는 환경에서는 service로 자동 등록된다.
sudo systemctl status ollama
sudo systemctl start ollama
 
#직접 실행도 가능하다.
ollama serve

간단한 Q&A 챗봇 만들기

LLM 테스트하기

Ollama 에서 실행할 LLM을 선택해 pull 하고 테스트해본다. 나는 MS의 Phi4 14B 모델을 사용하였다.
여기에서 Ollama가 지원하는 모델의 종류를 확인할 수 있다.

ollama pull phi4
ollama run phi4

phi4 모델이 실행되고 질의를 할 수 있다. 테스트 겸 몇 가지 질문을 던져보았다.

>>> Who is Joe Biden?
As of my last update in October 2023, Joseph Robinette "Joe" Biden Jr. is an American politician serving as the 46th President of the United States since January 20, 2021. He previously served as the 47th Vice President under President Barack Obama
from 2009 to 2017. Before his vice presidency, Biden represented Delaware in the U.S. Senate for over three decades, from 1973 until he became Vice President.

Born on November 20, 1942, in Scranton, Pennsylvania, Biden grew up in a politically active family and later settled in Wilmington, Delaware. He graduated from the University of Delaware and Syracuse Law School before entering politics. Throughout
his career, Biden has been involved in numerous legislative efforts, particularly focusing on foreign relations, crime prevention, and healthcare.
 
Biden's 2020 presidential campaign emphasized unity, rebuilding America after the COVID-19 pandemic, addressing climate change, and enhancing social justice. His administration has focused on issues like economic recovery, vaccine distribution for
COVID-19, infrastructure investment, and restoring alliances with international partners.
 
For more current information beyond my last update, please consult a reliable news source or official government website.

몇몇 질문은 Hallucination을 유도한다. (미국보다는 아무래도 한국에 관련된 질문을 하면 잘 모르는 듯...) 이명박 전 대통령의 모교를 묻는 질문이다.

>>> What's the alma mater of Lee Myung-bak?
Lee Myung-bak attended Kyunggi High School in South Korea before studying abroad at George Washington University, where he earned his bachelor’s degree in economics and business administration. He later completed a master's degree in urban planning
from Seoul National University in South Korea. Lee went on to further his education in the United States with a Ph.D. program in city planning at MIT (Massachusetts Institute of Technology), although he did not complete it, leaving after completing
his coursework but before submitting his dissertation.
 
Lee Myung-bak served as the 10th President of South Korea from 2008 to 2013.

이명박의 모교를 물었더니 실제로 졸업한 동지상고-고려대가 아니라 엉뚱한 학교 이름을 대답한다. 대체 경기고-조지워싱턴 대학교 출신에 서울대에서 도시공학을 전공한 정치인이 누구길래...
Phi4에서 학습된 내용만으로는 정확히 답하지 못해 hallucination이 생기는 것을 볼 수 있다. 너무 뻔뻔하게 얘기해서 순간적으로 이명박 전 대통령이 미국 유학파인 줄 알았다.

LangChain으로 간단한 Q&A 챗봇 구현하기

다음은 사용자로부터 입력을 받으면 Prompt를 생성해 LLM에 질의하고 답변을 출력하는 간단한 챗봇을 생성하는 코드이다.

from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
from langchain_ollama import ChatOllama
 
template = '''
You are a smart, well-educated Question-Answer assistant. Your mission is to answer the question briefly.
 
#Question:
{question}
 
#Answer:
'''
prompt = PromptTemplate.from_template(template)
 
llm = ChatOllama(
        model="phi4",
        temperature=0.01,
        )
 
chain = (
        {"question": RunnablePassthrough()} # 넘겨주는 그대로 입력함
        | prompt
        | llm
        | StrOutputParser()
    )
 
#main
def stream_answer(answer):
    for chunk in answer:
        print(f"{chunk}", end="")
 
while True:
    print("Input: ", end=" ")
    question = input()
    if question.lower().strip() == "quit":
        print("goodbye~")
        break
    else:
        answer = rag_chain.stream(question)
        stream_answer(answer)
    print()

사용자로부터 입력을 받아 그대로 Prompt로 넘겨준다.(RunnablePassthrough) 넘겨받은 question 을 prompt가 LLM으로 전달하면 생성하는 답변을 그대로 출력하는 코드이다.

RAG 구현하기 (naive RAG)

위에서 생성한 chatbot은 knowledge base를 따로 갖고 있지 않아서 때로 hallucination을 포함하는 답을 생성한다. 멍청한 챗봇이 어떻게 하면 '제대로' 답변을 하게 만들 수 있을까?

RAG이 그래서 필요합니다!

Retrieval-Augmented Generation(RAG)는 이러한 Hallucination을 예방하기 위한 혁신적인 방법이다.
RAG를 쉽게 설명하자면 'LLM에 검색엔진을 붙여주는 것', '검색과 생성을 통합하는 것'이라고 할 수 있겠다. 최신 뉴스 이벤트나 특정 분야의 전문 지식과 같은 주제에 대해 물어보면, RAG는 관련 문서를 찾아 그 내용을 바탕으로 답변을 구성한다.

RAG는 8단계로 구성된다. 가장 단순하고 기본적인 RAG (a.k.a naive RAG)를 기준으로 한 설명이다.

  1. Document Load - 기반 지식을 담는 문서/웹페이지 등을 로딩하는 단계이다.
  2. Text split - 큰 문서를 몇 개의 덩어리로 분할하는 단계이다. naive RAG에서는 보통 토큰 수를 기준으로 정량적으로 나누는데, 의미를 분석해 semantic한 분할을 하는 것도 가능하다.
  3. Text embedding - 텍스트를 의미를 가진 vector로 임베딩한다.
  4. Vector store 저장 - 임베딩 된 벡터들을 Database에 저장한다.
  5. Retrieval - 질문이 들어오면 벡터로 변환 후 해당 질문에 대한 정보를 vector store내에서 검색한다.
  6. Prompt - 언어 모델에 질문을 하기 위해 prompt를 구성한다.
  7. LLM - 구성된 프롬프트를 기반으로 거대언어모델이 답을 생성한다.
  8. 향상된 답변 출력 - LLM이 생성한 답변을 후처리&출력한다.

보다 정확하게 reference로 부터 정보를 얻어서 참고하여 답변할 수 있도록 RAG 를 사용하는 Chatbot으로 소스코드를 수정해보도록 하였다.

검색할 문서 전처리하기

참고할 문서를 DocumentLoader 로 가져오기 + Text Splitter로 분할하기

PDF, WORD, Web 문서 등을 DocumentLoader를 사용해서 로드할 수 있다.

다음의 코드는 wikipedia의 Lee Myung-bak 항목을 읽어와서 knowledge base로 삼아 RAG를 수행하도록 문서를 로딩하는 코드이다.

wikipedia 본문은 mw-body-content 라는 클래스를 가진 div 내에 있어서 해당 태그를 가져와 파싱하도록 구성하였다. 이후 chunk 1000개 단위 (overlap 100) 로 문서를 잘라주었다.(총 4개 정도로 나뉘었다.)

chunk size는 목적에 따라 다르게 지정하는 게 좋은데, 너무 크케 지정할 경우 Q&A 챗봇이 대답을 잘 못하는 것으로 보인다.
일반적으로 검색을 통해 질문에 응답하는 태스크는 상대적으로 작은 chunk 크기가 알맞다고 하며, 요약과 같이 전체 문서를 다 참고해야 할 task를 수행할 경우 chunk 크기가 보다 큰 게 좋다고 한다.

import bs4
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import WebBaseLoader
from langchain_community.vectorstores import FAISS
from langchain_ollama import OllamaEmbeddings
 
#Web document parser
parser = bs4.SoupStrainer(
        "div",
        attrs={"class": ["mw-body-content"]},
        )
 
loader = WebBaseLoader(
        web_paths=(
            "https://en.wikipedia.org/wiki/Lee_Myung-bak",
            ),
        bs_kwargs=dict(
            parse_only=parser
            )
        )
 
docs = loader.load()
print(f"Num of docs: {len(docs)}") # Will print 1.
 
#Split the loaded documents
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=100)
splits = text_splitter.split_documents(docs)

문서 vectorstore 생성하기

다음 단계는 빠른 검색(retrieve)을 위해 주어진 문서를 벡터로 임베딩하는 과정이다.

나는 FAISS를 사용해서 vector store를 생성했다. FAISS에 대해 자세한 정보는 FAISS란?을 참고하자.

Text embedding 모델을 사용해 자연어 문장들을 vector로 임베딩한 후, FAISS로 vector store를 구축하였다. 임베딩 모델로는 ollama가 제공하는 'nomic-embed-text' 를 사용했다.

ollama_embeddings = OllamaEmbeddings(model="nomic-embed-text")
vectorstore = FAISS.from_documents(documents=splits, embedding=ollama_embeddings)
retriever = vectorstore.as_retriever()

RAG chain 구성하기

Prompt에서 context 사용하게 구성하기

LLM이 주어진 맥락(context)를 참고하여 답안을 생성할 수 있도록 모델에게 context를 제공하는 prompt를 작성했다. 여기에 더해, 모르면 모른다고 말하라는 명령을 추가하였다.

from langchain_core.prompts import PromptTemplate
 
template = '''
You are a smart, well-educated Question-Answer assistant. Your mission is to answer the question using given contexts.
If you don't find any useful answer, just say you don't know the correct answer and do not try to generate incorrect answers.
 
#Question:
{question}
 
#Context:
{context}
 
#Answer:
'''
 
prompt = PromptTemplate.from_template(template)

LLM과 연결한 체인 구성

위의 소스코드에서 langchain을 구성하는 부분을 다음과 같이 수정했다. context 로 retriever를 넣어주었다.

#Create lang chain.
rag_chain = (
        {"context": retriever, "question": RunnablePassthrough()}
        | prompt
        | llm
        | StrOutputParser()
    )
 
#main
def stream_answer(answer):
    for chunk in answer:
        print(f"{chunk}", end="")
 
while True:
    print("Input: ", end=" ")
    question = input()
    if question.lower().strip() == "quit":
        print("goodbye~")
        break
    else:
        answer = rag_chain.stream(question)
        stream_answer(answer)
    print()

RAG 테스트하기

RAG이 잘 동작해서 hallucination을 유발하던 질문에 정확히 대답하는지 테스트해본다.

Input: What's the alma mater of Lee Myung-bak?
Lee Myung-bak's alma mater is Korea University.
 
**Explanation:**
The context provided includes information from Wikipedia about Lee Myung-bak. It mentions that he is a graduate of Korea University. This detail can be found in the first document, which provides an overview of his educational background along with other aspects of his life and career.

RAG 없이는 틀린 대답을 생성했지만 RAG를 적용하니 '고려대'를 정확하게 대답하는 것을 볼 수 있다.

Input: What's the alma mater of Moon Jae-in?
The provided contexts do not contain information about Moon Jae-in's alma mater. Therefore, I don't know the correct answer based on the given documents.
 
Moon Jae-in is a well-known South Korean politician who served as President of South Korea from 2017 to 2022. However, his educational background is not mentioned in the provided contexts. If you are looking for this information, it might be helpful to consult other reliable sources or databases that specifically cover Moon Jae-in's biography and education.

문재인 전 대통령의 모교를 물어보자 이번에는 문서 내에 해당 내용이 없으므로 이 문서를 통해 정확한 답을 알기 어렵다고 한다. 모르면 모른다는 대답을 하라는 프롬프트에 충실한 것을 확인할 수 있다.

profile
잘 먹고 잘 살고 싶습니다.

0개의 댓글