ChatGPT의 한계를 넘다 – RAG 완벽 가이드: 사내 문서 챗봇부터 프로덕션까지


Table of Contents

핵심 요약

“ChatGPT는 왜 우리 회사 내부 문서를 모르고, 어제 발표된 뉴스도 모를까?”
이 문제를 해결하는 기술이 바로 RAG(Retrieval-Augmented Generation, 검색 증강 생성)입니다.

RAG는 LLM이 답변하기 전에 외부 지식베이스에서 관련 정보를 검색하여, 환각(Hallucination)을 줄이고 최신/전문 정보를 제공합니다.
작동 원리는 간단합니다: 문서를 청크로 분할 → 벡터로 임베딩 → 벡터 DB에 저장 → 질문과 유사한 문서 검색 → LLM에 컨텍스트로 전달 → 정확한 답변 생성. Chunking 전략에서 최적 크기는 300-500 토큰, 20% 오버랩이 권장되며, Semantic Chunking이 정확도를 높입니다.
벡터 DBPinecone(관리형, 확장성), Chroma(경량, 프로토타입), Weaviate(오픈소스, GraphQL)가 대표적입니다.
Embedding 모델OpenAI text-embedding-3-small(성능↑, 가격↓), 한국어는 KoSimCSE, multilingual-e5 추천.
고급 기법으로 HyDE(가상 문서 생성), Reranking(재정렬), GraphRAG(지식 그래프)가 성능을 10-30% 향상시킵니다. LangChainLlamaIndex사내 문서 챗봇을 몇 시간 만에 구축 가능합니다.

본 포스팅에서는 RAG 아키텍처부터 벡터 DB 비교, Chunking 최적화, 실전 코드, 사내 챗봇 구축까지 완벽하게 다룹니다.


📍 목차

  1. RAG란? – LLM의 한계를 극복하다
  2. RAG 아키텍처와 작동 원리
  3. Chunking 전략 – 문서 분할의 기술
  4. Embedding 모델 선택 가이드
  5. 벡터 DB 비교: Pinecone vs Chroma vs Weaviate
  6. LangChain & LlamaIndex 실전 구현
  7. Advanced RAG: HyDE, Reranking, GraphRAG
  8. 사내 문서 챗봇 구축 실전

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+ 언어
└── 추천: 다국어 RAG

4-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 서비스 OK

4-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 RAG

7-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 Rerank

8. 사내 문서 챗봇 구축 실전

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.md

8-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 documents

chatbot.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 --reload

8-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
프로덕션 대규모 → Pinecone

Q4. RAG에서 환각을 줄이는 방법은?

A. 다층적 접근:

1. 프롬프트 설계:
   - "컨텍스트에 없으면 모른다고 해"
   - "추측하지 마"
   - "출처를 명시해"

2. 검색 품질 향상:
   - Reranking 적용
   - Top-K 줄이기
   - 관련성 임계값 설정

3. 답변 검증:
   - 신뢰도 점수 표시
   - 출처 링크 제공
   - 사용자 피드백 수집

4. 폴백 메시지:
   - 관련 문서 없으면 명시
   - "확실하지 않습니다" 허용
   - 추가 질문 유도

외부 참고 자료

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 적용
☑ 캐싱 전략
☑ 모니터링

같이보기

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다