RAG(Retrieval-Augmented Generation) 완전 가이드 - 아키텍처부터 실전 구축까지
RAG의 핵심 아키텍처, 청킹 전략, 벡터 DB, 검색 기법, 고급 패턴(Self-RAG, Graph RAG, Agentic RAG), 평가 방법, 엔터프라이즈 구축 노하우를 체계적으로 정리합니다.
RAG(Retrieval-Augmented Generation)는 LLM의 한계를 극복하기 위해 외부 지식을 검색하여 응답에 활용하는 기법입니다. 이 글에서는 RAG의 기본 개념부터 고급 기법, 엔터프라이즈 구축 실전까지 체계적으로 다룹니다.
1. RAG란 무엇인가
정의와 등장 배경
RAG(Retrieval-Augmented Generation)는 2020년 Meta(Facebook) AI Research의 Patrick Lewis 등이 제안한 기법으로, 정보 검색(Retrieval)과 텍스트 생성(Generation)을 결합한 아키텍처입니다.
핵심 아이디어는 단순합니다. LLM이 답변을 생성하기 전에, 먼저 관련 문서를 검색하여 컨텍스트로 제공하면 더 정확하고 최신의 답변을 생성할 수 있다는 것입니다.
[기존 LLM]
사용자 질문 → LLM → 응답 (학습 데이터에만 의존)
[RAG]
사용자 질문 → 관련 문서 검색 → 검색 결과 + 질문 → LLM → 응답 (외부 지식 기반)
LLM 단독 사용의 한계와 RAG의 필요성
LLM을 단독으로 사용할 때 다음과 같은 근본적인 한계가 있습니다.
| 한계 | 설명 | RAG의 해결 방식 |
|---|---|---|
| 지식 단절 (Knowledge Cutoff) | 학습 시점 이후 정보를 알 수 없음 | 최신 문서를 실시간 검색하여 제공 |
| 할루시네이션 (Hallucination) | 사실이 아닌 정보를 자신 있게 생성 | 검색된 문서를 근거로 응답, 출처 명시 |
| 도메인 지식 부재 | 기업 내부 데이터, 전문 분야 지식 부족 | 사내 문서를 벡터 DB에 저장하여 검색 |
| 비용 | 미세 조정(Fine-tuning)에 높은 비용 | 문서 업데이트만으로 지식 갱신 가능 |
참고: RAG는 Fine-tuning의 대안이 아닌 보완적 기법입니다. Fine-tuning은 모델의 행동 방식(스타일, 형식)을 변경하는 데 적합하고, RAG는 최신 사실 정보를 제공하는 데 적합합니다. 두 기법을 함께 사용하면 최적의 결과를 얻을 수 있습니다.
2. RAG 아키텍처 상세
전체 파이프라인 구조
RAG 시스템은 크게 오프라인 인덱싱 파이프라인과 온라인 쿼리 파이프라인으로 구성됩니다.
[오프라인: 인덱싱 파이프라인]
원본 문서 → 문서 로딩 → 청킹(Chunking) → 임베딩 → 벡터 DB 저장
[온라인: 쿼리 파이프라인]
사용자 질의 → 질의 임베딩 → 벡터 검색 → 리랭킹 → 컨텍스트 구성 → LLM 생성 → 응답
각 단계별 역할:
| 단계 | 역할 | 주요 도구 |
|---|---|---|
| 문서 로딩 | 다양한 형식의 문서를 텍스트로 변환 | LangChain Loaders, Unstructured |
| 청킹 | 문서를 적절한 크기의 조각으로 분할 | RecursiveCharacterTextSplitter |
| 임베딩 | 텍스트를 고차원 벡터로 변환 | OpenAI, Cohere, BGE, E5 |
| 벡터 저장 | 임베딩 벡터를 인덱싱하여 저장 | Chroma, Pinecone, Milvus |
| 검색 | 유사한 문서 청크를 빠르게 검색 | ANN (Approximate Nearest Neighbor) |
| 리랭킹 | 검색 결과를 관련도 순으로 재정렬 | Cross-Encoder, Cohere Rerank |
| 생성 | 검색 결과를 바탕으로 응답 생성 | GPT-4, Claude, LLaMA |
Naive RAG vs Advanced RAG vs Modular RAG
RAG는 발전 과정에 따라 세 가지 세대로 구분됩니다.
Naive RAG (1세대)
가장 기본적인 RAG 구현으로, 단순한 검색-생성 파이프라인입니다.
질의 → 임베딩 → Top-K 검색 → 프롬프트에 삽입 → LLM 생성
- 장점: 구현이 간단
- 한계: 검색 품질에 전적으로 의존, 노이즈 문서 포함 가능
Advanced RAG (2세대)
검색 전/후에 최적화 단계를 추가한 개선된 파이프라인입니다.
질의 → [질의 변환] → 임베딩 → [하이브리드 검색] → [리랭킹] → [컨텍스트 압축] → LLM 생성
- 질의 변환: 질의를 재구성하여 검색 품질 향상
- 하이브리드 검색: 벡터 + 키워드 검색 결합
- 리랭킹: 검색 결과의 관련도 재평가
- 컨텍스트 압축: 불필요한 정보 제거
Modular RAG (3세대)
각 구성 요소를 독립 모듈로 분리하여 유연하게 조합하는 아키텍처입니다.
질의 분석 → 라우팅 → [검색 모듈 선택] → [후처리 모듈 조합] → 생성 → 검증
├─ 벡터 검색
├─ 그래프 검색
├─ SQL 검색
└─ 웹 검색
- 라우팅: 질의 유형에 따라 적절한 검색 전략 선택
- 모듈 교체: 각 단계의 컴포넌트를 독립적으로 교체 가능
- 피드백 루프: 생성 결과를 평가하여 검색 재시도
3. 데이터 준비: 문서 로딩과 청킹
문서 로딩
RAG의 첫 단계는 다양한 형식의 원본 문서를 텍스트로 변환하는 것입니다.
| 문서 형식 | 로더 | 특이사항 |
|---|---|---|
| PyPDFLoader, Unstructured | 표, 이미지 내 텍스트 추출 주의 | |
| HTML | BeautifulSoupLoader | 태그 제거, 본문 추출 |
| Markdown | MarkdownLoader | 헤딩 기반 구조 보존 |
| Word/PPT | Unstructured | 서식 정보 활용 가능 |
| DB (SQL) | SQLDatabaseLoader | 쿼리 결과를 문서화 |
| Confluence/Notion | 전용 API Loader | 페이지 계층 구조 반영 |
# 다양한 문서 로딩 예시
from langchain_community.document_loaders import (
PyPDFLoader,
WebBaseLoader,
UnstructuredMarkdownLoader,
CSVLoader
)
# PDF 로딩
pdf_loader = PyPDFLoader("report.pdf")
pdf_docs = pdf_loader.load()
# 웹 페이지 로딩
web_loader = WebBaseLoader("https://docs.example.com/guide")
web_docs = web_loader.load()
# 마크다운 로딩
md_loader = UnstructuredMarkdownLoader("README.md")
md_docs = md_loader.load()청킹 전략
청킹(Chunking)은 RAG 성능에 가장 큰 영향을 미치는 단계 중 하나입니다. 청크가 너무 크면 노이즈가 포함되고, 너무 작으면 문맥이 손실됩니다.
주요 청킹 전략:
| 전략 | 설명 | 적합 대상 |
|---|---|---|
| 고정 크기 (Fixed Size) | 일정 문자/토큰 수로 분할 | 구조가 없는 텍스트 |
| 재귀적 분할 (Recursive) | 단락 → 문장 → 단어 순서로 분할 시도 | 범용 (가장 널리 사용) |
| 의미 기반 (Semantic) | 임베딩 유사도 변화 지점에서 분할 | 주제 전환이 잦은 문서 |
| 문서 구조 기반 | 헤딩, 섹션 등 구조를 활용 | 기술 문서, 매뉴얼 |
| Agentic Chunking | LLM이 직접 청킹 수행 | 복잡한 문서 |
from langchain.text_splitter import RecursiveCharacterTextSplitter
# 재귀적 분할 (가장 많이 사용)
splitter = RecursiveCharacterTextSplitter(
chunk_size=500, # 청크 최대 크기 (문자 수)
chunk_overlap=50, # 청크 간 겹침 (문맥 유지)
separators=["\n\n", "\n", ". ", " ", ""], # 분할 우선순위
length_function=len
)
chunks = splitter.split_documents(documents)from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai import OpenAIEmbeddings
# 의미 기반 분할
semantic_splitter = SemanticChunker(
OpenAIEmbeddings(),
breakpoint_threshold_type="percentile",
breakpoint_threshold_amount=95 # 유사도 변화 상위 5%에서 분할
)
semantic_chunks = semantic_splitter.split_documents(documents)참고: 청크 크기의 일반적인 가이드라인은 200
1000 토큰입니다. 작은 청크(200400)는 정밀한 검색에, 큰 청크(6001000)는 풍부한 문맥 제공에 유리합니다. overlap은 chunk_size의 1020%가 적절합니다.
메타데이터 관리
청크에 메타데이터를 첨부하면 검색 시 필터링과 가중치 조정에 활용할 수 있습니다.
# 메타데이터가 포함된 청크 예시
{
"content": "분기별 매출은 전년 대비 15% 증가했습니다...",
"metadata": {
"source": "2025_Q4_report.pdf",
"page": 12,
"department": "finance",
"date": "2025-12-31",
"doc_type": "quarterly_report",
"access_level": "internal"
}
}활용 사례:
- 날짜 필터: "2025년 4분기 매출" →
date필드로 범위 검색 - 부서 필터: "마케팅팀 관련 문서" →
department == "marketing"필터 - 접근 제어: 사용자 권한에 따라
access_level필터링
4. 임베딩과 벡터 데이터베이스
임베딩 모델 선택
임베딩 모델은 텍스트를 고차원 벡터로 변환하여 의미적 유사도를 계산할 수 있게 합니다. 모델 선택은 RAG 성능에 직접적인 영향을 미칩니다.
| 모델 | 차원 | 다국어 | 특징 |
|---|---|---|---|
| OpenAI text-embedding-3-large | 3072 | O | 높은 성능, API 비용 발생 |
| OpenAI text-embedding-3-small | 1536 | O | 비용 대비 우수한 성능 |
| Cohere embed-v3 | 1024 | O | 검색 특화, 다국어 강점 |
| BGE-M3 (BAAI) | 1024 | O | 오픈소스 최고 수준, 다국어 |
| E5-Mistral-7B | 4096 | O | 오픈소스, 긴 문서에 강점 |
| multilingual-e5-large | 1024 | O | 다국어 특화, 경량 |
# OpenAI 임베딩
from langchain_openai import OpenAIEmbeddings
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vector = embeddings.embed_query("RAG는 검색과 생성을 결합한 기법입니다")
# 결과: 1536차원 벡터
# 오픈소스 임베딩 (Hugging Face)
from langchain_huggingface import HuggingFaceEmbeddings
embeddings = HuggingFaceEmbeddings(
model_name="BAAI/bge-m3",
model_kwargs={"device": "cuda"},
encode_kwargs={"normalize_embeddings": True}
)참고: 한국어 RAG를 구축할 때는 다국어 모델(BGE-M3, multilingual-e5 등) 사용을 권장합니다. 영어 전용 모델은 한국어 의미를 정확히 포착하지 못할 수 있습니다.
주요 벡터 DB 비교
| 벡터 DB | 유형 | 확장성 | 하이브리드 검색 | 메타데이터 필터 | 적합 환경 |
|---|---|---|---|---|---|
| Chroma | 임베디드 | 소규모 | X | O | 프로토타입, PoC |
| Pinecone | 관리형 SaaS | 대규모 | O | O | 운영 부담 최소화 |
| Weaviate | 자체 호스팅/클라우드 | 대규모 | O | O | 하이브리드 검색 중심 |
| Milvus | 자체 호스팅 | 초대규모 | O | O | 엔터프라이즈 대용량 |
| Qdrant | 자체 호스팅/클라우드 | 대규모 | O | O | 고성능 필터링 |
| pgvector | PostgreSQL 확장 | 중규모 | X | O (SQL) | 기존 PostgreSQL 활용 |
# Chroma를 이용한 벡터 저장 및 검색
from langchain_chroma import Chroma
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
collection_name="company_docs",
persist_directory="./chroma_db"
)
# 유사도 검색
results = vectorstore.similarity_search_with_score(
query="데이터 보존 정책은?",
k=5
)
for doc, score in results:
print(f"[점수: {score:.4f}] {doc.page_content[:100]}...")인덱싱 전략
벡터 검색의 속도와 정확도는 인덱싱 알고리즘에 크게 의존합니다.
| 알고리즘 | 원리 | 속도 | 정확도 | 메모리 |
|---|---|---|---|---|
| Flat (Brute Force) | 모든 벡터와 비교 | 느림 | 100% | 낮음 |
| HNSW | 계층적 그래프 탐색 | 빠름 | 매우 높음 | 높음 |
| IVF (Inverted File) | 클러스터 기반 검색 | 빠름 | 높음 | 중간 |
| PQ (Product Quantization) | 벡터 압축 후 검색 | 매우 빠름 | 중간 | 매우 낮음 |
| HNSW + PQ | HNSW와 PQ 결합 | 빠름 | 높음 | 중간 |
- 소규모 (< 10만 벡터): Flat 또는 HNSW
- 중규모 (10만 ~ 1,000만): HNSW
- 대규모 (> 1,000만): IVF + PQ 또는 HNSW + PQ
5. 검색 전략
벡터 유사도 검색
벡터 검색은 질의 벡터와 저장된 문서 벡터 간의 유사도를 계산하여 가장 관련성 높은 문서를 찾습니다.
주요 유사도 메트릭:
| 메트릭 | 수식 | 특징 |
|---|---|---|
| Cosine Similarity | cos(θ) = (A·B) / (‖A‖·‖B‖) | 방향 기반, 가장 널리 사용 |
| Euclidean Distance (L2) | d = √Σ(a_i - b_i)² | 거리 기반, 정규화 필요 |
| Inner Product (Dot Product) | s = Σ(a_i × b_i) | 빠른 계산, 정규화된 벡터 시 Cosine과 동일 |
참고: 대부분의 임베딩 모델은 정규화된 벡터를 출력하므로, Cosine Similarity와 Inner Product의 결과가 동일합니다. 성능이 중요한 경우 연산이 간단한 Inner Product를 사용하세요.
키워드 검색 (BM25)
BM25는 전통적인 정보 검색 알고리즘으로, 단어의 빈도(TF)와 희소성(IDF)을 고려합니다.
from langchain_community.retrievers import BM25Retriever
# BM25 검색기 생성
bm25_retriever = BM25Retriever.from_documents(
documents=chunks,
k=5
)
# 키워드 검색
results = bm25_retriever.invoke("NiFi LDAP 인증 설정 방법")벡터 검색 vs BM25 비교:
| 상황 | 벡터 검색 | BM25 |
|---|---|---|
| "NiFi 인증 설정 방법" | 의미적으로 유사한 문서 검색 (좋음) | "NiFi", "인증" 키워드 매칭 (좋음) |
| "보안 접근 제어" | "인증", "권한 관리" 등 유사 개념 문서도 검색 (좋음) | 정확한 키워드가 없으면 누락 (약함) |
| "error code 0x8007" | 의미 유사도 낮아 부정확 (약함) | 정확한 코드 매칭 (좋음) |
하이브리드 검색
하이브리드 검색은 벡터 검색과 키워드 검색의 장점을 결합합니다. 두 검색 결과를 결합하기 위해 Reciprocal Rank Fusion (RRF) 또는 가중 점수 방식을 사용합니다.
from langchain.retrievers import EnsembleRetriever
# 벡터 검색기
vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 5})
# BM25 검색기
bm25_retriever = BM25Retriever.from_documents(chunks, k=5)
# 하이브리드 검색 (앙상블)
hybrid_retriever = EnsembleRetriever(
retrievers=[vector_retriever, bm25_retriever],
weights=[0.6, 0.4] # 벡터 검색 60%, BM25 40%
)
results = hybrid_retriever.invoke("NiFi 클러스터 TLS 인증서 설정")RRF (Reciprocal Rank Fusion) 공식:
RRF_score(d) = Σ 1 / (k + rank_i(d))
- d: 문서
- k: 상수 (보통 60)
- rank_i(d): i번째 검색기에서의 순위
리랭킹
검색된 문서를 더 정밀하게 관련도 순으로 재정렬하는 단계입니다. 초기 검색(bi-encoder)이 속도를 위해 정확도를 일부 희생한다면, 리랭킹(cross-encoder)은 소수의 후보에 대해 정밀한 관련도를 평가합니다.
from langchain.retrievers import ContextualCompressionRetriever
from langchain_cohere import CohereRerank
# Cohere Reranker
reranker = CohereRerank(
model="rerank-v3.5",
top_n=3 # 상위 3개만 반환
)
# 리랭킹 적용 검색기
compression_retriever = ContextualCompressionRetriever(
base_compressor=reranker,
base_retriever=hybrid_retriever # 하이브리드 검색 결과를 리랭킹
)
results = compression_retriever.invoke("Kudu 파티셔닝 전략")리랭킹의 효과:
| 단계 | 처리 대상 | 속도 | 정확도 |
|---|---|---|---|
| 초기 검색 (Bi-Encoder) | 전체 문서 (수만~수백만) | 빠름 | 중간 |
| 리랭킹 (Cross-Encoder) | 후보 문서 (10~50개) | 느림 | 높음 |
6. 프롬프트 구성과 응답 생성
컨텍스트 주입 방식
검색된 문서를 LLM 프롬프트에 삽입하는 방법입니다.
Stuff 방식 (가장 기본): 모든 검색 결과를 하나의 프롬프트에 삽입
컨텍스트:
[문서 1 내용]
[문서 2 내용]
[문서 3 내용]
위 컨텍스트를 바탕으로 다음 질문에 답하세요:
{질문}
Map-Reduce 방식: 각 문서에 대해 개별 요약 후 통합
[문서 1] → LLM → 요약 1 ─┐
[문서 2] → LLM → 요약 2 ─┼→ 통합 LLM → 최종 응답
[문서 3] → LLM → 요약 3 ─┘
Map-Rerank 방식: 각 문서로 응답 생성 후 점수 기반 선택
[문서 1] → LLM → 응답 1 (점수: 0.9) ← 선택
[문서 2] → LLM → 응답 2 (점수: 0.3)
[문서 3] → LLM → 응답 3 (점수: 0.7)
프롬프트 템플릿 설계
효과적인 RAG 프롬프트 설계를 위한 원칙:
from langchain_core.prompts import ChatPromptTemplate
rag_prompt = ChatPromptTemplate.from_template("""
당신은 Data Dynamics의 기술 지원 전문가입니다.
아래 제공된 컨텍스트 정보만을 사용하여 질문에 답변하세요.
## 규칙
1. 컨텍스트에 없는 정보는 "제공된 문서에서 해당 정보를 찾을 수 없습니다"라고 답하세요.
2. 답변에 사용한 문서의 출처를 명시하세요.
3. 기술적인 내용은 코드 예시와 함께 설명하세요.
## 컨텍스트
{context}
## 질문
{question}
## 답변
""")주요 설계 원칙:
- 역할 명시: 모델의 전문 분야 지정
- 컨텍스트 기반 응답 강제: 할루시네이션 방지
- 모르면 모른다고 답하도록 지시: 정직한 응답 유도
- 출처 인용 요구: 응답의 신뢰도 향상
출처 인용(Citation) 처리
RAG 응답에 출처를 포함하면 사용자가 정보를 검증할 수 있습니다.
# 출처가 포함된 RAG 체인 구현
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
def format_docs_with_sources(docs):
formatted = []
for i, doc in enumerate(docs):
source = doc.metadata.get("source", "Unknown")
page = doc.metadata.get("page", "")
ref = f"[{i+1}] {source}" + (f" (p.{page})" if page else "")
formatted.append(f"{ref}\n{doc.page_content}")
return "\n\n---\n\n".join(formatted)
rag_chain = (
{"context": retriever | format_docs_with_sources,
"question": RunnablePassthrough()}
| rag_prompt
| llm
| StrOutputParser()
)
answer = rag_chain.invoke("Kudu의 파티셔닝 제한사항은?")7. 고급 RAG 기법
Query Transformation
질의를 변환하여 검색 품질을 향상시키는 기법입니다.
HyDE (Hypothetical Document Embedding)
질의에 대한 가상의 답변을 생성하고, 그 답변의 임베딩으로 검색하는 방식입니다.
원래 질의: "Kudu의 파티셔닝 방식은?"
↓ LLM이 가상 답변 생성
가상 답변: "Apache Kudu는 Hash Partitioning과 Range Partitioning을
지원합니다. Hash Partitioning은 데이터를 균등 분배하고..."
↓ 가상 답변을 임베딩하여 검색
검색 결과: 실제 Kudu 파티셔닝 관련 문서
Multi-Query
하나의 질의를 여러 관점으로 재작성하여 검색 커버리지를 확대합니다.
from langchain.retrievers.multi_query import MultiQueryRetriever
multi_retriever = MultiQueryRetriever.from_llm(
retriever=vectorstore.as_retriever(),
llm=llm
)
# 원래 질의: "Kudu 성능 최적화 방법"
# 자동 생성되는 쿼리들:
# 1. "Apache Kudu 테이블 스캔 성능을 높이는 방법"
# 2. "Kudu 파티셔닝으로 쿼리 속도를 개선하는 전략"
# 3. "Kudu 클러스터 하드웨어 및 설정 튜닝 가이드"
results = multi_retriever.invoke("Kudu 성능 최적화 방법")Step-back Prompting
구체적인 질의를 더 일반적인 질의로 변환하여 배경 지식을 먼저 검색합니다.
원래 질의: "NiFi 1.23에서 LDAP 인증 시 TLS 에러가 발생합니다"
↓ Step-back
변환 질의: "Apache NiFi의 LDAP 인증 및 TLS 설정 구조는?"
↓ 배경 지식 검색 후 원래 질의와 결합
Self-RAG / Corrective RAG (CRAG)
Self-RAG
LLM이 스스로 검색 필요 여부를 판단하고, 검색된 문서의 관련성을 평가하며, 생성한 응답을 검증하는 기법입니다.
질의 수신
↓
[검색 필요한가?] → No → 직접 응답
↓ Yes
[문서 검색]
↓
[검색된 문서가 관련 있는가?] → No → 다른 전략으로 재검색
↓ Yes
[응답 생성]
↓
[응답이 문서에 근거하는가?] → No → 재생성
↓ Yes
[응답이 질문에 유용한가?] → No → 재생성
↓ Yes
최종 응답 출력
Corrective RAG (CRAG)
검색 결과의 품질을 평가하여, 부정확하면 웹 검색 등 대체 소스로 보완합니다.
[문서 검색] → [관련성 평가]
├─ Correct → 지식 정제 → 생성
├─ Ambiguous → 웹 검색으로 보완 → 생성
└─ Incorrect → 웹 검색으로 대체 → 생성
Graph RAG
지식 그래프를 활용하여 엔티티 간의 관계를 기반으로 검색하는 기법입니다. 단순 텍스트 유사도로는 포착하기 어려운 관계 기반 질의에 효과적입니다.
[일반 RAG]
"이 프로젝트의 데이터 아키텍트는 누구인가?" → 텍스트 유사도 검색 → 정확한 결과 어려움
[Graph RAG]
"이 프로젝트의 데이터 아키텍트는 누구인가?"
→ 지식 그래프에서 "프로젝트" 노드 탐색
→ "역할: 데이터 아키텍트" 관계 추적
→ 연결된 "사람" 노드 반환
활용 사례:
- 조직 구조 기반 질의 ("A 부서의 팀장은?")
- 인과 관계 추적 ("이 장애의 근본 원인은?")
- 다단계 추론 ("A 제품을 구매한 고객이 함께 구매한 제품은?")
Agentic RAG
LLM 에이전트가 도구(Tool)로서 검색을 수행하며, 복잡한 질의에 대해 계획-실행-반성 루프를 통해 답변을 구축합니다.
[사용자 질의: "Q3 매출이 Q2보다 얼마나 증가했고, 주요 원인은?"]
에이전트 계획:
1. Q2 매출 데이터 검색 → tool: vector_search("Q2 매출")
2. Q3 매출 데이터 검색 → tool: vector_search("Q3 매출")
3. 매출 증감 비교 → tool: calculator(Q3 - Q2)
4. 증가 원인 분석 → tool: vector_search("Q3 매출 증가 원인")
5. 종합 보고서 생성
에이전트 실행: 각 단계 수행 → 중간 결과 반성 → 필요 시 재검색
from langchain.agents import create_tool_calling_agent, AgentExecutor
from langchain.tools.retriever import create_retriever_tool
# 검색 도구 생성
search_tool = create_retriever_tool(
retriever=retriever,
name="company_docs_search",
description="회사 내부 문서에서 정보를 검색합니다. 정책, 기술 가이드, 보고서 등을 찾을 때 사용하세요."
)
# 에이전트 생성
agent = create_tool_calling_agent(llm, [search_tool], prompt)
agent_executor = AgentExecutor(agent=agent, tools=[search_tool], verbose=True)
result = agent_executor.invoke({
"input": "Q3 매출이 Q2 대비 얼마나 증가했고, 주요 원인은?"
})8. RAG 평가와 최적화
평가 지표
RAG 시스템의 품질은 검색 품질과 생성 품질 두 축으로 평가합니다.
검색 품질 지표:
| 지표 | 설명 |
|---|---|
| Recall@K | 상위 K개 검색 결과에 정답 문서가 포함된 비율 |
| Precision@K | 상위 K개 검색 결과 중 관련 문서의 비율 |
| MRR (Mean Reciprocal Rank) | 첫 번째 정답 문서의 순위 역수 평균 |
| NDCG | 순위를 고려한 관련도 점수 |
생성 품질 지표:
| 지표 | 설명 |
|---|---|
| Faithfulness | 응답이 검색된 문서에 근거하는 정도 |
| Answer Relevancy | 응답이 질문에 적절한 정도 |
| Context Relevancy | 검색된 컨텍스트가 질문에 관련된 정도 |
| Context Utilization | 검색된 컨텍스트를 실제로 활용한 정도 |
RAGAS 프레임워크
RAGAS(Retrieval Augmented Generation Assessment)는 RAG 시스템을 자동 평가하는 오픈소스 프레임워크입니다.
from ragas import evaluate
from ragas.metrics import (
faithfulness,
answer_relevancy,
context_precision,
context_recall
)
from datasets import Dataset
# 평가 데이터 구성
eval_data = {
"question": ["Kudu의 파티셔닝 방식은?"],
"answer": ["Kudu는 Hash와 Range 파티셔닝을 지원합니다..."],
"contexts": [["Kudu는 Hash Partitioning과 Range Partitioning..."]],
"ground_truth": ["Kudu는 Hash, Range 파티셔닝을 지원하며..."]
}
dataset = Dataset.from_dict(eval_data)
# RAGAS 평가 실행
results = evaluate(
dataset,
metrics=[faithfulness, answer_relevancy, context_precision, context_recall]
)
print(results)
# {'faithfulness': 0.92, 'answer_relevancy': 0.88,
# 'context_precision': 0.85, 'context_recall': 0.90}성능 튜닝 포인트
RAG 성능이 기대에 미치지 못할 때 점검할 핵심 포인트:
| 문제 | 증상 | 튜닝 방향 |
|---|---|---|
| 검색 누락 | 관련 문서를 못 찾음 | 청킹 크기 조정, 하이브리드 검색 적용, K값 증가 |
| 노이즈 문서 | 무관한 문서가 포함됨 | 리랭킹 추가, 메타데이터 필터링, 임계값 설정 |
| 할루시네이션 | 검색 결과와 무관한 응답 | 프롬프트 개선, Temperature 낮추기, 출처 인용 강제 |
| 불완전한 응답 | 일부 정보만 포함 | 청크 크기 증가, Multi-Query 적용, K값 증가 |
| 느린 응답 | 지연시간이 김 | 인덱스 최적화, 캐싱, 스트리밍 적용 |
| 임베딩 품질 | 의미 유사도 부정확 | 임베딩 모델 변경, 도메인 미세 조정 |
튜닝 우선순위 (권장):
- 청킹 전략 최적화 (영향도 최대)
- 하이브리드 검색 적용
- 리랭킹 추가
- 임베딩 모델 변경
- 프롬프트 개선
- Query Transformation 적용
9. 엔터프라이즈 RAG 구축 실전
보안과 접근 제어
기업 환경에서는 문서별 접근 권한을 RAG 시스템에 반영해야 합니다.
# 접근 제어가 적용된 검색 예시
def secure_search(query: str, user_role: str, department: str):
# 사용자 권한에 따른 메타데이터 필터 구성
filter_conditions = {
"access_level": {"$in": get_allowed_levels(user_role)},
"department": {"$in": get_allowed_departments(user_role, department)}
}
results = vectorstore.similarity_search(
query=query,
k=5,
filter=filter_conditions
)
return results
# 일반 직원: 공개 문서만 검색
results = secure_search("인사 정책", user_role="employee", department="engineering")
# 관리자: 내부 문서까지 검색
results = secure_search("인사 정책", user_role="manager", department="hr")주요 보안 고려사항:
- 문서 수준 ACL: 문서별 접근 권한을 메타데이터로 관리
- 행 수준 보안: 검색 결과에서 권한 없는 문서 필터링
- 프롬프트 인젝션 방지: 사용자 입력 검증 및 새니타이징
- 데이터 암호화: 벡터 DB 저장 시 암호화 적용
- 감사 로그: 검색 질의 및 접근 이력 기록
멀티테넌트 아키텍처
여러 팀이나 고객이 하나의 RAG 시스템을 공유할 때의 아키텍처입니다.
[멀티테넌트 RAG 아키텍처]
테넌트 A ─┐ ┌─ 컬렉션 A (벡터 DB)
테넌트 B ─┼→ API Gateway → ─┼─ 컬렉션 B (벡터 DB)
테넌트 C ─┘ (인증/라우팅) └─ 컬렉션 C (벡터 DB)
↓
공유 LLM 엔드포인트
격리 전략:
| 방식 | 설명 | 장점 | 단점 |
|---|---|---|---|
| 컬렉션 격리 | 테넌트별 별도 컬렉션 | 완전한 데이터 격리 | 관리 오버헤드 |
| 네임스페이스 격리 | 동일 컬렉션 내 네임스페이스 분리 | 효율적 자원 활용 | 소프트 격리 |
| 메타데이터 필터링 | 테넌트 ID를 메타데이터로 필터 | 구현 간단 | 대규모 시 성능 저하 |
운영 모니터링과 피드백 루프
프로덕션 RAG 시스템의 지속적인 품질 관리를 위한 모니터링 체계입니다.
핵심 모니터링 지표:
| 카테고리 | 지표 | 목표 |
|---|---|---|
| 성능 | 응답 지연시간 (P50/P95/P99) | P95 < 3초 |
| 품질 | 사용자 피드백 (좋아요/싫어요) | 긍정 > 80% |
| 검색 | 검색 결과 없음 비율 | < 5% |
| 비용 | 일일 토큰 사용량 | 예산 범위 내 |
| 안정성 | 에러율 | < 0.1% |
피드백 루프 구축:
[사용자 질의] → [RAG 응답] → [사용자 피드백]
↓
[피드백 분석]
├─ 부정 피드백 → 실패 사례 수집 → 개선
│ ├─ 검색 누락 → 문서 추가/청킹 조정
│ ├─ 잘못된 응답 → 프롬프트 개선
│ └─ 느린 응답 → 인프라 최적화
└─ 긍정 피드백 → 성공 패턴 분석 → 유지/확대
참고: RAG 시스템은 "한 번 구축하고 끝"이 아닙니다. 문서가 추가/변경되고 사용자 질의 패턴이 변화하므로, 지속적인 모니터링과 개선이 필수입니다.
References
- Lewis, P. et al. (2020). "Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks." NeurIPS
- Gao, Y. et al. (2024). "Retrieval-Augmented Generation for Large Language Models: A Survey." arXiv
- Asai, A. et al. (2023). "Self-RAG: Learning to Retrieve, Generate, and Critique through Self-Reflection." ICLR
- Yan, S. et al. (2024). "Corrective Retrieval Augmented Generation." arXiv
- Edge, D. et al. (2024). "From Local to Global: A Graph RAG Approach to Query-Focused Summarization." arXiv
- Es, S. et al. (2024). "RAGAS: Automated Evaluation of Retrieval Augmented Generation." EACL
- LangChain Documentation — https://python.langchain.com/docs/
- LlamaIndex Documentation — https://docs.llamaindex.ai/
— Data Dynamics 엔지니어링 팀