LangChain 독학 가이드 8 - RAG 구현하기
RAG가 뭔가요?
RAG는 Retrieval-Augmented Generation의 약자예요. 한글로 풀면 “검색 증강 생성”인데, 쉽게 말하면 AI가 우리가 제공한 문서를 검색해서 그 내용을 바탕으로 답변을 만드는 기술입니다.
일반 ChatGPT는 학습된 데이터까지만 알고 있죠. 우리 회사의 내부 규정이나 최신 제품 매뉴얼 같은 건 모릅니다. 하지만 RAG를 사용하면 이런 문서들을 AI에게 제공해서 정확한 답변을 받을 수 있어요.
제 경험상 RAG는 LangChain에서 가장 실용적인 기능 중 하나예요. 실제로 많은 기업이 내부 문서 검색 시스템이나 고객 지원 챗봇을 RAG로 만들고 있습니다.
RAG의 작동 원리
RAG는 크게 4단계로 작동해요:
- 문서 로딩: 텍스트 파일, PDF, 웹페이지 등을 읽어옴
- 텍스트 분할: 긴 문서를 작은 청크(chunk)로 나눔
- 임베딩 & 저장: 각 청크를 벡터로 변환해서 벡터 데이터베이스에 저장
- 검색 & 답변 생성: 질문이 들어오면 관련 청크를 찾아서 AI가 답변 생성
말로 들으면 복잡해 보이지만, LangChain을 쓰면 의외로 간단해요. 하나씩 따라해볼게요.
문서 로더 - 데이터 읽어오기
먼저 분석할 문서를 준비해야겠죠? LangChain은 다양한 형식의 문서를 읽을 수 있어요.
# 필요한 라이브러리 설치
# pip install langchain langchain-openai faiss-cpu pypdf
from langchain_community.document_loaders import TextLoader
# 텍스트 파일 읽기
loader = TextLoader("company_policy.txt", encoding="utf-8")
documents = loader.load()
print(f"문서 개수: {len(documents)}")
print(f"첫 번째 문서 내용 일부:\n{documents[0].page_content[:200]}")
실행 결과:
PDF 파일도 읽을 수 있어요:
from langchain_community.document_loaders import PyPDFLoader
# PDF 파일 읽기
pdf_loader = PyPDFLoader("product_manual.pdf")
pdf_documents = pdf_loader.load()
print(f"PDF 페이지 수: {len(pdf_documents)}")
실행 결과:
텍스트 분할 - 적절한 크기로 나누기
문서를 읽었으면 이제 작은 조각으로 나눠야 해요. 너무 크면 검색이 부정확하고, 너무 작으면 문맥을 잃어버릴 수 있거든요.
from langchain.text_splitter import RecursiveCharacterTextSplitter
# 텍스트 분할기 설정
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500, # 청크당 500자
chunk_overlap=50, # 청크 간 50자 겹침 (문맥 유지)
length_function=len
)
# 문서 분할
chunks = text_splitter.split_documents(documents)
print(f"분할된 청크 개수: {len(chunks)}")
print(f"\n첫 번째 청크:\n{chunks[0].page_content}")
print(f"\n두 번째 청크:\n{chunks[1].page_content}")
실행 결과:
chunk_overlap=50을 설정한 이유는 청크 경계에서 문맥이 끊기는 걸 방지하기 위해서예요. 50자씩 겹치게 하면 더 자연스럽게 이어집니다.
임베딩과 벡터 스토어 - 검색 가능하게 만들기
이제 가장 중요한 단계예요. 텍스트를 벡터(숫자 배열)로 변환해서 검색할 수 있게 만드는 거죠.
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import FAISS
# OpenAI 임베딩 모델 사용
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
# FAISS 벡터 스토어 생성
vectorstore = FAISS.from_documents(chunks, embeddings)
print("벡터 스토어 생성 완료!")
# 유사도 검색 테스트
query = "재택근무는 어떻게 하나요?"
results = vectorstore.similarity_search(query, k=2)
print(f"\n질문: {query}")
print(f"\n가장 관련있는 문서:")
print(results[0].page_content)
실행 결과:
신기하죠? “재택근무”라는 키워드를 직접 찾은 게 아니라, 의미적으로 유사한 문서를 찾아낸 거예요. 이게 임베딩의 힘입니다.
FAISS 대신 Chroma를 사용할 수도 있어요:
from langchain_community.vectorstores import Chroma
# Chroma 벡터 스토어 (로컬에 저장됨)
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_db"
)
RAG 체인 구성하기
이제 모든 준비가 끝났어요! 검색과 답변 생성을 하나의 체인으로 연결해볼게요.
from langchain_openai import ChatOpenAI
from langchain.chains import RetrievalQA
# LLM 모델 설정
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
# RAG 체인 생성
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff", # 검색된 문서를 모두 프롬프트에 포함
retriever=vectorstore.as_retriever(search_kwargs={"k": 3})
)
# 질문하기
question = "연차는 몇 일 받을 수 있나요?"
answer = qa_chain.invoke({"query": question})
print(f"질문: {question}")
print(f"답변: {answer['result']}")
실행 결과:
완벽하죠? AI가 문서 내용을 정확하게 찾아서 답변했어요.
더 나은 RAG - 프롬프트 커스터마이징
기본 RAG도 좋지만, 프롬프트를 직접 작성하면 더 정확한 답변을 받을 수 있어요.
from langchain.chains import RetrievalQA
from langchain_core.prompts import PromptTemplate
# 커스텀 프롬프트 작성
template = """당신은 회사 규정을 안내하는 친절한 HR 담당자입니다.
아래 제공된 문서 내용을 바탕으로 질문에 답변해주세요.
답변할 수 없는 내용이라면 모른다고 솔직히 말하세요.
참고 문서:
{context}
질문: {question}
답변:"""
prompt = PromptTemplate(
template=template,
input_variables=["context", "question"]
)
# 커스텀 프롬프트로 체인 생성
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=vectorstore.as_retriever(),
chain_type_kwargs={"prompt": prompt}
)
# 질문하기
questions = [
"병가는 어떻게 사용하나요?",
"해외 출장 규정이 있나요?", # 문서에 없는 내용
]
for q in questions:
result = qa_chain.invoke({"query": q})
print(f"\n질문: {q}")
print(f"답변: {result['result']}")
실행 결과:
문서에 없는 내용은 모른다고 정직하게 답변하는 걸 볼 수 있죠? 이게 바로 프롬프트 엔지니어링의 힘입니다.
실전 활용 - 제품 매뉴얼 QA 시스템
이제 실제로 쓸 수 있는 제품 매뉴얼 QA 시스템을 만들어볼게요.
# 전체 RAG 시스템 구축
from langchain_community.document_loaders import TextLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_community.vectorstores import FAISS
from langchain.chains import RetrievalQA
# 1. 문서 로드
loader = TextLoader("product_manual.txt", encoding="utf-8")
documents = loader.load()
# 2. 텍스트 분할
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=100
)
chunks = splitter.split_documents(documents)
# 3. 임베딩 & 벡터 스토어 생성
embeddings = OpenAIEmbeddings()
vectorstore = FAISS.from_documents(chunks, embeddings)
# 4. RAG 체인 생성
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
qa = RetrievalQA.from_chain_type(
llm=llm,
retriever=vectorstore.as_retriever(search_kwargs={"k": 2})
)
# 5. QA 시스템 실행
print("=== 제품 매뉴얼 QA 시스템 ===\n")
user_questions = [
"제품을 어떻게 켜나요?",
"배터리 수명은 얼마나 되나요?",
"A/S는 어떻게 받나요?"
]
for question in user_questions:
result = qa.invoke({"query": question})
print(f"Q: {question}")
print(f"A: {result['result']}\n")
실행 결과:
RAG 성능 개선 팁
RAG를 실제로 사용하면서 알게 된 팁들을 공유할게요:
1. 청크 크기 조절: chunk_size를 500~1500 사이에서 실험해보세요. 문서 특성에 따라 최적값이 다릅니다.
2. 검색 문서 개수 조정: k 값을 2~5 정도로 설정하세요. 너무 많으면 노이즈가 생기고, 너무 적으면 정보가 부족해요.
3. 메타데이터 활용: 문서에 출처나 날짜 같은 메타데이터를 추가하면 더 정확한 검색이 가능합니다.
4. 하이브리드 검색: 키워드 검색과 벡터 검색을 함께 사용하면 더 좋은 결과를 얻을 수 있어요.
다음 단계
RAG까지 마스터했다면 이제 정말 고급 기능을 배울 차례예요. 다음 글에서는 Agent와 Tools를 다룰 건데요, 이걸 배우면 AI가 스스로 판단해서 필요한 도구를 선택하고 실행할 수 있게 됩니다.
예를 들어 날씨를 물어보면 날씨 API를 호출하고, 계산이 필요하면 계산기를 사용하는 식이죠. AI가 정말 똑똑한 비서가 되는 순간입니다!