LangChain 독학 가이드 8 - RAG 구현하기

learning by Seven Fingers Studio 15분
LangChainAIPythonLLM챗봇

RAG가 뭔가요?

RAG는 Retrieval-Augmented Generation의 약자예요. 한글로 풀면 “검색 증강 생성”인데, 쉽게 말하면 AI가 우리가 제공한 문서를 검색해서 그 내용을 바탕으로 답변을 만드는 기술입니다.

일반 ChatGPT는 학습된 데이터까지만 알고 있죠. 우리 회사의 내부 규정이나 최신 제품 매뉴얼 같은 건 모릅니다. 하지만 RAG를 사용하면 이런 문서들을 AI에게 제공해서 정확한 답변을 받을 수 있어요.

제 경험상 RAG는 LangChain에서 가장 실용적인 기능 중 하나예요. 실제로 많은 기업이 내부 문서 검색 시스템이나 고객 지원 챗봇을 RAG로 만들고 있습니다.

RAG의 작동 원리

RAG는 크게 4단계로 작동해요:

  1. 문서 로딩: 텍스트 파일, PDF, 웹페이지 등을 읽어옴
  2. 텍스트 분할: 긴 문서를 작은 청크(chunk)로 나눔
  3. 임베딩 & 저장: 각 청크를 벡터로 변환해서 벡터 데이터베이스에 저장
  4. 검색 & 답변 생성: 질문이 들어오면 관련 청크를 찾아서 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]}")

실행 결과:

문서 개수: 1
첫 번째 문서 내용 일부:
우리 회사의 근무 시간은 오전 9시부터 오후 6시까지입니다. 점심시간은 12시부터 1시까지이며, 이 시간은 근무 시간에 포함되지 않습니다. 재택근무는 주 2회까지 가능하며, 사전에 팀장의 승인을 받아야 합니다...

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)}")

실행 결과:

PDF 페이지 수: 15

텍스트 분할 - 적절한 크기로 나누기

문서를 읽었으면 이제 작은 조각으로 나눠야 해요. 너무 크면 검색이 부정확하고, 너무 작으면 문맥을 잃어버릴 수 있거든요.

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}")

실행 결과:

분할된 청크 개수: 8
첫 번째 청크:
우리 회사의 근무 시간은 오전 9시부터 오후 6시까지입니다. 점심시간은 12시부터 1시까지이며, 이 시간은 근무 시간에 포함되지 않습니다. 재택근무는 주 2회까지 가능하며, 사전에 팀장의 승인을 받아야 합니다. 연차는 입사 후 1년이 지나면 15일이 부여됩니다.
두 번째 청크:
연차는 입사 후 1년이 지나면 15일이 부여됩니다. 경조사 휴가는 결혼 시 5일, 직계가족 상 시 3일이 주어지며, 이는 연차에 포함되지 않습니다. 병가는 의사 진단서 제출 시 최대 10일까지 사용 가능합니다.

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)

실행 결과:

벡터 스토어 생성 완료!
질문: 재택근무는 어떻게 하나요?
가장 관련있는 문서:
우리 회사의 근무 시간은 오전 9시부터 오후 6시까지입니다. 점심시간은 12시부터 1시까지이며, 이 시간은 근무 시간에 포함되지 않습니다. 재택근무는 주 2회까지 가능하며, 사전에 팀장의 승인을 받아야 합니다.

신기하죠? “재택근무”라는 키워드를 직접 찾은 게 아니라, 의미적으로 유사한 문서를 찾아낸 거예요. 이게 임베딩의 힘입니다.

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']}")

실행 결과:

질문: 연차는 몇 일 받을 수 있나요?
답변: 입사 후 1년이 지나면 15일의 연차가 부여됩니다.

완벽하죠? 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']}")

실행 결과:

질문: 병가는 어떻게 사용하나요?
답변: 병가는 의사 진단서를 제출하시면 최대 10일까지 사용 가능합니다.
질문: 해외 출장 규정이 있나요?
답변: 죄송하지만 제공된 문서에 해외 출장 규정에 대한 내용이 없습니다. HR 팀에 직접 문의해주세요.

문서에 없는 내용은 모른다고 정직하게 답변하는 걸 볼 수 있죠? 이게 바로 프롬프트 엔지니어링의 힘입니다.

실전 활용 - 제품 매뉴얼 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")

실행 결과:

=== 제품 매뉴얼 QA 시스템 ===
Q: 제품을 어떻게 켜나요?
A: 측면의 전원 버튼을 3초간 길게 누르면 제품이 켜집니다.
Q: 배터리 수명은 얼마나 되나요?
A: 완충 시 약 8시간 연속 사용이 가능하며, 대기 모드에서는 최대 30일까지 지속됩니다.
Q: A/S는 어떻게 받나요?
A: 구매 후 1년 이내 무상 A/S가 가능하며, 고객센터 1588-XXXX로 문의하시면 됩니다.

RAG 성능 개선 팁

RAG를 실제로 사용하면서 알게 된 팁들을 공유할게요:

1. 청크 크기 조절: chunk_size를 500~1500 사이에서 실험해보세요. 문서 특성에 따라 최적값이 다릅니다.

2. 검색 문서 개수 조정: k 값을 2~5 정도로 설정하세요. 너무 많으면 노이즈가 생기고, 너무 적으면 정보가 부족해요.

3. 메타데이터 활용: 문서에 출처나 날짜 같은 메타데이터를 추가하면 더 정확한 검색이 가능합니다.

4. 하이브리드 검색: 키워드 검색과 벡터 검색을 함께 사용하면 더 좋은 결과를 얻을 수 있어요.

다음 단계

RAG까지 마스터했다면 이제 정말 고급 기능을 배울 차례예요. 다음 글에서는 Agent와 Tools를 다룰 건데요, 이걸 배우면 AI가 스스로 판단해서 필요한 도구를 선택하고 실행할 수 있게 됩니다.

예를 들어 날씨를 물어보면 날씨 API를 호출하고, 계산이 필요하면 계산기를 사용하는 식이죠. AI가 정말 똑똑한 비서가 되는 순간입니다!


다음 글 보기

← 이전 글
LangChain 독학 가이드 7 - 대화 메모리
다음 글 →
LangChain 독학 가이드 9 - Agent와 Tools
← 블로그 목록으로