이 코드는 PDF 문서를 기반으로 한 RAG(Retrieval-Augmented Generation) 챗봇입니다.
RAG는 사용자의 질문에 대해 정보 검색(Retrieval)과 생성(Generation)을 결합한 방식으로 동작합니다.
즉, PDF 문서에서 질문과 관련 있는 내용을 찾아(OpenAI API 사용), 가장 적절한 답변을 제공합니다.
📌 FAISS (Facebook AI Similarity Search)
➡️ 고차원 벡터 검색을 위한 라이브러리로, 빠른 최근접 이웃(Nearest Neighbor) 검색을 지원한다.
➡️ 코사인 유사도(Cosine Similarity) 또는 내적(Inner Product) 기반의 검색이 가능하다.
📌 LangChain
➡️ LLM 기반 애플리케이션을 구축하는 라이브러리로, 문서 처리, 검색, LLM과의 상호작용을 지원한다.
➡️ PyPDFLoader: PDF 문서를 불러와서 텍스트로 변환
➡️ RecursiveCharacterTextSplitter: 문서를 적절한 크기의 청크(chunk)로 분할
➡️ HuggingFaceEmbeddings: 문장을 벡터로 변환하여 검색에 활용
📌 Gradio
➡️ 웹 UI 프레임워크로, Python 코드에서 간단하게 인터페이스를 만들 수 있도록 지원한다.
import os
import gradio as gr
import faiss
import numpy as np
import pandas as pd
import pickle
from langchain.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.embeddings import HuggingFaceEmbeddings
from langchain.chat_models import ChatOpenAI
from dotenv import load_dotenv
📌 GPT-4o-mini 모델
➡️ OpenAI에서 제공하는 경량화된 GPT-4 모델로, 빠른 응답 시간과 효율성을 제공
➡️ "temperature=0.3" 값은 생성된 응답의 일관성을 유지하면서도 어느 정도 창의성을 부여하는 역할
➡️ "predict(prompt)"를 사용하여 입력된 prompt에 대해 최적의 응답을 생성
# 환경 변수 로드
load_dotenv()
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
llm = ChatOpenAI(api_key=OPENAI_API_KEY, model_name="gpt-4o-mini", temperature=0.3)
📌 FAISS 인덱스 저장
➡️ FAISS_INDEX_PATH: 벡터화된 문서 데이터를 저장하는 파일
➡️ DOCUMENTS_PATH: 문서의 메타데이터(출처, 내용)를 저장하는 파일
FAISS_INDEX_PATH = "faiss_index.bin"
DOCUMENTS_PATH = "documents.pkl"
📌 Hugging Face Embedding
➡️ 문장을 벡터로 변환하는 임베딩 모델
➡️ "BAAI/bge-m3" 모델은 BioMedical 및 General Embeddings(BGE) 모델로, 의료 및 일반적인 문서 검색에 최적화
➡️ FAISS 검색을 위해 벡터화를 수행하여 의미론적 검색(Semantic Search) 적용
embedding_model = HuggingFaceEmbeddings(model_name="BAAI/bge-m3")
📌 IndexFlatIP(Inner Product)
➡️ FAISS의 벡터 검색 방식 중 하나로, 내적 기반(Inner Product) 유사도 검색을 수행
➡️ 코사인 유사도(Cosine Similarity) 기반의 벡터 정규화를 활용하여 최적의 검색 결과를 제공
embedding_dim = 1024 # 벡터 차원 수
index = faiss.IndexFlatIP(embedding_dim) # FAISS 인덱스 (내적 기반)
documents = [] # 문서 메타데이터 저장용
📌 PyPDFLoader
➡️ PDF 문서에서 페이지별 텍스트를 추출하여, 자연어 검색이 가능하도록 변환
def extract_text_from_pdf(pdf_path):
"""PDF에서 텍스트를 추출"""
loader = PyPDFLoader(pdf_path)
documents = loader.load()
text = "\n".join([doc.page_content for doc in documents])
return text
📌 RecursiveCharacterTextSplitter
➡️ 문서를 일정 크기(500 토큰)로 분할하여, 검색 성능 최적화
📌 L2 정규화(Normalization)
➡️ FAISS의 faiss.normalize_L2(embeddings)를 통해 코사인 유사도 기반 검색 최적화
def process_pdf(pdf_path):
"""PDF 문서를 벡터 저장소에 저장"""
text = extract_text_from_pdf(pdf_path)
text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=100)
texts = text_splitter.split_text(text)
embeddings = embedding_model.embed_documents(texts)
embeddings = np.array(embeddings).astype('float32')
faiss.normalize_L2(embeddings)
index.add(embeddings)
for i, chunk in enumerate(texts):
documents.append({"source": pdf_path, "chunk": chunk})
print(f"{pdf_path} 처리 완료. 총 {len(texts)}개의 청크 저장 완료.")
📌 Prompt Engineering 기법
➡️ context 내 문서를 질문과 함께 주고, 가장 적절한 답변을 생성하도록 LLM을 활용
➡️ "strip()"을 사용하여 불필요한 공백을 제거하고 응답을 최적화
def extract_most_relevant_summary(question, context):
"""OpenAI API를 사용하여 질문과 관련 있는 답변을 요약"""
prompt = f"""
다음은 질문과 관련 있는 문서 내용입니다:
{context}
위 내용을 바탕으로 질문에 대한 가장 적절한 답변을 짧고 명확하게 작성하세요:
질문: {question}
답변:
"""
response = llm.predict(prompt)
return response.strip()
📌 MMR (Maximal Marginal Relevance) 기반 검색
➡️ FAISS 검색 후 질문과 가장 관련 있는 문서 3개를 선택
➡️ "max_similarity < 0.4" → 유사도가 너무 낮으면 답변을 주지 않음
def query_chatbot(question):
"""MMR 검색을 적용한 주요 답변 생성 (유사도 0.4 이하이면 답변 없음)"""
if len(documents) == 0:
return "답변을 찾을 수 없습니다. (인덱스가 로드되지 않음)"
question_embedding = np.array(embedding_model.embed_query(question)).astype('float32').reshape(1, -1)
faiss.normalize_L2(question_embedding)
D, I = index.search(question_embedding, 10)
max_similarity = np.max(D[0])
if max_similarity < 0.4:
return "적절한 답변을 찾을 수 없습니다. PDF와 관련있는 질문을 해주세요."
valid_indices = [(idx, score) for idx, score in zip(I[0], D[0]) if score > 0.4]
if not valid_indices:
return "적절한 답변을 찾을 수 없습니다. PDF와 관련있는 질문을 해주세요."
selected_indices = valid_indices[:3]
results = []
sources = []
scores = []
context_texts = []
for idx, score in selected_indices:
chunk_text = documents[idx]["chunk"]
results.append(chunk_text)
sources.append(documents[idx]['source'])
scores.append(f"{score:.4f}")
context_texts.append(chunk_text)
context = "\n\n".join(context_texts)
main_answer = extract_most_relevant_summary(question, context)
df = pd.DataFrame({
"💡 주요 답변": [main_answer],
"🔍 유사도 점수": ["\n".join(scores)],
"📖 근거 발췌": ["\n\n".join(results)],
"📁 출처": ["\n".join(sources)]
})
return df.to_html(index=False, escape=False)
gr.Blocks() → Gradio UI 생성
question 입력 후 submit 버튼 클릭 시 query_chatbot() 호출
def chatbot_interface():
with gr.Blocks() as demo:
gr.Markdown("# 📚 PDF 검색 Q&A 챗봇")
with gr.Row():
question = gr.Textbox(label="💬 질문을 입력하세요")
submit = gr.Button("🔍 질문하기")
answer = gr.HTML(label="📢 답변")
submit.click(query_chatbot, inputs=question, outputs=answer)
demo.launch(share=True)
PDF 처리 후, 챗봇 UI 실행
if __name__ == "__main__":
pdf_dir = "/Users/SY/LLM_project/llm29/data/paper"
process_all_pdfs(pdf_dir)
chatbot_interface()
이 코드는 PDF 논문을 벡터 저장소에 저장하고, 사용자의 질문에 대해 관련 정보를 검색하여 응답을 생성하는 챗봇을 구현합니다. 🚀