RAG2026-5일38차시 · 미니프로젝트 ① 주제 선정과 설계8 / 17← 이전목차다음 →MetaContents

38차시. 미니프로젝트 ① 주제 선정과 설계

5. 실습 ② — 문서 준비와 로딩

출처: 원본 PDF 38차시_미니프로젝트주제선정.pdf 11~12쪽 · 쪽 그림 11쪽 12쪽

이제 손을 움직입니다. 6차시 코드를 그대로 씁니다.

5-1) 프로젝트 폴더 만들기

출력
my_rag_project/
├── config.py           ← 32차시 것을 복사 후 수정
├── state.py            ← 그대로 복사
├── prompts.py          ← 그대로 복사 (DOC_TOPIC만 수정)
├── indexer.py          ← 그대로 복사
├── routers.py          ← 그대로 복사
├── graph.py            ← 그대로 복사
├── app.py              ← 그대로 복사
├── nodes/              ← 폴더째 복사
├── eval/               ← 폴더째 복사
├── data/
│   └── my_doc.pdf      ← ★ 새 문서
└── docs/
    └── requirements.md ← ★ 오늘 작성

실습 코드 · rag_app_prj/indexer.py

실습 코드: 실습/src/rag_app_prj/indexer.py · 위의 원본 PDF 38차시 11쪽 「5-1) 프로젝트 폴더 만들기」과 이어집니다.

Python
# ==============================================================
# 보충 미니프로젝트. 코드 폴더는 rag_app_prj
# PPT: 미니프로젝트 부산 2박 3일 여행 가이드
# 역할: busan_guide.pdf를 청크로 나누어 FAISS에 저장하거나 기존 인덱스를 읽습니다.
# 읽는 키: 없음 (config)
# 쓰는 키: FAISS 저장소. 새로 만들면 인덱스 파일 저장
# 그래프:
#   docs/busan_guide.pdf
#     |
#     v
#   청크 + 임베딩 --> faiss_index
#     |
#     v
#   rag._search
# --------------------------------------------------------------
# rag_app/indexer.py와 같은 함수입니다. 문서 경로만 config를 통해 가이드 PDF를 가리킵니다.
# page_no는 PDF 로더 page에 1을 더한 값입니다.
# get_store는 API 키가 필요하므로 호출하지 않았습니다. faiss 파일은 복사하지 않습니다.
# ==============================================================

# 파일 경로를 운영체제와 상관없이 다루려고 Path 를 씁니다.
from pathlib import Path

# PDF 읽기, FAISS 저장소, OpenAI 임베딩, 글자 수 기준 분할기를 가져옵니다.
from langchain_community.document_loaders import PyPDFLoader
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter

# 문서 경로·청크 크기 같은 값은 모두 config 에서 읽습니다. 문서를 바꿀 때 이 파일은 손대지 않습니다.
import config

# 이 파일이 있는 폴더입니다. 상대 경로를 이 폴더 기준으로 바꿀 때 씁니다.
BASE = Path(__file__).resolve().parent

# rebuild=True 면 저장된 인덱스가 있어도 새로 만듭니다. 문서를 바꾼 뒤 한 번은 이렇게 부릅니다.
def get_store(rebuild=False):
    # INDEX_PATH 가 "faiss_index" 처럼 상대 경로면 이 폴더 아래로 붙입니다. 실행 위치가 달라도 같은 곳을 찾습니다.
    index_path = Path(config.INDEX_PATH)
    if not index_path.is_absolute():
        index_path = BASE / index_path

    # 저장할 때와 불러올 때 같은 임베딩 모델을 써야 점수가 맞습니다.
    embeddings = OpenAIEmbeddings(model=config.EMBED_MODEL)

    # 인덱스 파일 두 개가 다 있고 rebuild 가 아니면, API 비용 없이 저장된 것을 불러옵니다.
    if (index_path / "index.faiss").is_file() and (index_path / "index.pkl").is_file() and not rebuild:
        # allow_dangerous_deserialization 은 내가 만든 pkl 파일이라 믿고 연다는 표시입니다.
        return FAISS.load_local(
            str(index_path),
            embeddings,
            allow_dangerous_deserialization=True,
        )

    # 여기부터는 인덱스를 새로 만드는 길입니다. PDF 한 쪽이 Document 하나가 됩니다.
    documents = PyPDFLoader(str(config.DOC_PATH)).load()
    # CHUNK_SIZE 글자 안팎으로 자르고, 앞 조각과 CHUNK_OVERLAP 글자만큼 겹치게 합니다.
    splitter = RecursiveCharacterTextSplitter(
        chunk_size=config.CHUNK_SIZE,
        chunk_overlap=config.CHUNK_OVERLAP,
    )
    chunks = splitter.split_documents(documents)

    # 답변에 "파일 이름 몇 페이지"를 보여 주려고 조각마다 출처를 적어 둡니다.
    for document in chunks:
        document.metadata["filename"] = Path(config.DOC_PATH).name
        # PDF 로더의 page 는 0부터 세므로 1을 더해 사람이 읽는 쪽 번호로 바꿉니다.
        document.metadata["page_no"] = document.metadata.get("page", 0) + 1

    # 조각마다 임베딩을 만들어 저장소에 넣습니다. 이 줄에서 API 비용이 듭니다.
    store = FAISS.from_documents(chunks, embeddings)
    # 다음 실행부터는 위의 load_local 로 바로 불러오도록 파일로 저장합니다.
    store.save_local(str(index_path))
    return store

실행 결과 샘플 (교안 기록. API 키가 필요한 코드는 키 없이 맞춘 예시이고, 모델의 답은 실행할 때마다 달라집니다)

출력
===== 실행 결과 샘플 =====
직접 실행 시 출력 없음
직접 실행 시 출력 없음.
get_store() 호출은 API 키가 필요해 하지 않았습니다.

코드 해설

busan_guide.pdf를 청크로 나누어 FAISS에 저장하거나 기존 인덱스를 읽습니다.

코드 앞에서 고정해 두는 말입니다.

  • PPT: 미니프로젝트 부산 2박 3일 여행 가이드
  • 읽는 키: 없음 (config)
  • 쓰는 키: FAISS 저장소. 새로 만들면 인덱스 파일 저장
  • 그래프:
  • docs/busan_guide.pdf
  • |
  • v
  • 청크 + 임베딩 --> faiss_index

함수와 클래스로 나누어 읽습니다.

  • get_store: 이름 그대로 한 가지 일만 합니다. 본문 주석을 같이 읽습니다.

파일 맨 아래 실행 결과 샘플 주석은 실행되지 않습니다. 화면 출력은 다음 절에 다시 적어 두었습니다.

5-2) config 수정 — 세 곳만

출력
# config.py

DOC_PATH   = BASE / "data" / "2026_취업규칙.pdf"    # ① 문서 경로
INDEX_PATH = str(BASE / "faiss_index_hr")           # ② 인덱스 경로
DOC_TOPIC  = ("취업규칙, 근로시간, 휴가·휴직, "      # ③ 문서 주제
              "임금, 인사평가, 징계 규정")

⚠️ INDEX_PATH를 꼭 바꾸세요
기존 faiss_index를 그대로 쓰면
이전 문서의 인덱스를 불러옵니다.
→ 검색은 되는데 엉뚱한 내용이 나옵니다
→ 원인을 못 찾고 한참 헤맵니다
새 문서에는 새 인덱스 경로를 주세요.

실습 코드 · rag_app_prj/config.py

실습 코드: 실습/src/rag_app_prj/config.py · 위의 원본 PDF 38차시 11쪽 「5-2) config 수정 — 세 곳만」과 이어집니다.

Python
# ==============================================================
# 보충 미니프로젝트. 코드 폴더는 rag_app_prj
# PPT: 미니프로젝트 부산 2박 3일 여행 가이드
# 역할: 부산 가이드 앱의 상수. 문서 경로는 이 폴더의 docs/busan_guide.pdf 입니다.
# 읽는 키: 없음
# 쓰는 키: 없음
# 그래프:
#   build_guide가 PDF를 만들고
#   indexer가 그 PDF로 FAISS를 만들며
#   rag.py가 검색·생성 상수를 읽습니다.
# --------------------------------------------------------------
# 보충 미니프로젝트. PPT 차시 번호와는 별도이고, 코드 폴더는 rag_app_prj입니다.
# CHUNK_SIZE 500, CHUNK_OVERLAP 50, TOP_K 5, MIN_SCORE -0.01, 모델 gpt-4o-mini, 프롬프트 v3.
# 문서가 없을 때 문장은 '가이드에서 관련 내용을 찾지 못했습니다.' 입니다.
# faiss_index 폴더는 복사하지 않습니다.
# ==============================================================

from pathlib import Path

BASE = Path(__file__).resolve().parent

# ── 문서 처리 ──
DOC_PATH = BASE / "docs" / "busan_guide.pdf"
CHUNK_SIZE = 500
CHUNK_OVERLAP = 50
# ── 임베딩 · 저장소 ──
EMBED_MODEL = "text-embedding-3-small"
INDEX_PATH = "faiss_index"
# ── 검색 ──
TOP_K = 5
MIN_SCORE = -0.01
SEARCH_TYPE = "similarity"
# ── 생성 ──
LLM_MODEL = "gpt-4o-mini"
TEMPERATURE = 0
PROMPT_VER = "v3"

# ── 메시지 ──
MSG_NO_DOC = "가이드에서 관련 내용을 찾지 못했습니다."
MSG_ERROR = "일시적인 오류가 발생했습니다. 잠시 후 다시 시도해주세요."

실행 결과 샘플 (교안 기록. API 키가 필요한 코드는 키 없이 맞춘 예시이고, 모델의 답은 실행할 때마다 달라집니다)

출력
===== 실행 결과 샘플 =====
직접 실행 시 출력 없음
상수만 정의합니다. 직접 실행해도 출력이 없습니다.

코드 해설

부산 가이드 앱의 상수. 문서 경로는 이 폴더의 docs/busan_guide.pdf 입니다.

코드 앞에서 고정해 두는 말입니다.

  • PPT: 미니프로젝트 부산 2박 3일 여행 가이드
  • 읽는 키: 없음
  • 쓰는 키: 없음
  • 그래프:
  • build_guide가 PDF를 만들고
  • indexer가 그 PDF로 FAISS를 만들며
  • rag.py가 검색·생성 상수를 읽습니다.
  • CHUNK_SIZE 500, CHUNK_OVERLAP 50, TOP_K 5, MIN_SCORE -0.01, 모델 gpt-4o-mini, 프롬프트 v3.

함수로 나누지 않고 위에서 아래로 실행됩니다.

  • DOC_PATH = BASE / "docs" / "busan_guide.pdf" — ── 문서 처리 ──
  • EMBED_MODEL = "text-embedding-3-small" — ── 임베딩 · 저장소 ──
  • TOP_K = 5 — ── 검색 ──
  • LLM_MODEL = "gpt-4o-mini" — ── 생성 ──
  • MSG_NO_DOC = "가이드에서 관련 내용을 찾지 못했습니다." — ── 메시지 ──

파일 맨 아래 실행 결과 샘플 주석은 실행되지 않습니다. 화면 출력은 다음 절에 다시 적어 두었습니다.

5-3) DOC_TOPIC이 중요한 이유

출처: 원본 PDF 12쪽

29차시 분류 노드가 이 값을 씁니다.
"휴가 며칠이야?" → search ✅
"오늘 날씨는?" → reject ✅
DOC_TOPIC이 부실하면
답할 수 있는 질문을 reject로 보냅니다.
문서의 목차를 보고 주요 주제를 나열하세요.

← 이전 절38차시 · 4. 실습 ① — 요구사항 정의서다음 절 →38차시 · 6. 실습 ③ — 인덱싱과 첫 질문