LLM 25일 코스 - Day 17: RAG 파이프라인 구축

Day 17: RAG 파이프라인 구축

RAG(Retrieval-Augmented Generation)는 LLM의 한계인 할루시네이션과 최신 정보 부족을 해결하는 핵심 기법입니다. 외부 문서를 검색하여 LLM의 답변 근거로 제공하는 파이프라인을 처음부터 끝까지 만들어봅니다.

RAG의 작동 원리

RAG는 3단계로 구성됩니다. (1) 문서를 청크로 분할하고 임베딩하여 벡터 DB에 저장합니다. (2) 사용자 질문이 들어오면 관련 문서를 벡터 유사도로 검색합니다. (3) 검색된 문서를 컨텍스트로 넣어 LLM이 답변을 생성합니다.

문서 로딩과 분할

# pip install langchain langchain-community langchain-openai chromadb

from langchain_community.document_loaders import TextLoader, PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter

# 텍스트 파일 로딩
loader = TextLoader("company_docs.txt", encoding="utf-8")
documents = loader.load()

# 문서를 적절한 크기로 분할
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,       # 각 청크의 최대 문자 수
    chunk_overlap=50,     # 청크 간 겹치는 문자 수 (문맥 유지)
    separators=["\n\n", "\n", ".", " "],  # 분할 우선순위
)
chunks = text_splitter.split_documents(documents)
print(f"원본 문서: {len(documents)}개 -> 분할된 청크: {len(chunks)}개")
print(f"첫 번째 청크:\n{chunks[0].page_content[:200]}")

임베딩과 벡터 스토어 생성

from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma

# 임베딩 모델 설정
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")

# ChromaDB에 벡터 저장 (디스크에 영구 저장)
vectorstore = Chroma.from_documents(
    documents=chunks,
    embedding=embeddings,
    persist_directory="./chroma_db",
    collection_name="company_docs",
)

# 유사도 검색 테스트
query = "회사의 환불 정책은 무엇인가요?"
results = vectorstore.similarity_search(query, k=3)
for i, doc in enumerate(results):
    print(f"\n--- 검색 결과 {i+1} (유사도 상위) ---")
    print(doc.page_content[:200])

전체 RAG 파이프라인 구성

from langchain_openai import ChatOpenAI
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate

# LLM 설정
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

# 커스텀 프롬프트 (한국어 답변 유도)
prompt_template = PromptTemplate(
    input_variables=["context", "question"],
    template="""다음 문서를 참고하여 질문에 답변하세요.
문서에 없는 내용은 "제공된 문서에서 해당 정보를 찾을 수 없습니다"라고 답하세요.

참고 문서:
{context}

질문: {question}

답변:""",
)

# 검색 + 생성 파이프라인
retriever = vectorstore.as_retriever(
    search_type="similarity",
    search_kwargs={"k": 3},  # 상위 3개 문서 검색
)

qa_chain = RetrievalQA.from_chain_type(
    llm=llm,
    chain_type="stuff",  # 검색 문서를 하나의 프롬프트에 합침
    retriever=retriever,
    chain_type_kwargs={"prompt": prompt_template},
    return_source_documents=True,
)

# 질의 실행
response = qa_chain.invoke({"query": "환불 절차를 알려주세요"})
print("답변:", response["result"])
print("\n참고 문서 수:", len(response["source_documents"]))

RAG에서 가장 중요한 것은 청크 크기와 검색 품질입니다. 청크가 너무 크면 노이즈가 섞이고, 너무 작으면 문맥이 끊깁니다. chunk_size 300~1000 범위에서 실험하세요.

오늘의 연습문제

  1. 위키백과 한국어 문서 3개를 다운로드하여 RAG 파이프라인에 넣고, 문서 내용에 기반한 Q&A 시스템을 만들어보세요.
  2. similarity_search_with_score()를 사용하여 검색 결과에 유사도 점수를 포함하고, 점수 임계값(threshold) 기반 필터링을 추가해보세요.
  3. chunk_size를 200, 500, 1000으로 변경하며 같은 질문에 대한 답변 품질을 비교하고, 최적의 설정을 찾아보세요.

이 글이 도움이 되었나요?