Skip to content
Published on

RAG: Retrieval-Augmented Generation 논문 분석과 실전 아키텍처

Authors
  • Name
    Twitter

1. LLM의 Hallucination 문제와 RAG의 등장 배경

Large Language Model(LLM)은 방대한 텍스트 데이터로 사전 학습되어 자연어 이해와 생성에서 놀라운 성능을 보여준다. 그러나 LLM에는 근본적인 한계가 존재한다. 바로 Hallucination(환각) 문제다.

Hallucination이란 모델이 사실이 아닌 정보를 마치 사실인 것처럼 자신 있게 생성하는 현상을 말한다. 이 문제가 발생하는 근본 원인은 다음과 같다.

  • 지식의 정적 특성: LLM의 파라미터에 인코딩된 지식은 학습 시점에 고정된다. 학습 이후에 발생한 사건이나 업데이트된 정보를 반영하지 못한다.
  • 파라미터 메모리의 불완전성: 수십억 개의 파라미터가 있더라도, 세상의 모든 세부 사실을 정확하게 저장하고 재현하는 것은 불가능하다.
  • 확률적 생성 방식: LLM은 다음 토큰을 확률적으로 예측하여 생성하기 때문에, 통계적으로 그럴듯하지만 사실과 다른 텍스트를 만들어낼 수 있다.
  • 출처 추적 불가: 생성된 답변이 어떤 학습 데이터에서 비롯되었는지 추적할 수 없어, 검증 자체가 어렵다.

이러한 한계를 극복하기 위해 등장한 접근법이 **Retrieval-Augmented Generation(RAG)**이다. RAG의 핵심 아이디어는 단순하면서도 강력하다. LLM이 답변을 생성하기 전에, 외부 지식 저장소에서 관련 문서를 검색(Retrieve)하여 그 정보를 기반으로 답변을 생성(Generate)하는 것이다.

이를 통해 다음과 같은 이점을 얻을 수 있다.

  1. 사실 기반 응답: 검색된 실제 문서를 근거로 답변을 생성하므로 Hallucination이 줄어든다.
  2. 지식 업데이트 용이: 외부 데이터베이스만 업데이트하면 최신 정보를 반영할 수 있다. 모델 재학습이 필요 없다.
  3. 출처 제공 가능: 답변의 근거가 된 문서를 함께 제시하여 투명성과 신뢰성을 높일 수 있다.
  4. 도메인 특화 용이: 특정 도메인의 문서만 인덱싱하면 해당 분야에 특화된 시스템을 빠르게 구축할 수 있다.

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)

동작 원리는 다음과 같다.

  1. 먼저 \n\n(단락 구분)으로 분할을 시도한다.
  2. Chunk가 여전히 chunk_size를 초과하면 \n(줄바꿈)으로 분할한다.
  3. 그래도 초과하면 . (문장 단위)으로 분할한다.
  4. 최후의 수단으로 공백이나 문자 단위로 분할한다.

이 방식의 핵심은 큰 의미 단위를 먼저 보존하려고 시도하고, 필요한 경우에만 더 작은 단위로 내려간다는 점이다.

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 사이에서, chunk_overlap을 chunk_size의 1020% 정도로 설정하는 것이 일반적이다. 최적값은 데이터 특성과 사용 사례에 따라 실험적으로 결정해야 한다.


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만제한적프로토타이핑, 개발
PineconeManaged SaaS수십억지원프로덕션, 관리 최소화
Weaviate오픈소스/클라우드수억강력 지원Hybrid Search 중심
pgvectorPostgreSQL 확장~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}),
)

동작 흐름은 다음과 같다.

  1. Bi-Encoder로 넓은 범위의 후보 문서(top-20)를 빠르게 검색한다.
  2. Cross-Encoder가 쿼리와 각 후보 문서를 쌍으로 입력받아 직접적인 관련성 점수를 매긴다.
  3. 재정렬된 상위 문서(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의 핵심 아이디어는 다음과 같다.

  1. 사용자 쿼리를 받으면, LLM에게 "이 질문에 답하는 가상의 문서를 작성해 달라"고 요청한다.
  2. 생성된 가상 문서(Hypothetical Document)를 Embedding한다. 이 가상 문서는 사실과 다를 수 있지만, 실제 관련 문서와 유사한 형식과 어휘를 가진다.
  3. 이 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의 핵심 컴포넌트는 다음과 같다.

  1. Retrieval Evaluator: 경량 모델이 검색된 문서의 관련성을 Correct, Incorrect, Ambiguous 세 가지로 판단한다.
  2. Knowledge Refinement: 검색된 문서에서 핵심 정보만 추출하고 불필요한 부분을 제거한다. Decompose-then-Recompose 알고리즘을 사용한다.
  3. Web Search Fallback: Retrieval Evaluator가 Incorrect로 판단하면, 정적 코퍼스 대신 웹 검색으로 대체하여 더 나은 정보를 찾는다.
[Query][Retriever][Retrieval Evaluator]
              CorrectKnowledge RefinementGenerator
              IncorrectWeb SearchKnowledge RefinementGenerator
              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)

동작 방식은 다음과 같다.

  1. LLM이 생성된 답변에서 개별 Claim(주장)을 추출한다.
  2. 각 Claim이 제공된 Context에 의해 뒷받침(Support)되는지 판단한다.
  3. 뒷받침되는 Claim의 비율을 계산한다.

값은 0~1 사이이며, 1에 가까울수록 답변이 Context에 충실하다는 의미이다.

8.2 Answer Relevance (답변 관련성)

생성된 답변이 원래 질문에 얼마나 관련 있는지 측정한다.

동작 방식은 다음과 같다.

  1. 생성된 답변으로부터 역으로 질문을 생성(Reverse Engineering)한다.
  2. 생성된 질문들과 원래 질문의 Embedding 유사도를 계산한다.
  3. 평균 유사도가 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 시스템을 구축하기 위한 핵심 결정 사항을 정리하면 다음과 같다.

  1. Chunking 전략: RecursiveCharacterTextSplitter를 기본으로 시작하되, 데이터 특성에 따라 Semantic Chunking을 고려한다.
  2. Embedding 모델: 다국어가 필요하면 BGE-M3, 영어 중심이면 OpenAI text-embedding-3 시리즈가 안정적이다.
  3. Vector Database: 프로토타이핑에는 Chroma, 프로덕션에는 워크로드에 맞는 DB를 선택한다.
  4. Advanced 패턴: Re-ranking은 거의 모든 경우에 적용할 가치가 있으며, HyDE와 CRAG는 검색 품질이 부족할 때 고려한다.
  5. 평가: RAGAS 메트릭을 CI/CD에 통합하여 지속적으로 품질을 모니터링한다.

References