Blog
ragllmvectordbembeddingailangchainretrieval

RAG(Retrieval-Augmented Generation) 완전 가이드 - 아키텍처부터 실전 구축까지

RAG의 핵심 아키텍처, 청킹 전략, 벡터 DB, 검색 기법, 고급 패턴(Self-RAG, Graph RAG, Agentic RAG), 평가 방법, 엔터프라이즈 구축 노하우를 체계적으로 정리합니다.

Data Dynamics2026년 4월 16일46 min read

혹시 오픈북 시험을 떠올려 본 적 있나요? 머릿속 지식만으로 푸는 시험과 달리, 오픈북 시험에서는 필요한 페이지를 펼쳐 보고 답을 씁니다. 모든 걸 외울 필요 없이, 어디를 찾아봐야 하는지만 알면 되죠.

RAG(Retrieval-Augmented Generation)가 바로 이 "오픈북 시험" 방식을 LLM에게 적용한 기법입니다. LLM이 답을 만들기 전에 관련 문서를 먼저 찾아 펼쳐 주고, 그 내용을 보고 답하게 하는 것입니다.

한 문장으로: RAG는 LLM에게 "오픈북 시험"을 보게 하는 기법입니다. 외부 지식을 검색해 컨텍스트로 건네주고, 그 근거 위에서 답을 생성하게 합니다.

이 글은 RAG를 처음 접하는 분도 차근차근 따라올 수 있도록 개념 → 동작 원리 → 직접 구축까지 단계적으로 풀어 갑니다. 글보다 그림이 빠를 때가 많아 다이어그램도 적극적으로 썼습니다. 코딩이 익숙하지 않아도 천천히 따라오면 됩니다.

이 글에서 배우는 것

  • RAG가 정확히 무엇이고, 왜 LLM에게 필요한지
  • 문서를 어떻게 잘게 나누고(청킹) 벡터로 바꿔 저장하는지
  • 검색을 더 똑똑하게 만드는 방법(하이브리드 검색·리랭킹)
  • Self-RAG·Graph RAG·Agentic RAG 같은 고급 패턴의 직관
  • 만든 RAG가 잘 동작하는지 평가하고, 실무에 올리는 법

1. RAG란 무엇인가

정의와 등장 배경

RAG는 2020년 Meta(Facebook) AI Research의 Patrick Lewis 등이 제안한 기법입니다. 이름을 풀어 보면 그대로 정의가 됩니다 — 검색(Retrieval) 으로 자료를 찾고, 그 자료를 바탕으로 생성(Generation) 한다. 둘을 결합(Augmented)한 것이죠.

아이디어 자체는 놀랍도록 단순합니다. LLM에게 곧바로 "답해 봐"라고 시키는 대신, "먼저 이 문서들을 읽고, 그다음에 답해" 라고 시키는 것입니다. 사람도 모르는 걸 물어보면 일단 검색부터 하잖아요. RAG는 LLM에게 바로 그 "검색부터 하는 습관"을 붙여 줍니다.

Loading diagram…

LLM 단독 사용의 한계와 RAG의 필요성

그렇다면 왜 굳이 검색을 시킬까요? LLM 혼자서는 다음과 같은 벽에 부딪히기 때문입니다.

한계설명RAG의 해결 방식
지식 단절 (Knowledge Cutoff)학습 시점 이후 정보를 알 수 없음최신 문서를 실시간 검색하여 제공
할루시네이션 (Hallucination)사실이 아닌 정보를 자신 있게 생성검색된 문서를 근거로 응답, 출처 명시
도메인 지식 부재기업 내부 데이터, 전문 분야 지식 부족사내 문서를 벡터 DB에 저장하여 검색
비용미세 조정(Fine-tuning)에 높은 비용문서 업데이트만으로 지식 갱신 가능

참고: RAG는 Fine-tuning의 대안이 아닌 보완적 기법입니다. Fine-tuning은 모델의 행동 방식(스타일, 형식)을 변경하는 데 적합하고, RAG는 최신 사실 정보를 제공하는 데 적합합니다. 두 기법을 함께 사용하면 최적의 결과를 얻을 수 있습니다.


2. RAG 아키텍처 상세

전체 파이프라인 구조

RAG를 식당에 비유해 볼까요. 손님이 오기 전에 미리 재료를 다듬어 냉장고에 정리해 두는 일과, 주문이 들어오면 그 재료로 요리해 내는 일은 전혀 다른 시점에 일어납니다. RAG도 똑같이 두 개의 파이프라인으로 나뉩니다 — 미리 자료를 손질해 저장해 두는 오프라인 인덱싱 파이프라인, 그리고 질문이 들어올 때 비로소 도는 온라인 쿼리 파이프라인입니다.

Loading diagram…

각 단계별 역할:

단계역할주요 도구
문서 로딩다양한 형식의 문서를 텍스트로 변환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도 처음부터 지금 모습은 아니었습니다. 더 똑똑해지는 과정을 크게 3세대로 나눠 보면 흐름이 한눈에 들어옵니다.

Naive RAG (1세대)

가장 기본적인 형태입니다. 검색해서 나온 문서를 프롬프트에 그대로 욱여넣고 답하게 하는, "찾아서 그냥 넣기" 방식이죠.

Loading diagram…
  • 장점: 구현이 간단
  • 한계: 검색 품질에 전적으로 의존, 노이즈 문서 포함 가능

Advanced RAG (2세대)

1세대의 약점은 검색이 한 번 어긋나면 답도 함께 무너진다는 것이었습니다. 그래서 검색 전후에 다듬는 단계를 끼워 넣은 것이 2세대입니다.

Loading diagram…
  • 질의 변환: 질의를 재구성하여 검색 품질 향상
  • 하이브리드 검색: 벡터 + 키워드 검색 결합
  • 리랭킹: 검색 결과의 관련도 재평가
  • 컨텍스트 압축: 불필요한 정보 제거

Modular RAG (3세대)

각 단계를 레고 블록처럼 떼었다 붙였다 할 수 있게 만든 것이 3세대입니다. 질문 종류에 따라 검색 방식을 골라 끼우고, 결과가 미덥지 않으면 다시 검색하도록 되돌리기도 합니다.

Loading diagram…
  • 라우팅: 질의 유형에 따라 적절한 검색 전략 선택
  • 모듈 교체: 각 단계의 컴포넌트를 독립적으로 교체 가능
  • 피드백 루프: 생성 결과를 평가하여 검색 재시도

3. 데이터 준비: 문서 로딩과 청킹

문서 로딩

RAG의 출발점은 의외로 평범합니다. 우리가 가진 자료 — PDF, 웹페이지, 워드, 데이터베이스 — 를 LLM이 읽을 수 있는 순수한 텍스트로 바꾸는 일이죠. 화려하진 않지만 여기서 글자가 깨지면 뒤가 전부 흔들리니, 첫 단추부터 잘 끼워야 합니다.

문서 형식로더특이사항
PDFPyPDFLoader, Unstructured표, 이미지 내 텍스트 추출 주의
HTMLBeautifulSoupLoader태그 제거, 본문 추출
MarkdownMarkdownLoader헤딩 기반 구조 보존
Word/PPTUnstructured서식 정보 활용 가능
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 ChunkingLLM이 직접 청킹 수행복잡한 문서
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)

참고: 청크 크기의 일반적인 가이드라인은 2001000 토큰입니다. 작은 청크(200400)는 정밀한 검색에, 큰 청크(6001000)는 풍부한 문맥 제공에 유리합니다. overlap은 chunk_size의 1020%가 적절합니다.

메타데이터 관리

청크마다 '꼬리표(메타데이터)'를 달아 두면 나중에 큰 도움이 됩니다. 책에 붙이는 인덱스 탭처럼 '이건 재무팀 2025년 4분기 문서'라는 딱지를 붙여 두면, 검색할 때 원하는 범위만 골라내거나 가중치를 줄 수 있죠.

# 메타데이터가 포함된 청크 예시
{
    "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. 임베딩과 벡터 데이터베이스

임베딩 모델 선택

컴퓨터는 글자의 '의미'를 직접 이해하지 못합니다. 그래서 문장을 숫자 좌표로 바꿔 줍니다. 이게 임베딩이에요. 비슷한 의미의 문장은 좌표 공간에서 가까운 곳에, 다른 의미는 멀리 놓이도록 만드는 거죠. 지도 위에서 가까운 두 도시가 실제로도 가깝듯, 좌표가 가까우면 의미도 비슷하다고 보는 겁니다. 그래서 어떤 임베딩 모델을 쓰느냐가 검색 품질을 직접 좌우합니다.

모델차원다국어특징
OpenAI text-embedding-3-large3072O높은 성능, API 비용 발생
OpenAI text-embedding-3-small1536O비용 대비 우수한 성능
Cohere embed-v31024O검색 특화, 다국어 강점
BGE-M3 (BAAI)1024O오픈소스 최고 수준, 다국어
E5-Mistral-7B4096O오픈소스, 긴 문서에 강점
multilingual-e5-large1024O다국어 특화, 경량
# 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임베디드소규모XO프로토타입, PoC
Pinecone관리형 SaaS대규모OO운영 부담 최소화
Weaviate자체 호스팅/클라우드대규모OO하이브리드 검색 중심
Milvus자체 호스팅초대규모OO엔터프라이즈 대용량
Qdrant자체 호스팅/클라우드대규모OO고성능 필터링
pgvectorPostgreSQL 확장중규모XO (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 + PQHNSW와 PQ 결합빠름높음중간
  • 소규모 (< 10만 벡터): Flat 또는 HNSW
  • 중규모 (10만 ~ 1,000만): HNSW
  • 대규모 (> 1,000만): IVF + PQ 또는 HNSW + PQ

5. 검색 전략

벡터 유사도 검색

검색의 기본기는 이렇습니다. 질문도 똑같이 벡터로 바꾼 뒤, 저장해 둔 문서 벡터들 중에서 가장 가까운 것을 고르는 거죠. 좌표가 가까울수록 의미가 비슷하니까요. 그렇다면 '가깝다'를 어떻게 잴까요? 아래의 유사도 메트릭이 그 자를 정해 줍니다.

주요 유사도 메트릭:

메트릭수식특징
Cosine Similaritycos(θ) = (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는 검색엔진이 오래전부터 써 온 정직한 방식 — 단어를 그대로 매칭합니다. 워드에서 Ctrl+F를 누르는 것과 비슷하죠. 다만 단순 일치가 아니라, 그 단어가 문서에 얼마나 자주 나오는지(빈도, 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"의미 유사도 낮아 부정확 (약함)정확한 코드 매칭 (좋음)

하이브리드 검색

그럼 둘 중 무엇을 써야 할까요? 정답은 보통 "둘 다"입니다. 벡터 검색은 의미를, BM25는 정확한 단어를 잘 잡으니 서로의 빈틈을 메워 주거든요. 이렇게 두 검색을 함께 돌려 결과를 합치는 것이 하이브리드 검색입니다. 두 결과를 버무리는 대표적인 방법이 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번째 검색기에서의 순위

리랭킹

채용에 비유하면 검색은 서류 전형, 리랭킹은 면접입니다. 1차 검색은 수많은 후보를 빠르게 추리느라 정확도를 조금 양보합니다(bi-encoder). 그렇게 살아남은 소수의 후보만 데려와 한 명 한 명 꼼꼼히 다시 평가하는 단계가 리랭킹(cross-encoder)이죠. 느리지만 정확합니다. 그래서 전체가 아니라 추려진 10~50개에만 적용합니다.

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 방식: 각 문서에 대해 개별 요약 후 통합

Loading diagram…

Map-Rerank 방식: 각 문서로 응답 생성 후 점수 기반 선택

Loading diagram…

프롬프트 템플릿 설계

RAG 프롬프트에는 꼭 담아야 할 당부가 있습니다. 핵심은 "준 문서 안에서만 답하고, 모르면 모른다고 말하라" 예요. 이 한마디가 할루시네이션을 막는 첫 단추입니다.

from langchain_core.prompts import ChatPromptTemplate
 
rag_prompt = ChatPromptTemplate.from_template("""
당신은 Data Dynamics의 기술 지원 전문가입니다.
아래 제공된 컨텍스트 정보만을 사용하여 질문에 답변하세요.
 
## 규칙
1. 컨텍스트에 없는 정보는 "제공된 문서에서 해당 정보를 찾을 수 없습니다"라고 답하세요.
2. 답변에 사용한 문서의 출처를 명시하세요.
3. 기술적인 내용은 코드 예시와 함께 설명하세요.
 
## 컨텍스트
{context}
 
## 질문
{question}
 
## 답변
""")

주요 설계 원칙:

  • 역할 명시: 모델의 전문 분야 지정
  • 컨텍스트 기반 응답 강제: 할루시네이션 방지
  • 모르면 모른다고 답하도록 지시: 정직한 응답 유도
  • 출처 인용 요구: 응답의 신뢰도 향상

출처 인용(Citation) 처리

답변에 '어느 문서를 보고 한 말인지' 출처를 달아 주면, 사용자가 직접 원문을 확인할 수 있어 신뢰가 크게 올라갑니다. 논문 끝에 붙는 참고문헌과 같은 역할이죠.

# 출처가 포함된 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)

발상이 재미있습니다. 질문으로 곧장 검색하는 대신, LLM에게 그럴듯한 가짜 답을 먼저 지어내게 한 뒤 그 가짜 답으로 검색합니다. 짧은 질문보다 (가짜라도) 답 형태의 글이 실제 문서와 더 닮아 있어서, 오히려 검색이 잘 됩니다.

Loading diagram…

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

너무 구체적인 질문은 오히려 검색이 빗나가기 쉽습니다. 그래서 한 발 물러서서(step-back) 더 큰 질문으로 바꿔 배경 지식부터 찾고, 그걸 발판 삼아 원래의 구체적인 질문에 답합니다.

Loading diagram…

Self-RAG / Corrective RAG (CRAG)

Self-RAG

지금까지는 '무조건 검색 → 무조건 사용'이었습니다. Self-RAG는 LLM이 스스로 되묻게 합니다. "이건 검색이 필요한 질문인가?", "찾아온 문서가 진짜 관련 있나?", "내 답이 문서에 근거하고 있나?" — 단계마다 자가 점검을 끼워 넣어 품질을 끌어올립니다.

Loading diagram…

Corrective RAG (CRAG)

비슷한 정신인데, 초점은 검색 결과의 '신뢰도'입니다. 찾아온 문서가 미덥지 않으면 그대로 쓰지 않고, 웹 검색 같은 다른 출처로 보완하거나 아예 갈아 끼웁니다.

Loading diagram…

Graph RAG

"이 프로젝트의 데이터 아키텍트는 누구인가?" 같은 질문은 단어가 비슷한 문서를 찾는다고 풀리지 않습니다. 누가 누구와 어떤 관계인지를 알아야 하거든요. Graph RAG는 지식을 점(엔티티)과 선(관계)으로 이어 둔 지식 그래프 위에서, 관계를 따라가며 답을 찾습니다. 단순 텍스트 유사도로는 잡기 어려운 관계 기반 질문에 특히 강합니다.

Loading diagram…

활용 사례:

  • 조직 구조 기반 질의 ("A 부서의 팀장은?")
  • 인과 관계 추적 ("이 장애의 근본 원인은?")
  • 다단계 추론 ("A 제품을 구매한 고객이 함께 구매한 제품은?")

Agentic RAG

여기서 RAG는 한 걸음 더 나아가 에이전트가 됩니다. 검색을 '한 번 하고 끝'이 아니라 손에 쥔 여러 도구 중 하나로 보고, 복잡한 질문을 스스로 단계로 쪼개 — 계획하고, 검색·계산을 실행하고, 중간 결과를 돌아보며 — 답을 조립해 나갑니다.

[사용자 질의: "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)는 이 평가를 자동으로 해 주는 오픈소스 프레임워크입니다. 질문·답변·검색 컨텍스트·정답만 넣어 주면 점수를 매겨 줍니다.

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값 증가
느린 응답지연시간이 김인덱스 최적화, 캐싱, 스트리밍 적용
임베딩 품질의미 유사도 부정확임베딩 모델 변경, 도메인 미세 조정

튜닝 우선순위 (권장):

  1. 청킹 전략 최적화 (영향도 최대)
  2. 하이브리드 검색 적용
  3. 리랭킹 추가
  4. 임베딩 모델 변경
  5. 프롬프트 개선
  6. Query Transformation 적용

9. 엔터프라이즈 RAG 구축 실전

보안과 접근 제어

회사 안에서 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 시스템을 여러 팀이나 고객이 나눠 쓸 때는, 서로의 데이터가 절대 섞이지 않게 칸막이를 쳐야 합니다. 한 건물을 여러 세입자가 쓰되 각자 방은 따로 잠가 두는 것과 같죠.

Loading diagram…

격리 전략:

방식설명장점단점
컬렉션 격리테넌트별 별도 컬렉션완전한 데이터 격리관리 오버헤드
네임스페이스 격리동일 컬렉션 내 네임스페이스 분리효율적 자원 활용소프트 격리
메타데이터 필터링테넌트 ID를 메타데이터로 필터구현 간단대규모 시 성능 저하

운영 모니터링과 피드백 루프

운영에 올린 뒤가 진짜 시작입니다. 사용자가 실제로 던지는 질문은 늘 우리 예상을 벗어나거든요. 그래서 시스템이 잘 돌고 있는지 꾸준히 지켜보는 계기판이 필요합니다.

핵심 모니터링 지표:

카테고리지표목표
성능응답 지연시간 (P50/P95/P99)P95 < 3초
품질사용자 피드백 (좋아요/싫어요)긍정 > 80%
검색검색 결과 없음 비율< 5%
비용일일 토큰 사용량예산 범위 내
안정성에러율< 0.1%

피드백 루프 구축:

Loading diagram…

참고: RAG 시스템은 "한 번 구축하고 끝"이 아닙니다. 문서가 추가/변경되고 사용자 질의 패턴이 변화하므로, 지속적인 모니터링과 개선이 필수입니다.


마치며 — 핵심 요약

길었지만, 결국 RAG의 뼈대는 단순합니다. 한 장으로 정리하면 이렇습니다.

  • RAG = LLM의 오픈북 시험. 답하기 전에 관련 문서를 찾아 근거로 건네주어, 최신성·정확성·출처를 한 번에 잡는다.
  • 품질의 8할은 '데이터 준비'에서 갈린다. 특히 청킹(적당한 한 입 크기)과 임베딩 모델 선택이 결정적이다.
  • 검색은 한 가지로 충분하지 않다. 의미를 잡는 벡터 검색과 단어를 잡는 BM25를 합친 하이브리드 검색, 그리고 면접에 해당하는 리랭킹을 더하면 품질이 크게 오른다.
  • 프롬프트에선 정직을 강제한다. "준 문서 안에서만 답하고, 모르면 모른다고 하라"가 할루시네이션을 막는 첫 단추다.
  • 고급 패턴(HyDE·Multi-Query·Self-RAG·CRAG·Graph RAG·Agentic RAG)은 결국 "검색을 더 똑똑하게"라는 한 방향을 향한다.
  • RAG는 한 번 만들고 끝이 아니다. 평가(RAGAS)와 모니터링·피드백 루프로 계속 다듬어야 살아 있는 시스템이 된다.

처음 만드는 RAG라면 욕심내지 말고 Naive 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 엔지니어링 팀