AI 강의 정리 05 - 임베딩과 RAG: PDF를 읽고 답하는 AI 만들기
RAG가 필요한 이유
일반적인 AI 챗봇은 모델이 이미 학습한 지식을 바탕으로 답합니다. 하지만 회사 내부 문서, 내가 올린 PDF, 최신 공지사항처럼 모델이 모르는 자료에 대해서는 정확히 답하기 어렵습니다.
이 문제를 해결하는 대표적인 방식이 RAG입니다.
RAG는 Retrieval Augmented Generation의 약자입니다. 한국어로 풀면 “검색으로 보강한 생성” 정도로 이해할 수 있습니다.
강의 폴더에서는 다음 파일들이 이 주제에 해당합니다.
17_20250421_01_embedding.py
17_20250421_01_embedding02.py
17_20250421_01_embedding03_chunk_and_embedding.py
19_20250422_01_base_code_00.py
25.langchain_20250428_02_graph.py
27.langchain_20250430_02_web_doc_summary_retrieval.py
27.langchain_20250430_03_rerank.py
30.rag대화식챗봇.py
RAG 전체 흐름
RAG는 다음 순서로 동작합니다.
문서 읽기
-> 문서를 작은 조각으로 나누기
-> 각 조각을 임베딩 벡터로 변환
-> 벡터 DB에 저장
-> 질문도 임베딩으로 변환
-> 질문과 비슷한 문서 조각 검색
-> 검색된 조각을 AI에게 함께 전달
-> AI가 근거를 바탕으로 답변 생성
이 흐름을 이해하면 PDF 챗봇, 사내 문서 검색, FAQ 챗봇을 만들 수 있습니다.
임베딩이란?
임베딩은 글자를 숫자 벡터로 바꾸는 작업입니다.
사람은 “고양이”와 “강아지”가 어느 정도 비슷한 단어라는 것을 압니다. 컴퓨터는 글자 자체만 보면 그 의미를 알 수 없습니다.
그래서 AI 모델을 사용해 문장을 숫자 배열로 바꿉니다.
"고양이는 소파 위에서 잠을 잔다"
-> [0.12, -0.03, 0.88, ...]
이 숫자 배열을 벡터라고 부릅니다. 의미가 비슷한 문장은 벡터 공간에서도 가까운 위치에 놓이게 됩니다.
청크란?
문서 전체를 한 번에 AI에게 넣기는 어렵습니다. PDF 한 권은 너무 길고, 모델에는 한 번에 처리할 수 있는 길이 제한이 있습니다.
그래서 문서를 작은 조각으로 나눕니다. 이 조각을 청크라고 합니다.
강의 코드에서는 예를 들어 500자 단위로 나누었습니다.
chunk_size = 500
chunks = [text[i:i+chunk_size] for i in range(0, len(text), chunk_size)]
비전공자 관점에서는 책을 여러 장의 메모지로 잘라 둔다고 생각하면 됩니다.
PDF 텍스트 추출
강의 코드에서는 fitz를 사용해 PDF에서 텍스트를 추출합니다.
fitz는 PyMuPDF 패키지에서 사용하는 모듈입니다.
import fitz
text_chunks = []
chunk_size = 500
with fitz.open("pdf/The_Adventures_of_Tom_Sawyer.pdf") as doc:
for page in doc:
text = page.get_text()
chunks = [text[i:i+chunk_size] for i in range(0, len(text), chunk_size)]
text_chunks.extend(chunks)
흐름은 다음과 같습니다.
PDF 열기
-> 페이지별 텍스트 읽기
-> 500자 단위로 자르기
-> text_chunks 리스트에 저장
임베딩 생성
각 청크를 임베딩으로 변환합니다.
import numpy as np
from openai import OpenAI
client = OpenAI()
def get_embedding(text):
response = client.embeddings.create(
input=text,
model="text-embedding-ada-002",
)
return np.array(response.data[0].embedding, dtype=np.float32)
text-embedding-ada-002는 강의 코드에서 사용한 임베딩 모델입니다.
실습 시점에 다른 임베딩 모델을 사용한다면 벡터 차원이 달라질 수 있습니다.
FAISS에 저장하기
FAISS는 벡터 검색을 빠르게 해 주는 라이브러리입니다.
import faiss
import numpy as np
embeddings_matrix = np.vstack([emb for _, emb in chunk_embedding_pairs])
index = faiss.IndexFlatL2(embeddings_matrix.shape[1])
index.add(embeddings_matrix)
여기서 IndexFlatL2는 유클리드 거리 기반으로 가까운 벡터를 찾는 방식입니다.
쉽게 말하면 다음 질문에 답하는 도구입니다.
질문 벡터와 가장 가까운 문서 조각 5개를 찾아줘.
질문 검색하기
사용자의 질문도 임베딩으로 변환합니다.
question = "등장 인물에 대해 알려줄래?"
q_emb = get_embedding(question).reshape(1, -1)
_, top_index = index.search(q_emb, k=5)
k=5는 가장 비슷한 청크 5개를 가져오겠다는 뜻입니다.
context = " ".join([text_chunks[i] for i in top_index[0]])
이렇게 가져온 context를 AI에게 질문과 함께 전달합니다.
Context: 검색된 문서 조각들
Question: 등장 인물에 대해 알려줄래?
Answer:
RAG와 일반 질문의 차이
일반 질문:
톰 소여는 누구야?
RAG 질문:
다음 문서 내용을 참고해서 답해줘.
Context:
PDF에서 검색된 관련 문단...
Question:
톰 소여는 누구야?
RAG는 모델의 기억에만 의존하지 않고, 검색된 문서 조각을 근거로 답하게 만듭니다.
LangChain으로 단순화하기
직접 구현하면 PDF 읽기, 청크 나누기, 임베딩, FAISS 저장을 하나씩 처리해야 합니다. LangChain을 사용하면 이 과정을 더 간단히 구성할 수 있습니다.
강의 코드에는 다음 흐름이 있습니다.
from langchain.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain.vectorstores import FAISS
from langchain.chains import RetrievalQA
loader = PyPDFLoader("pdf/The_Adventures_of_Tom_Sawyer.pdf")
documents = loader.load()
splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
docs = splitter.split_documents(documents)
embeddings = OpenAIEmbeddings(model="text-embedding-ada-002")
vdb = FAISS.from_documents(docs, embeddings)
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
retriever = vdb.as_retriever()
qa = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=retriever,
return_source_documents=True,
)
result = qa.invoke({"query": "톰 소여는 누구인가?"})
print(result["result"])
chunk_size와 chunk_overlap
chunk_size는 한 조각의 크기입니다.
chunk_overlap은 조각끼리 겹치는 부분입니다.
RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
)
왜 겹치게 할까요?
문장을 자르다 보면 중요한 내용이 청크 경계에서 끊길 수 있습니다. 겹침을 주면 앞뒤 문맥이 일부 유지됩니다.
정리
RAG는 문서를 읽고 답하는 AI를 만들 때 가장 중요한 패턴입니다.
- 임베딩은 문장을 숫자 벡터로 바꾸는 작업입니다.
- 청크는 긴 문서를 작은 조각으로 나누는 것입니다.
- FAISS는 비슷한 벡터를 빠르게 찾는 도구입니다.
- RAG는 검색된 문서 조각을 AI 답변의 근거로 사용합니다.
다음 글에서는 LangChain의 PromptTemplate, OutputParser, Chain 개념을 정리합니다.
댓글남기기