⚡ AI 인텔리전스 레이더
🧪 전문가반 · 코드로 만드는 AI 엔지니어

20회차 · RAG를 코드로 — 임베딩·벡터검색에서 챗봇·평가까지

백두산 (Doosan Baek) · 게시 2026-06-13 · 최종 수정 2026-07-10

이 회차에서 배우는 것

🎯 목표'내 자료에서 관련 부분 찾기'를 코드로
🧮 임베딩글의 의미를 숫자 좌표로 (11회차 개념 → 코드)
✂️ 청킹문서를 의미 단위로 자르는 전략
🗂️ 벡터 검색코사인 유사도·top-k, 로컬 벡터DB
✅ 검색 완료 기준자연어 질문으로 내 자료 top-k를 근거와 반환
🔗 RAG 파이프라인retrieve → augment → generate
🏅 리랭킹·하이브리드검색 품질 끌어올리기
📊 첫 평가Ragas로 품질을 숫자로 (평가 입문)
✅ 챗봇 완료 기준청킹 바꿨을 때 점수 변화를 표로 비교

11회차에서 RAG·임베딩·벡터DB를 '개념'으로 봤죠('오픈북 시험' 비유). 이번엔 그걸 코드로 직접 만듭니다. 만들 것: 내 문서(메모·PDF에서 뽑은 텍스트 등)를 임베딩해 '비슷한 부분 찾기' 검색기 — 이 회차 후반부 챗봇의 '검색' 절반입니다. 자료는 레포가 아니라 '여러분 자신의 문서'를 씁니다.

❓ 그냥 GPT에 자료 던지고 물으면 안돼?

아주 자연스러운 질문이고 — 자료가 적으면 그게 정답입니다. 문서 몇 개면 그냥 ChatGPT 파일 업로드(1회차)·Claude Projects·NotebookLM(2회차)에 넣고 물으면 끝이에요. RAG가 항상 필요한 건 아닙니다.

그런데 '그냥 던지기'는 규모가 커지면 네 군데서 깨집니다 — 그때 RAG(임베딩·벡터검색)가 필요해져요.

⚙️ 먼저 환경 한 번에 — 설치 & API 키

아래 코드들이 쓰는 라이브러리와 키를 여기 한곳에 모았습니다. 한 번 해두면 이 회차 코드가 다 돕니다(겁먹지 말고 복붙). 임베딩은 OpenAI API를 쓰니 키가 필요해요.

# 1) 라이브러리 설치 (터미널에서 한 번)
pip install openai chromadb pypdf python-dotenv rank-bm25 sentence-transformers ragas

# 2) API 키 — 프로젝트 폴더에 .env 파일을 만들고 아래 한 줄
#    (코드에서 python-dotenv가 자동으로 읽어 OpenAI가 인식)
OPENAI_API_KEY=sk-여기에-본인-키

🧮 임베딩 — 의미를 숫자로 (복습 + 코드)

11회차 요약: 뜻이 비슷하면 좌표도 가깝다('강아지'와 '개'는 근처). 코드에선 임베딩 API/모델로 텍스트를 숫자 벡터로 바꿉니다. 질문도 같은 방식으로 벡터화해, 가까운 청크를 찾으면 그게 '의미로 검색(시맨틱 검색)'이에요.

# embed.py — 텍스트를 벡터로 (예: OpenAI 임베딩)
from openai import OpenAI
client = OpenAI()

def embed(text: str) -> list[float]:
    r = client.embeddings.create(model='text-embedding-3-small', input=text)
    return r.data[0].embedding   # 숫자 벡터(예: 1536차원)

📄 문서 로딩 — PDF·txt를 텍스트로 (실무 1단계)

임베딩하려면 먼저 '글자'가 있어야 합니다. PDF·워드는 그냥 열면 글자가 안 나오니, 텍스트만 뽑아내는 단계가 필요해요 — 실무에서 손이 제일 많이 가는 곳입니다. txt는 그냥 읽고, PDF는 pypdf로 페이지마다 글자를 긁어옵니다.

# loader.py — 파일에서 텍스트만 뽑기
from pathlib import Path
from pypdf import PdfReader   # pip install pypdf

def load_text(path: str) -> str:
    p = Path(path)
    if p.suffix.lower() == '.pdf':
        reader = PdfReader(path)
        return '\n'.join((pg.extract_text() or '') for pg in reader.pages)
    return p.read_text(encoding='utf-8')   # .txt / .md 등

✂️ 청킹 — 자르는 방식이 품질을 좌우

# chunk.py — 문단 단위로 자르되, 단어 몇 개씩 겹치기(오버랩)
def chunk(text: str, size=500, overlap=80) -> list[str]:
    words = text.split()
    out, i = [], 0
    while i < len(words):
        out.append(' '.join(words[i:i + size]))   # size개 단어 한 조각
        i += size - overlap                        # overlap만큼 되돌려 겹치기
    return out

# size·overlap 숫자를 바꿔보며 검색 품질을 비교하는 게 이 회차 과제

🗂️ 벡터 검색 — 가까운 것 빨리 찾기

질문 벡터와 각 청크 벡터의 '코사인 유사도'를 재서 가장 가까운 top-k를 고릅니다. 양이 적으면 numpy로 직접, 많아지면 로컬 벡터DB(Chroma·pgvector — 11회차에 나온 이름들)로 넘어갑니다.

# search.py — 가장 비슷한 top-k 청크 찾기 (간단 버전)
import numpy as np

def cosine(a, b):
    a, b = np.array(a), np.array(b)
    return float(a @ b / (np.linalg.norm(a) * np.linalg.norm(b)))

def top_k(query_vec, chunks, k=3):
    scored = [(c['text'], cosine(query_vec, c['vec'])) for c in chunks]
    scored.sort(key=lambda x: x[1], reverse=True)
    return scored[:k]   # (청크, 점수) 상위 k개

🗄️ 벡터DB 셋업 — Chroma에 저장하고 다시 안 만들기

numpy 버전은 매번 켤 때마다 전부 다시 임베딩합니다(느리고 돈 듦). Chroma의 PersistentClient는 결과를 디스크에 저장해, 두 번째 실행부터는 재임베딩 없이 바로 검색해요. 출처(source)도 같이 넣어 두면 답에 근거를 표시할 수 있습니다.

# store.py — Chroma에 저장(디스크 persist) & 검색
import chromadb
from embed import embed

client = chromadb.PersistentClient(path='./chroma_db')   # 이 폴더에 저장
col = client.get_or_create_collection('docs')            # 있으면 재사용

def index(chunks, source):   # chunks: 문자열 리스트, source: 문서명(출처)
    col.add(
        ids=[f"{source}-{i}" for i in range(len(chunks))],
        embeddings=[embed(c) for c in chunks],   # 미리 만든 벡터를 그대로
        documents=chunks,
        metadatas=[{'source': source} for _ in chunks],
    )

def search(question, k=3):
    r = col.query(query_embeddings=[embed(question)], n_results=k)
    docs, metas = r['documents'][0], r['metadatas'][0]
    return [{'text': t, 'source': m['source']} for t, m in zip(docs, metas)]

🔧 만들 것 — '내 자료 검색기'

  1. 내 문서를 모아 텍스트로 → 청킹(문단 단위 + 약간 오버랩)
  2. 각 청크를 embed()로 벡터화해 저장(파일 또는 Chroma)
  3. 질문을 받아 벡터화 → top_k로 관련 청크를 점수·출처와 함께 반환

✅ 완료 기준

📌 1부 정리 — 임베딩·벡터검색

② 챗봇 & 첫 평가 — 검색기에 두뇌를 붙이기

여기까지가 '검색' 토대였습니다. 이제 이 검색기에 19회차 LLM 호출을 합쳐 '내 자료로 답하는 챗봇'을 완성하고, 처음으로 품질을 '숫자로' 재봅니다 — 뒤에 나올 평가 심화의 맛보기예요. '답이 좋아 보인다'는 함정에서 벗어나는 첫걸음.

🔗 RAG 파이프라인 — 세 단계

# rag.py — 검색결과를 근거로 답하기 (augment 프롬프트)
PROMPT = '''너는 사내 문서 비서다. 아래 [근거]만 사용해 답하라.
근거에 없으면 '자료에 없음'이라고 답하라. 답 끝에 사용한 출처를 표기하라.

[근거]
{context}

[질문]
{question}'''

context = '\n\n'.join(f"({c['source']}) {c['text']}" for c in hits)
answer = ask(PROMPT.format(context=context, question=q))   # 19회차 ask() 재사용

🏅 검색 품질 올리기 — 리랭킹·하이브리드

말로만 보면 막연하니, '이렇게 켠다' 실전 포인터를 하나씩 — 둘 다 검색을 살짝 손보는 작은 추가입니다(전체를 다시 안 짜도 됨):

  1. 리랭킹 켜기 — 벡터 검색으로 top-20처럼 넉넉히 뽑은 뒤, 크로스인코더 reranker로 다시 점수 매겨 상위 3개만 남기기. 'pip install sentence-transformers' → CrossEncoder('BAAI/bge-reranker-v2-m3')에 (질문, 청크) 쌍을 .predict()하면 관련도 점수가 나옵니다(로컬·무료, CPU도 OK).
  2. 하이브리드 켜기 — 'pip install rank-bm25'로 키워드 점수(BM25Okapi.get_scores)를 따로 구해, 벡터 점수와 합산(또는 순위 합치기)해 정렬. 고유명사·코드·숫자처럼 '단어 그대로 맞아야 하는' 질문에서 특히 효과.
  3. 순서 팁 — 보통 '하이브리드로 후보를 넓게 → 리랭킹으로 좁게'가 정석. 둘 다 켜기 전에, 앞 단계 Ragas 점수를 먼저 찍어두고 켠 뒤와 비교하세요(효과를 숫자로 확인).

🧪 첫 평가 — Ragas로 숫자 만들기

눈으로 '괜찮네' 하는 건 평가가 아닙니다. 질문+기대답 몇 개를 직접 만든 '골든 테스트셋'에 Ragas를 돌리면, 검색·답변 품질이 점수로 나옵니다.

  1. 골든셋 만들기 — 대표 질문 10개 + 기대 답/근거를 직접 작성
  2. pip install ragas → 파이프라인 답변을 골든셋과 비교 채점
  3. retrieval/answer 점수를 기록

📁 프로젝트 골격 — 파일을 어떻게 두고 돌리나

지금까지 만든 조각들(loader·chunk·embed·store·rag)을 한 폴더에 모으면 이런 모양입니다. ingest로 '문서를 한 번 넣어두고', chat으로 '계속 질문'하는 2단계 — 재실행 시 임베딩을 다시 안 하게 나눠둔 거예요.

ask-my-docs/
├─ docs/            # 내 PDF·txt 원본을 여기 넣기
├─ chroma_db/       # Chroma가 자동 생성(임베딩 저장소)
├─ loader.py        # 파일 → 텍스트
├─ chunk.py         # 텍스트 → 조각(오버랩)
├─ embed.py         # 조각 → 벡터
├─ store.py         # Chroma 저장/검색
├─ rag.py           # 검색결과 + 19회차 ask()로 답하기
├─ ingest.py        # docs/ 전부 읽어 Chroma에 적재(한 번)
├─ chat.py          # 질문 받아 답+출처 출력(반복)
├─ .env             # OPENAI_API_KEY (깃 제외)
└─ requirements.txt # openai chromadb pypdf python-dotenv
                    # rank-bm25 sentence-transformers ragas
  1. 준비 — pip install -r requirements.txt 후 .env에 키 넣고, docs/에 내 문서 넣기
  2. 적재 — python ingest.py (loader→chunk→embed→store 순으로 돌며 chroma_db/에 저장)
  3. 대화 — python chat.py (질문 입력 → 검색 → 근거로 답 + 출처 표시, 재임베딩 없음)

🔧 만들 것 — Ask-My-Docs 챗봇

  1. 이 회차 전반부 검색기 + augment 프롬프트 + 19회차 ask()를 연결
  2. 답변에 출처(문서명·청크)를 반드시 표시
  3. 골든셋으로 Ragas 점수를 뽑고, 청킹/top-k를 바꿔가며 재측정

✅ 완료 기준

📌 20회차 핵심