목차
RAG(Retrieval-Augmented Generation) 란 ??
한 줄 요약: RAG는 LLM이 답변하기 전에 관련 문서를 검색해서 가져오고, 그 문서를 근거로만 답변하게 만드는 아키텍처입니다.
1. RAG란 무엇인가?
1.1 기본 개념
RAG(Retrieval-Augmented Generation)는 두 가지 핵심 컴포넌트로 구성됩니다:
🔍 Retriever (검색기)
- 사용자 질문과 관련된 문서나 정보 조각(chunk)을 찾아옵니다
- 벡터 검색, 키워드 검색, 하이브리드 검색 등 다양한 방식이 있습니다
✍️ Generator (생성기)
- 검색된 문서를 근거로 최종 답변을 생성합니다
- 대부분 LLM(Large Language Model)이 이 역할을 담당합니다
1.2 간단한 비유
전통적인 LLM 사용
- 학생에게 교과서 없이 시험을 보게 하는 것과 같습니다
- 기억에만 의존하므로 틀린 답이나 헷갈린 답을 할 가능성이 높습니다
RAG 시스템
- 학생에게 관련 자료를 찾아서 보고 답하게 하는 오픈북 시험과 같습니다
- 근거 자료를 참고하므로 정확도가 높고, 출처도 명확합니다
2. 왜 RAG가 필요한가?
2.1 환각(Hallucination) 문제 해결
환각이란? LLM이 그럴듯하지만 사실이 아닌 정보를 자신있게 생성하는 현상입니다.
예시:
질문: "우리 회사 2024년 하계 휴가 정책은?"
❌ LLM만 사용 (환각 발생)
답변: "일반적으로 기업들은 5일의 하계 휴가를 제공합니다..."
→ 회사마다 다른데 일반론을 답변
✅ RAG 사용
답변: "인사팀 공지(2024.05.15)에 따르면, 올해는 7월 29일~8월 2일
5일간 하계 휴가입니다. [출처: 2024_하계휴가_공지.pdf]"
→ 실제 문서 기반으로 정확한 답변
2.2 최신 정보 반영
LLM은 학습 시점까지의 데이터만 알고 있습니다. RAG를 사용하면:
- 실시간으로 업데이트되는 문서 활용 가능
- 학습 없이도 최신 정보 제공 가능
- 회사 내부 정보, 프로젝트별 문서 등 특수 지식 활용 가능
2.3 출처 투명성
RAG는 "어떤 문서를 근거로 답했는지" 명확히 알 수 있어:
- 답변 신뢰도 향상
- 감사(Audit) 및 컴플라이언스 대응 용이
- 사용자가 원본 문서를 직접 확인 가능
3. RAG의 핵심 개념
3.1 Parametric vs Non-parametric Knowledge
Parametric Knowledge (파라메트릭 지식)
정의: 모델의 가중치(파라미터) 안에 암묵적으로 저장된 지식
특징:
- ✅ 빠른 추론 속도
- ❌ 업데이트가 어려움 (재학습 필요)
- ❌ 최신 정보 반영 불가
- ❌ 출처 불명확
예시:
- "프랑스의 수도는 어디야?" → "파리입니다" (모델이 학습으로 알고 있음)
Non-parametric Knowledge (논-파라메트릭 지식)
정의: 외부 데이터베이스, 문서, API 등에서 검색으로 가져오는 지식
특징:
- ✅ 실시간 업데이트 가능
- ✅ 출처 명확
- ✅ 권한 관리 가능
- ❌ 검색 시간 추가
예시:
- "우리 팀 올해 Q3 목표는?" → 사내 문서 검색 → "팀 목표 문서.pdf를 기반으로..."
3.2 RAG가 해결하는 문제들
| 문제 | 전통적 LLM | RAG 시스템 |
| 최신 정보 | ❌ 학습 시점까지만 | ✅ 실시간 업데이트 |
| 사내 기밀 정보 | ❌ 학습 불가능/위험 | ✅ 안전하게 활용 |
| 환각 | ⚠️ 높은 위험 | ✅ 크게 감소 |
| 출처 제공 | ❌ 불가능 | ✅ 명확한 출처 |
| 업데이트 비용 | ❌ 재학습 필요 | ✅ 문서만 업데이트 |
4. RAG 아키텍처 상세
4.1 전체 데이터 흐름
flowchart TB
A[사용자 질문] --> B[질문 전처리/재작성]
B --> C[Retriever 검색기]
C --> D[(벡터 데이터베이스)]
D --> C
C --> E[Top-K 문서 선택]
E --> F[컨텍스트 패키징]
F --> G[Generator LLM]
G --> H[답변 + 출처]
style A fill:#e1f5ff
style H fill:#e1ffe1
style C fill:#fff4e1
style G fill:#ffe1f5
4.2 핵심 컴포넌트 상세 설명
🧮 Component 1: Embedder (임베딩 모델)
역할: 텍스트를 의미를 담은 숫자 벡터로 변환
작동 원리:
입력: "연차 이월 규정"
↓ (임베딩 모델)
출력: [0.23, -0.45, 0.67, ..., 0.12] ← 768차원 벡터
왜 필요한가?
- 단순 키워드 매칭을 넘어 의미적 유사도 검색 가능
- "연차 이월"과 "휴가 이월" 같이 다른 단어지만 비슷한 의미를 파악
실무 예시:
질문: "법인카드 분실했는데 어떻게 해야 돼?"
키워드 검색: "법인카드 분실" 정확히 일치하는 문서만 찾음
임베딩 검색: "회사 카드 도난/분실", "법인 신용카드 재발급" 등도 찾음
주요 모델:
- OpenAI: text-embedding-3-large, text-embedding-3-small
- 오픈소스: bge-large-en-v1.5, multilingual-e5-large
🔍 Component 2: Retriever (검색기)
역할: 질문과 관련된 문서/청크를 찾아오기
검색 방식 비교:
1) Dense Retrieval (벡터 검색)
# 의미 기반 검색
query = "퇴사할 때 뭐 해야 돼?"
# → "퇴직 절차", "이직 프로세스" 등 의미가 비슷한 문서 검색
- ✅ 의미적 유사도가 높음
- ❌ 정확한 용어/숫자 검색에 약함
2) Sparse Retrieval (BM25, 키워드 검색)
# 키워드 기반 검색
query = "연봉 3000만원 이상 경력 5년"
# → "3000만원", "경력 5년" 정확히 일치하는 문서 우선
- ✅ 정확한 키워드/숫자 검색에 강함
- ❌ 동의어, 의미적 유사성 파악 약함
3) Hybrid Retrieval (권장)
# 두 방식 조합
검색 1단계: Dense로 의미상 관련 문서 30개 추출
검색 2단계: Sparse로 키워드 일치도 재평가
최종 결과: 두 점수를 가중 평균하여 Top-5 선택
실전 파라미터:
retriever = vectorstore.as_retriever(
search_type="mmr", # Maximum Marginal Relevance (중복 제거)
search_kwargs={
"k": 5, # 최종 반환 문서 수
"fetch_k": 20, # 1차 후보 문서 수
"lambda_mult": 0.7 # 다양성 vs 유사도 균형
}
)
✍️ Component 3: Generator (생성기)
역할: 검색된 문서를 근거로 최종 답변 생성
프롬프트 설계가 90%
❌ 나쁜 프롬프트:
질문: {question}
문서: {context}
답변해줘.
→ 모델이 문서를 무시하고 자기 지식으로 답할 수 있음
✅ 좋은 프롬프트:
당신은 회사 규정 전문가입니다.
규칙:
1. 반드시 제공된 [문서]만을 근거로 답변하세요
2. 문서에 없는 내용은 "문서에서 해당 정보를 찾을 수 없습니다"라고 답하세요
3. 답변 형식:
- 핵심 답변 (2-3문장)
- 근거: [문서명, 페이지/섹션]
- 추가 참고사항 (있는 경우)
[문서]
{context}
[질문]
{question}
[답변]
주요 파라미터:
llm = ChatOpenAI(
model="gpt-4o-mini",
temperature=0, # 0 = 결정론적, 1 = 창의적
max_tokens=500, # 답변 길이 제한
)
4.3 문서 청킹(Chunking) 전략
청킹이란? 긴 문서를 작은 조각으로 나누는 과정
왜 필요한가?
- LLM 컨텍스트 길이 제한 (GPT-4: ~128K 토큰)
- 관련 없는 부분 제외하고 핵심만 전달
- 검색 정확도 향상
청킹 방법 비교
1) 고정 길이 청킹
# 800자마다 자르기
chunk_size = 800
overlap = 150 # 앞뒤 문맥 유지
- ✅ 간단함
- ❌ 문장/문단 중간에서 잘릴 수 있음
2) 재귀적 청킹 (권장)
RecursiveCharacterTextSplitter(
chunk_size=800,
chunk_overlap=150,
separators=["\n\n", "\n", ". ", " ", ""] # 우선순위대로 자르기
)
- ✅ 자연스러운 분할
- ✅ 문맥 보존
3) 의미 기반 청킹
# 제목, 섹션, 리스트 단위로 자르기
SemanticChunker(
breakpoint_threshold_type="percentile",
breakpoint_threshold_amount=85
)
- ✅ 논리적 구조 유지
- ❌ 처리 시간 증가
실전 청킹 팁
문서 유형별 전략:
문서 유형 권장 chunk_size 권장 방법 이유
| 문서 유형 | 권장 chunk_size | 권장 방법 | 이유 |
| FAQ | 200-400 | 질문-답변 쌍 단위 | 각 QA는 독립적 |
| 절차/가이드 | 600-1000 | 섹션/스텝 단위 | 순서가 중요 |
| 계약서/규정 | 800-1200 | 조항 단위 | 법적 맥락 보존 |
| 기술 문서 | 500-800 | 코드 블록 유지 | 예제 완전성 |
메타데이터 추가 (중요!)
for chunk in chunks:
chunk.metadata = {
"source": "인사규정.pdf",
"page": 3,
"section": "제2장 휴가",
"last_updated": "2024-06-15",
"department": "인사팀",
"access_level": "전직원" # 권한 관리용
}
5. 실패 유형과 해결책
5.1 검색 실패 (Retrieval Failure)
문제: 관련 문서가 있는데 검색에서 못 찾아옴
케이스 1: 용어 불일치
❌ 실패 케이스:
질문: "법인카드 분실 시 처리 절차는?"
문서: "회사 신용카드 도난 및 분실 대응 매뉴얼"
→ "법인카드"와 "회사 신용카드" 매칭 실패
✅ 해결책:
1. 하이브리드 검색 (Dense + BM25)
2. 동의어 사전 구축
3. 질문 리라이팅: "법인카드" → "회사 카드, 법인 신용카드" 등 확장
케이스 2: 청킹 오류
❌ 실패 케이스:
원본 문서:
"법인카드 분실 절차:
1. 즉시 카드사 분실 신고 (1588-xxxx)
2. 법무팀 이메일로 분실 보고
3. 재발급 신청서 제출"
나쁜 청킹:
chunk1: "법인카드 분실 절차: 1. 즉시 카드사"
chunk2: "분실 신고 (1588-xxxx) 2. 법무팀"
→ 각 청크가 불완전
✅ 해결책:
- 리스트 구조 인식하는 청킹 사용
- chunk_size 늘리기 (절차는 통째로 유지)
- 제목+내용 함께 청킹
케이스 3: 메타데이터 필터링 오류
❌ 실패 케이스:
질문: "우리 팀 2024년 Q3 목표는?"
검색 결과: 다른 팀의 목표 문서들 반환
✅ 해결책:
retriever = vectorstore.as_retriever(
search_kwargs={
"k": 5,
"filter": {
"team": "사용자의 팀명",
"year": 2024,
"quarter": "Q3"
}
}
)
5.2 생성 실패 (Generation Failure)
문제: 문서는 정확히 가져왔는데 LLM이 잘못 답변
케이스 1: 문서 무시하고 자기 지식으로 답변
❌ 실패 케이스:
문서: "우리 회사는 연차를 최대 2년까지 이월 가능"
질문: "연차 이월 규정은?"
LLM 답변: "일반적으로 연차는 1년 이월이 가능합니다"
→ 문서 내용 무시하고 일반 상식으로 답변
✅ 해결책:
시스템 프롬프트에 명시:
"당신의 일반 지식은 무시하세요. 오직 제공된 [문서]만 근거로 답하세요.
문서와 당신의 지식이 충돌하면, 반드시 문서를 우선하세요."
케이스 2: 환각 (문서에 없는 내용 추가)
❌ 실패 케이스:
문서: "법인카드 분실 시 카드사에 신고하세요"
질문: "법인카드 잃어버렸어요"
LLM 답변: "카드사 신고 후 경찰서에 분실 신고도 필수입니다"
→ "경찰서 신고"는 문서에 없는 내용
✅ 해결책:
1. temperature=0 (무작위성 제거)
2. 답변 형식 강제:
"""
답변 형식:
1. 핵심 답변
2. 근거: [문서의 정확한 인용문]
3. 확신도: (높음/중간/낮음)
문서에 명시되지 않은 내용은 절대 추가하지 마세요.
"""
케이스 3: 예외 조건 누락
❌ 실패 케이스:
문서: "연차는 2년 이월 가능. 단, 계약직은 이월 불가"
질문: "계약직도 연차 이월 되나요?"
LLM 답변: "네, 2년까지 이월 가능합니다"
→ 예외 조건 놓침
✅ 해결책:
프롬프트에 추가:
"문서의 '단,', '예외', '제외', '다만' 등의 조건을 반드시 확인하고
질문이 예외 케이스에 해당하는지 먼저 판단하세요."
5.3 종합 디버깅 체크리스트
| 단계 | 체크 항목 | 해결 방법 |
| 검색 | Top-K에 정답 문서가 있나? | k 늘리기, 하이브리드 검색 |
| 검색된 문서가 관련있나? | 쿼리 리라이팅, 임베딩 모델 변경 | |
| 문서 순서가 적절한가? | Re-ranking 추가 | |
| 생성 | LLM이 문서를 보고 있나? | 프롬프트에 문서 강조 |
| 환각이 발생하나? | temperature↓, 형식 강제 | |
| 예외 처리를 하나? | 조건문 명시 프롬프트 |
6. 실습: LangChain으로 RAG 구축
6.1 환경 설정
# 필수 패키지 설치
pip install langchain langchain-community langchain-openai
pip install chromadb pypdf python-dotenv
# 또는 한 번에
pip install -U langchain langchain-community langchain-openai chromadb pypdf python-dotenv
6.2 기본 RAG 파이프라인
import os
from dotenv import load_dotenv
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_community.document_loaders import PyPDFLoader, TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import Chroma
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate
# 환경 변수 로드
load_dotenv()
os.environ["OPENAI_API_KEY"] = os.getenv("OPENAI_API_KEY")
# ========== 1단계: 문서 로드 ==========
print("📂 문서 로딩 중...")
documents = []
# PDF 파일 로드
pdf_loader = PyPDFLoader("data/company_handbook.pdf")
documents.extend(pdf_loader.load())
# 텍스트 파일 로드
txt_loader = TextLoader("data/faq.txt", encoding="utf-8")
documents.extend(txt_loader.load())
print(f"✅ 총 {len(documents)}개 문서 로드 완료")
# ========== 2단계: 문서 청킹 ==========
print("✂️ 문서 청킹 중...")
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=800, # 청크 크기
chunk_overlap=150, # 겹치는 부분 (문맥 유지)
length_function=len,
separators=["\n\n", "\n", ". ", " ", ""] # 우선순위대로 분리
)
chunks = text_splitter.split_documents(documents)
print(f"✅ {len(chunks)}개 청크 생성 완료")
# 청크 예시 출력
print(f"📄 청크 샘플:\n{chunks[0].page_content[:200]}...\n")
# ========== 3단계: 임베딩 + 벡터 DB 생성 ==========
print("🔢 벡터 임베딩 생성 중...")
embeddings = OpenAIEmbeddings(
model="text-embedding-3-large" # 또는 text-embedding-3-small
)
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_db" # 로컬에 저장
)
print("✅ 벡터 DB 생성 완료\n")
# ========== 4단계: Retriever 설정 ==========
retriever = vectorstore.as_retriever(
search_type="mmr", # Maximum Marginal Relevance (중복 제거)
search_kwargs={
"k": 5, # 최종 반환 문서 수
"fetch_k": 20, # 1차 후보 문서 수
}
)
# ========== 5단계: 프롬프트 템플릿 (중요!) ==========
prompt_template = """당신은 회사 규정 전문 AI 어시스턴트입니다.
규칙:
1. 반드시 아래 [문서]만을 근거로 답변하세요
2. 문서에 없는 내용은 "제공된 문서에서 해당 정보를 찾을 수 없습니다"라고 답하세요
3. 답변 시 출처(문서명, 페이지)를 함께 제시하세요
4. 예외 조건("단,", "다만", "제외")을 반드시 확인하세요
[문서]
{context}
[질문]
{question}
[답변]"""
PROMPT = PromptTemplate(
template=prompt_template,
input_variables=["context", "question"]
)
# ========== 6단계: LLM + RAG 체인 구성 ==========
llm = ChatOpenAI(
model="gpt-4o-mini", # 또는 gpt-4o
temperature=0 # 일관된 답변
)
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff", # 모든 문서를 한 번에 전달
retriever=retriever,
return_source_documents=True, # 출처 문서 반환
chain_type_kwargs={"prompt": PROMPT}
)
# ========== 7단계: 질의응답 ==========
def ask_question(question):
print(f"\n💬 질문: {question}")
print("-" * 80)
result = qa_chain.invoke({"query": question})
# 답변 출력
print(f"🤖 답변:\n{result['result']}\n")
# 출처 문서 출력
print("📚 참조한 문서:")
for i, doc in enumerate(result['source_documents'], 1):
source = doc.metadata.get('source', '알 수 없음')
page = doc.metadata.get('page', '?')
print(f" {i}. {source} (페이지 {page})")
print(f" 내용: {doc.page_content[:100]}...\n")
# ========== 테스트 질문들 ==========
questions = [
"연차 이월 규정이 어떻게 돼?",
"법인카드 분실하면 어떻게 해야 돼?",
"재택근무 신청 방법은?",
]
for q in questions:
ask_question(q)
print("=" * 80)
6.3 고급 기능: 하이브리드 검색
from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever
# Dense 검색 (벡터)
dense_retriever = vectorstore.as_retriever(search_kwargs={"k": 10})
# Sparse 검색 (BM25)
bm25_retriever = BM25Retriever.from_documents(chunks)
bm25_retriever.k = 10
# 하이브리드 (50:50 가중치)
hybrid_retriever = EnsembleRetriever(
retrievers=[dense_retriever, bm25_retriever],
weights=[0.5, 0.5]
)
# 하이브리드 retriever 사용
qa_chain_hybrid = RetrievalQA.from_chain_type(
llm=llm,
retriever=hybrid_retriever, # 기존 retriever 대신
return_source_documents=True
)
6.4 메타데이터 필터링
# 메타데이터 추가 예시
for chunk in chunks:
chunk.metadata.update({
"department": "HR",
"access_level": "all_employees",
"last_updated": "2024-06-15",
"document_type": "policy"
})
# 필터링 검색
filtered_retriever = vectorstore.as_retriever(
search_kwargs={
"k": 5,
"filter": {
"department": "HR",
"access_level": "all_employees"
}
}
)
7. 실습: LlamaIndex로 RAG 구축
7.1 왜 LlamaIndex인가?
LangChain vs LlamaIndex
| 특징 | LangChain | LlamaIndex |
| 학습 곡선 | 중간 | 쉬움 |
| 유연성 | 높음 (다양한 체인) | 중간 |
| RAG 최적화 | 범용적 | RAG 특화 |
| 문서 처리 | 수동 설정 많음 | 자동화 많음 |
| 추천 용도 | 복잡한 워크플로우 | 빠른 RAG 프로토타입 |
7.2 기본 RAG 파이프라인
import os
from dotenv import load_dotenv
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, Settings
from llama_index.llms.openai import OpenAI
from llama_index.embeddings.openai import OpenAIEmbedding
# 환경 변수 로드
load_dotenv()
os.environ["OPENAI_API_KEY"] = os.getenv("OPENAI_API_KEY")
# ========== 1단계: 전역 설정 ==========
Settings.llm = OpenAI(model="gpt-4o-mini", temperature=0)
Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-large")
Settings.chunk_size = 800
Settings.chunk_overlap = 150
# ========== 2단계: 문서 로드 (폴더 전체) ==========
print("📂 문서 로딩 중...")
documents = SimpleDirectoryReader(
input_dir="data/",
required_exts=[".pdf", ".txt", ".docx"] # 지원 형식
).load_data()
print(f"✅ {len(documents)}개 문서 로드 완료")
# ========== 3단계: 인덱스 생성 (자동 청킹+임베딩) ==========
print("🔢 인덱스 생성 중...")
index = VectorStoreIndex.from_documents(
documents,
show_progress=True # 진행률 표시
)
print("✅ 인덱스 생성 완료\n")
# ========== 4단계: Query Engine 생성 ==========
query_engine = index.as_query_engine(
similarity_top_k=5, # Top-K 문서
response_mode="compact", # 응답 모드
)
# ========== 5단계: 질의응답 ==========
def ask(question):
print(f"💬 질문: {question}")
print("-" * 80)
response = query_engine.query(question)
print(f"🤖 답변:\n{response}\n")
# 출처 확인
if response.source_nodes:
print("📚 참조 문서:")
for i, node in enumerate(response.source_nodes, 1):
print(f" {i}. {node.node.metadata.get('file_name', '알 수 없음')}")
print(f" 점수: {node.score:.3f}")
print(f" 내용: {node.node.text[:100]}...\n")
# 테스트
questions = [
"연차 이월 규정은?",
"재택근무 신청 방법 알려줘",
"법인카드 분실 시 처리 절차는?"
]
for q in questions:
ask(q)
print("=" * 80)
7.3 커스텀 프롬프트
from llama_index.core import PromptTemplate
# 커스텀 프롬프트 정의
qa_prompt_tmpl = PromptTemplate(
"당신은 회사 규정 전문 AI입니다.\n\n"
"규칙:\n"
"1. 아래 [컨텍스트]만을 근거로 답변하세요\n"
"2. 컨텍스트에 없으면 '정보를 찾을 수 없습니다'라고 하세요\n"
"3. 출처를 명시하세요\n\n"
"컨텍스트:\n"
"---------------------\n"
"{context_str}\n"
"---------------------\n\n"
"질문: {query_str}\n"
"답변: "
)
# Query Engine에 적용
query_engine = index.as_query_engine(
text_qa_template=qa_prompt_tmpl,
similarity_top_k=5
)
7.4 고급: 인덱스 영속화 (저장/로드)
# 인덱스 저장
index.storage_context.persist(persist_dir="./storage")
print("💾 인덱스 저장 완료")
# ========== 나중에 다시 로드 ==========
from llama_index.core import StorageContext, load_index_from_storage
storage_context = StorageContext.from_defaults(persist_dir="./storage")
loaded_index = load_index_from_storage(storage_context)
query_engine = loaded_index.as_query_engine(similarity_top_k=5)
print("📂 인덱스 로드 완료")
7.5 LlamaIndex의 강력한 기능들
(1) 자동 메타데이터 추출
from llama_index.core.extractors import TitleExtractor, KeywordExtractor
# 메타데이터 자동 추출
from llama_index.core.node_parser import SimpleNodeParser
node_parser = SimpleNodeParser.from_defaults(
chunk_size=800,
chunk_overlap=150,
metadata_extractor=[
TitleExtractor(), # 제목 추출
KeywordExtractor(keywords=10) # 키워드 추출
]
)
nodes = node_parser.get_nodes_from_documents(documents)
index = VectorStoreIndex(nodes)
(2) 쿼리 변환 (Query Transformation)
from llama_index.core.indices.query.query_transform import HyDEQueryTransform
# HyDE: 질문 → 가상 답변 생성 → 답변으로 검색
hyde = HyDEQueryTransform(include_original=True)
query_engine = index.as_query_engine(
similarity_top_k=5,
query_transform=hyde
)
# "연차 이월 규정?"
# → HyDE가 먼저 가상 답변 생성
# → 그 답변과 유사한 문서 검색
8. RAG 평가하기: RAGAS
8.1 왜 평가가 중요한가?
RAG의 함정:
- 개발 중에는 잘 작동하는 것 같지만...
- 특정 질문에서 갑자기 무너짐
- 사용자가 발견하기 전에 미리 찾아야 함
평가 없이 배포하면:
❌ "이 시스템 믿을 수 없어요" (신뢰도 하락)
❌ "엉뚱한 답변만 해요" (사용자 이탈)
❌ "출처가 틀렸어요" (법적 리스크)
8.2 RAGAS 핵심 메트릭 3가지
flowchart LR
A[사용자 질문] --> B[Retriever]
B --> C[검색된 문서들]
C --> D[Generator LLM]
D --> E[최종 답변]
C -.Context Recall.-> F[평가 1]
C -.Context Precision.-> G[평가 2]
E -.Faithfulness.-> H[평가 3]
style F fill:#ffe1e1
style G fill:#fff4e1
style H fill:#e1ffe1
메트릭 1: Context Recall (컨텍스트 재현율)
정의: "정답을 만드는 데 필요한 정보가 검색 결과에 포함되었는가?"
예시:
질문: "연차는 몇 년까지 이월 가능?"
정답 (Ground Truth): "2년까지 이월 가능"
검색된 문서:
Doc1: "연차 사용 기간은 발생일로부터 1년"
Doc2: "미사용 연차는 최대 2년까지 이월" ← 정답 포함!
Doc3: "연차 사용 시 사전 승인 필요"
Context Recall = 1.0 (정답 정보가 Doc2에 있음)
낮은 경우 해결책:
- Retriever 개선 (하이브리드 검색)
- Top-K 늘리기
- 청킹 전략 변경
- 쿼리 리라이팅
메트릭 2: Context Precision (컨텍스트 정밀도)
정의: "검색된 문서들이 얼마나 관련성 높은가? (노이즈가 적은가?)"
예시:
질문: "법인카드 분실 시 처리 절차는?"
검색 결과 (순서대로):
1. "법인카드 분실 신고 및 재발급 절차" ✅ 관련 높음
2. "개인정보 유출 시 대응 매뉴얼" ❌ 관련 없음
3. "법인카드 사용 가이드라인" ⚠️ 약간 관련
4. "출장 경비 처리 규정" ❌ 관련 없음
5. "법인카드 분실 사례 및 처벌" ✅ 관련 높음
Context Precision = 낮음 (노이즈가 많고, 관련 문서가 분산)
낮은 경우 해결책:
- Re-ranking 추가
- Top-K 줄이기
- 메타데이터 필터링 강화
- 임베딩 모델 업그레이드
메트릭 3: Faithfulness (충실도)
정의: "생성된 답변이 검색된 문서에 근거하는가? (환각 없는가?)"
예시:
검색된 문서:
"법인카드 분실 시 즉시 카드사(1588-xxxx)에 신고하세요."
❌ 낮은 Faithfulness (환각 발생):
"법인카드 분실 시 카드사 신고 후 경찰서에도 분실 신고를 해야 합니다."
→ "경찰서 신고"는 문서에 없는 내용!
✅ 높은 Faithfulness:
"법인카드 분실 시 즉시 카드사(1588-xxxx)에 신고하세요.
[출처: 법인카드 사용 규정 3.2절]"
→ 문서 내용만 사용
낮은 경우 해결책:
- Temperature = 0
- 프롬프트에 "문서에만 근거" 명시
- 답변 형식 강제 (출처 필수)
- 모델 업그레이드 (GPT-4 등)
8.3 RAGAS 실습 코드
# 설치
pip install ragas datasets
from datasets import Dataset
from ragas import evaluate
from ragas.metrics import (
faithfulness,
context_recall,
context_precision,
answer_relevancy
)
# ========== 평가 데이터셋 준비 ==========
# 실제로는 QA 시스템에서 자동 수집
evaluation_data = {
"question": [
"연차는 몇 년까지 이월 가능?",
"법인카드 분실 시 처리 절차는?",
"재택근무 신청 방법은?"
],
"answer": [
"미사용 연차는 최대 2년까지 이월 가능합니다. [출처: 인사규정 12조]",
"즉시 카드사(1588-xxxx)에 신고 후 법무팀에 이메일 보고하세요.",
"사내 그룹웨어 → 근태관리 → 재택근무 신청서 제출하세요."
],
"contexts": [
# 각 질문에 대해 검색된 문서 조각들
[
"제12조 (연차 이월) 미사용 연차는 익년도로 최대 2년까지 이월할 수 있다.",
"단, 계약직 및 인턴은 이월 대상에서 제외된다."
],
[
"법인카드 분실 시 즉시 카드사(1588-xxxx)로 분실 신고해야 한다.",
"분실 신고 후 24시간 내 법무팀(legal@company.com)에 이메일 보고",
"재발급 신청서는 인트라넷에서 다운로드 가능"
],
[
"재택근무는 사내 그룹웨어 근태관리 메뉴에서 신청한다.",
"신청 시 업무 계획서 및 팀장 승인이 필요하다."
]
],
"ground_truth": [
# 정답 (있으면 더 정확한 평가 가능)
"연차는 최대 2년까지 이월 가능하며, 계약직과 인턴은 제외됩니다.",
"카드사 즉시 신고 후 24시간 내 법무팀에 이메일 보고해야 합니다.",
"그룹웨어 근태관리에서 신청하며 팀장 승인이 필요합니다."
]
}
# Dataset 변환
dataset = Dataset.from_dict(evaluation_data)
# ========== RAGAS 평가 실행 ==========
print("🔍 RAGAS 평가 시작...\n")
result = evaluate(
dataset,
metrics=[
faithfulness, # 충실도
context_recall, # 검색 재현율
context_precision, # 검색 정밀도
answer_relevancy # 답변 관련성
]
)
print("=" * 80)
print("📊 평가 결과:")
print("=" * 80)
print(result)
print()
# 개별 점수 확인
print("📈 메트릭별 점수:")
print(f" Faithfulness (충실도): {result['faithfulness']:.3f}")
print(f" Context Recall (검색 재현율): {result['context_recall']:.3f}")
print(f" Context Precision (검색 정밀도): {result['context_precision']:.3f}")
print(f" Answer Relevancy (답변 관련성): {result['answer_relevancy']:.3f}")
8.4 결과 해석 및 액션
# 점수별 해석 가이드
def interpret_scores(result):
scores = {
'faithfulness': result['faithfulness'],
'context_recall': result['context_recall'],
'context_precision': result['context_precision'],
'answer_relevancy': result['answer_relevancy']
}
print("\n🎯 개선 방향:")
print("-" * 80)
# Faithfulness 낮음
if scores['faithfulness'] < 0.7:
print("⚠️ Faithfulness 낮음 (< 0.7) → 환각 발생!")
print(" 해결책:")
print(" - temperature를 0으로 설정")
print(" - 프롬프트에 '문서에만 근거' 명시")
print(" - 답변 형식 강제 (출처 필수)")
print()
# Context Recall 낮음
if scores['context_recall'] < 0.7:
print("⚠️ Context Recall 낮음 (< 0.7) → 필요한 정보 못 찾음!")
print(" 해결책:")
print(" - Top-K 늘리기 (5 → 10)")
print(" - 하이브리드 검색 (Dense + BM25)")
print(" - 청킹 크기 조정")
print(" - 쿼리 리라이팅 추가")
print()
# Context Precision 낮음
if scores['context_precision'] < 0.7:
print("⚠️ Context Precision 낮음 (< 0.7) → 노이즈 많음!")
print(" 해결책:")
print(" - Re-ranking 추가")
print(" - Top-K 줄이기")
print(" - 메타데이터 필터링")
print(" - 임베딩 모델 업그레이드")
print()
# Answer Relevancy 낮음
if scores['answer_relevancy'] < 0.7:
print("⚠️ Answer Relevancy 낮음 (< 0.7) → 질문과 답변 불일치!")
print(" 해결책:")
print(" - 프롬프트 개선 (질문 강조)")
print(" - LLM 모델 업그레이드")
print()
# 모두 좋음
if all(score >= 0.8 for score in scores.values()):
print("✅ 모든 메트릭이 우수합니다! (≥ 0.8)")
print(" 현재 시스템을 프로덕션에 배포할 준비가 되었습니다.")
print()
# 실행
interpret_scores(result)
8.5 실전 평가 워크플로우
# 1) 테스트셋 구축 (실제 사용자 질문 수집)
test_questions = [
"우리 회사 연차 규정이 어떻게 돼?",
"출장 중 경비 처리는 어떻게 해?",
# ... 50~100개 질문
]
# 2) RAG 시스템으로 답변 생성
results = []
for q in test_questions:
response = qa_chain.invoke({"query": q})
results.append({
"question": q,
"answer": response['result'],
"contexts": [doc.page_content for doc in response['source_documents']]
})
# 3) 사람이 ground_truth 레이블링 (정답 작성)
# → CSV나 DB에 저장
# 4) 주기적으로 RAGAS 평가 실행
# → 성능 모니터링 대시보드 구축
9. 실전 사용 사례
9.1 사내 문서 기반 Q&A 챗봇
구현 예시:
from langchain.memory import ConversationBufferMemory
from langchain.chains import ConversationalRetrievalChain
# 대화 히스토리 관리
memory = ConversationBufferMemory(
memory_key="chat_history",
return_messages=True,
output_key="answer"
)
# 대화형 RAG
conversational_chain = ConversationalRetrievalChain.from_llm(
llm=llm,
retriever=retriever,
memory=memory,
return_source_documents=True
)
# 연속 대화
print(conversational_chain.invoke({"question": "연차 규정 알려줘"})["answer"])
print(conversational_chain.invoke({"question": "그럼 이월은 가능해?"})["answer"])
# → "그럼"이 앞 대화 맥락을 참조
주요 기능:
- 인사/법무/IT/보안 규정 통합 검색
- 문서 버전 관리 (최신 규정만 검색)
- 권한 기반 필터링 (직급별 접근 제어)
- 출처 링크 제공 (SharePoint/Confluence)
효과:
- 📉 HR 팀 단순 문의 70% 감소
- ⏱️ 직원 정보 검색 시간 80% 단축
- 📚 온보딩 기간 50% 단축
9.2 고객 지원 자동 응답 시스템
아키텍처:
flowchart LR
A[고객 문의] --> B{의도 분류}
B -->|FAQ| C[RAG 검색]
B -->|복잡| D[상담사 연결]
C --> E[자동 답변]
E --> F{고객 만족?}
F -->|Yes| G[티켓 종료]
F -->|No| D
코드 예시: 의도 분류 + RAG
def classify_intent(question):
"""질문 복잡도 분류"""
prompt = f"""
이 질문이 FAQ로 답변 가능한지 판단하세요.
질문: {question}
답변 형식 (JSON):
{{"can_answer_with_faq": true/false, "reason": "이유"}}
"""
response = llm.predict(prompt)
return eval(response) # 실제로는 JSON 파싱
# 워크플로우
def handle_customer_query(question):
# 1) 의도 분류
intent = classify_intent(question)
if intent["can_answer_with_faq"]:
# 2) RAG로 자동 답변
response = qa_chain.invoke({"query": question})
return {
"type": "automated",
"answer": response["result"],
"sources": response["source_documents"]
}
else:
# 3) 상담사 연결
return {
"type": "escalate",
"reason": intent["reason"]
}
# 테스트
print(handle_customer_query("배송 조회 방법은?")) # FAQ 가능
print(handle_customer_query("제품이 고장났는데 AS 받고 싶어요")) # 상담사 필요
실전 팁:
- FAQ + 매뉴얼 + 릴리즈 노트 통합 검색
- 티켓 자동 분류 (긴급도/카테고리)
- 다국어 지원 (번역 + 각 언어별 임베딩)
- 답변 신뢰도 점수 표시 (낮으면 상담사 연결)
9.3 개발자 도구 (DevEx)
사례: "내부 기술 문서 검색 봇"
# 다양한 소스 통합
sources = [
"Architecture Decision Records (ADR)",
"API 문서 (Swagger/OpenAPI)",
"코딩 컨벤션",
"런북 (Runbook)",
"장애 대응 매뉴얼",
"온보딩 가이드"
]
# 코드 검색 특화 임베딩
from langchain.embeddings import HuggingFaceEmbeddings
code_embeddings = HuggingFaceEmbeddings(
model_name="microsoft/codebert-base" # 코드 특화 모델
)
# 메타데이터 필터링
def search_dev_docs(query, filters=None):
"""
filters 예시:
{
"doc_type": "runbook",
"service": "payment-api",
"severity": "critical"
}
"""
retriever = vectorstore.as_retriever(
search_kwargs={"k": 5, "filter": filters}
)
return qa_chain.invoke({"query": query})
# 사용 예시
print(search_dev_docs(
"결제 API 타임아웃 에러 발생 시 대응 방법은?",
filters={"doc_type": "runbook", "service": "payment-api"}
))
주요 질문 유형:
- "이 에러 코드는 무슨 뜻이야?"
- "X 서비스 배포 롤백 절차는?"
- "새 팀원 온보딩 체크리스트 알려줘"
- "API 인증 방식이 어떻게 돼?"
효과:
- 📚 문서 검색 시간 90% 단축
- 🚨 장애 대응 시간 50% 감소
- 👨💻 온보딩 기간 2주 → 3일
9.4 규정/정책 준수 (Compliance)
사례: "규정 위반 사전 예방 시스템"
# 계약서 검토 자동화
def review_contract(contract_text):
"""계약서가 회사 정책을 준수하는지 검토"""
prompt = f"""
아래 계약서 초안이 회사 법무 정책을 위반하는지 검토하세요.
[회사 정책] (검색된 문서)
{{context}}
[계약서 초안]
{contract_text}
출력 형식:
1. 위반 사항: (있으면 명시, 없으면 "없음")
2. 근거 조항: (정책 문서의 해당 조항)
3. 수정 권장사항:
"""
# RAG 체인 실행
review = qa_chain.invoke({"query": prompt})
return review["result"]
# 예시
contract = """
계약 기간: 2024.1.1 ~ 2026.12.31 (3년)
위약금: 계약 금액의 50%
...
"""
print(review_contract(contract))
# 출력:
# 위반 사항: 계약 기간이 정책 위반 (최대 2년)
# 근거 조항: 계약 관리 규정 제5조
# 수정 권장사항: 계약 기간을 2년으로 단축 필요
적용 분야:
- 계약서 자동 검토
- 이메일 발송 전 정보 유출 체크
- 코드 보안 정책 준수 검증
- 규제 대응 (GDPR, 개인정보보호법)
10. 결론: RAG는 "똑똑한 검색 시스템"이다
10.1 핵심 정리
RAG를 한 문장으로 요약하면:
"LLM에게 Google을 쥐어준 것"
더 정확히 말하면:
- LLM = 글쓰기 엔진 (문장 생성의 달인)
- RAG = 검색 시스템 + 글쓰기 엔진 (근거 기반 답변)
10.2 RAG의 진짜 가치
✅ 할 수 있는 것
- 최신 정보 제공: 학습 없이도 새 문서 반영
- 환각 대폭 감소: 근거 문서로 사실 검증 가능
- 출처 투명성: "어디서 가져온 정보인지" 명확
- 도메인 특화: 사내/전문 지식 활용 가능
- 비용 효율적: 재학습보다 훨씬 저렴
❌ 할 수 없는 것
- 완벽한 정확도: 검색 실패 시 답변 불가
- 창의적 생성: 문서 기반이므로 창작 어려움
- 실시간 계산: "현재 주가는?" 같은 API 필요 질문
- 추론: 복잡한 논리/수학 문제는 약함
10.3 성공하는 RAG의 3요소
graph TD
A[성공적인 RAG] --> B[좋은 검색]
A --> C[좋은 생성]
A --> D[좋은 평가]
B --> B1[청킹 전략]
B --> B2[하이브리드 검색]
B --> B3[메타데이터 관리]
C --> C1[프롬프트 설계]
C --> C2[환각 방지]
C --> C3[출처 강제]
D --> D1[RAGAS 메트릭]
D --> D2[사용자 피드백]
D --> D3[지속 개선]
style A fill:#e1f5ff
style B fill:#ffe1e1
style C fill:#e1ffe1
style D fill:#fff4e1
1. 좋은 검색 (70%의 중요도)
- 관련 문서를 못 찾으면 아무리 좋은 LLM도 소용없음
- 청킹 + 임베딩 + 하이브리드 검색 + Re-ranking
2. 좋은 생성 (20%의 중요도)
- 프롬프트로 LLM의 행동 통제
- 근거 강제 + 환각 방지 + 출처 필수
3. 좋은 평가 (10%의 중요도)
- RAGAS로 정량적 측정
- 사용자 피드백으로 정성적 개선
- 지속적인 모니터링과 개선
10.4 RAG vs Fine-tuning: 언제 무엇을 쓸까?
| 상황 | RAG | Fine-tuning |
| 최신 정보 필요 | ✅ 적합 | ❌ 부적합 |
| 문서가 자주 바뀜 | ✅ 적합 | ❌ 부적합 |
| 출처 명시 필요 | ✅ 적합 | ❌ 불가능 |
| 특정 스타일/톤 학습 | ❌ 부적합 | ✅ 적합 |
| 도메인 용어 이해 | △ 가능 | ✅ 더 좋음 |
| 비용 | 💰 저렴 | 💰💰💰 비쌈 |
| 유지보수 | ✅ 쉬움 | ❌ 어려움 |
결론: 대부분의 기업 유스케이스에서는 RAG가 더 적합합니다.
11. 마치며
RAG는 "마법"이 아닙니다. 잘 설계된 검색 시스템입니다.
성공의 열쇠는:
- 📚 좋은 문서 관리 (청킹, 메타데이터, 버전)
- 🔍 정확한 검색 (하이브리드, Re-ranking, 필터링)
- ✍️ 통제된 생성 (프롬프트, 환각 방지, 출처 강제)
- 📊 지속적인 평가 (RAGAS, 모니터링, 개선)
기억하세요:
"RAG 시스템의 품질 = 검색 품질 × 생성 품질"
검색이 0점이면, 아무리 좋은 LLM도 0점입니다.
이제 여러분도 프로덕션 수준의 RAG 시스템을 만들 수 있습니다.
'AI' 카테고리의 다른 글
| 🚀분야별 주요 AI 툴 정리 (75) | 2025.06.01 |
|---|---|
| 구글 Veo AI 완벽 가이드: 영상 제작의 혁신!! (31) | 2025.05.22 |
| Stable Diffusion을 한 방에! Stability Matrix와 함께하는 AI 아트 여정 (24) | 2025.03.27 |
| 무료인데 성능 미쳤다! 활용도 甲 AI 툴 구글 AI 스튜디오 (39) | 2025.03.25 |
| 구글 AI 스튜디오(Google AI Studio)란? (13) | 2025.03.25 |