나만의 RAG Chatbot 만들기 – 4. PDF 파일 읽기

나만의 RAG Chatbot 만들기 – 4. PDF 파일 읽기

오늘은 PDF를 읽어와서 나만의 RAG를 만들어보겠습니다. 앞선 강의는 아래에서 참고해보세요.

PDF파일

먼저 PDF가 있으시면 그걸로 하시고, 없을 경우 테슬라의 사용자 매뉴얼을 샘플로 테스트하겠습니다.

https://www.tesla.com/ownersmanual/modely/ko_kr/Owners_Manual.pdf

먼저 코드에서는 pdf를 불러오기 위해 패키지를 설치합니다.

기본적으로 랭체인에서 불러오기 위해서는 pypdf를 사용하고 loader를 통해서 파일을 불러옵니다.

pip install pypdf #PDF 불러오기
pip install cryptography #암호화를 위한 패키지
pip install tiktoken #토큰 자르기

from langchain_community.document_loaders import PyPDFLoader
loader = PyPDFLoader('/workspaces/LLMTest/Owners_Manual.pdf')  # 아래를 참고하여 파일 위치 확인
pages = loader.load_and_split()
print(len(pages))
print(pages[0])

codespace에 파일 추가는 드래그 앤 드랍 하면 되는데요. 간단하게 드래그앤 드랍으로 추가가 가능합니다.

추가하고 마우스 우 클릭 – 경로 복사 해서 불러올 파일의 주소를 복사 붙여 넣기 합니다.

RAG(검색 증강 생성) 시스템을 구축할 때 가장 중요한 단계 중 하나가 바로 ‘텍스트 분할(Chunking)’입니다. RecursiveCharacterTextSplitter는 LangChain에서 가장 권장되는 기본 텍스트 분할기입니다.

쉽게 말해, 아주 긴 문서(PDF, 블로그 포스트 등)를 AI가 한 번에 읽기 좋은 크기의 덩어리로 잘라주는 역할을 합니다.

from langchain_openai import AzureChatOpenAI  
from dotenv import load_dotenv
import os
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage

load_dotenv()

MODEL_DEPLOYMENT_NAME = "gpt-4.1" # 실제 Azure 배포명을 입력하세요

llm = AzureChatOpenAI(
        deployment_name=MODEL_DEPLOYMENT_NAME,                        
        temperature=1,                        
)  

messages = [
         SystemMessage(content='당신은 업무 계획을 세워주는 업무 플래너 머신입니다. 사용자의 업무를 입력 받으면 이를 위한 계획을 작성합니다.'),     
         HumanMessage(content='신입사원 교육을 해야됩니다.') 
]  

# answer = llm.invoke(messages) 
#print(answer.content)

from langchain_community.document_loaders import PyPDFLoader

loader = PyPDFLoader('/workspaces/LLMTest/Owners_Manual.pdf') 
pages = loader.load_and_split()

# print(len(pages))
# print(pages[0].page_content)

import tiktoken 
tokenizer = tiktoken.get_encoding('cl100k_base')  
def tiktoken_len(text):
    tokens = tokenizer.encode(text)  
    return len(tokens)

from langchain_text_splitters import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(     
    chunk_size=1000, 
    chunk_overlap=100, 
    length_function=tiktoken_len 
) 

texts = text_splitter.split_documents(pages)

print("pages: ", len(pages))
print("texts: ", len(texts))

Ctrl+ K + C가 주석

Ctrl +K+ U가 주석 해제

텍스트 임베딩 모델 생성

RAG를 하기 위해서 langchain에서 임베딩 모델을 생성합니다.

from langchain_openai import AzureOpenAIEmbeddings  
embedding_model = AzureOpenAIEmbeddings(
                        azure_deployment=EMBEDDING_MODEL_NAME, 
                        chunk_size=1000)

그리고 실습을 위해서 임베딩 테스트 데이터로 테스트를 해봅니다.

이게 가장 많이 사용하는 샘플이라고 합니다.

유사한 값끼리 비교해봅니다.

from langchain_openai import AzureChatOpenAI  
from dotenv import load_dotenv
import os
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage

load_dotenv()

MODEL_DEPLOYMENT_NAME = "gpt-4.1" # 실제 Azure 배포명을 입력하세요

EMBEDDING_MODEL_NAME = 'text-embedding-3-large' # 사용할 OpenAI의 고성능 임베딩 모델 이름을 지정합니다.

from langchain_openai import AzureOpenAIEmbeddings  
embedding_model = AzureOpenAIEmbeddings(
                        azure_deployment=EMBEDDING_MODEL_NAME, # Azure 환경에 배포된 임베딩 모델을 연결합니다.
                        chunk_size=1000) # 한 번에 보낼 데이터 크기를 설정합니다.

examples = {
         "안녕하세요",     
         "제 이름은 홍길동 입니다.",     
         "이름이 무엇인가요?",     
         "랭체인은 유용합니다. ",
         "Hello World" 
         }  

embeddings = embedding_model.embed_documents(examples)

from numpy import dot
from numpy.linalg import norm
import numpy as np

def cos_sim(A,B):
  # 두 벡터 사이의 각도를 계산하여 1에 가까울수록 의미가 비슷하다고 판별합니다.
  return dot(A,B)/(norm(A)*norm(B))

print(cos_sim(embeddings[0], embeddings[1]))
# 질문과 문서 뭉치 중 1번 인덱스 문장의 유사도를 구합니다.

print(cos_sim(embeddings[0], embeddings[2]))
# 질문과 문서 뭉치 중 2번 인덱스 문장의 유사도를 구합니다.

PDF 파일에서 불러와서 스플릿하고 임베딩해서 저장까지 합니다.

loader = PyPDFLoader('/workspaces/LLMTest/Owners_Manual.pdf') # PDF 파일을 불러오는 로더 객체를 생성합니다.
pages = loader.load_and_split() # PDF를 읽어오고, 페이지 단위(기본)로 데이터를 나눕니다.

import tiktoken 
tokenizer = tiktoken.get_encoding('cl100k_base') # OpenAI 모델(GPT-4 등)이 사용하는 토큰 인코딩 방식을 가져옵니다.

def tiktoken_len(text):
    tokens = tokenizer.encode(text) # 텍스트를 토큰으로 변환합니다.
    return len(tokens) # 변환된 토큰의 개수를 반환합니다. (글자 수가 아닌 AI 비용/제한 기준)

from langchain_text_splitters import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter(     
    chunk_size=1000,           # 각 덩어리(Chunk)의 최대 길이를 1000 토큰으로 설정합니다.
    chunk_overlap=100,         # 덩어리 간 문맥 연결을 위해 앞뒤 100 토큰씩 겹치게 자릅니다.
    length_function=tiktoken_len # 길이를 재는 기준으로 위에서 만든 토큰 계산 함수를 사용합니다.
) 

texts = text_splitter.split_documents(pages) # 페이지 단위 문서를 위 설정대로 더 작은 덩어리로 잘라 리스트로 만듭니다.

EMBEDDING_MODEL_NAME = 'text-embedding-3-large' # 사용할 OpenAI의 고성능 임베딩 모델 이름을 지정합니다.

from langchain_openai import AzureOpenAIEmbeddings  
embedding_model = AzureOpenAIEmbeddings(
                        azure_deployment=EMBEDDING_MODEL_NAME, # Azure 환경에 배포된 임베딩 모델을 연결합니다.
                        chunk_size=1000) # 한 번에 보낼 데이터 크기를 설정합니다.

from numpy import dot 
from numpy.linalg import norm 
import numpy as np  

def cos_sim(A, B):   
    # 두 벡터(문장) 사이의 거리(유사도)를 계산하는 함수입니다. 1에 가까울수록 의미가 비슷합니다.
    return dot(A, B)/(norm(A)*norm(B))

from langchain_community.vectorstores import Chroma  

db = Chroma.from_documents(
             texts,               # 위에서 자른 텍스트 덩어리들을 넣습니다.
             embedding=embedding_model, # 각 텍스트를 숫자로 변환(임베딩)할 모델을 지정합니다.
             persist_directory="./chroma_db" # 변환된 데이터를 './chroma_db' 폴더에 저장합니다. (나중에 다시 로드 가능)
     )

체인 설명

from langchain_community.vectorstores import Chroma
from langchain_openai import AzureOpenAIEmbeddings  
from dotenv import load_dotenv
import os


load_dotenv()


EMBEDDING_MODEL_NAME = 'text-embedding-3-large'
embedding_model = AzureOpenAIEmbeddings(
                        azure_deployment=EMBEDDING_MODEL_NAME, 
                        chunk_size=1000)

persist_directory = './chroma_db'  # 데이터베이스가 저장될 디렉토리 경로


# 저장된 데이터베이스 불러오기
db = Chroma(
    persist_directory=persist_directory,
    embedding_function=embedding_model
)

query = "실내 온도 조절" # 사용자가 궁금해하는 질문 키워드입니다.
results = db.similarity_search(query) 
# [핵심] 질문을 숫자로 바꾼 뒤, DB 내의 데이터들과 비교하여 가장 유사한 문서 4개(기본값)를 가져옵니다.

print(results[0].page_content) 
print("-" * 100) 
print(results[1].page_content) 
print("-" * 100) 
print(results[2].page_content) 
print("-" * 100) 
print(results[3].page_content)

전체 RAG 코드는 아래와 같습니다.

from langchain_community.vectorstores import Chroma
from langchain_openai import AzureOpenAIEmbeddings  
from dotenv import load_dotenv
import os
from langchain_openai import AzureChatOpenAI  


load_dotenv()

MODEL_DEPLOYMENT_NAME = "gpt-4.1" # 실제 Azure 배포명을 입력하세요

llm = AzureChatOpenAI(
        deployment_name=MODEL_DEPLOYMENT_NAME,                        
        temperature=1,                        
)  

EMBEDDING_MODEL_NAME = 'text-embedding-3-large'
embedding_model = AzureOpenAIEmbeddings(
                        azure_deployment=EMBEDDING_MODEL_NAME, 
                        chunk_size=1000)

persist_directory = './chroma_db'  # 데이터베이스가 저장될 디렉토리 경로


# 저장된 데이터베이스 불러오기
db = Chroma(
    persist_directory=persist_directory,
    embedding_function=embedding_model
)

from langchain_classic.chains import RetrievalQA 
from re import search 

qa = RetrievalQA.from_chain_type(
         llm=llm,
         chain_type='stuff',
         retriever=db.as_retriever(
                 search_type='mmr',  # [검색 알고리즘] Maximum Marginal Relevance(최대 한계 관련성) 방식입니다.
                 search_kwargs={
            'k': 3,         # [최종 선택] 최종적으로 AI에게 전달할 문서의 개수입니다.
            'fetch_k': 10   # [후보 선발] 먼저 DB에서 관련성이 높은 10개를 후보로 뽑습니다.
                            # 그 10개 중에서 서로 내용이 가장 안 겹치는 3개(k)를 MMR이 골라냅니다.
        }
    ),
    return_source_documents=True # [출처 표시] 답변과 함께 AI가 참고한 '원본 문서 덩어리'도 반환할지 결정합니다.
)

query = '실내 온도 조절 장치의 전기 공급은?' 
result = qa.invoke(query)
print(result)

Similar Posts

One Comment

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다