Split View: RAG: Retrieval-Augmented Generation 논문 분석과 실전 아키텍처
RAG: Retrieval-Augmented Generation 논문 분석과 실전 아키텍처
- 1. LLM의 Hallucination 문제와 RAG의 등장 배경
- 2. 원본 RAG 논문 구조: Retriever + Generator
- 3. Dense Passage Retrieval (DPR) 원리
- 4. Chunking 전략
- 5. Embedding 모델 선택
- 6. Vector Database 비교
- 7. Advanced RAG 패턴
- 8. Evaluation 메트릭: RAGAS 프레임워크
- 9. LangChain + ChromaDB 실전 구현 예제
- 10. 정리
- References
1. LLM의 Hallucination 문제와 RAG의 등장 배경
Large Language Model(LLM)은 방대한 텍스트 데이터로 사전 학습되어 자연어 이해와 생성에서 놀라운 성능을 보여준다. 그러나 LLM에는 근본적인 한계가 존재한다. 바로 Hallucination(환각) 문제다.
Hallucination이란 모델이 사실이 아닌 정보를 마치 사실인 것처럼 자신 있게 생성하는 현상을 말한다. 이 문제가 발생하는 근본 원인은 다음과 같다.
- 지식의 정적 특성: LLM의 파라미터에 인코딩된 지식은 학습 시점에 고정된다. 학습 이후에 발생한 사건이나 업데이트된 정보를 반영하지 못한다.
- 파라미터 메모리의 불완전성: 수십억 개의 파라미터가 있더라도, 세상의 모든 세부 사실을 정확하게 저장하고 재현하는 것은 불가능하다.
- 확률적 생성 방식: LLM은 다음 토큰을 확률적으로 예측하여 생성하기 때문에, 통계적으로 그럴듯하지만 사실과 다른 텍스트를 만들어낼 수 있다.
- 출처 추적 불가: 생성된 답변이 어떤 학습 데이터에서 비롯되었는지 추적할 수 없어, 검증 자체가 어렵다.
이러한 한계를 극복하기 위해 등장한 접근법이 **Retrieval-Augmented Generation(RAG)**이다. RAG의 핵심 아이디어는 단순하면서도 강력하다. LLM이 답변을 생성하기 전에, 외부 지식 저장소에서 관련 문서를 검색(Retrieve)하여 그 정보를 기반으로 답변을 생성(Generate)하는 것이다.
이를 통해 다음과 같은 이점을 얻을 수 있다.
- 사실 기반 응답: 검색된 실제 문서를 근거로 답변을 생성하므로 Hallucination이 줄어든다.
- 지식 업데이트 용이: 외부 데이터베이스만 업데이트하면 최신 정보를 반영할 수 있다. 모델 재학습이 필요 없다.
- 출처 제공 가능: 답변의 근거가 된 문서를 함께 제시하여 투명성과 신뢰성을 높일 수 있다.
- 도메인 특화 용이: 특정 도메인의 문서만 인덱싱하면 해당 분야에 특화된 시스템을 빠르게 구축할 수 있다.
2. 원본 RAG 논문 구조: Retriever + Generator
2020년 Facebook AI Research(현 Meta AI)의 Patrick Lewis 등이 발표한 논문 "Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks"(NeurIPS 2020)는 RAG라는 개념을 최초로 공식화한 핵심 연구다.
2.1 논문의 핵심 제안
Lewis et al.은 사전 학습된 언어 모델의 Parametric Memory(파라미터 내 암묵적 지식)와 Non-Parametric Memory(외부 문서 인덱스의 명시적 지식)를 결합하는 일반적인(fine-tuning 가능한) 방법론을 제안했다. 구체적으로, Parametric Memory는 사전 학습된 seq2seq 모델(BART)이고, Non-Parametric Memory는 Wikipedia 전체를 Dense Vector Index로 구축한 것이다.
2.2 아키텍처 구성
RAG 모델의 아키텍처는 크게 두 가지 컴포넌트로 구성된다.
Retriever (검색기) - p_eta(z|x)
입력 쿼리 x가 주어지면, 관련 문서(passage) z를 검색하는 컴포넌트다. 논문에서는 Dense Passage Retrieval(DPR)을 사용하여 쿼리와 문서를 각각 Dense Vector로 인코딩한 후, Maximum Inner Product Search(MIPS)를 통해 top-k 관련 문서를 검색한다.
Generator (생성기) - p_theta(y_i|x, z, y_{1:i-1})
검색된 문서 z와 원래 입력 x를 함께 컨텍스트로 받아, 최종 출력 y를 생성하는 컴포넌트다. 논문에서는 BART-large를 Generator로 사용했다.
2.3 두 가지 RAG 변형
논문에서는 검색된 문서를 활용하는 방식에 따라 두 가지 변형을 제안한다.
RAG-Sequence
전체 출력 시퀀스를 생성할 때 하나의 검색된 문서를 일관되게 사용한다. 각 검색된 문서 z에 대해 전체 시퀀스를 생성한 후, 각 문서에 대한 확률을 marginalize한다.
p_RAG-Sequence(y|x) ≈ Σ_z p_eta(z|x) Π_i p_theta(y_i|x, z, y_{1:i-1})
RAG-Token
각 출력 토큰을 생성할 때마다 다른 검색된 문서를 참조할 수 있다. 토큰 레벨에서 문서별 확률을 marginalize한다.
p_RAG-Token(y|x) ≈ Π_i Σ_z p_eta(z|x) p_theta(y_i|x, z, y_{1:i-1})
2.4 주요 실험 결과
RAG 모델은 세 가지 Open-Domain QA 벤치마크(Natural Questions, TriviaQA, WebQuestions)에서 기존의 Parametric seq2seq 모델과 task-specific retrieve-and-extract 아키텍처를 모두 능가하는 state-of-the-art 성능을 달성했다. 특히 주목할 점은, RAG 모델이 기존 parametric-only 모델에 비해 더 specific하고, diverse하며, factual한 텍스트를 생성한다는 것이다.
3. Dense Passage Retrieval (DPR) 원리
RAG의 Retriever 컴포넌트에서 핵심적인 역할을 하는 것이 **Dense Passage Retrieval(DPR)**이다. Karpukhin et al.이 2020년 EMNLP에서 발표한 논문 **"Dense Passage Retrieval for Open-Domain Question Answering"**에서 제안된 방법이다.
3.1 기존 Sparse Retrieval의 한계
전통적인 정보 검색에서는 BM25 같은 Sparse Retrieval 방법이 주로 사용되었다. BM25는 TF-IDF 기반으로 키워드 매칭을 수행하는데, 다음과 같은 한계가 있다.
- 어휘 불일치(Lexical Mismatch): 동의어나 다른 표현을 사용하면 관련 문서를 찾지 못한다. 예를 들어, "머신러닝"으로 질문했는데 문서에는 "기계학습"이라고 되어 있으면 매칭에 실패한다.
- 의미적 유사성 미반영: 단어의 출현 빈도만 고려하므로, 문맥적 의미를 파악하지 못한다.
3.2 DPR의 Bi-Encoder 아키텍처
DPR은 Bi-Encoder(이중 인코더) 아키텍처를 사용한다. 두 개의 독립적인 BERT-base 인코더를 사용하여 쿼리와 문서를 각각 Dense Vector로 변환한다.
- Query Encoder: E_Q(q) → d차원 벡터
- Passage Encoder: E_P(p) → d차원 벡터
유사도는 두 벡터의 **Inner Product(내적)**으로 계산한다.
sim(q, p) = E_Q(q)^T · E_P(p)
이 아키텍처의 핵심 장점은 쿼리와 문서의 인코딩이 독립적이라는 것이다. 문서 인코딩은 오프라인에서 사전에 수행하고 FAISS 같은 ANN(Approximate Nearest Neighbor) 라이브러리에 인덱싱해 둘 수 있다. 검색 시에는 쿼리만 인코딩하면 되므로, 수백만 개의 문서에서도 밀리초 단위의 검색이 가능하다.
3.3 학습 방법
DPR은 In-Batch Negatives 전략으로 학습된다. 배치 내의 다른 질문에 대한 정답 passage를 Negative Sample로 활용하는 방법이다. 추가적으로 BM25로 검색했지만 정답이 아닌 passage를 Hard Negative로 사용하여 학습 효과를 높인다.
3.4 성능
DPR은 BM25 대비 Top-20 Passage Retrieval Accuracy에서 **9%~19%**의 절대적 성능 향상을 달성했다. 이는 제한된 양의 query-passage 쌍만으로도 고품질의 Dense Retriever를 학습할 수 있음을 보여준다.
4. Chunking 전략
RAG 시스템에서 문서를 적절한 크기의 Chunk로 분할하는 것은 검색 품질에 직접적인 영향을 미치는 핵심 단계다. LangChain은 다양한 Text Splitter를 제공하며, 주요 Chunking 전략은 다음과 같다.
4.1 Fixed-Size Chunking (고정 크기 분할)
가장 단순한 방법으로, 지정된 문자 수(Character) 또는 토큰 수(Token)를 기준으로 텍스트를 분할한다.
from langchain_text_splitters import CharacterTextSplitter
text_splitter = CharacterTextSplitter(
separator="\n\n",
chunk_size=1000,
chunk_overlap=200,
length_function=len,
)
docs = text_splitter.split_documents(documents)
- 장점: 구현이 간단하고 예측 가능하다.
- 단점: 문장 중간이나 의미 단위 중간에서 잘릴 수 있다.
chunk_overlap 파라미터는 인접한 Chunk 사이에 겹치는 부분을 두어 문맥 유실을 완화한다.
4.2 Recursive Character Splitting (재귀적 분할)
LangChain이 가장 권장하는 범용 Text Splitter다. 여러 단계의 구분자를 재귀적으로 적용하여 의미 단위를 최대한 보존한다.
from langchain_text_splitters import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
separators=["\n\n", "\n", ". ", " ", ""],
length_function=len,
)
docs = text_splitter.split_documents(documents)
동작 원리는 다음과 같다.
- 먼저
\n\n(단락 구분)으로 분할을 시도한다. - Chunk가 여전히
chunk_size를 초과하면\n(줄바꿈)으로 분할한다. - 그래도 초과하면
.(문장 단위)으로 분할한다. - 최후의 수단으로 공백이나 문자 단위로 분할한다.
이 방식의 핵심은 큰 의미 단위를 먼저 보존하려고 시도하고, 필요한 경우에만 더 작은 단위로 내려간다는 점이다.
4.3 Semantic Chunking (의미 기반 분할)
Embedding 유사도를 기반으로 의미가 변하는 지점을 감지하여 분할하는 가장 고급 전략이다.
from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai.embeddings import OpenAIEmbeddings
text_splitter = SemanticChunker(
OpenAIEmbeddings(),
breakpoint_threshold_type="percentile",
)
docs = text_splitter.split_documents(documents)
인접한 문장 간의 Embedding Cosine Similarity를 계산하고, 유사도가 급격히 떨어지는 지점을 Chunk 경계로 설정한다. 의미적으로 일관된 Chunk를 생성할 수 있지만, Embedding 계산 비용이 추가로 발생한다.
4.4 Chunking 전략 선택 가이드
| 전략 | 적합한 상황 | 비용 |
|---|---|---|
| Fixed-Size | 균일한 구조의 문서, 빠른 프로토타이핑 | 낮음 |
| Recursive | 대부분의 일반적인 사용 사례 (권장 기본값) | 낮음 |
| Semantic | 의미 경계가 중요한 문서, 고품질 요구 | 높음 |
실전에서는 chunk_size를 5001500 사이에서, 20% 정도로 설정하는 것이 일반적이다. 최적값은 데이터 특성과 사용 사례에 따라 실험적으로 결정해야 한다.chunk_overlap을 chunk_size의 10
5. Embedding 모델 선택
Chunk를 Vector로 변환하는 Embedding 모델의 선택은 검색 품질을 좌우하는 중요한 결정이다. MTEB(Massive Text Embedding Benchmark) 리더보드를 참고하면 주요 모델들을 비교할 수 있다.
5.1 OpenAI text-embedding-3 시리즈
from langchain_openai import OpenAIEmbeddings
# 고성능 모델
embeddings = OpenAIEmbeddings(model="text-embedding-3-large") # 3072차원
# 비용 효율 모델
embeddings = OpenAIEmbeddings(model="text-embedding-3-small") # 1536차원
- MTEB 점수: text-embedding-3-large 약 64.6
- 장점: API 호출만으로 간편하게 사용 가능하며, 안정적인 품질을 제공한다. Matryoshka Representation을 지원하여 차원을 줄여도 성능 저하가 적다.
- 단점: API 비용이 발생하며, 데이터가 외부 서버로 전송된다.
5.2 Sentence-Transformers
from langchain_huggingface import HuggingFaceEmbeddings
embeddings = HuggingFaceEmbeddings(
model_name="sentence-transformers/all-MiniLM-L6-v2"
)
- 장점: 오픈소스이며, 로컬에서 실행 가능하다. 영어 기준 빠르고 가벼운 모델이 다수 존재한다. all-MiniLM-L6-v2는 384차원으로 경량이면서도 준수한 성능을 제공한다.
- 단점: 다국어 지원이 제한적이며, 대규모 모델 대비 성능이 낮을 수 있다.
5.3 BGE (BAAI General Embedding) 시리즈
from langchain_huggingface import HuggingFaceEmbeddings
embeddings = HuggingFaceEmbeddings(
model_name="BAAI/bge-m3",
model_kwargs={"device": "cuda"},
encode_kwargs={"normalize_embeddings": True},
)
- MTEB 점수: BGE-M3 약 63.0
- 장점: 오픈소스 최고 수준의 성능이며, 100개 이상의 언어를 지원한다. 한국어를 포함한 다국어 RAG에 특히 적합하다. Dense, Sparse, Multi-Vector Retrieval을 모두 지원하는 Hybrid 모델이다.
- 단점: 모델 크기가 크기 때문에 GPU가 필요하다.
5.4 선택 기준
| 기준 | 추천 모델 |
|---|---|
| 빠른 프로토타이핑 | OpenAI text-embedding-3-small |
| 프로덕션 (품질 우선) | OpenAI text-embedding-3-large 또는 Cohere embed-v4 |
| 프로덕션 (비용 우선, 셀프 호스팅) | BGE-M3 |
| 한국어/다국어 | BGE-M3 |
| 경량/엣지 환경 | all-MiniLM-L6-v2 |
핵심 원칙은 반드시 자신의 실제 데이터로 벤치마크하는 것이다. MTEB 점수는 범용 벤치마크이므로, 특정 도메인에서의 성능은 다를 수 있다.
6. Vector Database 비교
Embedding 벡터를 저장하고 유사도 검색을 수행하는 Vector Database는 RAG 시스템의 인프라 핵심이다. 주요 Vector Database를 비교한다.
6.1 Chroma
from langchain_chroma import Chroma
vectorstore = Chroma.from_documents(
documents=docs,
embedding=embeddings,
persist_directory="./chroma_db",
)
- 유형: 오픈소스, 임베디드(In-Process)
- 적합한 상황: 로컬 개발, 프로토타이핑, 소규모 프로젝트
- 장점: 설치가 간단하고(
pip install chromadb), 별도 서버 없이 Python 프로세스 내에서 동작한다. LangChain과의 통합이 매우 잘 되어 있다. - 한계: 대규모 데이터(수백만 벡터 이상)에서 성능이 저하될 수 있다. 프로덕션 수준의 가용성과 확장성이 제한적이다.
6.2 Pinecone
- 유형: Managed SaaS (완전 관리형)
- 적합한 상황: 프로덕션 환경, 운영 부담 최소화가 목표인 팀
- 장점: 서버리스 아키텍처로 인프라 관리가 불필요하다. 멀티 리전 지원, 높은 가용성, 자동 스케일링을 제공한다. 수십억 벡터까지 확장 가능하다.
- 한계: 비용이 상대적으로 높다. 벤더 종속(Vendor Lock-in) 우려가 있다. 오픈소스가 아니다.
6.3 Weaviate
- 유형: 오픈소스 + 관리형 클라우드
- 적합한 상황: Hybrid Search(벡터 + 키워드)가 중요한 경우, 유연한 스키마가 필요한 경우
- 장점: Native Hybrid Search를 강력하게 지원한다. GraphQL API, 모듈러 아키텍처, 자체 벡터화 모듈을 제공한다. 오픈소스이면서도 관리형 클라우드 옵션이 있어 유연하다.
- 한계: 학습 곡선이 있으며, 설정이 다소 복잡할 수 있다.
6.4 pgvector
CREATE EXTENSION vector;
CREATE TABLE documents (
id SERIAL PRIMARY KEY,
content TEXT,
embedding vector(1536)
);
-- 유사도 검색
SELECT content, embedding <=> '[0.1, 0.2, ...]'::vector AS distance
FROM documents
ORDER BY distance
LIMIT 5;
- 유형: PostgreSQL 확장
- 적합한 상황: 이미 PostgreSQL을 사용하는 조직, 별도 인프라 추가를 원하지 않는 경우
- 장점: 기존 PostgreSQL 인프라를 그대로 활용할 수 있다. SQL과 벡터 검색을 하나의 데이터베이스에서 수행할 수 있어 아키텍처가 단순해진다. HNSW, IVFFlat 인덱스를 지원한다.
- 한계: 1억 벡터 이상에서 성능이 저하될 수 있다. 전용 Vector Database 대비 기능이 제한적이다.
6.5 Milvus
- 유형: 오픈소스, 분산 아키텍처
- 적합한 상황: 수십억 벡터 규모의 대규모 시스템, 데이터 엔지니어링 역량이 있는 팀
- 장점: 산업 수준의 대규모 벡터 검색에서 검증된 성능을 가진다. 다양한 인덱스 유형(IVF, HNSW, DiskANN 등)을 지원하며, GPU 가속이 가능하다. Zilliz Cloud를 통한 관리형 서비스도 제공한다.
- 한계: 운영 복잡도가 높다. 클러스터 설정과 관리에 전문 지식이 필요하다.
6.6 비교 요약
| DB | 유형 | 최대 규모 | Hybrid Search | 추천 시나리오 |
|---|---|---|---|---|
| Chroma | 임베디드 | ~100만 | 제한적 | 프로토타이핑, 개발 |
| Pinecone | Managed SaaS | 수십억 | 지원 | 프로덕션, 관리 최소화 |
| Weaviate | 오픈소스/클라우드 | 수억 | 강력 지원 | Hybrid Search 중심 |
| pgvector | PostgreSQL 확장 | ~1억 | SQL 조합 | PostgreSQL 기존 인프라 |
| Milvus | 오픈소스 분산 | 수십억 | 지원 | 대규모 시스템 |
7. Advanced RAG 패턴
기본 RAG(Naive RAG)는 단순한 "검색 후 생성" 파이프라인이다. 프로덕션 환경에서는 검색 품질과 생성 품질을 높이기 위한 다양한 Advanced RAG 패턴이 필요하다.
7.1 Re-ranking
기본 RAG에서 초기 검색(Retrieval)은 Bi-Encoder의 벡터 유사도를 사용하는데, 이는 속도는 빠르지만 정밀도가 부족할 수 있다. Re-ranking은 초기 검색 결과를 Cross-Encoder로 다시 평가하여 정밀도를 높이는 패턴이다.
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import CrossEncoderReranker
from langchain_community.cross_encoders import HuggingFaceCrossEncoder
# Cross-Encoder 모델 로드
model = HuggingFaceCrossEncoder(model_name="BAAI/bge-reranker-v2-m3")
compressor = CrossEncoderReranker(model=model, top_n=3)
# Re-ranking Retriever 구성
compression_retriever = ContextualCompressionRetriever(
base_compressor=compressor,
base_retriever=vectorstore.as_retriever(search_kwargs={"k": 20}),
)
동작 흐름은 다음과 같다.
- Bi-Encoder로 넓은 범위의 후보 문서(top-20)를 빠르게 검색한다.
- Cross-Encoder가 쿼리와 각 후보 문서를 쌍으로 입력받아 직접적인 관련성 점수를 매긴다.
- 재정렬된 상위 문서(top-3)만 Generator에 전달한다.
Cross-Encoder는 Bi-Encoder보다 정밀하지만, 모든 후보에 대해 개별 추론이 필요하므로 대규모 검색에는 적합하지 않다. 따라서 Bi-Encoder로 후보를 좁힌 후 Cross-Encoder로 재정렬하는 2단계 파이프라인이 표준 패턴이다.
7.2 HyDE (Hypothetical Document Embeddings)
Gao et al.(2022)이 제안한 HyDE는 쿼리와 문서 사이의 의미적 갭(Semantic Gap)을 해소하기 위한 패턴이다. 사용자의 질문은 짧고 추상적인 반면, 답변이 포함된 문서는 길고 구체적이다. 이 차이로 인해 직접적인 벡터 유사도 검색이 최적이 아닐 수 있다.
HyDE의 핵심 아이디어는 다음과 같다.
- 사용자 쿼리를 받으면, LLM에게 "이 질문에 답하는 가상의 문서를 작성해 달라"고 요청한다.
- 생성된 가상 문서(Hypothetical Document)를 Embedding한다. 이 가상 문서는 사실과 다를 수 있지만, 실제 관련 문서와 유사한 형식과 어휘를 가진다.
- 이 Embedding으로 실제 문서를 검색한다.
from langchain.chains import HypotheticalDocumentEmbedder
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
llm = ChatOpenAI(model="gpt-4o-mini")
base_embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
hyde_embeddings = HypotheticalDocumentEmbedder.from_llm(
llm=llm,
base_embeddings=base_embeddings,
prompt_key="web_search",
)
# HyDE Embedding으로 검색
results = vectorstore.similarity_search_by_vector(
hyde_embeddings.embed_query("RAG 시스템의 성능을 평가하는 방법은?")
)
Encoder의 Dense Bottleneck이 가상 문서의 Hallucination을 필터링하는 역할을 하므로, 가상 문서가 정확하지 않아도 관련 실제 문서를 효과적으로 검색할 수 있다.
7.3 Self-RAG
Asai et al.(2023)이 제안한 Self-RAG는 LLM이 스스로 검색의 필요성을 판단하고, 생성 결과를 자기 비판적으로 평가하는 패턴이다.
Self-RAG의 핵심 메커니즘은 Reflection Token(반성 토큰)이다.
- [Retrieve]: 현재 시점에서 외부 검색이 필요한지 판단한다.
- [IsRel]: 검색된 문서가 질문과 관련 있는지 평가한다.
- [IsSup]: 생성된 응답이 검색된 문서에 의해 뒷받침되는지 평가한다.
- [IsUse]: 생성된 응답이 전체적으로 유용한지 평가한다.
이 Reflection Token들은 모델의 어휘에 추가되어 일반 토큰처럼 학습되며, 추론 시 모델이 자율적으로 생성한다. Self-RAG는 7B, 13B 파라미터 규모에서 ChatGPT와 retrieval-augmented Llama2-chat을 Open-Domain QA, Reasoning, Fact Verification 등에서 능가하는 성능을 보였다.
7.4 Corrective RAG (CRAG)
Yan et al.(2024)이 제안한 CRAG는 검색된 문서의 품질을 능동적으로 평가하고 보정하는 패턴이다.
CRAG의 핵심 컴포넌트는 다음과 같다.
- Retrieval Evaluator: 경량 모델이 검색된 문서의 관련성을 Correct, Incorrect, Ambiguous 세 가지로 판단한다.
- Knowledge Refinement: 검색된 문서에서 핵심 정보만 추출하고 불필요한 부분을 제거한다. Decompose-then-Recompose 알고리즘을 사용한다.
- Web Search Fallback: Retrieval Evaluator가 Incorrect로 판단하면, 정적 코퍼스 대신 웹 검색으로 대체하여 더 나은 정보를 찾는다.
[Query] → [Retriever] → [Retrieval Evaluator]
↓
Correct → Knowledge Refinement → Generator
Incorrect → Web Search → Knowledge Refinement → Generator
Ambiguous → 두 경로 결합 → Generator
이 패턴의 강점은 검색 품질이 낮은 상황에서도 자동으로 대체 경로를 활성화하여 강건한 응답을 생성한다는 점이다.
8. Evaluation 메트릭: RAGAS 프레임워크
RAG 시스템의 성능을 체계적으로 평가하기 위해 RAGAS(Retrieval Augmented Generation Assessment) 프레임워크가 널리 사용된다. Es et al.(2023)이 제안한 RAGAS는 Ground Truth 없이도 RAG 파이프라인을 평가할 수 있는 자동화된 메트릭을 제공한다.
8.1 Faithfulness (충실도)
생성된 답변이 검색된 Context에 얼마나 충실한지 측정한다. Hallucination 감지의 핵심 메트릭이다.
Faithfulness = (Context에 의해 뒷받침되는 Claim 수) / (전체 Claim 수)
동작 방식은 다음과 같다.
- LLM이 생성된 답변에서 개별 Claim(주장)을 추출한다.
- 각 Claim이 제공된 Context에 의해 뒷받침(Support)되는지 판단한다.
- 뒷받침되는 Claim의 비율을 계산한다.
값은 0~1 사이이며, 1에 가까울수록 답변이 Context에 충실하다는 의미이다.
8.2 Answer Relevance (답변 관련성)
생성된 답변이 원래 질문에 얼마나 관련 있는지 측정한다.
동작 방식은 다음과 같다.
- 생성된 답변으로부터 역으로 질문을 생성(Reverse Engineering)한다.
- 생성된 질문들과 원래 질문의 Embedding 유사도를 계산한다.
- 평균 유사도가 Answer Relevance 점수가 된다.
이 방식은 답변이 질문의 핵심을 다루고 있는지, 불필요한 정보를 포함하고 있지는 않은지를 간접적으로 측정한다.
8.3 Context Recall (맥락 재현율)
검색된 Context가 Ground Truth 답변을 생성하는 데 필요한 정보를 얼마나 포함하고 있는지 측정한다.
Context Recall = (Context에서 지지되는 GT 문장 수) / (전체 GT 문장 수)
이 메트릭은 Ground Truth가 필요한 유일한 메트릭이다. Retriever의 성능을 직접적으로 평가한다.
8.4 Context Precision (맥락 정밀도)
검색된 Context 중에서 실제로 관련 있는 항목이 상위에 위치하는지 측정한다. 관련 문서가 검색 결과의 앞쪽에 올수록 점수가 높다.
8.5 RAGAS 사용 예시
from ragas import evaluate
from ragas.metrics import (
faithfulness,
answer_relevancy,
context_recall,
context_precision,
)
from datasets import Dataset
# 평가 데이터셋 구성
eval_data = {
"question": ["RAG란 무엇인가?"],
"answer": ["RAG는 검색 증강 생성으로, LLM이 외부 문서를 검색하여 답변을 생성하는 방법이다."],
"contexts": [["RAG(Retrieval-Augmented Generation)는 외부 지식을 검색하여 LLM의 생성에 활용하는 기법이다."]],
"ground_truth": ["RAG는 Retrieval-Augmented Generation의 약자로, 외부 지식 소스에서 관련 정보를 검색하여 LLM의 응답 생성을 보강하는 방법론이다."],
}
dataset = Dataset.from_dict(eval_data)
# 평가 실행
result = evaluate(
dataset=dataset,
metrics=[faithfulness, answer_relevancy, context_recall, context_precision],
)
print(result)
프로덕션 환경에서는 이러한 메트릭을 CI/CD 파이프라인에 통합하여 RAG 시스템의 변경(Chunking 전략 수정, 모델 교체 등)이 품질에 미치는 영향을 자동으로 모니터링하는 것이 권장된다.
9. LangChain + ChromaDB 실전 구현 예제
지금까지 다룬 개념을 종합하여, LangChain과 ChromaDB로 실전 RAG 파이프라인을 구현하는 전체 코드를 작성한다.
9.1 환경 설정 및 패키지 설치
pip install langchain langchain-openai langchain-chroma langchain-community
pip install chromadb pypdf sentence-transformers
9.2 Document Loading 및 Chunking
import os
from langchain_community.document_loaders import PyPDFLoader, DirectoryLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
# PDF 문서 로딩
loader = DirectoryLoader(
"./documents",
glob="**/*.pdf",
loader_cls=PyPDFLoader,
)
documents = loader.load()
print(f"로드된 문서 수: {len(documents)}")
# Recursive Character Splitting
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
separators=["\n\n", "\n", ". ", " ", ""],
length_function=len,
)
splits = text_splitter.split_documents(documents)
print(f"생성된 Chunk 수: {len(splits)}")
9.3 Embedding 및 Vector Store 구축
from langchain_openai import OpenAIEmbeddings
from langchain_chroma import Chroma
# Embedding 모델 설정
embeddings = OpenAIEmbeddings(
model="text-embedding-3-small",
openai_api_key=os.getenv("OPENAI_API_KEY"),
)
# ChromaDB Vector Store 구축 및 영구 저장
vectorstore = Chroma.from_documents(
documents=splits,
embedding=embeddings,
persist_directory="./chroma_db",
collection_name="rag_collection",
)
print(f"Vector Store에 저장된 문서 수: {vectorstore._collection.count()}")
9.4 Retriever 구성
# 기본 Retriever
retriever = vectorstore.as_retriever(
search_type="similarity",
search_kwargs={"k": 5},
)
# MMR(Maximal Marginal Relevance) Retriever - 다양성 확보
retriever_mmr = vectorstore.as_retriever(
search_type="mmr",
search_kwargs={
"k": 5,
"fetch_k": 20, # 초기 검색 후보 수
"lambda_mult": 0.7, # 관련성(1.0)과 다양성(0.0) 사이 균형
},
)
9.5 RAG Chain 구성 및 실행
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
# LLM 설정
llm = ChatOpenAI(
model="gpt-4o-mini",
temperature=0,
openai_api_key=os.getenv("OPENAI_API_KEY"),
)
# Prompt Template
prompt = ChatPromptTemplate.from_template("""
다음 Context를 기반으로 질문에 답변하세요.
Context에 답변에 필요한 정보가 없으면 "제공된 문서에서 관련 정보를 찾을 수 없습니다."라고 답변하세요.
Context:
{context}
질문: {question}
답변:
""")
# Context 포맷팅 함수
def format_docs(docs):
return "\n\n---\n\n".join(
f"[출처: {doc.metadata.get('source', 'unknown')}, "
f"페이지: {doc.metadata.get('page', 'N/A')}]\n{doc.page_content}"
for doc in docs
)
# LCEL(LangChain Expression Language)로 RAG Chain 구성
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
# 실행
question = "RAG 시스템에서 Chunking 전략의 종류와 각각의 장단점은 무엇인가?"
answer = rag_chain.invoke(question)
print(answer)
9.6 출처 정보를 포함한 응답
from langchain_core.runnables import RunnableParallel
# 출처 정보와 답변을 함께 반환하는 Chain
rag_chain_with_sources = RunnableParallel(
{"context": retriever, "question": RunnablePassthrough()}
).assign(
answer=lambda x: (
prompt.invoke(
{"context": format_docs(x["context"]), "question": x["question"]}
)
| llm
| StrOutputParser()
).invoke(x["question"])
)
# 더 간결한 방식
from langchain.chains import create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
# Stuff Documents Chain (검색된 문서를 하나의 Context로 합침)
combine_docs_chain = create_stuff_documents_chain(llm, prompt)
# Retrieval Chain
retrieval_chain = create_retrieval_chain(retriever, combine_docs_chain)
# 실행 - context와 answer가 함께 반환됨
response = retrieval_chain.invoke({"input": question})
print("답변:", response["answer"])
print("\n참조 문서:")
for i, doc in enumerate(response["context"], 1):
print(f" [{i}] {doc.metadata.get('source', 'unknown')} "
f"(p.{doc.metadata.get('page', 'N/A')})")
9.7 Conversational RAG (대화형 RAG)
from langchain_core.prompts import MessagesPlaceholder
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
# 대화 히스토리를 고려한 Prompt
contextualize_prompt = ChatPromptTemplate.from_messages([
("system", "이전 대화 맥락을 고려하여 사용자의 최신 질문을 독립적으로 이해할 수 있도록 재구성하세요."),
MessagesPlaceholder("chat_history"),
("human", "{input}"),
])
# 세션별 히스토리 관리
store = {}
def get_session_history(session_id: str):
if session_id not in store:
store[session_id] = ChatMessageHistory()
return store[session_id]
# 대화형 RAG Chain 구성
conversational_rag = RunnableWithMessageHistory(
retrieval_chain,
get_session_history,
input_messages_key="input",
history_messages_key="chat_history",
output_messages_key="answer",
)
# 대화 실행
config = {"configurable": {"session_id": "user_001"}}
response1 = conversational_rag.invoke(
{"input": "RAG란 무엇인가?"},
config=config,
)
print(response1["answer"])
response2 = conversational_rag.invoke(
{"input": "그것의 주요 장점은?"}, # "그것" = 이전 대화의 RAG
config=config,
)
print(response2["answer"])
이 구현은 기본적인 RAG 파이프라인이다. 프로덕션으로 가기 위해서는 앞서 다룬 Re-ranking, HyDE 등의 Advanced 패턴 적용, RAGAS를 통한 체계적 평가, 그리고 모니터링과 로깅 인프라 구축이 추가로 필요하다.
10. 정리
RAG는 LLM의 Hallucination 문제를 해결하기 위한 가장 실용적이고 효과적인 접근법이다. Lewis et al.(2020)의 원본 논문에서 제안된 Retriever + Generator 구조는 이후 다양한 Advanced 패턴으로 발전하며 프로덕션 수준의 AI 시스템 구축의 핵심 아키텍처로 자리잡았다.
효과적인 RAG 시스템을 구축하기 위한 핵심 결정 사항을 정리하면 다음과 같다.
- Chunking 전략: RecursiveCharacterTextSplitter를 기본으로 시작하되, 데이터 특성에 따라 Semantic Chunking을 고려한다.
- Embedding 모델: 다국어가 필요하면 BGE-M3, 영어 중심이면 OpenAI text-embedding-3 시리즈가 안정적이다.
- Vector Database: 프로토타이핑에는 Chroma, 프로덕션에는 워크로드에 맞는 DB를 선택한다.
- Advanced 패턴: Re-ranking은 거의 모든 경우에 적용할 가치가 있으며, HyDE와 CRAG는 검색 품질이 부족할 때 고려한다.
- 평가: RAGAS 메트릭을 CI/CD에 통합하여 지속적으로 품질을 모니터링한다.
References
- Lewis, P., Perez, E., Piktus, A., Petroni, F., Karpukhin, V., Goyal, N., ... & Kiela, D. (2020). Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks. NeurIPS 2020. https://arxiv.org/abs/2005.11401
- Karpukhin, V., Oguz, B., Min, S., Lewis, P., Wu, L., Edunov, S., Chen, D., & Yih, W. (2020). Dense Passage Retrieval for Open-Domain Question Answering. EMNLP 2020. https://arxiv.org/abs/2004.04906
- Gao, L., Ma, X., Lin, J., & Callan, J. (2022). Precise Zero-Shot Dense Retrieval without Relevance Labels (HyDE). ACL 2023. https://arxiv.org/abs/2212.10496
- Asai, A., Wu, Z., Wang, Y., Sil, A., & Hajishirzi, H. (2023). Self-RAG: Learning to Retrieve, Generate, and Critique through Self-Reflection. ICLR 2024. https://arxiv.org/abs/2310.11511
- Yan, S., Gu, J., Zhu, Y., & Ling, Z. (2024). Corrective Retrieval Augmented Generation. https://arxiv.org/abs/2401.15884
- Es, S., James, J., Espinosa-Anke, L., & Schockaert, S. (2023). RAGAS: Automated Evaluation of Retrieval Augmented Generation. https://arxiv.org/abs/2309.15217
- LangChain Text Splitters Documentation. https://docs.langchain.com/oss/python/integrations/splitters
- RAGAS Documentation - Available Metrics. https://docs.ragas.io/en/stable/concepts/metrics/available_metrics/
- Pinecone - Chunking Strategies. https://www.pinecone.io/learn/chunking-strategies/
- MTEB: Massive Text Embedding Benchmark. https://huggingface.co/blog/mteb
RAG: Retrieval-Augmented Generation — Paper Analysis and Production Architecture
- 1. The LLM Hallucination Problem and the Emergence of RAG
- 2. Original RAG Paper Architecture: Retriever + Generator
- 3. Dense Passage Retrieval (DPR) Principles
- 4. Chunking Strategies
- 5. Embedding Model Selection
- 6. Vector Database Comparison
- 7. Advanced RAG Patterns
- 8. Evaluation Metrics: RAGAS Framework
- 9. Practical Implementation with LangChain + ChromaDB
- 10. Summary
- References
- Quiz
1. The LLM Hallucination Problem and the Emergence of RAG
Large Language Models (LLMs) are pre-trained on vast amounts of text data and demonstrate remarkable performance in natural language understanding and generation. However, LLMs have a fundamental limitation: the Hallucination problem.
Hallucination refers to the phenomenon where a model confidently generates information that is not factually correct. The root causes of this problem are as follows.
- Static nature of knowledge: Knowledge encoded in LLM parameters is fixed at training time. It cannot reflect events or updated information that occurred after training.
- Imperfect parametric memory: Even with billions of parameters, it is impossible to accurately store and reproduce every detail about the world.
- Probabilistic generation: LLMs predict and generate the next token probabilistically, which means they can produce text that is statistically plausible but factually incorrect.
- Inability to trace sources: It is impossible to trace which training data a generated answer originated from, making verification inherently difficult.
The approach that emerged to overcome these limitations is Retrieval-Augmented Generation (RAG). The core idea of RAG is simple yet powerful: before the LLM generates an answer, it retrieves relevant documents from an external knowledge store and generates the answer based on that information.
This yields the following benefits.
- Fact-based responses: Answers are generated based on actual retrieved documents, reducing hallucination.
- Easy knowledge updates: Simply updating the external database reflects the latest information. No model retraining is required.
- Source attribution: The documents underlying the answer can be presented alongside it, improving transparency and trustworthiness.
- Easy domain specialization: By indexing only domain-specific documents, a specialized system can be built quickly.
2. Original RAG Paper Architecture: Retriever + Generator
The paper "Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks" (NeurIPS 2020), published by Patrick Lewis et al. at Facebook AI Research (now Meta AI) in 2020, is the seminal work that first formalized the concept of RAG.
2.1 Core Proposal
Lewis et al. proposed a general (fine-tunable) methodology for combining the Parametric Memory (implicit knowledge within parameters) of a pre-trained language model with Non-Parametric Memory (explicit knowledge from an external document index). Specifically, the Parametric Memory is a pre-trained seq2seq model (BART), and the Non-Parametric Memory is the entirety of Wikipedia built as a Dense Vector Index.
2.2 Architecture Components
The RAG model architecture consists of two major components.
Retriever - p_eta(z|x)
Given an input query x, this component retrieves relevant documents (passages) z. The paper uses Dense Passage Retrieval (DPR) to encode queries and documents as Dense Vectors, then retrieves the top-k relevant documents via Maximum Inner Product Search (MIPS).
Generator - p_theta(y_i|x, z, y_{1:i-1})
This component receives the retrieved documents z along with the original input x as context and generates the final output y. The paper uses BART-large as the Generator.
2.3 Two RAG Variants
The paper proposes two variants based on how retrieved documents are utilized.
RAG-Sequence
A single retrieved document is used consistently when generating the entire output sequence. For each retrieved document z, the entire sequence is generated, and then the probabilities across documents are marginalized.
p_RAG-Sequence(y|x) ≈ Σ_z p_eta(z|x) Π_i p_theta(y_i|x, z, y_{1:i-1})
RAG-Token
Different retrieved documents can be referenced for each output token generated. Probabilities across documents are marginalized at the token level.
p_RAG-Token(y|x) ≈ Π_i Σ_z p_eta(z|x) p_theta(y_i|x, z, y_{1:i-1})
2.4 Key Experimental Results
The RAG model achieved state-of-the-art performance on three Open-Domain QA benchmarks (Natural Questions, TriviaQA, WebQuestions), surpassing both existing parametric seq2seq models and task-specific retrieve-and-extract architectures. Notably, the RAG model generated text that was more specific, diverse, and factual compared to parametric-only models.
3. Dense Passage Retrieval (DPR) Principles
The critical component in RAG's Retriever is Dense Passage Retrieval (DPR). It was proposed in the paper "Dense Passage Retrieval for Open-Domain Question Answering" by Karpukhin et al. at EMNLP 2020.
3.1 Limitations of Sparse Retrieval
Traditional information retrieval primarily used Sparse Retrieval methods like BM25. BM25 performs keyword matching based on TF-IDF but has the following limitations.
- Lexical Mismatch: Relevant documents cannot be found when synonyms or different expressions are used. For example, a query about "machine learning" will fail to match a document that uses "ML" or an equivalent term in another language.
- No semantic similarity: Since only word frequency is considered, contextual meaning is not captured.
3.2 DPR's Bi-Encoder Architecture
DPR uses a Bi-Encoder architecture. Two independent BERT-base encoders convert queries and documents into Dense Vectors respectively.
- Query Encoder: E_Q(q) → d-dimensional vector
- Passage Encoder: E_P(p) → d-dimensional vector
Similarity is computed as the Inner Product of the two vectors.
sim(q, p) = E_Q(q)^T · E_P(p)
The key advantage of this architecture is that query and document encoding are independent. Document encoding can be performed offline and indexed in ANN (Approximate Nearest Neighbor) libraries like FAISS. At search time, only the query needs to be encoded, enabling millisecond-level retrieval even across millions of documents.
3.3 Training Method
DPR is trained using an In-Batch Negatives strategy. Correct passages for other questions in the batch are used as Negative Samples. Additionally, passages retrieved by BM25 that are not correct answers are used as Hard Negatives to improve training effectiveness.
3.4 Performance
DPR achieved an absolute improvement of 9% to 19% in Top-20 Passage Retrieval Accuracy compared to BM25. This demonstrates that high-quality Dense Retrievers can be trained with only a limited number of query-passage pairs.
4. Chunking Strategies
Splitting documents into appropriately sized chunks is a critical step in RAG systems that directly impacts retrieval quality. LangChain provides various Text Splitters, and the key chunking strategies are as follows.
4.1 Fixed-Size Chunking
The simplest method, splitting text based on a specified number of characters or tokens.
from langchain_text_splitters import CharacterTextSplitter
text_splitter = CharacterTextSplitter(
separator="\n\n",
chunk_size=1000,
chunk_overlap=200,
length_function=len,
)
docs = text_splitter.split_documents(documents)
- Pros: Simple to implement and predictable.
- Cons: May split in the middle of sentences or semantic units.
The chunk_overlap parameter creates overlap between adjacent chunks to mitigate context loss.
4.2 Recursive Character Splitting
LangChain's most recommended general-purpose Text Splitter. It recursively applies multiple levels of separators to preserve semantic units as much as possible.
from langchain_text_splitters import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
separators=["\n\n", "\n", ". ", " ", ""],
length_function=len,
)
docs = text_splitter.split_documents(documents)
The operation logic is as follows.
- First, attempt to split by
\n\n(paragraph breaks). - If a chunk still exceeds
chunk_size, split by\n(line breaks). - If still too large, split by
.(sentence boundaries). - As a last resort, split by spaces or individual characters.
The key principle is trying to preserve larger semantic units first, and only breaking down into smaller units when necessary.
4.3 Semantic Chunking
The most advanced strategy, detecting points where meaning changes based on embedding similarity.
from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai.embeddings import OpenAIEmbeddings
text_splitter = SemanticChunker(
OpenAIEmbeddings(),
breakpoint_threshold_type="percentile",
)
docs = text_splitter.split_documents(documents)
It computes the Embedding Cosine Similarity between adjacent sentences and sets chunk boundaries where similarity drops sharply. While it produces semantically coherent chunks, additional embedding computation costs are incurred.
4.4 Chunking Strategy Selection Guide
| Strategy | Best For | Cost |
|---|---|---|
| Fixed-Size | Uniformly structured documents, rapid prototyping | Low |
| Recursive | Most general use cases (recommended default) | Low |
| Semantic | Documents where semantic boundaries matter, high quality required | High |
In practice, chunk_size is typically set between 500 and 1500, with chunk_overlap at 10-20% of the chunk_size. Optimal values should be determined experimentally based on data characteristics and use case.
5. Embedding Model Selection
The choice of embedding model for converting chunks to vectors is a crucial decision that determines retrieval quality. The MTEB (Massive Text Embedding Benchmark) leaderboard provides useful comparisons of major models.
5.1 OpenAI text-embedding-3 Series
from langchain_openai import OpenAIEmbeddings
# High-performance model
embeddings = OpenAIEmbeddings(model="text-embedding-3-large") # 3072 dimensions
# Cost-efficient model
embeddings = OpenAIEmbeddings(model="text-embedding-3-small") # 1536 dimensions
- MTEB Score: text-embedding-3-large approximately 64.6
- Pros: Convenient to use via API calls with consistent quality. Supports Matryoshka Representation, allowing dimension reduction with minimal performance loss.
- Cons: API costs apply, and data is sent to external servers.
5.2 Sentence-Transformers
from langchain_huggingface import HuggingFaceEmbeddings
embeddings = HuggingFaceEmbeddings(
model_name="sentence-transformers/all-MiniLM-L6-v2"
)
- Pros: Open-source and can run locally. Many fast, lightweight models are available for English. all-MiniLM-L6-v2 is lightweight at 384 dimensions while offering decent performance.
- Cons: Limited multilingual support, and performance may be lower compared to larger models.
5.3 BGE (BAAI General Embedding) Series
from langchain_huggingface import HuggingFaceEmbeddings
embeddings = HuggingFaceEmbeddings(
model_name="BAAI/bge-m3",
model_kwargs={"device": "cuda"},
encode_kwargs={"normalize_embeddings": True},
)
- MTEB Score: BGE-M3 approximately 63.0
- Pros: Top-tier open-source performance, supporting over 100 languages. Particularly suitable for multilingual RAG including Korean. Supports Dense, Sparse, and Multi-Vector Retrieval as a hybrid model.
- Cons: The large model size requires a GPU.
5.4 Selection Criteria
| Criterion | Recommended Model |
|---|---|
| Rapid prototyping | OpenAI text-embedding-3-small |
| Production (quality-first) | OpenAI text-embedding-3-large or Cohere embed-v4 |
| Production (cost-first, self-hosted) | BGE-M3 |
| Multilingual | BGE-M3 |
| Lightweight / edge environments | all-MiniLM-L6-v2 |
The key principle is to always benchmark with your actual data. MTEB scores are based on general benchmarks, so performance on specific domains may differ.
6. Vector Database Comparison
The Vector Database that stores embedding vectors and performs similarity search is a core piece of RAG system infrastructure. Here is a comparison of major Vector Databases.
6.1 Chroma
from langchain_chroma import Chroma
vectorstore = Chroma.from_documents(
documents=docs,
embedding=embeddings,
persist_directory="./chroma_db",
)
- Type: Open-source, Embedded (In-Process)
- Best for: Local development, prototyping, small-scale projects
- Pros: Easy to install (
pip install chromadb), runs within the Python process without a separate server. Excellent LangChain integration. - Limitations: Performance may degrade at large scale (millions of vectors or more). Production-level availability and scalability are limited.
6.2 Pinecone
- Type: Managed SaaS (fully managed)
- Best for: Production environments, teams wanting to minimize operational burden
- Pros: Serverless architecture eliminates infrastructure management. Multi-region support, high availability, and auto-scaling. Scales to billions of vectors.
- Limitations: Relatively high cost. Vendor lock-in concerns. Not open-source.
6.3 Weaviate
- Type: Open-source + Managed Cloud
- Best for: When Hybrid Search (vector + keyword) is important, when flexible schemas are needed
- Pros: Strong native Hybrid Search support. GraphQL API, modular architecture, and built-in vectorization modules. Flexible with both open-source and managed cloud options.
- Limitations: Has a learning curve and configuration can be somewhat complex.
6.4 pgvector
CREATE EXTENSION vector;
CREATE TABLE documents (
id SERIAL PRIMARY KEY,
content TEXT,
embedding vector(1536)
);
-- Similarity search
SELECT content, embedding <=> '[0.1, 0.2, ...]'::vector AS distance
FROM documents
ORDER BY distance
LIMIT 5;
- Type: PostgreSQL extension
- Best for: Organizations already using PostgreSQL, those not wanting to add separate infrastructure
- Pros: Leverages existing PostgreSQL infrastructure. SQL and vector search in a single database simplifies architecture. Supports HNSW and IVFFlat indexes.
- Limitations: Performance may degrade beyond 100 million vectors. Features are limited compared to dedicated Vector Databases.
6.5 Milvus
- Type: Open-source, distributed architecture
- Best for: Billion-scale vector systems, teams with data engineering capabilities
- Pros: Proven performance for industrial-scale large-scale vector search. Supports various index types (IVF, HNSW, DiskANN, etc.) with GPU acceleration. Managed service available via Zilliz Cloud.
- Limitations: High operational complexity. Cluster setup and management require specialized expertise.
6.6 Comparison Summary
| DB | Type | Max Scale | Hybrid Search | Recommended Scenario |
|---|---|---|---|---|
| Chroma | Embedded | ~1 million | Limited | Prototyping, development |
| Pinecone | Managed SaaS | Billions | Supported | Production, minimal management |
| Weaviate | Open-source/Cloud | Hundreds of millions | Strong support | Hybrid Search-centric |
| pgvector | PostgreSQL ext. | ~100 million | SQL combined | Existing PostgreSQL infra |
| Milvus | Open-source dist. | Billions | Supported | Large-scale systems |
7. Advanced RAG Patterns
Basic RAG (Naive RAG) is a simple "retrieve then generate" pipeline. In production environments, various Advanced RAG patterns are needed to improve both retrieval and generation quality.
7.1 Re-ranking
In basic RAG, initial retrieval uses Bi-Encoder vector similarity, which is fast but may lack precision. Re-ranking is a pattern that re-evaluates initial retrieval results with a Cross-Encoder to improve precision.
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import CrossEncoderReranker
from langchain_community.cross_encoders import HuggingFaceCrossEncoder
# Load Cross-Encoder model
model = HuggingFaceCrossEncoder(model_name="BAAI/bge-reranker-v2-m3")
compressor = CrossEncoderReranker(model=model, top_n=3)
# Configure Re-ranking Retriever
compression_retriever = ContextualCompressionRetriever(
base_compressor=compressor,
base_retriever=vectorstore.as_retriever(search_kwargs={"k": 20}),
)
The workflow is as follows.
- The Bi-Encoder quickly retrieves a broad set of candidate documents (top-20).
- The Cross-Encoder takes each candidate document paired with the query and scores direct relevance.
- Only the top re-ranked documents (top-3) are passed to the Generator.
Cross-Encoders are more precise than Bi-Encoders but require individual inference for every candidate, making them unsuitable for large-scale retrieval. Therefore, the standard pattern is a two-stage pipeline: narrow candidates with a Bi-Encoder, then re-rank with a Cross-Encoder.
7.2 HyDE (Hypothetical Document Embeddings)
HyDE, proposed by Gao et al. (2022), is a pattern designed to bridge the semantic gap between queries and documents. User questions are typically short and abstract, while documents containing the answers are long and detailed. This difference can make direct vector similarity search suboptimal.
The core idea of HyDE is as follows.
- When a user query is received, ask the LLM to "write a hypothetical document that answers this question."
- Embed the generated hypothetical document. This hypothetical document may not be factually accurate, but it has similar format and vocabulary to actual relevant documents.
- Use this embedding to search for actual documents.
from langchain.chains import HypotheticalDocumentEmbedder
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
llm = ChatOpenAI(model="gpt-4o-mini")
base_embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
hyde_embeddings = HypotheticalDocumentEmbedder.from_llm(
llm=llm,
base_embeddings=base_embeddings,
prompt_key="web_search",
)
# Search using HyDE embeddings
results = vectorstore.similarity_search_by_vector(
hyde_embeddings.embed_query("How to evaluate RAG system performance?")
)
The encoder's Dense Bottleneck serves to filter out hallucinations in the hypothetical document, so even if the hypothetical document is not accurate, it can effectively retrieve relevant real documents.
7.3 Self-RAG
Self-RAG, proposed by Asai et al. (2023), is a pattern where the LLM autonomously judges the need for retrieval and self-critically evaluates its generated results.
The core mechanism of Self-RAG is the Reflection Token.
- [Retrieve]: Determines whether external retrieval is needed at the current point.
- [IsRel]: Evaluates whether the retrieved document is relevant to the question.
- [IsSup]: Evaluates whether the generated response is supported by the retrieved document.
- [IsUse]: Evaluates whether the generated response is overall useful.
These Reflection Tokens are added to the model's vocabulary and trained like regular tokens, and the model generates them autonomously during inference. Self-RAG outperformed ChatGPT and retrieval-augmented Llama2-chat at the 7B and 13B parameter scales on Open-Domain QA, Reasoning, and Fact Verification tasks.
7.4 Corrective RAG (CRAG)
CRAG, proposed by Yan et al. (2024), is a pattern that actively evaluates and corrects the quality of retrieved documents.
The core components of CRAG are as follows.
- Retrieval Evaluator: A lightweight model judges the relevance of retrieved documents as Correct, Incorrect, or Ambiguous.
- Knowledge Refinement: Extracts key information from retrieved documents and removes unnecessary parts. Uses a Decompose-then-Recompose algorithm.
- Web Search Fallback: If the Retrieval Evaluator judges Incorrect, it falls back to web search instead of the static corpus to find better information.
[Query] → [Retriever] → [Retrieval Evaluator]
↓
Correct → Knowledge Refinement → Generator
Incorrect → Web Search → Knowledge Refinement → Generator
Ambiguous → Combine both paths → Generator
The strength of this pattern is that it automatically activates fallback paths even when retrieval quality is low, generating robust responses.
8. Evaluation Metrics: RAGAS Framework
The RAGAS (Retrieval Augmented Generation Assessment) framework is widely used for systematically evaluating RAG system performance. Proposed by Es et al. (2023), RAGAS provides automated metrics that can evaluate RAG pipelines even without Ground Truth.
8.1 Faithfulness
Measures how faithful the generated answer is to the retrieved Context. This is the key metric for detecting hallucination.
Faithfulness = (Number of Claims supported by Context) / (Total number of Claims)
The process works as follows.
- The LLM extracts individual Claims from the generated answer.
- It determines whether each Claim is supported by the provided Context.
- The proportion of supported Claims is calculated.
Values range from 0 to 1, with values closer to 1 meaning the answer is more faithful to the Context.
8.2 Answer Relevance
Measures how relevant the generated answer is to the original question.
The process works as follows.
- Questions are reverse-engineered from the generated answer.
- The Embedding similarity between the generated questions and the original question is computed.
- The average similarity becomes the Answer Relevance score.
This approach indirectly measures whether the answer addresses the core of the question and whether it contains unnecessary information.
8.3 Context Recall
Measures how much of the information needed to generate the Ground Truth answer is contained in the retrieved Context.
Context Recall = (Number of GT sentences supported by Context) / (Total number of GT sentences)
This is the only metric that requires Ground Truth. It directly evaluates the Retriever's performance.
8.4 Context Precision
Measures whether the actually relevant items among the retrieved Context are positioned at the top. The score is higher when relevant documents appear earlier in the search results.
8.5 RAGAS Usage Example
from ragas import evaluate
from ragas.metrics import (
faithfulness,
answer_relevancy,
context_recall,
context_precision,
)
from datasets import Dataset
# Compose evaluation dataset
eval_data = {
"question": ["What is RAG?"],
"answer": ["RAG is Retrieval-Augmented Generation, a method where the LLM retrieves external documents to generate answers."],
"contexts": [["RAG (Retrieval-Augmented Generation) is a technique that retrieves external knowledge to leverage in LLM generation."]],
"ground_truth": ["RAG stands for Retrieval-Augmented Generation, a methodology that augments LLM response generation by retrieving relevant information from external knowledge sources."],
}
dataset = Dataset.from_dict(eval_data)
# Run evaluation
result = evaluate(
dataset=dataset,
metrics=[faithfulness, answer_relevancy, context_recall, context_precision],
)
print(result)
In production environments, it is recommended to integrate these metrics into CI/CD pipelines to automatically monitor the impact of RAG system changes (chunking strategy modifications, model swaps, etc.) on quality.
9. Practical Implementation with LangChain + ChromaDB
Synthesizing all the concepts covered so far, here is a complete code implementation of a production RAG pipeline using LangChain and ChromaDB.
9.1 Environment Setup and Package Installation
pip install langchain langchain-openai langchain-chroma langchain-community
pip install chromadb pypdf sentence-transformers
9.2 Document Loading and Chunking
import os
from langchain_community.document_loaders import PyPDFLoader, DirectoryLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
# Load PDF documents
loader = DirectoryLoader(
"./documents",
glob="**/*.pdf",
loader_cls=PyPDFLoader,
)
documents = loader.load()
print(f"Number of documents loaded: {len(documents)}")
# Recursive Character Splitting
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
separators=["\n\n", "\n", ". ", " ", ""],
length_function=len,
)
splits = text_splitter.split_documents(documents)
print(f"Number of chunks generated: {len(splits)}")
9.3 Embedding and Vector Store Construction
from langchain_openai import OpenAIEmbeddings
from langchain_chroma import Chroma
# Embedding model setup
embeddings = OpenAIEmbeddings(
model="text-embedding-3-small",
openai_api_key=os.getenv("OPENAI_API_KEY"),
)
# Build and persist ChromaDB Vector Store
vectorstore = Chroma.from_documents(
documents=splits,
embedding=embeddings,
persist_directory="./chroma_db",
collection_name="rag_collection",
)
print(f"Documents stored in Vector Store: {vectorstore._collection.count()}")
9.4 Retriever Configuration
# Basic Retriever
retriever = vectorstore.as_retriever(
search_type="similarity",
search_kwargs={"k": 5},
)
# MMR (Maximal Marginal Relevance) Retriever - ensures diversity
retriever_mmr = vectorstore.as_retriever(
search_type="mmr",
search_kwargs={
"k": 5,
"fetch_k": 20, # Initial retrieval candidate count
"lambda_mult": 0.7, # Balance between relevance (1.0) and diversity (0.0)
},
)
9.5 RAG Chain Construction and Execution
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
# LLM setup
llm = ChatOpenAI(
model="gpt-4o-mini",
temperature=0,
openai_api_key=os.getenv("OPENAI_API_KEY"),
)
# Prompt Template
prompt = ChatPromptTemplate.from_template("""
Answer the question based on the following Context.
If the Context does not contain the information needed to answer, respond with "No relevant information was found in the provided documents."
Context:
{context}
Question: {question}
Answer:
""")
# Context formatting function
def format_docs(docs):
return "\n\n---\n\n".join(
f"[Source: {doc.metadata.get('source', 'unknown')}, "
f"Page: {doc.metadata.get('page', 'N/A')}]\n{doc.page_content}"
for doc in docs
)
# Construct RAG Chain with LCEL (LangChain Expression Language)
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
# Execute
question = "What are the types of chunking strategies in a RAG system and their pros and cons?"
answer = rag_chain.invoke(question)
print(answer)
9.6 Responses with Source Information
from langchain_core.runnables import RunnableParallel
# Chain that returns both source information and the answer
rag_chain_with_sources = RunnableParallel(
{"context": retriever, "question": RunnablePassthrough()}
).assign(
answer=lambda x: (
prompt.invoke(
{"context": format_docs(x["context"]), "question": x["question"]}
)
| llm
| StrOutputParser()
).invoke(x["question"])
)
# More concise approach
from langchain.chains import create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
# Stuff Documents Chain (combines retrieved documents into a single Context)
combine_docs_chain = create_stuff_documents_chain(llm, prompt)
# Retrieval Chain
retrieval_chain = create_retrieval_chain(retriever, combine_docs_chain)
# Execute - returns both context and answer
response = retrieval_chain.invoke({"input": question})
print("Answer:", response["answer"])
print("\nReference documents:")
for i, doc in enumerate(response["context"], 1):
print(f" [{i}] {doc.metadata.get('source', 'unknown')} "
f"(p.{doc.metadata.get('page', 'N/A')})")
9.7 Conversational RAG
from langchain_core.prompts import MessagesPlaceholder
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
# Prompt that considers conversation history
contextualize_prompt = ChatPromptTemplate.from_messages([
("system", "Reformulate the user's latest question so it can be understood independently, considering the previous conversation context."),
MessagesPlaceholder("chat_history"),
("human", "{input}"),
])
# Per-session history management
store = {}
def get_session_history(session_id: str):
if session_id not in store:
store[session_id] = ChatMessageHistory()
return store[session_id]
# Construct Conversational RAG Chain
conversational_rag = RunnableWithMessageHistory(
retrieval_chain,
get_session_history,
input_messages_key="input",
history_messages_key="chat_history",
output_messages_key="answer",
)
# Execute conversation
config = {"configurable": {"session_id": "user_001"}}
response1 = conversational_rag.invoke(
{"input": "What is RAG?"},
config=config,
)
print(response1["answer"])
response2 = conversational_rag.invoke(
{"input": "What are its main advantages?"}, # "its" = RAG from previous conversation
config=config,
)
print(response2["answer"])
This implementation is a basic RAG pipeline. Moving to production requires applying Advanced patterns such as Re-ranking and HyDE as covered above, systematic evaluation through RAGAS, and building monitoring and logging infrastructure.
10. Summary
RAG is the most practical and effective approach for addressing the hallucination problem in LLMs. The Retriever + Generator architecture proposed in the original paper by Lewis et al. (2020) has since evolved into various Advanced patterns, establishing itself as the core architecture for building production-level AI systems.
The key decisions for building an effective RAG system can be summarized as follows.
- Chunking Strategy: Start with RecursiveCharacterTextSplitter as the default, and consider Semantic Chunking based on data characteristics.
- Embedding Model: BGE-M3 for multilingual needs; OpenAI text-embedding-3 series for English-centric, reliable performance.
- Vector Database: Chroma for prototyping; choose a DB suited to the workload for production.
- Advanced Patterns: Re-ranking is worth applying in almost all cases; HyDE and CRAG are worth considering when retrieval quality is insufficient.
- Evaluation: Integrate RAGAS metrics into CI/CD to continuously monitor quality.
References
- Lewis, P., Perez, E., Piktus, A., Petroni, F., Karpukhin, V., Goyal, N., ... & Kiela, D. (2020). Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks. NeurIPS 2020. https://arxiv.org/abs/2005.11401
- Karpukhin, V., Oguz, B., Min, S., Lewis, P., Wu, L., Edunov, S., Chen, D., & Yih, W. (2020). Dense Passage Retrieval for Open-Domain Question Answering. EMNLP 2020. https://arxiv.org/abs/2004.04906
- Gao, L., Ma, X., Lin, J., & Callan, J. (2022). Precise Zero-Shot Dense Retrieval without Relevance Labels (HyDE). ACL 2023. https://arxiv.org/abs/2212.10496
- Asai, A., Wu, Z., Wang, Y., Sil, A., & Hajishirzi, H. (2023). Self-RAG: Learning to Retrieve, Generate, and Critique through Self-Reflection. ICLR 2024. https://arxiv.org/abs/2310.11511
- Yan, S., Gu, J., Zhu, Y., & Ling, Z. (2024). Corrective Retrieval Augmented Generation. https://arxiv.org/abs/2401.15884
- Es, S., James, J., Espinosa-Anke, L., & Schockaert, S. (2023). RAGAS: Automated Evaluation of Retrieval Augmented Generation. https://arxiv.org/abs/2309.15217
- LangChain Text Splitters Documentation. https://docs.langchain.com/oss/python/integrations/splitters
- RAGAS Documentation - Available Metrics. https://docs.ragas.io/en/stable/concepts/metrics/available_metrics/
- Pinecone - Chunking Strategies. https://www.pinecone.io/learn/chunking-strategies/
- MTEB: Massive Text Embedding Benchmark. https://huggingface.co/blog/mteb
Quiz
Q1: What is the main topic covered in "RAG: Retrieval-Augmented Generation — Paper Analysis and
Production Architecture"?
Analyzing the core concepts of the RAG paper and covering chunking strategies, Vector DB selection, and Advanced RAG patterns for designing production-level RAG systems.
Q2: What is The LLM Hallucination Problem and the Emergence of RAG?
Large Language Models (LLMs) are pre-trained on vast amounts of text data and demonstrate
remarkable performance in natural language understanding and generation. However, LLMs have a
fundamental limitation: the Hallucination problem.
Q3: Describe the Original RAG Paper Architecture: Retriever + Generator.
The paper "Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks" (NeurIPS 2020),
published by Patrick Lewis et al. at Facebook AI Research (now Meta AI) in 2020, is the seminal
work that first formalized the concept of RAG. 2.1 Core Proposal Lewis et al.
Q4: What are the key aspects of Dense Passage Retrieval (DPR) Principles?
The critical component in RAG's Retriever is Dense Passage Retrieval (DPR). It was proposed in the
paper "Dense Passage Retrieval for Open-Domain Question Answering" by Karpukhin et al. at EMNLP
2020.
Q5: How does Chunking Strategies work?
Splitting documents into appropriately sized chunks is a critical step in RAG systems that
directly impacts retrieval quality. LangChain provides various Text Splitters, and the key
chunking strategies are as follows.