우와한 개발자

[RAG] RAG 기반 문서 질의응답 챗봇 구현하기 — LangChain, ChromaDB, Streamlit

by 우와한개발자

AI

1. RAG란?

  • Retrieval-Augmented Generation의 약자
  • 외부 문서를 검색(Retrieval)해서 LLM의 답변 생성(Generation)에 활용하는 기법
  • LLM이 학습하지 않은 사내 문서, PDF 등에 대한 질의응답이 가능해짐

 

2. 전체 처리 흐름

PDF 업로드
    ↓
PDF Loader       → PDF를 페이지 단위로 읽기
    ↓
Chunking         → 긴 문서를 일정 크기의 청크로 분할
    ↓
Embedding        → 각 청크를 숫자 벡터로 변환
    ↓
Vector DB        → 벡터를 ChromaDB에 저장
    ↓
사용자 질문 입력
    ↓
Retriever        → 질문과 가장 유사한 청크 검색
    ↓
LLM (GPT)        → 검색된 청크를 참고하여 답변 생성
    ↓
답변 출력

 

3. 설치

pip install streamlit langchain langchain-openai langchain-community
pip install chromadb pypdf python-dotenv
  • langchain : LLM 파이프라인 구축 프레임워크
  • langchain-openai : OpenAI 모델 연동
  • langchain-community : PDF 로더 등 서드파티 연동 도구
  • chromadb : 벡터 데이터베이스
  • pypdf : PDF 파일 파싱
  • python-dotenv : .env 파일로 환경변수 관리

 

4. 구성 요소

1) PDF 로딩 및 청크 분할

  • PyPDFLoader : PDF를 페이지 단위로 로딩
  • RecursiveCharacterTextSplitter : 긴 문서를 청크로 분할
    • chunk_size=1000 : 청크당 최대 문자 수
    • chunk_overlap=200 : 청크 간 겹치는 문자 수 → 문맥이 잘리지 않도록 유지

 

2) 임베딩 및 벡터 DB 저장

  • OpenAIEmbeddings : 텍스트를 숫자 벡터로 변환
  • Chroma.from_documents() : 변환된 벡터를 ChromaDB에 저장

 

3) 검색 및 답변 생성

  • vectordb.as_retriever(search_kwargs={"k": 3}) : 질문과 유사한 청크 3개 검색
  • ChatOpenAI(model="gpt-4o-mini", temperature=0) : 검색된 청크를 컨텍스트로 넣어 답변 생성

 

5. 전체 코드

import tempfile
import streamlit as st
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_chroma import Chroma

load_dotenv()

st.title("RAG 기반 문서 질의응답 챗봇")

# ① PDF 업로드
uploaded_file = st.file_uploader("PDF 업로드", type=["pdf"])

if uploaded_file:
    # ② 임시 파일로 저장 후 로딩
    with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp_file:
        tmp_file.write(uploaded_file.read())
        pdf_path = tmp_file.name

    loader = PyPDFLoader(pdf_path)
    documents = loader.load()

    # ③ 청크 분할
    splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
    chunks = splitter.split_documents(documents)

    # ④ 임베딩 → 벡터 DB 저장
    embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
    vectordb = Chroma.from_documents(documents=chunks, embedding=embeddings)
    retriever = vectordb.as_retriever(search_kwargs={"k": 3})

    st.success(f"문서 로드 완료 ({len(chunks)}개 청크)")

    # ⑤ 세션 상태로 대화 이력 초기화
    if "messages" not in st.session_state:
        st.session_state.messages = []

    # ⑥ 이전 대화 출력
    for msg in st.session_state.messages:
        with st.chat_message(msg["role"]):
            st.write(msg["content"])

    # ⑦ 사용자 질문 입력
    if prompt := st.chat_input("질문을 입력하세요"):
        st.session_state.messages.append({"role": "user", "content": prompt})
        with st.chat_message("user"):
            st.write(prompt)

        # ⑧ 관련 청크 검색 → LLM 답변 생성
        docs = retriever.invoke(prompt)
        context = "\\n\\n".join([doc.page_content for doc in docs])
        llm_prompt = f"""
        당신은 문서 기반 QA 시스템입니다.
        다음 문서를 참고하여 답변하세요.
        문서: {context}
        질문: {prompt}
        답변:
        """
        llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
        response = llm.invoke(llm_prompt)

        # ⑨ 답변 출력 및 저장
        with st.chat_message("assistant"):
            st.write(response.content)
        st.session_state.messages.append({"role": "assistant", "content": response.content})

        # ⑩ 검색된 청크 확인 (선택)
        with st.expander("검색된 문서 보기"):
            for i, doc in enumerate(docs):
                st.write(f"Chunk {i+1}")
                st.write(doc.page_content)

블로그의 정보

우와한개발자 님의 블로그

우와한개발자

활동하기