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

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

실습 1 — 미니프로젝트 ① 주제 선정과 설계 실습 따라 하기

관련 수업 자료: 38차시 「5. 실습 ② — 문서 준비와 로딩」(11~12쪽) · 실습 코드 rag_app_prj/config.py, 32/graph32.py, rag_app_prj/build_guide.py, rag_app_prj/indexer.py

이 실습은 교안 해설(보충_미니프로젝트주제.md, 보충_미니프로젝트기획.md, 보충_미니프로젝트설계.md)의 실습 순서를 옮긴 것입니다. OpenAI 임베딩이나 채팅 모델을 부르는 파일은 .env 에 API 키가 있어야 돌아갑니다. 키가 없으면 실행 결과 샘플로 출력 모양을 확인합니다.

수업 자료와 이어지는 곳

실습 코드원본 PDF이어지는 내용
rag_app_prj/config.py11쪽38차시 「5. 실습 ② — 문서 준비와 로딩」 › 5-2) config 수정 — 세 곳만 아래에 실었습니다
32/graph32.py-원본 PDF 에 짝이 되는 쪽이 없어 이 페이지 아래에 실었습니다
rag_app_prj/build_guide.py-원본 PDF 에 짝이 되는 쪽이 없어 이 페이지 아래에 실었습니다
rag_app_prj/indexer.py11쪽38차시 「5. 실습 ② — 문서 준비와 로딩」 › 5-1) 프로젝트 폴더 만들기 아래에 실었습니다

보충 미니프로젝트주제

주제는 문서 한 묶음으로 답할 수 있어야 합니다. 예시는 부산 방문 안내입니다. PDF와 사진이 이미 rag_app_prj에 있습니다. 다른 주제를 고르면 회사 규정, 제품 매뉴얼처럼 답이 문서 안에 있는 것을 고릅니다. 실시간 날씨는 scope로 거절할 대상입니다.

주제 카드에 적을 내용입니다. 사용자, 문서 파일, 시스템이 답하는 질문, 일부러 거절할 질문, 쓸 노드(최소 retrieve, generate, fallback. 여유가 있으면 verify와 rewrite).

수준을 나눕니다. 기본은 15차시 앱을 새 문서로 돌리는 것입니다. 확장은 31차시 분기와 32차시 재작성을 붙이는 것입니다. 오늘은 코드를 늘리지 않고 카드를 확정합니다.

실행 흐름

출력
주제 카드 → 문서
주제 카드 → 답할 질문
주제 카드 → 거절할 질문
주제 카드 → 쓸 노드

실습 순서

주제 카드를 한 장 작성합니다. 거절 질문 3개가 문서에 정말 없는지 파일을 검색해 확인합니다. 사진이 있는 주제라면 사진이 근거가 아니라 화면이라는 점을 카드에 적습니다.

스스로 확인

  • 문서 밖에 있는 질문을 scope로 분류했는가?
  • 기본 범위와 확장 범위를 나누었는가?
  • 평가 루브릭의 구현 칸과 카드의 노드가 맞는가?

보충 미니프로젝트기획

기획 산출물은 실행 파일이 아니라 표와 그림입니다. State 키, 노드별 읽기/쓰기, 분기 표, 실패 시 안내 문장, 질문 10개의 기대 경로입니다.

전체 그림을 그대로 쓸 필요는 없습니다. 기본 주제는 classify를 생략하고 retrieve와 generate와 fallback만 써도 됩니다. 확장 주제는 graph32.py의 분기 네 개를 가져옵니다. 분류 이후, 검색 이후, 검증 이후, 재작성 이후입니다.

question은 불변, query는 재작성 가능, log와 tried_queries만 reducer라는 18차시 결정을 표에 다시 적습니다. 코드를 내일 붙일 때 이 표와 어긋나면 표를 먼저 고칩니다.

실행 흐름

출력
START → classify
classify → retrieve  (document)
classify → 즉시 응답  (그_외)
retrieve → generate  (ok)
retrieve → rewrite  (research)
generate → verify
verify → END  (done)
rewrite → retrieve

실습 순서

graph32.py 머리말의 그래프와 자기 주제의 그림을 나란히 놓고, 빼는 노드에 이유를 적습니다. 분기 표의 모든 반환 문자열이 도착 노드를 갖는지 확인합니다.

스스로 확인

  • 빼는 노드와 남기는 노드의 이유가 적혀 있는가?
  • reducer가 붙는 키가 log와 tried_queries뿐인가?
  • 실패 안내가 이유별로 다른가?

보충 미니프로젝트설계

22차시 그래프와 15차시 앱은 고객 안내문 기준입니다. 오늘은 코드를 새로 쓰기 전에, 다른 문서로 바꿀 때 어디만 손대는지 설계합니다. 대상은 rag_app_prj입니다. 문서는 docs/busan_guide.pdf이고 화면용 사진이 assets에 있습니다.

설계서에 적을 것은 여섯 가지입니다. 사용자 질문 예시 10개, 문서에 없을 질문 3개, 답에 반드시 들어갈 키워드, chunk와 k의 출발값, 실패 시 안내 문장, 콘솔과 웹 중 어떤 진입점을 쓸지.

build_guide.py는 PDF를 생성하므로 이미 있는 docs/busan_guide.pdf가 있으면 다시 실행하지 않아도 됩니다. 샘플은 예시입니다. config, indexer, prompts는 임포트만 하면 출력이 없습니다.

실행 흐름

출력
busan guide.pdf → config 경로
config 경로 → indexer
indexer → ask
ask → app 또는 web
사진 → app 또는 web

실습 순서

질문 10개의 기대 키워드를 표로 적고, config.py에서 문서 경로와 청크만 다른지 15차시 rag_app/config.py와 비교합니다. 그래프가 필요하면 22차시 경로(retrieve, generate, fallback)를 그대로 스케치합니다.

스스로 확인

  • 문서가 바뀌어도 ask의 반환 딕셔너리 모양을 유지하는가?
  • 없는 질문의 안내 문장을 미리 적었는가?
  • 사진을 답변 근거와 혼동하지 않고 화면 자산으로 두었는가?

이어지는 실습 코드

실습 코드 · 32/graph32.py

실습 코드: 실습/src/32/graph32.py · 원본 PDF 에 같은 이름의 쪽이 없어 여기에 실었습니다.

이 파일은 32차시 「2. 실습 ① — 통합 그래프 완성」에 주석과 함께 전체가 실려 있습니다. 여기서는 이 차시에서 보는 부분의 해설과 실행 결과만 옮깁니다.

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

출력
===== 실행 결과 샘플 =====
경로: classify → retrieve → generate → verify → END
예시 출력 (API 키 필요, 실행하지 않음)
__main__ 질문 '환불은 며칠 이내에 신청하고 대표이사 이름은?'은 기록된 로그에 없습니다.
그 질문의 판정과 점수는 적지 않습니다.
기록된 질문:
안녕하세요 intent=greeting grade=pass elapsed=0.01
10 + 20 intent=calc grade=pass elapsed=0.0
오늘 날씨 intent=scope grade=pass
환불은 며칠 이내에 신청해야 하나요? intent=document grade=pass retries=0 rewrites=0 elapsed=6.69
  다른 실행: elapsed=2.39 n_docs=2 top_score=0.056
대표이사가 누구인가요? intent=document grade=giveup rewrites=2 elapsed=3.41 fail_reason=low_score top_score=-0.219

코드 해설

분류, 검색, 생성, 검증, 재생성, 재작성, fallback을 한 그래프로 모은 최종 연결입니다.

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

  • 읽는 키: intent, retrieval_ok, fail_reason, rewrites, grade, retries, query
  • 쓰는 키: retries, log, ask()는 최종 State
  • 그래프:
  • START --> classify
  • |-- greeting|calc|scope --> END
  • +-- document --> retrieve
  • |-- ok --> generate --> verify
  • | |-- done --> END

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

  • bump_node: 답변 재생성 횟수를 1 증가시킵니다.
  • route_by_intent: 분류된 질문 종류에 따라 다음 경로를 결정합니다.
  • route_after_retrieve: 검색 성공 여부에 따라 생성, 재검색, 포기를 결정합니다.
  • route_after_verify: 검증 결과에 따라 종료, 재생성, 재검색을 결정합니다.
  • route_after_rewrite: 새 검색어가 만들어졌으면 다시 검색합니다.
  • build_graph: 모든 노드를 등록하고 실행 순서를 연결합니다.
  • ask: 질문을 받아 초기 State를 만들고 그래프를 실행합니다.

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

실습 코드 · rag_app_prj/build_guide.py

실습 코드: 실습/src/rag_app_prj/build_guide.py · 원본 PDF 에 같은 이름의 쪽이 없어 여기에 실었습니다.

Python
# ==============================================================
# 보충 미니프로젝트. 코드 폴더는 rag_app_prj
# PPT: 미니프로젝트 부산 2박 3일 여행 가이드
# 역할: 부산 2박 3일 여행 가이드 PDF를 만듭니다. 검색 인덱스의 원본 문서입니다.
# 읽는 키: 없음
# 쓰는 키: docs/busan_guide.pdf 파일을 생성하고 그 경로를 반환
# 그래프:
#   PAGES 8장
#     |
#     v
#   FPDF + 맑은고딕 --> docs/busan_guide.pdf
#     |
#     v
#   indexer.get_store 가 이 PDF를 읽음
# --------------------------------------------------------------
# 실행하면 이 스크립트 옆 docs 폴더에 busan_guide.pdf를 씁니다.
# 원본 위치에서 실행하면 D:\RAG_Project\src\rag_app_prj\docs\busan_guide.pdf 가 되므로 실행하지 않았습니다.
# 글꼴은 C:\Windows\Fonts\malgun.ttf 입니다. 없으면 FileNotFoundError.
# 각 장은 A4 한 페이지입니다. 본문이 268pt를 넘으면 RuntimeError.
# 쪽 번호는 '현재 / 8' 형식입니다.
# ==============================================================

from pathlib import Path

# fpdf2 라이브러리입니다. 파이썬 코드로 PDF 를 한 장씩 그립니다.
from fpdf import FPDF

# PDF 는 이 파일 옆 docs 폴더에 만듭니다. 글꼴은 윈도우의 맑은 고딕을 씁니다.
BASE = Path(__file__).resolve().parent
FONT = Path(r"C:\Windows\Fonts\malgun.ttf")
OUT = BASE / "docs" / "busan_guide.pdf"

# PDF 한 장 = (제목, 문단 목록) 한 쌍입니다. 모두 8장이고, 이 글자들이 나중에 검색 대상이 됩니다.
# 같은 요금·시간이 여러 쪽에 나옵니다(태종대 입장료는 4쪽과 7쪽). 그래서 검색이 두 쪽을 함께 찾아올 수 있습니다.
PAGES = [
    (
        "여행 개요",
        [
            "이 가이드는 부산 2박 3일 일정입니다. 숙소는 남포동을 기준으로 합니다. 체크인 시각은 15:00, 체크아웃 시각은 11:00입니다. 2인 기준 남포동 숙소는 비수기에 1박 80,000원입니다.",
            "추천 시기는 4월부터 6월, 그리고 9월부터 11월입니다. 7월과 8월은 성수기라 숙소와 해수욕장이 붐빕니다.",
            "1인 예산은 숙박을 제외하고 3일 교통비 약 18,000원, 식비는 하루 30,000원으로 3일 90,000원입니다. 입장료는 부산타워와 태종대를 포함해 1인 15,000원 안팎입니다.",
            "짐은 걷기 편한 운동화, 얇은 바람막이, 접이우산을 준비합니다. 해운대와 광안리는 바닷바람이 셉니다.",
            "이동의 중심은 지하철 1호선과 2호선입니다. 1호선은 남포, 자갈치, 부산역을 지나고 2호선은 서면, 해운대, 광안을 지납니다.",
        ],
    ),
    (
        "1일차 남포 · 자갈치",
        [
            "1일차는 남포동과 자갈치에 머뭅니다. 오전에는 도착 후 짐을 맡깁니다. 부산역에서 남포동 숙소까지는 도보 15분입니다. 지하철 1호선을 타면 부산역에서 남포역까지 1정거장, 약 3분입니다.",
            "오후 1시부터 국제시장과 부평깡통시장을 걷습니다. 국제시장에서는 씨앗호떡과 비빔당면을 먹습니다. 시장 구경은 2시간에서 3시간이면 충분합니다.",
            "오후 4시에 남포동 숙소로 돌아와 체크인합니다. 체크인 시각은 15:00입니다.",
            "저녁은 자갈치시장입니다. 회 모둠은 1인 30,000원부터이고, 주문 전에 가격을 확인합니다. 회가 부담되면 남포동 돼지국밥을 먹습니다. 돼지국밥은 1인 10,000원입니다.",
            "밤에는 용두산공원에 올라 부산타워 전망대를 봅니다. 전망대는 성인 10,000원이고 운영 시간은 09:00부터 22:00까지입니다. 전망대 관람은 40분이면 됩니다.",
        ],
    ),
    (
        "2일차 해운대 · 광안",
        [
            "2일차는 해운대와 광안리입니다. 오전 9시에 남포역에서 지하철로 이동합니다. 남포에서 해운대까지는 서면역 환승을 포함해 약 40분입니다. 서면역에서 1호선에서 2호선으로 갈아탑니다.",
            "해운대 해수욕장 산책은 무료입니다. 이어서 동백섬을 한 바퀴 돕니다. 동백섬 산책은 무료이고 약 1시간이 걸립니다.",
            "점심은 해운대 밀면입니다. 1인 9,000원입니다. 밀면 대신 돼지국밥을 먹어도 됩니다.",
            "오후에는 청사포 달맞이길 전망을 봅니다. 해운대에서 청사포까지는 택시로 약 15분이고 요금은 약 8,000원입니다.",
            "저녁은 광안리 민락수변공원 회센터입니다. 구이와 회를 포함해 1인 20,000원 안팎입니다. 식후에는 광안리 해변에서 광안대교 야경을 봅니다. 해변 입장료는 없습니다.",
            "해운대에서 광안리까지는 지하철 2호선으로 약 15분입니다. 남포동으로 돌아올 때는 서면역에서 1호선으로 환승합니다.",
        ],
    ),
    (
        "3일차 감천 · 태종대",
        [
            "3일차는 감천문화마을과 태종대입니다. 오후에 출발합니다. 오전 9시에 감천문화마을로 갑니다. 남포동에서 택시를 타면 약 15분, 요금은 약 7,000원입니다.",
            "감천문화마을 입장은 무료입니다. 안내소 운영 시간은 09:00부터 18:00까지입니다. 골목 경사가 가파르니 운동화를 신습니다. 체류 시간은 1시간 30분입니다.",
            "점심은 토성동에서 씨앗호떡과 비빔당면을 먹습니다. 씨앗호떡은 1개 2,000원, 비빔당면은 1인 8,000원입니다.",
            "오후 1시에 태종대로 이동합니다. 감천문화마을에서 태종대까지 택시는 약 25분, 요금은 약 12,000원입니다.",
            "태종대 공원 입장료는 성인 2,000원입니다. 다누비열차 왕복은 성인 3,000원이고 운영 시간은 09:00부터 17:00까지입니다. 연중무휴입니다. 비가 많이 오면 열차 운행이 짧아질 수 있습니다. 전망 산책은 1시간 30분입니다.",
            "부산역에서 출발하면 태종대에서 부산역까지 택시 약 30분, 요금은 약 15,000원입니다. 김해공항으로 갈 때는 부산역 리무진을 이용합니다.",
        ],
    ),
    (
        "교통",
        [
            "김해공항에서 서면역까지는 부산김해경전철을 타고 사상역에서 지하철 2호선으로 환승합니다. 소요 시간은 약 50분입니다. 교통카드를 쓰면 됩니다.",
            "김해공항에서 남포동까지 공항리무진은 약 70분이고 성인 요금은 7,000원입니다. 리무진은 서면과 부산역을 경유합니다.",
            "부산역에서 남포동까지는 도보 15분, 지하철 1호선은 1정거장으로 약 3분입니다.",
            "시내 이동은 지하철이 기본입니다. 1호선은 부산역, 남포, 자갈치를 지나고 2호선은 서면, 해운대, 광안을 지납니다.",
            "하루 교통비는 지하철만 이용하면 1인 약 6,000원입니다. 3일이면 약 18,000원입니다.",
            "택시 기본요금은 4,800원입니다. 김해공항에서 남포동까지 택시를 타면 약 60분, 요금은 약 35,000원입니다. 짐이 많거나 밤 도착일 때만 택시를 권합니다.",
        ],
    ),
    (
        "식사",
        [
            "이 가이드의 식비 기준은 1인 하루 30,000원입니다. 3일이면 90,000원입니다.",
            "돼지국밥은 남포동과 서면에서 먹습니다. 1인 10,000원입니다. 1일차 저녁 대안으로 적당합니다.",
            "밀면은 해운대에서 먹습니다. 1인 9,000원입니다. 2일차 점심으로 적습니다.",
            "씨앗호떡은 국제시장과 토성동에서 1개 2,000원입니다. 비빔당면은 1인 8,000원입니다.",
            "자갈치시장 회 모둠은 1인 30,000원부터입니다. 상차림과 매운탕을 더하면 1인 40,000원까지 올라갑니다. 주문 전에 가격을 확인합니다.",
            "광안리 민락수변공원 회센터의 구이와 회는 1인 20,000원 안팎입니다. 2일차 저녁 예산에 맞습니다. 해운대 카페 음료는 1잔 6,000원 안팎입니다.",
        ],
    ),
    (
        "입장료 · 운영시간",
        [
            "부산타워 전망대는 성인 10,000원입니다. 운영 시간은 09:00부터 22:00까지입니다. 용두산공원 산책 자체는 무료이고, 요금은 전망대에만 있습니다.",
            "태종대 공원 입장료는 성인 2,000원입니다. 다누비열차 왕복은 성인 3,000원입니다. 열차 운영 시간은 09:00부터 17:00까지이고 연중무휴입니다. 비가 많이 오면 운행이 짧아질 수 있습니다.",
            "감천문화마을 입장은 무료입니다. 안내소는 09:00부터 18:00까지입니다.",
            "해운대 해수욕장, 동백섬, 광안리 해변은 입장료가 없습니다.",
            "1인 입장료 합계는 부산타워 10,000원, 태종대 공원 2,000원, 다누비열차 3,000원을 더해 15,000원입니다.",
            "무료 장소만 돌면 입장료는 0원입니다. 그때는 부산타워 전망대를 빼고 용두산공원 산책만 합니다.",
        ],
    ),
    (
        "주의사항",
        [
            "7월과 8월 성수기에는 남포동 숙소를 최소 2주 전에 예약합니다. 성수기 1박 요금은 2인 기준 150,000원부터입니다. 비수기 1박 80,000원과 차이가 큽니다.",
            "비가 오는 날에는 태종대 절벽 전망대가 미끄럽습니다. 감천문화마을 골목도 경사가 있어 운동화가 필요합니다. 우산은 접이우산으로 준비합니다.",
            "자갈치시장에서는 회 가격을 확인한 뒤에 주문합니다. 모둠 가격이 1인 30,000원부터 40,000원까지 달라집니다.",
            "분실물은 부산역 고객센터 또는 지하철 유실물 센터에 문의합니다. 운영 안내는 각 역 안내 데스크에서 받습니다.",
            "이 가이드는 일정, 교통, 식사, 입장료, 주의사항만 다룹니다.",
        ],
    ),
]

# PDF 를 만들고 저장한 경로를 돌려줍니다.
def build() -> Path:
    # 한글 글꼴이 없으면 글자가 깨지므로, 그리기 전에 먼저 멈춥니다.
    if not FONT.is_file():
        raise FileNotFoundError(f"한글 글꼴이 없습니다: {FONT}")

    # docs 폴더가 없으면 만듭니다. exist_ok=True 라서 이미 있어도 오류가 나지 않습니다.
    OUT.parent.mkdir(parents=True, exist_ok=True)

    # 쪽 넘김은 코드가 직접 정합니다. 한 장이 A4 한 쪽에 들어가야 하기 때문입니다.
    pdf = FPDF(format="A4")
    pdf.set_auto_page_break(auto=False)
    pdf.add_font("Malgun", "", str(FONT))

    # PAGES 의 한 항목이 PDF 한 쪽이 됩니다.
    for title, paragraphs in PAGES:
        pdf.add_page()
        # 맨 위 띠: 짙은 파란 사각형과 그 아래 얇은 주황 선입니다. 숫자는 (R, G, B) 색과 (x, y, 너비, 높이) mm 입니다.
        pdf.set_fill_color(14, 77, 108)
        pdf.rect(0, 0, 210, 28, "F")
        pdf.set_fill_color(224, 122, 61)
        pdf.rect(0, 28, 210, 2.2, "F")

        # 띠 위에 머리말과 장 제목을 씁니다.
        pdf.set_text_color(246, 243, 238)
        pdf.set_font("Malgun", size=10)
        pdf.set_xy(16, 7)
        pdf.cell(0, 6, "부산 2박 3일 여행 가이드")
        pdf.set_font("Malgun", size=16)
        pdf.set_xy(16, 15)
        pdf.cell(0, 8, title)

        # 본문은 y=40mm 에서 시작해 문단마다 아래로 내려갑니다.
        pdf.set_text_color(28, 42, 51)
        pdf.set_font("Malgun", size=11)
        y = 40
        for paragraph in paragraphs:
            pdf.set_xy(16, y)
            # multi_cell 은 너비 178mm 를 넘으면 줄을 바꿔 줍니다. 쓴 뒤의 위치를 get_y 로 읽어 다음 문단 자리를 잡습니다.
            pdf.multi_cell(178, 7, paragraph)
            y = pdf.get_y() + 4
            # 268mm 를 넘으면 쪽 번호 자리를 덮으므로 오류로 알립니다. 문단을 줄이라는 신호입니다.
            if y > 268:
                raise RuntimeError(f"페이지를 넘었습니다: {title}")

        # 맨 아래에 "현재 쪽 / 전체 쪽" 을 씁니다.
        pdf.set_text_color(31, 138, 112)
        pdf.set_font("Malgun", size=9)
        pdf.set_xy(16, 282)
        pdf.cell(0, 6, f"{pdf.page_no()}  /  {len(PAGES)}")

    pdf.output(str(OUT))
    return OUT

# python build_guide.py 로 직접 실행할 때만 PDF 를 만듭니다. import 할 때는 만들지 않습니다.
if __name__ == "__main__":
    path = build()
    print(path)

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

출력
===== 실행 결과 샘플 =====
예시 (실행하지 않음. 실행하면 D:\RAG_Project 안에 PDF를 씁니다)
만드는 파일: docs/busan_guide.pdf (A4, 8페이지)
1 여행 개요
2 1일차 남포 · 자갈치
3 2일차 해운대 · 광안
4 3일차 감천 · 태종대
5 교통
6 식사
7 입장료 · 운영시간
8 주의사항
직접 실행하면 마지막에 저장된 PDF 경로를 print 합니다.

코드 해설

부산 2박 3일 여행 가이드 PDF를 만듭니다. 검색 인덱스의 원본 문서입니다.

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

  • PPT: 미니프로젝트 부산 2박 3일 여행 가이드
  • 읽는 키: 없음
  • 쓰는 키: docs/busan_guide.pdf 파일을 생성하고 그 경로를 반환
  • 그래프:
  • PAGES 8장
  • |
  • v
  • FPDF + 맑은고딕 --> docs/busan_guide.pdf

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

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

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

코드 설명

  • (index_path / "index.faiss").is_file() and (index_path / "index.pkl").is_file() and not rebuild 는 인덱스 파일 두 개가 다 있을 때만 불러온다는 뜻입니다. 문서를 바꾼 뒤에는 get_store(rebuild=True) 로 한 번 새로 만들어야 새 문서가 들어갑니다.
  • document.metadata["page_no"] = document.metadata.get("page", 0) + 1 은 0부터 세는 PDF 쪽 번호를 사람이 읽는 번호로 바꿉니다. 이 줄이 빠지면 7-2절 표의 KeyError: 'page_no' 를 만나게 됩니다.
  • INDEX_PATH = "faiss_index" 는 부산 가이드용 값입니다. 다른 문서로 바꿀 때 이 이름을 그대로 두면 검색은 되는데 엉뚱한 내용이 나옵니다. 여기서 많이들 한참 헤매십니다.
  • MIN_SCORE = -0.01 도 부산 가이드 문서에 맞춘 값입니다. 새 문서에서 모든 질문이 fallback 으로 가면 이 값부터 의심하고, 39차시에 다시 잽니다.
  • len(d.page_content.strip()) < 50 은 공백을 빼고 50자가 안 되는 쪽을 빈 페이지로 보는 기준입니다. 스캔본 PDF 는 이 목록이 길게 나오므로 바로 알아챌 수 있습니다.
  • build_guide.py 의 if y > 268: raise RuntimeError(...) 는 문단이 A4 한 쪽을 넘치면 조용히 잘리지 않고 멈추게 합니다. 이미 docs/busan_guide.pdf 가 있으면 이 파일은 다시 돌리지 않아도 됩니다.
← 이전 절38차시 · 체크리스트와 과제다음 절 →38차시 · 퀴즈 1 — 문서 사전 점검 합격 판정하기