Home RAG Overview
Post
Cancel

RAG Overview

RAG

목차


RAG의 역할

RAG (Retrieval Augmented Generation)는 LLM이 답변하기 전에 외부 지식 저장소에서 관련 문서를 검색해 프롬프트에 주입하는 기법이다. 할루시네이션을 줄이고 최신 정보를 활용할 수 있다.

1
2
3
4
5
6
7
8
9
10
RAG 없이 (Parametric Knowledge):
Q: "회사 정책은?"
A: "LLM 학습 데이터에만 기반
→ 오래된 정보, 할루시네이션 위험"

RAG 있이 (Retrieved Knowledge):
Q: "회사 정책은?"
→ 사내 정책 문서 검색
→ 최신 정책 문단을 prompt에 포함
A: "최신 정책에 기반한 정확한 답변"

RAG vs Fine-tuning

RAG는 동적 지식 추가(검색), Fine-tuning은 정적 지식 추가(재학습)이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
RAG (Retrieval Augmented Generation):
- 방식: 질문 시 외부 문서 검색
- 시간: 즉시 (밀리초)
- 비용: 저렴 (검색만)
- 업데이트: 즉시 (문서 추가하면 바로 반영)
- 사용 시점: 자주 바뀌는 정보 (뉴스, 정책)

Fine-tuning:
- 방식: 모델 학습
- 시간: 오래 걸림 (몇 시간~몇 일)
- 비용: 비쌈 (GPU, 학습)
- 업데이트: 느림 (재학습 필요)
- 사용 시점: 기본 스타일/능력 조정 (전문 용어, 톤)

RAG 아키텍처

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
1. Indexing (오프라인)
   문서 수집
       ↓
   청킹 (Chunking)
       ↓
   Embedding 변환
       ↓
   Vector DB 저장

2. Retrieval (온라인)
   사용자 질문
       ↓
   질문 Embedding
       ↓
   Vector Search
       ↓
   상위 K개 문서 선택

3. Generation
   검색된 문서 + 질문
       ↓
   프롬프트 구성
       ↓
   LLM 생성

Retrieval

“Retrieval은 어떤 방식으로 동작하는가?”

Retrieval은 사용자 질문과 유사한 문서를 저장소에서 찾아내는 과정이다. 키워드 검색과 의미 기반 검색이 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
키워드 검색 (Keyword Search):
질문: "Python 설치 방법"
→ "Python", "설치" 포함 문서 검색
→ 단어 매칭 (TF-IDF)
장점: 빠르고 정확
단점: 동의어, 의미 변형 못 감지

의미 기반 검색 (Semantic Search):
질문: "Python 설치 방법"
→ 문장 의미를 벡터로 변환
→ 벡터 유사도로 검색
→ "파이썬 세팅", "Python 구성" 등도 찾음
장점: 의미 이해
단점: 느림, 모델 의존

하이브리드 검색

1
2
3
4
5
6
7
8
9
장점만 결합:
1. 키워드 검색으로 후보 선별
2. 벡터 검색으로 순서 정렬
3. 두 결과 통합

예:
질문: "Apache Kafka의 성능 최적화"
키워드: Apache, Kafka, 성능, 최적화
벡터 유사도: "메시지 큐 처리량" 도 포함

“chunk size는 검색 품질에 어떤 영향을 주는가?”

청크 크기는 검색의 정확도와 맥락의 완전성 사이의 균형이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
청크 너무 작음 (예: 50단어):
장점: 정확한 결과
단점: 맥락 부족
→ "이 문장만으로는 이해 어려움"

청크 적당함 (예: 300단어):
장점: 맥락 충분, 정확도 높음
단점: 없음

청크 너무 큼 (예: 2000단어):
장점: 완전한 맥락
단점: 노이즈 많음, 검색 부정확
→ "너무 많은 정보, 관련 없는 내용 포함"

청크 크기 가이드

1
2
3
4
5
6
7
일반 문서: 300-500 단어 (약 1500-2500 글자)
코드 문서: 200-300 단어 (함수 하나 정도)
법률 문서: 500-800 단어 (조항 단위)

오버래핑:
청크 간 겹침 (50-100 단어)
→ 경계 부분에서 맥락 손실 방지

Embedding

“embedding model은 왜 중요한가?”

Embedding Model은 텍스트를 벡터로 변환하는 핵심 요소로, 모델 품질이 검색 성능을 좌우한다.

1
2
3
4
5
6
7
8
9
나쁜 Embedding:
- "강아지"와 "개"를 다른 벡터로 변환
- 의미 유사성 못 포착
→ 검색 실패

좋은 Embedding:
- "강아지"와 "개"를 비슷한 벡터로 변환
- 의미 관계 반영
→ 정확한 검색

인기 있는 Embedding 모델

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
OpenAI text-embedding-3:
- 가격: 유료
- 성능: 최고 수준
- 차원: 1536 또는 256

Sentence Transformers (오픈소스):
- 가격: 무료
- 성능: 좋음
- 차원: 384-768
- 예: all-MiniLM-L6-v2

Cohere Embed:
- 가격: 유료 (저렴)
- 성능: 좋음
- 특징: 동적 차원 조정

기준 선택:
- 비용 민감 → 오픈소스
- 성능 중시 → OpenAI, Cohere
- 특화 도메인 → Fine-tuned 모델

Embedding 구현 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from sentence_transformers import SentenceTransformer

# 모델 로드
model = SentenceTransformer('all-MiniLM-L6-v2')

# 문서 Embedding
documents = [
    "Python은 프로그래밍 언어다",
    "Java도 프로그래밍 언어다",
    "고양이는 동물이다"
]

embeddings = model.encode(documents)
# embeddings[0]: [0.1, -0.2, 0.3, ...] (384 차원)

# 쿼리 Embedding
query = "프로그래밍 언어"
query_embedding = model.encode(query)

# 유사도 계산 (코사인 거리)
from sklearn.metrics.pairwise import cosine_similarity
scores = cosine_similarity([query_embedding], embeddings)
# scores[0]: [0.95, 0.92, 0.15] (높을수록 유사)

“Vector Search는 어떻게 효율적으로 동작하는가?”

Vector Search는 고차원 벡터에서 유사한 벡터를 빠르게 찾는 방식이다. 정확한 매칭이 아니라 근사(approximate) 검색을 사용한다.

1
2
3
4
5
6
7
정확 검색 (Brute Force):
모든 벡터와 비교 (O(n))
→ 정확하지만 느림 (백만개 데이터: 초 단위)

근사 검색 (ANN: Approximate Nearest Neighbor):
Indexing 활용 (O(log n))
→ 빠르지만 약간 부정확 (백만개 데이터: 밀리초)

Vector DB (벡터 데이터베이스)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Pinecone:
- SaaS (서버 관리 필요 없음)
- 가격: 비쌈
- 특징: 서버리스, 스케일링 쉬움

Weaviate:
- 오픈소스 + 클라우드
- 가격: 무료/유료
- 특징: GraphQL API, 하이브리드 검색

Chroma:
- 오픈소스, 경량
- 가격: 무료
- 특징: 임베드 가능, 개발용

Milvus:
- 오픈소스, 대규모
- 가격: 무료
- 특징: 고성능, 엔터프라이즈

Vector Search 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Pinecone 예시
import pinecone

# 초기화
pinecone.init(api_key="xxx", environment="us-west1-gcp")
index = pinecone.Index("documents")

# Embedding 저장
embeddings = [
    ("doc1", [0.1, -0.2, 0.3, ...]),
    ("doc2", [0.15, -0.18, 0.32, ...])
]
index.upsert(vectors=embeddings)

# 검색
query_embedding = [0.12, -0.19, 0.31, ...]
results = index.query(query_embedding, top_k=5)
# results: [("doc1", 0.98), ("doc2", 0.95), ...]

Context Injection

“검색된 문서를 프롬프트에 어떻게 삽입할 것인가?”

Context Injection은 검색 결과를 LLM 프롬프트에 구조화된 방식으로 포함하는 방법이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
나쁜 삽입:
Q: "회사 휴가 정책?"
Context 포함한 Prompt:
"여기 무작위 문서들이 있어. 답변해.
[문서1: PDF 원본 그대로]
[문서2: 관계없는 텍스트]
..."
→ 노이즈 많음

좋은 삽입:
Q: "회사 휴가 정책?"
Context 포함한 Prompt:
"다음은 회사 정책 문서 발췌다:

## 휴가 정책
- 연차: 15일
- 사용 방법: 인사팀 신청
- 기한: 연말까지 미사용 폐기

이를 기반으로 질문에 답해."
→ 명확하고 구조화됨

효과적 Context 구성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1. 명확한 구분
   "다음은 검색된 문서다:"
   "---"
   [검색 결과]
   "---"

2. 출처 표시
   "[문서: policy.md]"
   "[페이지: 3]"
   → 할루시네이션 추적 용이

3. 우선순위 정렬
   가장 관련 높은 것부터
   → 토큰 제한 시 상위 것부터 포함

4. 길이 제한
   각 문서 200-300 단어로 자르기
   → Context window 효율성

구현 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def build_rag_prompt(query, retrieved_docs):
    context = ""
    for i, doc in enumerate(retrieved_docs, 1):
        context += f"[문서 {i}: {doc['source']}]\n"
        context += f"{doc['content'][:300]}\n\n"  # 300단어 자르기
    
    prompt = f"""다음은 관련 문서 발췌다:

{context}

---

사용자 질문: {query}

위 문서를 기반으로 정확하게 답변해."""
    
    return prompt

Grounding

“Grounding은 왜 중요한가?”

Grounding은 LLM의 답변이 검색된 문서에 기반하도록 강제해 할루시네이션을 줄인다.

1
2
3
4
5
6
7
8
9
10
Grounding 없이:
Q: "회사 정책은?"
A: "회사는 유연한 근무를 지원합니다
    (학습 데이터 기반, 틀릴 가능성)"

Grounding 있이:
Q: "회사 정책은?"
A: "[문서: policy.md에 따르면]
    회사는 주 3일 재택근무를 지원합니다"
    (검색된 문서 명시)

Grounding 구현

1
2
3
4
5
6
7
8
9
10
11
1. 명시적 출처 요구
   Prompt: "답변할 때 출처를 명시해"
   A: "[HR 정책 v2.1] 유연근무..."

2. Citation 강제
   Prompt: "답변을 [문서 1, 문서 2]로 뒷받침해"
   A: "[1] 첫 번째 정보 [2] 두 번째 정보"

3. 모른다 강제
   Prompt: "문서에 없으면 '모른다'고 말해"
   A: "문서에 명시되지 않아 알 수 없습니다"

“retrieved context가 틀리면 답변은 어떻게 망가지는가?”

검색 결과가 부정확하면 LLM은 이를 신뢰하고 부정확한 답변을 생성한다. 이것이 RAG의 약점이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
상황: 가격 정보 검색
검색 결과 (잘못됨):
"제품 A 가격: $100"
(실제: $200)

LLM 답변:
"제품 A는 $100입니다" (잘못된 정보 전파)

예방책:
1. 검색 정확도 향상
   - 더 나은 Embedding 모델
   - 더 나은 청크 크기
   
2. 재검증
   - 중요한 정보는 별도 검증
   - 출처 확인
   
3. Prompt 강화
   - "정보가 불확실하면 표시해"
   - "모순을 감지하면 언급해"

Evaluation

“RAG 성능을 어떻게 평가할 것인가?”

RAG 평가는 Retrieval 품질과 Generation 품질을 분리해서 측정해야 한다.

1
2
3
4
5
6
7
8
9
전체 평가만으로는:
Q: "A는?"
A: "B입니다" (틀렸음)

→ 검색 잘못? 생성 잘못? 알 수 없음

분리 평가:
1. Retrieval 평가: 관련 문서가 검색되었나?
2. Generation 평가: 검색된 문서를 잘 활용했나?

Retrieval 평가

1
2
3
4
5
6
7
8
9
10
11
Recall@K:
상위 K개 중 관련 문서의 비율
→ K=5일 때 5개 중 몇 개가 관련있나?

NDCG (Normalized Discounted Cumulative Gain):
순서를 고려한 평가
→ 1위가 틀리면 더 큰 페널티

MRR (Mean Reciprocal Rank):
첫 정답 위치의 역수 평균
→ 첫 정답이 3위면 1/3

Generation 평가

1
2
3
4
5
6
7
8
9
10
11
12
13
BLEU Score:
생성 텍스트와 정답의 n-gram 일치도
(기계 번역용, RAG에서는 덜 적합)

ROUGE Score:
회수 기반 평가
→ 요약 작업에 유용

Human Evaluation:
1. 정확도: 정보가 맞나?
2. 완전성: 질문을 모두 답했나?
3. 신뢰성: 출처가 명확한가?
4. 가독성: 이해하기 쉬운가?

평가 자동화 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def evaluate_rag(questions, retrieved_docs, answers):
    results = {
        'retrieval': [],
        'generation': []
    }
    
    # Retrieval 평가
    for q, docs in zip(questions, retrieved_docs):
        relevant_count = sum(1 for d in docs if is_relevant(q, d))
        recall = relevant_count / len(docs)
        results['retrieval'].append(recall)
    
    # Generation 평가
    for ans, true_ans in zip(answers, true_answers):
        bleu = calculate_bleu(ans, true_ans)
        rouge = calculate_rouge(ans, true_ans)
        results['generation'].append({'bleu': bleu, 'rouge': rouge})
    
    return results

정리

개념설명
Retrieval질문과 유사한 문서 검색
Embedding텍스트를 벡터로 변환
Vector Search벡터 유사도로 검색
Context Injection검색 결과를 프롬프트에 삽입
Grounding답변을 검색 문서에 기반하게
EvaluationRAG 성능 평가

This post is licensed under CC BY 4.0 by the author.