ChatGPT의 한계를 넘다 – RAG 완벽 가이드: 사내 문서 챗봇부터 프로덕션까지
핵심 요약
“ChatGPT는 왜 우리 회사 내부 문서를 모르고, 어제 발표된 뉴스도 모를까?”
이 문제를 해결하는 기술이 바로 RAG(Retrieval-Augmented Generation, 검색 증강 생성)입니다.
RAG는 LLM이 답변하기 전에 외부 지식베이스에서 관련 정보를 검색하여, 환각(Hallucination)을 줄이고 최신/전문 정보를 제공합니다.
작동 원리는 간단합니다: 문서를 청크로 분할 → 벡터로 임베딩 → 벡터 DB에 저장 → 질문과 유사한 문서 검색 → LLM에 컨텍스트로 전달 → 정확한 답변 생성. Chunking 전략에서 최적 크기는 300-500 토큰, 20% 오버랩이 권장되며, Semantic Chunking이 정확도를 높입니다.
벡터 DB는 Pinecone(관리형, 확장성), Chroma(경량, 프로토타입), Weaviate(오픈소스, GraphQL)가 대표적입니다.
Embedding 모델은 OpenAI text-embedding-3-small(성능↑, 가격↓), 한국어는 KoSimCSE, multilingual-e5 추천.
고급 기법으로 HyDE(가상 문서 생성), Reranking(재정렬), GraphRAG(지식 그래프)가 성능을 10-30% 향상시킵니다. LangChain과 LlamaIndex로 사내 문서 챗봇을 몇 시간 만에 구축 가능합니다.
본 포스팅에서는 RAG 아키텍처부터 벡터 DB 비교, Chunking 최적화, 실전 코드, 사내 챗봇 구축까지 완벽하게 다룹니다.
📍 목차
- RAG란? – LLM의 한계를 극복하다
- RAG 아키텍처와 작동 원리
- Chunking 전략 – 문서 분할의 기술
- Embedding 모델 선택 가이드
- 벡터 DB 비교: Pinecone vs Chroma vs Weaviate
- LangChain & LlamaIndex 실전 구현
- Advanced RAG: HyDE, Reranking, GraphRAG
- 사내 문서 챗봇 구축 실전
1. RAG란? – LLM의 한계를 극복하다
1-1. LLM의 근본적 한계
왜 ChatGPT는 모든 질문에 답할 수 없을까?
LLM의 3가지 근본적 한계:
1. 지식 컷오프 (Knowledge Cutoff):
- GPT-4: 2023년 4월까지 학습
- 어제 발표된 뉴스? 모름
- 최신 API 문서? 모름
2. 전문/내부 지식 부재:
- 우리 회사 내부 문서? 모름
- 특정 산업 노하우? 모름
- 비공개 데이터? 모름
3. 환각 (Hallucination):
- 모르는 것도 자신 있게 답변
- 그럴듯하지만 틀린 정보
- 출처 없는 주장비유로 이해하기:
LLM = 책을 많이 읽은 학생
- 시험 볼 때 자기 기억만 의존
- 모르면 추측으로 답변
- 최신 교과서 내용은 모름
RAG = 오픈북 시험
- 필요한 자료를 찾아서 참고
- 정확한 정보 기반 답변
- 최신 자료도 활용 가능1-2. RAG의 정의
RAG (Retrieval-Augmented Generation):
검색(Retrieval) + 증강(Augmented) + 생성(Generation)
= LLM이 답변을 생성하기 전에
외부 지식베이스에서 관련 정보를 검색하여
그 정보를 바탕으로 답변을 생성하는 기술
핵심 아이디어:
"모든 지식을 LLM 내부에 저장하지 말고,
필요할 때 외부에서 검색해서 가져오자"1-3. RAG의 장점
1. 최신 정보 반영:
- 지식베이스만 업데이트하면 됨
- 모델 재학습 불필요
- 실시간 정보 활용 가능
2. 환각 감소:
- 검색된 문서 기반 답변
- 출처 제시 가능
- 사실 기반 응답
3. 전문 지식 통합:
- 사내 문서, 매뉴얼
- 도메인 전문 자료
- 비공개 데이터
4. 비용 효율:
- Fine-tuning 대비 저렴
- 빠른 구축 가능
- 유연한 확장
5. 투명성:
- 어떤 문서를 참조했는지 표시
- 검증 가능한 답변
- 신뢰도 향상2. RAG 아키텍처와 작동 원리
2-1. 전체 아키텍처
┌─────────────────────────────────────────────────────┐
│ RAG Pipeline │
├─────────────────────────────────────────────────────┤
│ │
│ [오프라인: 인덱싱 단계] │
│ │
│ 문서들 → 청킹 → 임베딩 → 벡터 DB 저장 │
│ (PDF, (분할) (벡터화) (Pinecone/ │
│ Wiki, Chroma 등) │
│ Docs) │
│ │
├─────────────────────────────────────────────────────┤
│ │
│ [온라인: 쿼리 단계] │
│ │
│ 사용자 질문 → 임베딩 → 벡터 검색 → 관련 문서 추출 │
│ │ │ │
│ └──────────────────────────────┘ │
│ ↓ │
│ [질문 + 검색된 문서] │
│ ↓ │
│ LLM 생성 │
│ ↓ │
│ 최종 답변 │
│ │
└─────────────────────────────────────────────────────┘2-2. 단계별 상세 설명
1단계: 문서 로딩 (Document Loading)
# 다양한 형식의 문서 로딩
from langchain_community.document_loaders import (
PyPDFLoader,
TextLoader,
WebBaseLoader,
UnstructuredWordDocumentLoader
)
# PDF 로딩
pdf_loader = PyPDFLoader("manual.pdf")
pdf_docs = pdf_loader.load()
# 웹페이지 로딩
web_loader = WebBaseLoader("https://docs.example.com")
web_docs = web_loader.load()
# Word 문서 로딩
word_loader = UnstructuredWordDocumentLoader("policy.docx")
word_docs = word_loader.load()2단계: 문서 분할 (Chunking)
from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500, # 청크 크기 (토큰/문자)
chunk_overlap=100, # 청크 간 중복 (연속성 유지)
separators=["\n\n", "\n", ".", " "] # 분할 우선순위
)
chunks = text_splitter.split_documents(documents)
print(f"총 {len(chunks)}개 청크 생성")3단계: 임베딩 (Embedding)
from langchain_openai import OpenAIEmbeddings
# OpenAI 임베딩 모델
embeddings = OpenAIEmbeddings(
model="text-embedding-3-small" # 또는 text-embedding-ada-002
)
# 텍스트를 벡터로 변환
vector = embeddings.embed_query("RAG란 무엇인가?")
print(f"벡터 차원: {len(vector)}") # 1536 차원4단계: 벡터 DB 저장
from langchain_community.vectorstores import Chroma
# 벡터 DB 생성 및 저장
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_db" # 영구 저장
)
print("벡터 DB 저장 완료")5단계: 검색 (Retrieval)
# Retriever 생성
retriever = vectorstore.as_retriever(
search_type="similarity", # 유사도 검색
search_kwargs={"k": 3} # 상위 3개 문서
)
# 관련 문서 검색
query = "우리 회사의 휴가 정책은?"
relevant_docs = retriever.invoke(query)
for i, doc in enumerate(relevant_docs):
print(f"[문서 {i+1}] {doc.page_content[:200]}...")6단계: 생성 (Generation)
from langchain_openai import ChatOpenAI
from langchain.chains import RetrievalQA
# LLM 초기화
llm = ChatOpenAI(model="gpt-4", temperature=0)
# RAG 체인 구성
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff", # 모든 문서를 한 번에 전달
retriever=retriever,
return_source_documents=True # 출처 반환
)
# 질문-답변
result = qa_chain.invoke({"query": "우리 회사의 휴가 정책은?"})
print("답변:", result["result"])
print("\n출처 문서:")
for doc in result["source_documents"]:
print(f"- {doc.metadata.get('source', '알 수 없음')}")3. Chunking 전략 – 문서 분할의 기술
3-1. Chunking이 중요한 이유
Chunking = 문서를 작은 조각으로 나누는 것
왜 필요한가?
1. LLM 토큰 제한: GPT-4는 128K 토큰 제한
2. 검색 정확도: 작은 청크가 더 정밀한 검색
3. 컨텍스트 품질: 관련 정보만 LLM에 전달
청크 크기의 딜레마:
- 너무 작으면: 맥락 손실, 불완전한 정보
- 너무 크면: 노이즈 증가, 검색 정확도 하락3-2. Chunking 전략 비교
1. 고정 크기 청킹 (Fixed-Size Chunking)
from langchain.text_splitter import CharacterTextSplitter
# 단순히 500자씩 분할
splitter = CharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
separator="" # 문자 단위
)
장점: 구현 간단, 빠른 처리
단점: 문장/문단 중간에서 끊김, 맥락 손실
사용: 빠른 프로토타이핑, 균일한 텍스트2. 재귀적 청킹 (Recursive Character Splitting)
from langchain.text_splitter import RecursiveCharacterTextSplitter
# 구분자 우선순위에 따라 분할
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=100,
separators=[
"\n\n", # 문단 우선
"\n", # 줄바꿈
".", # 문장
" ", # 단어
"" # 문자
]
)
장점: 문맥 보존, 자연스러운 분할
단점: 청크 크기 불균일
추천: 대부분의 프로덕션 환경에서 권장!3. 시맨틱 청킹 (Semantic Chunking)
from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai import OpenAIEmbeddings
# 의미 기반 분할
embeddings = OpenAIEmbeddings()
splitter = SemanticChunker(
embeddings=embeddings,
breakpoint_threshold_type="percentile",
breakpoint_threshold_amount=95
)
장점: 의미적으로 완결된 청크
단점: 느린 처리, 비용 발생
사용: 고품질 RAG, 복잡한 문서4. 문서 유형별 청킹
# 마크다운 문서
from langchain.text_splitter import MarkdownHeaderTextSplitter
headers_to_split_on = [
("#", "Header 1"),
("##", "Header 2"),
("###", "Header 3"),
]
md_splitter = MarkdownHeaderTextSplitter(
headers_to_split_on=headers_to_split_on
)
# 코드 문서
from langchain.text_splitter import Language, RecursiveCharacterTextSplitter
python_splitter = RecursiveCharacterTextSplitter.from_language(
language=Language.PYTHON,
chunk_size=500,
chunk_overlap=50
)3-3. 최적 청크 크기 가이드
연구 및 실무 권장 사항:
청크 크기:
├── 일반 텍스트: 300-500 토큰
├── 기술 문서: 500-800 토큰
├── 법률/의료: 200-400 토큰 (정밀도 중요)
└── FAQ: 100-300 토큰 (짧은 답변)
오버랩:
├── 권장: 청크 크기의 15-25%
├── 최소: 50 토큰
└── 최대: 200 토큰
실험이 필수:
- 도메인별로 최적값 다름
- A/B 테스트로 검증
- 검색 정확도 + 답변 품질 함께 평가4. Embedding 모델 선택 가이드
4-1. Embedding이란?
Embedding = 텍스트를 숫자 벡터로 변환
"고양이" → [0.12, -0.45, 0.78, ..., 0.33] # 1536차원
의미:
- 의미적으로 비슷한 텍스트 → 비슷한 벡터
- 벡터 간 거리로 유사도 측정
- 코사인 유사도가 가장 일반적
예시:
"고양이" ↔ "강아지" : 유사도 0.85 (높음)
"고양이" ↔ "자동차" : 유사도 0.23 (낮음)4-2. 주요 Embedding 모델 비교
OpenAI Embedding 모델:
text-embedding-3-small (2024):
├── 차원: 1536 (조정 가능)
├── 가격: $0.00002 / 1K 토큰
├── 성능: MTEB 62.3%
├── 컨텍스트: 8,191 토큰
└── 추천: 비용 효율 최고!
text-embedding-3-large (2024):
├── 차원: 3072 (조정 가능)
├── 가격: $0.00013 / 1K 토큰
├── 성능: MTEB 64.6%
├── 컨텍스트: 8,191 토큰
└── 추천: 최고 성능 필요 시
text-embedding-ada-002 (2022, 레거시):
├── 차원: 1536 (고정)
├── 가격: $0.0001 / 1K 토큰
├── 성능: MTEB 61.0%
└── 추천: 기존 시스템 호환오픈소스 Embedding 모델:
sentence-transformers (HuggingFace):
├── all-MiniLM-L6-v2
│ ├── 차원: 384
│ ├── 속도: 매우 빠름
│ └── 추천: 빠른 프로토타입
│
├── all-mpnet-base-v2
│ ├── 차원: 768
│ ├── 성능: 우수
│ └── 추천: 균형 잡힌 선택
│
└── multi-qa-mpnet-base-dot-v1
├── 차원: 768
├── 특화: Q&A
└── 추천: RAG에 최적화
BERT 계열:
├── BERT-base: 768차원
├── BERT-large: 1024차원
└── 한계: 문장 임베딩에 부적합 (CLS 토큰)한국어 특화 모델:
KoSimCSE (KAIST):
├── 차원: 768
├── 특화: 한국어 문장 유사도
└── 추천: 한국어 RAG 최적!
ko-sbert (HuggingFace):
├── 차원: 768
├── 기반: Sentence-BERT 한국어
└── 추천: 한국어 일반 용도
multilingual-e5-large:
├── 차원: 1024
├── 지원: 100+ 언어
└── 추천: 다국어 RAG4-3. Embedding 모델 선택 가이드
의사결정 트리:
Q1. 예산이 있는가?
├── Yes → OpenAI text-embedding-3-small (가성비 최고)
└── No → 오픈소스 (all-mpnet-base-v2)
Q2. 한국어 전용인가?
├── Yes → KoSimCSE 또는 multilingual-e5
└── No → 영어 모델도 OK
Q3. 속도가 중요한가?
├── Yes → all-MiniLM-L6-v2 (384차원, 초고속)
└── No → 대형 모델 (더 높은 정확도)
Q4. 보안이 중요한가?
├── Yes → 로컬 오픈소스 모델
└── No → API 서비스 OK4-4. Python 구현 예시
# 1. OpenAI Embedding
from langchain_openai import OpenAIEmbeddings
openai_embeddings = OpenAIEmbeddings(
model="text-embedding-3-small",
dimensions=1536 # 차원 조정 가능
)
# 2. HuggingFace Embedding
from langchain_huggingface import HuggingFaceEmbeddings
hf_embeddings = HuggingFaceEmbeddings(
model_name="sentence-transformers/all-mpnet-base-v2",
model_kwargs={'device': 'cuda'} # GPU 사용
)
# 3. 한국어 Embedding
ko_embeddings = HuggingFaceEmbeddings(
model_name="BM-K/KoSimCSE-roberta-multitask",
model_kwargs={'device': 'cuda'}
)
# 임베딩 테스트
text = "RAG는 검색 증강 생성의 약자입니다."
vector = openai_embeddings.embed_query(text)
print(f"벡터 차원: {len(vector)}")5. 벡터 DB 비교: Pinecone vs Chroma vs Weaviate
5-1. 벡터 DB란?
벡터 데이터베이스:
고차원 벡터를 저장하고 유사도 검색을 수행하는
특수 목적 데이터베이스
기존 DB와 차이:
┌─────────────┬───────────────┬───────────────┐
│ 항목 │ 기존 DB │ 벡터 DB │
├─────────────┼───────────────┼───────────────┤
│ 데이터 형태 │ 행/열, JSON │ 고차원 벡터 │
│ 검색 방식 │ 정확 일치 │ 유사도 검색 │
│ 쿼리 예시 │ WHERE id=1 │ 코사인 유사도 │
│ 인덱싱 │ B-Tree, Hash │ HNSW, IVF │
│ 용도 │ CRUD │ 시맨틱 검색 │
└─────────────┴───────────────┴───────────────┘5-2. 주요 벡터 DB 상세 비교
Pinecone:
특징:
├── 완전 관리형 (Serverless)
├── 인프라 관리 불필요
├── 자동 확장
└── 엔터프라이즈 보안
장점:
✅ 설정 간단, 즉시 사용
✅ 대규모 확장성 우수
✅ 메타데이터 필터링
✅ 하이브리드 검색
단점:
❌ 유료 (무료 플랜 제한적)
❌ 클로즈드 소스
❌ 데이터 외부 저장
가격:
├── 무료: 1개 인덱스, 100K 벡터
├── Standard: $70/월~
└── Enterprise: 협의
추천 상황:
- 프로덕션 환경
- 빠른 MVP
- 인프라 관리 리소스 없을 때Chroma:
특징:
├── 오픈소스
├── 경량, 임베딩 DB
├── Python 네이티브
└── 로컬/클라우드 모두 지원
장점:
✅ 무료, 오픈소스
✅ 설치/사용 간단
✅ LangChain 완벽 통합
✅ 로컬 개발에 최적
단점:
❌ 대규모 확장 한계
❌ 프로덕션 기능 부족
❌ 분산 처리 제한
가격:
└── 무료 (오픈소스)
추천 상황:
- 프로토타이핑
- 소규모 프로젝트
- 로컬 개발 환경
- 학습 목적Weaviate:
특징:
├── 오픈소스 + 클라우드
├── GraphQL API
├── 스키마 기반
└── 모듈식 아키텍처
장점:
✅ 풍부한 쿼리 기능
✅ 하이브리드 검색 내장
✅ 다양한 AI 모델 통합
✅ 자체 벡터화 지원
단점:
❌ 설정 복잡도 높음
❌ 학습 곡선 존재
❌ 리소스 사용량 높음
가격:
├── 오픈소스: 무료
├── Cloud: $25/월~
└── Enterprise: 협의
추천 상황:
- GraphQL 선호
- 복잡한 쿼리 필요
- 자체 호스팅 선호
- 하이브리드 검색5-3. 비교표
┌────────────────┬───────────┬───────────┬───────────┐
│ 항목 │ Pinecone │ Chroma │ Weaviate │
├────────────────┼───────────┼───────────┼───────────┤
│ 호스팅 │ 관리형 │ 로컬/클라우드│ 둘 다 │
│ 오픈소스 │ ❌ │ ✅ │ ✅ │
│ 확장성 │ ⭐⭐⭐⭐⭐ │ ⭐⭐ │ ⭐⭐⭐⭐ │
│ 사용 편의성 │ ⭐⭐⭐⭐⭐ │ ⭐⭐⭐⭐⭐ │ ⭐⭐⭐ │
│ 검색 속도 │ 매우 빠름 │ 빠름 │ 빠름 │
│ 하이브리드 │ ✅ │ ❌ │ ✅ │
│ 메타데이터 │ ✅ │ ✅ │ ✅ │
│ 무료 플랜 │ 제한적 │ 무제한 │ 무제한 │
├────────────────┼───────────┼───────────┼───────────┤
│ 추천 용도 │ 프로덕션 │ 프로토타입│ 중규모 │
└────────────────┴───────────┴───────────┴───────────┘5-4. 벡터 DB 구현 예시
Chroma 예시:
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings
# 임베딩 모델
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
# Chroma DB 생성
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_db",
collection_name="my_collection"
)
# 검색
results = vectorstore.similarity_search(
query="RAG란?",
k=3
)Pinecone 예시:
from langchain_pinecone import PineconeVectorStore
from pinecone import Pinecone
# Pinecone 초기화
pc = Pinecone(api_key="your-api-key")
# 인덱스 생성 (최초 1회)
pc.create_index(
name="my-index",
dimension=1536,
metric="cosine",
spec=ServerlessSpec(cloud="aws", region="us-east-1")
)
# 벡터 저장소
vectorstore = PineconeVectorStore.from_documents(
documents=chunks,
embedding=embeddings,
index_name="my-index"
)Weaviate 예시:
from langchain_weaviate import WeaviateVectorStore
import weaviate
# Weaviate 클라이언트
client = weaviate.Client(
url="http://localhost:8080",
auth_client_secret=weaviate.AuthApiKey(api_key="your-key")
)
# 벡터 저장소
vectorstore = WeaviateVectorStore.from_documents(
documents=chunks,
embedding=embeddings,
client=client,
index_name="MyCollection"
)6. LangChain & LlamaIndex 실전 구현
6-1. LangChain vs LlamaIndex
LangChain:
├── 범용 LLM 애플리케이션 프레임워크
├── 체인, 에이전트, 도구 중심
├── 유연성 높음
├── 커뮤니티 활발
└── 문서 방대
LlamaIndex:
├── RAG 전문 프레임워크
├── 인덱싱, 검색 최적화
├── 다양한 데이터 소스 지원
├── RAG 파이프라인 간편
└── 더 간결한 코드
선택 가이드:
├── RAG만 필요 → LlamaIndex (더 간단)
├── 다양한 LLM 기능 → LangChain (더 유연)
└── 둘 다 → 함께 사용 가능!6-2. LangChain RAG 완전 구현
# 필요 패키지 설치
# pip install langchain langchain-openai langchain-chroma
import os
from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_chroma import Chroma
from langchain_core.prompts import ChatPromptTemplate
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain.chains import create_retrieval_chain
# 환경 설정
os.environ["OPENAI_API_KEY"] = "your-api-key"
# 1. 문서 로딩
loader = PyPDFLoader("company_manual.pdf")
documents = loader.load()
print(f"로딩된 페이지 수: {len(documents)}")
# 2. 청킹
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=100,
separators=["\n\n", "\n", ".", " "]
)
chunks = text_splitter.split_documents(documents)
print(f"생성된 청크 수: {len(chunks)}")
# 3. 임베딩 & 벡터 저장
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./vectordb"
)
# 4. Retriever 설정
retriever = vectorstore.as_retriever(
search_type="similarity",
search_kwargs={"k": 4}
)
# 5. 프롬프트 템플릿
system_prompt = """
당신은 사내 문서 기반 질문 답변 전문가입니다.
아래 제공된 컨텍스트만을 사용하여 질문에 답변하세요.
컨텍스트에 없는 내용은 "해당 정보를 찾을 수 없습니다"라고 답변하세요.
컨텍스트:
{context}
"""
prompt = ChatPromptTemplate.from_messages([
("system", system_prompt),
("human", "{input}")
])
# 6. LLM 및 체인 구성
llm = ChatOpenAI(model="gpt-4", temperature=0)
question_answer_chain = create_stuff_documents_chain(llm, prompt)
rag_chain = create_retrieval_chain(retriever, question_answer_chain)
# 7. 질의응답
def ask(question: str) -> str:
response = rag_chain.invoke({"input": question})
return response["answer"]
# 사용 예시
answer = ask("우리 회사의 연차 휴가 정책은 어떻게 되나요?")
print(answer)6-3. LlamaIndex RAG 구현
# 필요 패키지 설치
# pip install llama-index llama-index-embeddings-openai
import os
from llama_index.core import (
VectorStoreIndex,
SimpleDirectoryReader,
Settings,
StorageContext,
load_index_from_storage
)
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.llms.openai import OpenAI
# 환경 설정
os.environ["OPENAI_API_KEY"] = "your-api-key"
# 전역 설정
Settings.llm = OpenAI(model="gpt-4", temperature=0)
Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small")
Settings.chunk_size = 512
Settings.chunk_overlap = 50
# 1. 문서 로딩
documents = SimpleDirectoryReader("./documents").load_data()
print(f"로딩된 문서 수: {len(documents)}")
# 2. 인덱스 생성 (임베딩 + 저장 자동)
index = VectorStoreIndex.from_documents(
documents,
show_progress=True
)
# 인덱스 저장
index.storage_context.persist(persist_dir="./storage")
# 기존 인덱스 로드 (재사용 시)
# storage_context = StorageContext.from_defaults(persist_dir="./storage")
# index = load_index_from_storage(storage_context)
# 3. 쿼리 엔진 생성
query_engine = index.as_query_engine(
similarity_top_k=4,
response_mode="compact" # 또는 "refine", "tree_summarize"
)
# 4. 질의응답
response = query_engine.query("우리 회사의 보안 정책은?")
print(response.response)
# 출처 확인
for node in response.source_nodes:
print(f"- 출처: {node.metadata.get('file_name', 'N/A')}")
print(f" 관련도: {node.score:.4f}")6-4. LlamaIndex 고급 기능
# 대화형 쿼리 엔진 (히스토리 유지)
from llama_index.core.memory import ChatMemoryBuffer
memory = ChatMemoryBuffer.from_defaults(token_limit=3000)
chat_engine = index.as_chat_engine(
chat_mode="context",
memory=memory,
system_prompt="당신은 사내 문서 전문 AI 어시스턴트입니다."
)
# 대화
response1 = chat_engine.chat("휴가 정책 알려줘")
print(response1.response)
response2 = chat_engine.chat("그럼 경조사 휴가는?") # 컨텍스트 유지
print(response2.response)
# 스트리밍 응답
response = chat_engine.stream_chat("보안 지침 요약해줘")
for token in response.response_gen:
print(token, end="", flush=True)7. Advanced RAG: HyDE, Reranking, GraphRAG
7-1. Advanced RAG 개요
Naive RAG의 한계:
├── 검색 정확도 부족
├── 관련 없는 문서 포함
├── 복잡한 질문 처리 어려움
└── 다단계 추론 불가
Advanced RAG 기법:
├── Pre-Retrieval: 쿼리 변환 (HyDE, Multi-Query)
├── Retrieval: 하이브리드 검색, 재귀적 검색
├── Post-Retrieval: Reranking, 압축
└── 구조: GraphRAG, Agentic RAG7-2. HyDE (Hypothetical Document Embeddings)
원리:
기존 방식:
질문 → 질문 임베딩 → 문서와 유사도 비교
문제:
"질문"과 "답변(문서)"은 형태가 다름
질문: "RAG란?" (짧음, 의문형)
답변: "RAG는 검색과 생성을... 기술입니다." (길음, 서술형)
HyDE 방식:
질문 → LLM이 가상 답변 생성 → 가상 답변 임베딩 → 문서와 비교
효과:
"답변 형태"끼리 비교하므로 유사도 정확도 향상!
연구 결과: 검색 정확도 10-30% 향상구현:
from langchain.chains import HypotheticalDocumentEmbedder
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
# LLM과 기본 임베딩
llm = ChatOpenAI(model="gpt-4", temperature=0)
base_embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
# HyDE 임베딩
hyde_embeddings = HypotheticalDocumentEmbedder.from_llm(
llm=llm,
base_embeddings=base_embeddings,
prompt_key="web_search" # 도메인에 맞게 조정
)
# 사용
query = "RAG의 장점은?"
hyde_vector = hyde_embeddings.embed_query(query)
# 내부적으로: 질문 → 가상 답변 생성 → 임베딩7-3. Reranking (재정렬)
원리:
기존 방식:
벡터 유사도로 Top-K 검색 → 바로 LLM에 전달
문제:
벡터 유사도가 항상 "진짜 관련성"을 반영하지 않음
Reranking 방식:
1. 벡터 검색으로 후보 문서 20-50개 검색
2. Reranker 모델로 질문-문서 쌍 평가
3. 재정렬하여 Top-K만 LLM에 전달
장점:
- Cross-Encoder로 정밀한 관련성 평가
- 더 높은 품질의 컨텍스트 제공
- 답변 품질 향상
단점:
- 추가 지연 시간 발생
- Reranker 모델 비용구현:
# Cohere Reranker 사용
from langchain.retrievers import ContextualCompressionRetriever
from langchain_cohere import CohereRerank
# 기본 Retriever
base_retriever = vectorstore.as_retriever(search_kwargs={"k": 20})
# Cohere Reranker
reranker = CohereRerank(
cohere_api_key="your-cohere-key",
top_n=5 # 최종 5개만 반환
)
# 압축 Retriever (Reranking 적용)
compression_retriever = ContextualCompressionRetriever(
base_compressor=reranker,
base_retriever=base_retriever
)
# 사용
docs = compression_retriever.invoke("RAG 구현 방법")
# 20개 검색 → Reranking → 5개 반환
# BGE Reranker (오픈소스) 사용
from langchain_community.cross_encoders import HuggingFaceCrossEncoder
from langchain.retrievers.document_compressors import CrossEncoderReranker
# BGE Reranker 모델
model = HuggingFaceCrossEncoder(model_name="BAAI/bge-reranker-large")
reranker = CrossEncoderReranker(model=model, top_n=5)
# 압축 Retriever
compression_retriever = ContextualCompressionRetriever(
base_compressor=reranker,
base_retriever=base_retriever
)7-4. GraphRAG (지식 그래프 RAG)
원리:
기존 Vector RAG:
- 문서를 벡터로 변환
- 의미적 유사도로 검색
- 관계 추론 어려움
GraphRAG:
- 문서에서 엔티티와 관계 추출
- 지식 그래프 구축 (노드 + 엣지)
- 그래프 탐색으로 검색
- 다단계 추론 가능
예시:
질문: "애플의 CEO가 일하는 회사의 본사는 어디?"
Vector RAG:
- "애플", "CEO", "본사" 관련 문서 검색
- 관계 연결 어려움
GraphRAG:
1. 애플 → hasCEO → 팀 쿡
2. 팀 쿡 → worksAt → 애플
3. 애플 → headquarters → 쿠퍼티노
→ 관계 추적으로 정확한 답변!Microsoft GraphRAG 예시:
# Microsoft GraphRAG 설치
# pip install graphrag
from graphrag.index import create_pipeline_config
from graphrag.query import LocalSearch, GlobalSearch
# 1. 인덱싱 (지식 그래프 구축)
# 설정 파일 생성
config = create_pipeline_config(
root_dir="./ragtest",
input_dir="./documents",
output_dir="./output"
)
# CLI로 인덱싱 실행
# graphrag index --root ./ragtest
# 2. 로컬 검색 (세부 질문)
local_search = LocalSearch(
config=config,
context_builder=context_builder
)
result = local_search.search("팀 쿡의 연봉은?")
print(result.response)
# 3. 글로벌 검색 (요약/분석 질문)
global_search = GlobalSearch(
config=config,
context_builder=context_builder
)
result = global_search.search("이 문서의 주요 주제는?")
print(result.response)7-5. Advanced RAG 성능 비교
ARAGOG 벤치마크 결과:
기법 │ Retrieval Precision │ Answer Similarity
─────────────────────────────────────────────────────────────
Naive RAG │ 기준 │ 기준
HyDE │ +15% │ +8%
Cohere Rerank │ +5% │ +3%
LLM Rerank │ +18% │ +10%
Sentence Window │ +25% │ -5%
HyDE + LLM Rerank │ +30% │ +12%
─────────────────────────────────────────────────────────────
권장 조합:
1. 속도 중시: Naive RAG + Cohere Rerank
2. 품질 중시: HyDE + LLM Rerank
3. 균형: Sentence Window + Cohere Rerank8. 사내 문서 챗봇 구축 실전
8-1. 프로젝트 구조
my-rag-chatbot/
├── data/
│ ├── documents/ # 원본 문서 (PDF, DOCX 등)
│ └── processed/ # 전처리된 문서
├── vectordb/ # 벡터 DB 저장소
├── src/
│ ├── __init__.py
│ ├── config.py # 설정 파일
│ ├── loader.py # 문서 로더
│ ├── chunker.py # 청킹 로직
│ ├── embedder.py # 임베딩 처리
│ ├── retriever.py # 검색 로직
│ ├── generator.py # 답변 생성
│ └── chatbot.py # 챗봇 메인
├── api/
│ └── main.py # FastAPI 서버
├── tests/
│ └── test_rag.py # 테스트
├── requirements.txt
├── docker-compose.yml
└── README.md8-2. 완전한 사내 챗봇 코드
config.py:
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
# OpenAI 설정
OPENAI_API_KEY: str
EMBEDDING_MODEL: str = "text-embedding-3-small"
LLM_MODEL: str = "gpt-4"
# 벡터 DB 설정
VECTORDB_PATH: str = "./vectordb"
COLLECTION_NAME: str = "company_docs"
# 청킹 설정
CHUNK_SIZE: int = 500
CHUNK_OVERLAP: int = 100
# 검색 설정
TOP_K: int = 5
class Config:
env_file = ".env"
settings = Settings()loader.py:
from pathlib import Path
from typing import List
from langchain_core.documents import Document
from langchain_community.document_loaders import (
PyPDFLoader,
Docx2txtLoader,
TextLoader,
UnstructuredMarkdownLoader
)
class DocumentLoader:
"""다양한 형식의 문서 로더"""
LOADER_MAPPING = {
".pdf": PyPDFLoader,
".docx": Docx2txtLoader,
".txt": TextLoader,
".md": UnstructuredMarkdownLoader,
}
@classmethod
def load_document(cls, file_path: str) -> List[Document]:
ext = Path(file_path).suffix.lower()
if ext not in cls.LOADER_MAPPING:
raise ValueError(f"지원하지 않는 파일 형식: {ext}")
loader_class = cls.LOADER_MAPPING[ext]
loader = loader_class(file_path)
return loader.load()
@classmethod
def load_directory(cls, dir_path: str) -> List[Document]:
documents = []
path = Path(dir_path)
for file_path in path.rglob("*"):
if file_path.suffix.lower() in cls.LOADER_MAPPING:
try:
docs = cls.load_document(str(file_path))
# 메타데이터 추가
for doc in docs:
doc.metadata["source"] = str(file_path)
doc.metadata["file_name"] = file_path.name
documents.extend(docs)
except Exception as e:
print(f"로딩 실패 {file_path}: {e}")
return documentschatbot.py:
import os
from typing import List, Dict, Any
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_chroma import Chroma
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_core.prompts import ChatPromptTemplate
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain.chains import create_retrieval_chain
from .config import settings
from .loader import DocumentLoader
class RAGChatbot:
def __init__(self):
self.embeddings = OpenAIEmbeddings(
model=settings.EMBEDDING_MODEL,
openai_api_key=settings.OPENAI_API_KEY
)
self.llm = ChatOpenAI(
model=settings.LLM_MODEL,
temperature=0,
openai_api_key=settings.OPENAI_API_KEY
)
self.vectorstore = None
self.rag_chain = None
def index_documents(self, documents_path: str):
"""문서 인덱싱"""
# 1. 문서 로딩
print(f"문서 로딩 중: {documents_path}")
documents = DocumentLoader.load_directory(documents_path)
print(f"로딩된 문서: {len(documents)}개")
# 2. 청킹
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=settings.CHUNK_SIZE,
chunk_overlap=settings.CHUNK_OVERLAP,
separators=["\n\n", "\n", ".", " "]
)
chunks = text_splitter.split_documents(documents)
print(f"생성된 청크: {len(chunks)}개")
# 3. 벡터 DB 생성
self.vectorstore = Chroma.from_documents(
documents=chunks,
embedding=self.embeddings,
persist_directory=settings.VECTORDB_PATH,
collection_name=settings.COLLECTION_NAME
)
print("벡터 DB 저장 완료")
# 4. RAG 체인 구성
self._setup_rag_chain()
def load_index(self):
"""기존 인덱스 로드"""
self.vectorstore = Chroma(
persist_directory=settings.VECTORDB_PATH,
embedding_function=self.embeddings,
collection_name=settings.COLLECTION_NAME
)
self._setup_rag_chain()
print("기존 인덱스 로드 완료")
def _setup_rag_chain(self):
"""RAG 체인 설정"""
retriever = self.vectorstore.as_retriever(
search_type="similarity",
search_kwargs={"k": settings.TOP_K}
)
system_prompt = """
당신은 사내 문서 기반 AI 어시스턴트입니다.
아래 제공된 컨텍스트만을 사용하여 질문에 정확하게 답변하세요.
규칙:
1. 컨텍스트에 있는 정보만 사용하세요.
2. 컨텍스트에 없으면 "해당 정보를 찾을 수 없습니다"라고 답변하세요.
3. 추측하지 마세요.
4. 가능하면 출처를 언급하세요.
컨텍스트:
{context}
"""
prompt = ChatPromptTemplate.from_messages([
("system", system_prompt),
("human", "{input}")
])
question_answer_chain = create_stuff_documents_chain(self.llm, prompt)
self.rag_chain = create_retrieval_chain(retriever, question_answer_chain)
def ask(self, question: str) -> Dict[str, Any]:
"""질문에 답변"""
if not self.rag_chain:
raise ValueError("먼저 index_documents() 또는 load_index()를 호출하세요.")
response = self.rag_chain.invoke({"input": question})
# 출처 정보 추출
sources = []
for doc in response.get("context", []):
source = doc.metadata.get("file_name", "알 수 없음")
if source not in sources:
sources.append(source)
return {
"answer": response["answer"],
"sources": sources,
"context_count": len(response.get("context", []))
}
def chat(self):
"""대화형 인터페이스"""
print("=" * 50)
print("사내 문서 챗봇입니다. 'quit'을 입력하면 종료됩니다.")
print("=" * 50)
while True:
question = input("\n질문: ").strip()
if question.lower() in ["quit", "exit", "q"]:
print("챗봇을 종료합니다.")
break
if not question:
continue
try:
result = self.ask(question)
print(f"\n답변: {result['answer']}")
print(f"\n출처: {', '.join(result['sources'])}")
except Exception as e:
print(f"오류 발생: {e}")
# 사용 예시
if __name__ == "__main__":
chatbot = RAGChatbot()
# 최초 인덱싱
chatbot.index_documents("./data/documents")
# 또는 기존 인덱스 로드
# chatbot.load_index()
# 대화형 모드
chatbot.chat()8-3. FastAPI 서버
api/main.py:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List, Optional
from src.chatbot import RAGChatbot
app = FastAPI(title="사내 문서 RAG API")
# 챗봇 인스턴스
chatbot = RAGChatbot()
chatbot.load_index()
class QuestionRequest(BaseModel):
question: str
class AnswerResponse(BaseModel):
answer: str
sources: List[str]
context_count: int
@app.post("/ask", response_model=AnswerResponse)
async def ask_question(request: QuestionRequest):
"""질문에 답변"""
try:
result = chatbot.ask(request.question)
return AnswerResponse(**result)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/health")
async def health_check():
return {"status": "healthy"}
# 실행: uvicorn api.main:app --reload8-4. 성능 최적화 팁
1. 검색 품질 개선:
☑ 청크 크기 실험 (300-500 토큰)
☑ 오버랩 비율 조정 (15-25%)
☑ Reranking 적용
☑ HyDE 적용
2. 응답 속도 개선:
☑ 벡터 DB 인덱스 최적화
☑ Top-K 줄이기 (3-5)
☑ 캐싱 적용 (Redis)
☑ 스트리밍 응답
3. 답변 품질 개선:
☑ 프롬프트 최적화
☑ 출처 표시
☑ 신뢰도 점수 표시
☑ 폴백 메시지
4. 운영 안정성:
☑ 에러 핸들링
☑ 로깅
☑ 모니터링
☑ 주기적 인덱스 업데이트FAQ: RAG Q&A
Q1. 언제 RAG를 쓰고, 언제 Fine-tuning을 써야 하나요?
A. 용도에 따라:
RAG 추천:
✅ 자주 변하는 정보 (뉴스, 정책)
✅ 출처가 중요한 경우 (법률, 의료)
✅ 빠른 구축이 필요할 때
✅ 비용 효율 중시
✅ 투명성이 중요할 때
Fine-tuning 추천:
✅ 특정 스타일/톤 필요 (브랜드 보이스)
✅ 도메인 특화 언어 (전문 용어)
✅ 복잡한 추론 패턴
✅ 일관된 형식 필요
둘 다 (Hybrid):
✅ 가장 효과적
✅ Fine-tuned 모델 + RAG
✅ 스타일 + 최신 정보Q2. 청크 크기를 어떻게 정해야 하나요?
A. 실험이 필수:
시작점:
├── 일반 텍스트: 400-500 토큰
├── 기술 문서: 600-800 토큰
├── FAQ: 200-300 토큰
└── 법률/의료: 300-400 토큰
실험 방법:
1. 대표 질문 10-20개 준비
2. 다양한 청크 크기로 인덱싱
3. 검색 정확도 + 답변 품질 평가
4. 최적 크기 선택
팁:
- 오버랩은 청크의 15-25%
- 의미 단위(문단) 경계 존중
- 메타데이터 활용Q3. 벡터 DB 선택이 성능에 영향을 주나요?
A. 크게 영향 없음 (대부분):
검색 품질:
- 임베딩 모델이 99% 결정
- 벡터 DB는 저장/검색 효율
선택 기준:
├── 규모: 100K 이하 → Chroma
├── 규모: 100K-10M → Weaviate
├── 규모: 10M+ → Pinecone, Milvus
├── 비용: 오픈소스 → Chroma, Weaviate
├── 편의: 관리형 → Pinecone
└── 기능: 하이브리드 → Weaviate
결론:
프로토타입 → Chroma
프로덕션 소규모 → Weaviate
프로덕션 대규모 → PineconeQ4. RAG에서 환각을 줄이는 방법은?
A. 다층적 접근:
1. 프롬프트 설계:
- "컨텍스트에 없으면 모른다고 해"
- "추측하지 마"
- "출처를 명시해"
2. 검색 품질 향상:
- Reranking 적용
- Top-K 줄이기
- 관련성 임계값 설정
3. 답변 검증:
- 신뢰도 점수 표시
- 출처 링크 제공
- 사용자 피드백 수집
4. 폴백 메시지:
- 관련 문서 없으면 명시
- "확실하지 않습니다" 허용
- 추가 질문 유도외부 참고 자료
RAG를 더 깊게 배우고 싶다면:
- LangChain 공식 문서 – RAG 구현 가이드
- LlamaIndex 공식 문서 – 인덱싱 전문
- Pinecone Learning Center – 벡터 DB 이해
- Prompting Guide RAG – 기법 정리
- RAGAS (RAG 평가) – 품질 평가 프레임워크
최종 정리: RAG 마스터
핵심 메시지:
✅ RAG = 검색(Retrieval) + 생성(Generation)
✅ LLM 환각과 지식 컷오프 문제 해결
✅ Chunking: 300-500 토큰, 20% 오버랩
✅ Embedding: OpenAI text-embedding-3-small 추천
✅ 벡터 DB: 프로토타입(Chroma), 프로덕션(Pinecone)
✅ LangChain/LlamaIndex로 빠른 구축
✅ Advanced: HyDE, Reranking, GraphRAG
✅ 사내 챗봇: 몇 시간 만에 구축 가능!실전 체크리스트:
RAG 구축 시:
☑ 문서 형식 분석 (PDF, DOCX, etc.)
☑ 청킹 전략 결정
☑ 임베딩 모델 선택
☑ 벡터 DB 선택
☑ 프롬프트 최적화
☑ 출처 표시 기능
☑ 에러 핸들링
성능 최적화:
☑ 검색 품질 평가 (RAGAS)
☑ Reranking 적용
☑ 캐싱 전략
☑ 모니터링같이보기
- ChatGPT 10배 활용법 – Prompt Engineering 완벽 마스터 가이드
- 스스로 일하는 AI – AI Agent 완벽 가이드: ReAct부터 Multi-Agent까지
- 멀티모달 AI 완벽 가이드: 텍스트, 이미지, 음성을 하나로 – AI가 세상을 보고 듣고 말하는 법!
- MLOps 완벽 가이드: AI 모델이 실험실을 탈출해 세상과 만나는 법 – 배포부터 운영까지 모든 것!
- Vector DB 완벽 가이드: AI 시대의 새로운 데이터베이스 – 의미를 검색하는 마법!
- AI 에이전트 프레임워크 완벽 비교: LangChain vs LlamaIndex vs CrewAI – 당신의 AI 팀을 구축하라!
