🧠 RAG 시스템 CS 지식 정리

시리즈


📑 목차


1. 전체 파이프라인

[문서 349개]
     │
     ▼ (1) 청킹
[청크 5337개]
     │
     ▼ (2) 임베딩
[벡터 5337개]  ──────▶  [ChromaDB]
                             │
                             │ (3) 유사도 검색
                             ▼
                    "kubectl pod" ──▶ [관련 청크 5개]
                                            │
                                            ▼ (4) LLM
                                        [답변 생성]

2. 청킹 (Chunking)

핵심 개념

긴 문서를 작은 조각(청크)으로 분할하는 과정

💡 왜 필요한가?

문제:
├─ LLM 컨텍스트 제한 (32K, 128K 등)
├─ 긴 문서 통째로 → 비효율적
└─ 검색 정밀도 떨어짐

해결:
└─ 문서를 작은 조각(청크)으로 분할

📋 우리가 쓴 방식: 헤딩 기반

# 원본 문서
"""
# 제목
내용...
 
## 섹션1
내용...
 
## 섹션2
내용...
"""
 
# 청킹 결과
청크1: "# 제목\n내용..."
청크2: "## 섹션1\n내용..."
청크3: "## 섹션2\n내용..."

📊 청킹 방식 비교

방식장점단점
고정 길이 (500자씩)단순함문맥 끊김
헤딩 기반 (우리 방식)의미 단위 유지크기 불균일
시맨틱 (의미 분석)가장 정확느림, 복잡

💻 코드 흐름

# src/rag/chunker.py 핵심 로직
 
def chunk_document(content, file_path):
    # 1. frontmatter 분리
    metadata, body = split_frontmatter(content)
 
    # 2. 헤딩(##) 기준으로 자르기
    sections = re.split(r'^(#{1,3}\s+.+)', body)
 
    # 3. 각 섹션을 Chunk 객체로
    for section in sections:
        if len(section) > min_size:
            chunks.append(Chunk(
                content=section,
                doc_path=file_path,
                metadata=metadata
            ))
 
    return chunks

3. 임베딩 (Embedding)

핵심 개념

텍스트를 숫자 벡터로 변환하는 과정. 의미가 비슷하면 벡터도 가까움.

💡 비유

텍스트 → 숫자 벡터 변환 = 도서관 책에 바코드 붙이기

"kubectl pod 삭제"
    ↓
[0.12, -0.45, 0.78, 0.33, ..., 0.21]  (384차원)

왜?
├─ 컴퓨터는 텍스트 직접 비교 못함
├─ 벡터로 바꾸면 "거리" 계산 가능
└─ 의미가 비슷하면 벡터도 가까움

📊 의미 공간 시각화

                의미 공간 (2D로 단순화)

    "kubectl"  ●
                  ╲
                   ╲  가까움!
                    ╲
"pod 삭제"  ●────────● "kubernetes 파드"



"terraform"  ●                 ● "AWS EC2"
               ╲             ╱
                ╲  중간 거리╱
                 ╲       ╱
                  ● "인프라"

📋 우리가 쓴 모델

# sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2
 
# 특징:
# ├─ 다국어 지원 (한국어 OK)
# ├─ 384차원 벡터 출력
# ├─ 빠름 (소형 모델)
# └─ 품질은 중간 (트레이드오프)
 
embedder.encode("kubectl pod")
# → [0.12, -0.45, 0.78, ...] (384개 숫자)

📊 임베딩 모델 비교

모델차원속도품질
MiniLM (우리 것)384빠름중간
BGE-large1024중간좋음
OpenAI text-embedding1536API최상
Cohere multilingual1024API좋음

나중에 모델 바꿔서 품질 비교 가능!


4. 벡터DB (ChromaDB)

핵심 개념

벡터 전용 데이터베이스. 인덱싱 + 빠른 유사도 검색.

💡 왜 필요한가?

문제:
├─ 5337개 벡터 중 비슷한 것 찾기
├─ 매번 전체 비교? → O(n) 느림
└─ 메모리에 다 올리기? → 비효율

해결:
└─ 벡터 전용 DB (인덱싱 + 빠른 검색)

📋 작동 원리

저장 시:
┌─────────────────────────────────────────────┐
│  ChromaDB                                   │
│                                             │
│  chunk_0: [0.1, 0.2, ...] → "kubectl..."   │
│  chunk_1: [0.3, 0.1, ...] → "terraform..." │
│  chunk_2: [0.2, 0.4, ...] → "docker..."    │
│  ...                                        │
│  chunk_5336: [...]                          │
│                                             │
│  + 인덱스 구조 (빠른 검색용)               │
└─────────────────────────────────────────────┘

검색 시:
query: "kubectl pod"
    ↓
query_vector: [0.12, 0.21, ...]
    ↓
ChromaDB: "이 벡터와 가장 가까운 5개 찾아!"
    ↓
[chunk_42, chunk_156, chunk_789, ...]

📊 코사인 유사도

두 벡터가 얼마나 비슷한지 = 각도로 측정

        A
       /
      /  θ (각도)
     /
    ●─────────── B

cos(θ) = 1  → 완전 같은 방향 (동일)
cos(θ) = 0  → 직각 (무관)
cos(θ) = -1 → 반대 방향 (반대 의미)
# 코드로:
similarity = dot(A, B) / (norm(A) * norm(B))

💻 우리 코드 흐름

# src/rag/vectordb.py
 
# 저장
collection.add(
    ids=["chunk_0", "chunk_1", ...],
    embeddings=[[0.1, 0.2], [0.3, 0.1]],
    documents=["kubectl...", "terraform..."],
    metadatas=[{"title": "..."}, {...}]
)
 
# 검색
results = collection.query(
    query_embeddings=[[0.12, 0.21, ...]],
    n_results=5
)
# → 가장 가까운 5개 청크 반환

5. 전체 검색 흐름

사용자: "kubectl pod 삭제 방법"
            │
            ▼
┌─────────────────────────────────┐
│  1. 쿼리 임베딩                 │
│  "kubectl pod 삭제 방법"        │
│      ↓                          │
│  [0.12, -0.45, 0.78, ...]      │
└─────────────────────────────────┘
            │
            ▼
┌─────────────────────────────────┐
│  2. 벡터DB 검색                 │
│  ChromaDB에서 가까운 벡터 찾기  │
│      ↓                          │
│  상위 5개 청크 ID 반환          │
└─────────────────────────────────┘
            │
            ▼
┌─────────────────────────────────┐
│  3. 결과 반환                   │
│  chunk_42: "kubectl delete..."  │
│  chunk_156: "pod 삭제 시..."    │
│  ...                            │
└─────────────────────────────────┘
            │
            ▼
┌─────────────────────────────────┐
│  4. (나중에) LLM 답변 생성      │
│  Context + Question → Answer    │
└─────────────────────────────────┘

6. 현재 문제점과 개선 방향

⚠️ 문제 1: frontmatter가 검색됨

원인: 청킹 시 "---" 포함된 채로 저장됨
해결: 청킹 전에 frontmatter 완전 제거

⚠️ 문제 2: 관련 없는 문서 검색됨

원인:
├─ 임베딩 모델이 기술용어에 약함
├─ "CrashLoopBackOff" 같은 용어 → 일반적 의미로 해석
└─ 한국어+영어 혼합 → 임베딩 품질 저하

해결 (나중에):
├─ 더 좋은 임베딩 모델 (BGE, OpenAI 등)
├─ 기술용어 사전 추가
└─ 하이브리드 검색 (벡터 + 키워드)

⚠️ 문제 3: 검색 정밀도

현재: "의미적으로 비슷한" 것만 찾음

개선 방향:
├─ 키워드 매칭 병행 (BM25)
├─ 메타데이터 필터링 (category로 제한)
└─ Re-ranking (검색 후 재정렬)

7. 핵심 개념 요약

개념한 줄 설명비유
청킹긴 문서 → 작은 조각으로 분할책을 챕터별로 분리
임베딩텍스트 → 숫자 벡터 (의미 보존)도서관 책에 바코드 붙이기
벡터DB벡터 저장 + 빠른 유사도 검색바코드 스캔기 달린 서가
코사인 유사도두 벡터가 얼마나 같은 방향인지두 화살표의 각도
RAG검색(Retrieval) + 생성(Generation)도서관에서 찾아서 요약해주는 사서

8. 딥다이브: 임베딩이 “의미”를 보존하는 원리

💡 비유: 의미 세계의 지도 좌표

현실 세계:
"서울" "부산" "대전" "대구" → 그냥 글자

지도 (2D 좌표):
서울 (126.9, 37.5)
부산 (129.0, 35.1)
대전 (127.4, 36.3)

→ 좌표로 바꾸니까 "거리" 계산 가능!
→ 서울-대전 vs 서울-부산 어디가 가까운지 알 수 있음

임베딩 = "의미 세계의 지도 좌표"

📋 Transformer Encoder 작동 과정

입력: "kubectl pod delete"

Step 1: 토큰화 (Tokenization)
─────────────────────────────
"kubectl pod delete"
       ↓
["[CLS]", "kube", "##ctl", "pod", "delete", "[SEP]"]

* [CLS]: 문장 시작 토큰 (나중에 전체 의미 담음)
* ##ctl: 서브워드 (kubectl을 kube + ctl로 분리)

Step 2: 토큰 → 초기 벡터 (Token Embedding)
─────────────────────────────────────────
각 토큰을 룩업 테이블에서 벡터로 변환

[CLS]   → [0.1, 0.2, 0.3, ..., 0.5]  (384차원)
kube    → [0.4, 0.1, 0.7, ..., 0.2]
##ctl   → [0.2, 0.3, 0.1, ..., 0.8]
pod     → [0.6, 0.4, 0.2, ..., 0.3]
delete  → [0.3, 0.5, 0.4, ..., 0.1]

* 이 단계에선 아직 "문맥" 반영 안 됨
* "pod"가 kubernetes pod인지 pea pod인지 모름

Step 3: Self-Attention (핵심!)
─────────────────────────────
"각 단어가 다른 단어들을 얼마나 참고할지 계산"

         [CLS]  kube   ##ctl   pod    delete
[CLS]    0.1    0.2    0.1    0.3    0.3
kube     0.1    0.4    0.3    0.1    0.1     ← Attention
##ctl    0.1    0.3    0.4    0.1    0.1        Weights
pod      0.2    0.3    0.1    0.2    0.2
delete   0.2    0.2    0.1    0.3    0.2

예: "pod" 입장에서
→ "kube"를 0.3만큼 참고 (아 kubernetes 관련이구나)
→ "delete"를 0.2만큼 참고 (삭제하려는 거구나)

결과: 문맥이 반영된 새로운 벡터들

Step 4: 여러 층 통과 (12개 레이어)
─────────────────────────────────
Layer 1: 기본 문법/구조 파악
   ↓
Layer 2-4: 단어 관계 파악
   ↓
Layer 5-8: 의미적 관계 파악
   ↓
Layer 9-12: 고수준 의미 추상화

* 층이 깊어질수록 추상적인 의미 학습

Step 5: [CLS] 토큰 추출 → 최종 임베딩
────────────────────────────────────
[CLS] 토큰의 최종 벡터 = 문장 전체 의미

"kubectl pod delete"
       ↓
[0.42, -0.31, 0.78, 0.15, ..., 0.56]  (384차원)

이 벡터 하나가 전체 문장의 "의미 좌표"

📋 왜 비슷한 의미 = 가까운 벡터? (Contrastive Learning)

학습 데이터 예시:
┌─────────────────┬─────────────────┬────────┐
│     문장 A      │     문장 B      │ 관계   │
├─────────────────┼─────────────────┼────────┤
│ kubectl delete  │ k8s pod 삭제    │ 유사   │
│ kubectl delete  │ 오늘 날씨 좋다  │ 무관   │
│ terraform plan  │ tf 변경사항 확인│ 유사   │
└─────────────────┴─────────────────┴────────┘

학습 목표:
유사한 쌍 → 벡터 거리 가깝게! (당기기)
무관한 쌍 → 벡터 거리 멀게!   (밀기)

학습 전:                        학습 후:

  kubectl●    ●terraform         kubectl●───●k8s pod
              ●k8s pod                   (가까워짐)
  날씨●       ●pod삭제
                                 terraform●───●tf plan
  (의미와 무관하게 흩어져있음)           (가까워짐)

                                 날씨●
                                      (멀리 떨어짐)
# 단순화된 Contrastive Loss 개념
 
def contrastive_loss(anchor, positive, negatives):
    """
    anchor: "kubectl pod delete" 벡터
    positive: "k8s 파드 삭제" 벡터 (유사 문장)
    negatives: ["오늘 날씨", "맛집 추천", ...] 벡터들
    """
    # 유사한 건 가깝게 (거리 최소화)
    pos_distance = distance(anchor, positive)
 
    # 무관한 건 멀게 (거리 최대화)
    neg_distances = [distance(anchor, neg) for neg in negatives]
 
    # 손실 = 양성 거리 - 음성 거리들의 평균
    loss = pos_distance - mean(neg_distances)
    return loss

📊 의미가 인코딩되는 과정 (차원별)

384차원 = 384개의 "의미 축"

(실제로 이렇게 깔끔하진 않지만, 개념적으로:)

차원 1-50:   기술 분야 (인프라/앱/데이터/...)
차원 51-100: 동작 유형 (생성/삭제/조회/수정/...)
차원 101-150: 대상 종류 (서버/컨테이너/파일/...)
차원 151-200: 긍부정/감정
차원 201-300: 문법/구조적 특성
차원 301-384: 미세한 의미 차이들

예시:
"kubectl delete pod"   "terraform destroy"
 ↓                      ↓
[인프라:0.9]           [인프라:0.9]      ← 비슷
[삭제:0.95]            [삭제:0.90]       ← 비슷
[컨테이너:0.8]         [클라우드:0.7]    ← 좀 다름
[K8s:0.9]              [IaC:0.85]        ← 다름

→ 전체적으로 비슷한 방향 = 코사인 유사도 높음

💻 면접 답변 템플릿: 임베딩 원리

Q: "임베딩이 어떻게 의미를 보존하나요?"

A: “세 단계로 설명드리겠습니다. 첫째, Transformer의 Self-Attention이 단어들 간의 관계를 파악합니다. ‘pod’라는 단어가 ‘kubectl’ 옆에 있으면 kubernetes 컨텍스트임을 인식합니다. 둘째, 학습 과정에서 ‘비슷한 의미의 문장 쌍’을 가깝게, ‘다른 의미’는 멀게 배치하도록 학습합니다. 이걸 Contrastive Learning이라고 합니다. 셋째, 결과적으로 384차원 벡터의 각 차원이 ‘의미의 특성’을 담게 되어, 비슷한 의미는 비슷한 벡터 좌표를 갖게 됩니다. 마치 지도에서 가까운 도시들이 비슷한 좌표를 갖는 것과 같은 원리입니다.”


9. 딥다이브: 벡터DB가 빠르게 찾는 원리 (HNSW)

💡 Brute-Force vs ANN

Brute-Force (전수 조사):
검색 쿼리 Q → 벡터 5337개 전부 거리 계산
→ 정확도: 100%
→ 속도: O(n) - 데이터 많아지면 선형 증가

  5천 개: ~5ms
  50만 개: ~500ms (0.5초)
  5천만 개: ~50초

ANN (Approximate Nearest Neighbor):
핵심 아이디어: "전부 안 봐도 대충 어디쯤인지 알 수 있잖아"

비유: 도서관에서 책 찾기
Brute-Force: 1번 책부터 10만 번까지 다 봄
ANN: "컴퓨터 책이니까 500번대 서가로 가자" → 500~600번대만 봄

→ 속도: O(log n) - 데이터 많아져도 느리게 증가

  5천 개: ~1ms
  50만 개: ~3ms
  5천만 개: ~10ms

📋 HNSW 알고리즘 (ChromaDB 기본)

HNSW = Hierarchical Navigable Small World

Small World 개념

“6단계 분리 이론” - 지구상 아무나 두 사람 → 6명 거치면 연결됨. 벡터에도 적용: 아무 벡터 두 개 → 몇 번 점프하면 도달 가능하게 연결.

HNSW 계층 구조:

Layer 2 (최상위, 적은 노드, 긴 연결)
════════════════════════════════════
        ●─────────────────────●
        A                     B

Layer 1 (중간, 더 많은 노드)
════════════════════════════════════
        ●───────●─────────────●───────●
        A       C             B       D

Layer 0 (최하위, 모든 노드, 짧은 연결)
════════════════════════════════════
    ●───●───●───●───●───●───●───●───●───●
    A   E   C   F   G   B   H   D   I   J

* 위로 갈수록: 노드 적음, 연결 김 (고속도로)
* 아래로 갈수록: 노드 많음, 연결 짧음 (골목길)

📋 검색 과정

쿼리: Q

Step 1: 최상위 레이어에서 시작
═════════════════════════════
Layer 2:    ●──────────────────●
            A        Q→        B
            ↑
       여기서 시작

"A와 B 중 Q에 가까운 건? → B!"  → B로 이동

Step 2: 한 단계 아래로
═════════════════════════════
Layer 1:    ●───────●──────────●───────●
            A       C    Q→    B       D

"B의 이웃 C, D 중 Q에 가까운 건? → B 그대로!"

Step 3: 최하위 레이어에서 정밀 탐색
═════════════════════════════
Layer 0:  ●───●───●───●───●───●───●───●───●───●
          A   E   C   F   G   B   H   D   I   J
                          ↑   ↑
                          Q   B에서 시작

"B의 이웃 G, H 확인 → G가 제일 가깝다!"
"G의 이웃도 확인 → F 확인 → G가 여전히 최고"

→ 결과: G (단 몇 번의 비교로 찾음!)

⚠️ 왜 “근사(Approximate)“인가?

HNSW가 G를 찾았는데, 사실 F가 더 가까웠을 수 있음!

왜?
→ 상위 레이어에서 B로 갔는데
→ F는 C 방향에 있었음
→ 한 번 B로 가면 F 쪽은 안 봄

파라미터로 정확도/속도 조절:

ef_search (탐색 범위):
├─ 작은 값 (16): 빠름, 덜 정확 (95% 정확도)
├─ 중간 값 (64): 균형 (98% 정확도)
└─ 큰 값 (256): 느림, 더 정확 (99.5% 정확도)

대부분 상황에서 98% 정확도면 충분!

📊 규모별 성능 비교

데이터 개수Brute-ForceHNSW정확도
5,0005ms1ms99%
50,00050ms2ms98%
500,000500ms4ms97%
5,000,0005초8ms96%
50,000,00050초15ms95%

📋 우리 시스템 (5337개)에서는?

청크 수: 5,337개 / 차원: 384

검색 시간:
├─ Brute-Force: ~5ms (사실 이것도 괜찮음)
└─ HNSW: ~1ms

왜 HNSW 쓰나?
→ 지금은 차이 적지만
→ 문서 늘어나면 (5만, 50만) 차이 커짐
→ 미리 확장 가능한 구조로

예상 확장 시나리오:
현재:     5,337 청크 → 1ms
1년 후:   50,000 청크 → 2ms   (문서 계속 추가)
3년 후:   500,000 청크 → 4ms  (팀 전체 문서)

💻 면접 답변 템플릿: 벡터DB 검색 원리

Q: "벡터DB가 어떻게 빠르게 검색하나요?"

A: “ANN, 특히 HNSW 알고리즘을 사용합니다. 모든 벡터를 다 비교하면 O(n)이라 데이터가 많아지면 느려집니다. HNSW는 벡터들을 계층적 그래프로 연결해서, 위에서 아래로 탐색하며 좁혀갑니다. 마치 고속도로로 대략적 위치 이동 후 골목길로 정밀 탐색하는 것과 같습니다. 덕분에 O(log n) 복잡도로, 5천만 개 데이터도 10-15ms에 검색됩니다. 다만 100% 정확하진 않고 약 95-99% 정확도인데, 대부분의 RAG 사용 케이스에서는 충분합니다.”


10. 핵심 요약 치트시트

임베딩 원리:
├─ Transformer가 Self-Attention으로 문맥 파악
├─ Contrastive Learning으로 유사=가깝게 학습
└─ 384차원 = 384개의 의미 특성 축

벡터DB 원리:
├─ Brute-Force: O(n), 정확, 느림
├─ ANN (HNSW): O(log n), 근사, 빠름
├─ 계층적 그래프로 "점프"하며 탐색
└─ 95-99% 정확도로 1000배 빠름

트레이드오프:
├─ 임베딩 차원: 높을수록 정확 but 느림/용량 큼
├─ ef_search: 높을수록 정확 but 느림
└─ 대부분 기본값으로 충분

📋 심화 학습 주제

더 깊게 파고 싶다면

  • 코사인 유사도 수학적 의미 → 선형대수 내적
  • 하이브리드 검색 → BM25 + Dense Retrieval 조합
  • Re-ranking 알고리즘 → Cross-Encoder 기반 재정렬
  • Q&A 데이터셋 생성 → Week 3 주제

한 줄 요약

“텍스트를 숫자로 바꾸고, 비슷한 숫자를 빠르게 찾고, AI한테 정리시킨다 = RAG”