RAG2026-5일14차시 · 성능 비교 실습6 / 18← 이전목차다음 →MetaContents

14차시. 성능 비교 실습

3. 실습 ① — 자동 측정 도구 만들기

출처: 원본 PDF 14차시_체계적성능평가.pdf 7쪽 · 쪽 그림 7쪽

손으로 하나씩 돌리면 지칩니다. 자동으로 돌리고 표를 뽑는 도구를 만듭니다.

📝 src/benchmark.py

Python
import time
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_community.vectorstores import FAISS
from langchain_core.output_parsers import StrOutputParser
from langchain_text_splitters import RecursiveCharacterTextSplitter
from ingest import load_documents
from prompts import PROMPTS
from dotenv import load_dotenv
load_dotenv()

emb = OpenAIEmbeddings(model="text-embedding-3-small")
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

DOCS = load_documents("data/manual.pdf")   # 로딩은 한 번만

_cache = {}
def make_store(chunk_size, overlap):
    # 같은 설정이면 재사용해서 임베딩 비용 절약
    key = (chunk_size, overlap)
    if key in _cache:
        return _cache[key]
    sp = RecursiveCharacterTextSplitter(
        chunk_size=chunk_size, chunk_overlap=overlap,
        separators=["\n\n", "\n", ". ", " ", ""])
    chunks = sp.split_documents(DOCS)
    _cache[key] = FAISS.from_documents(chunks, emb)
    print(f"   (인덱스 생성: chunk={chunk_size} 조각={len(chunks)})")
    return _cache[key]

실행 결과 예시 (코드를 읽고 만든 예상 화면입니다. 실제 실행 결과, 특히 모델의 답은 다를 수 있습니다)

출력
✓ 3쪽 로딩 완료 (총 1529자)

📝 측정 함수

Python
def build_context(docs):
    parts = []
    for i, d in enumerate(docs, 1):
        parts.append(f"[{i}] {d.page_content}")
    return "\n\n---\n\n".join(parts)
def run(name, chunk_size, overlap, k, min_score, version, tests):
    store = make_store(chunk_size, overlap)
    chain = PROMPTS[version] | llm | StrOutputParser()
    hit, tokens, t0 = 0, 0, time.time()
    fails = []
    for t in tests:
        pairs = store.similarity_search_with_relevance_scores(
            t["q"], k=k)
        # 기준(min_score)을 넘는 것만 골라 담기
        docs = []
        for d, s in pairs:
            if s >= min_score:
                docs.append(d)
        ctx  = build_context(docs) if docs else "(자료 없음)"
        ans  = chain.invoke({"context": ctx, "question": t["q"]})
        tokens += len(ctx) // 2        # 한글 대략 2자 = 1토큰
        # 정답 키워드 중 하나라도 답변에 들어있으면 정답
        ok = False
        for key in t["keys"]:
            if key in ans:
                ok = True
                break
        if ok:
            hit += 1
        else:
            fails.append(t["q"])
    sec  = time.time() - t0
    rate = hit / len(tests) * 100
    print(f"{name:<12} chunk={chunk_size:<5} k={k:<3} "
          f"{version:<3} | 정답 {hit:>2}/{len(tests)} ({rate:>3.0f}%) "
          f"| 평균토큰 {tokens//len(tests):>5} | {sec:>4.0f}초")
    return {"name": name, "rate": rate,
            "tokens": tokens // len(tests), "fails": fails}

실습 코드 · 07/prepare.py

실습 코드: 실습/src/07/prepare.py · 위의 원본 PDF 14차시 7쪽 「3️⃣ 실습 ① — 자동 측정 도구 만들기」과 이어집니다.

이 파일은 7차시 「10. 이번 차시 완성 코드」에 주석과 함께 전체가 실려 있습니다. 여기서는 이 차시에서 보는 부분의 해설과 실행 결과만 옮깁니다.

코드 해설

설명서를 chunk로 나누고 각 조각에 chunk_id를 붙이는 함수입니다. 입력은 ../../data/manual.pdf이고, 출력은 로딩 문장, 조각 개수, 앞 3개 본문과 chunk_id입니다.

주석에 적힌 흐름입니다.

출력
(import 시) manual.pdf -> load_documents
prepare_chuncks(경로)
-> load_documents -> splitter -> chunk_id 부여 -> 반환

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

  • Document: 본문(page_content)과 metadata 딕셔너리를 담는 객체
  • chunk: 검색에 쓰려고 잘라 낸 문서 조각
  • overlap: chunk_overlap. 이웃 조각이 이 글자 수만큼 겹칩니다.
  • metadata: 본문 밖의 부가 정보. 여기서 chunk_id(조각 번호)를 추가합니다.

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

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

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

실습 코드 · 11/retriever.py

실습 코드: 실습/src/11/retriever.py · 위의 원본 PDF 14차시 7쪽 「3️⃣ 실습 ① — 자동 측정 도구 만들기」과 이어집니다.

이 파일은 11차시 「8. 오늘의 완성 코드」에 주석과 함께 전체가 실려 있습니다. 여기서는 이 차시에서 보는 부분의 해설과 실행 결과만 옮깁니다.

코드 해설

similarity와 MMR 검색, k별 점수, 점수 기준 필터, 자료 문자열 만들기를 비교입니다. 입력은 10차시 FAISS 인덱스, 환불/교환 관련 질문이고, 출력은 검색기별 미리보기, k별 점수, 기준을 통과한 조각을 이어 붙인 자료입니다.

주석에 적힌 흐름입니다.

출력
get_store() -> FAISS
-> retriever.invoke 또는 similarity_search_with_relevance_scores
-> 점수 필터 -> build_context

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

  • retriever: 질문을 넣어 가까운 문서 조각을 받는 검색기
  • FAISS: 벡터(숫자 목록)를 저장한 인덱스
  • embedding: 질문 문장을 그 숫자 목록으로 바꾸는 과정 (검색 때 네트워크 호출)
  • vector: embedding이 만든 숫자 목록
  • chunk: 인덱스 안의 문서 조각. metadata는 쪽 번호, 파일명 같은 부가 정보
  • citation: 뒤 차시에서 답변 문장 끝의 [1] 같은 근거 번호.
  • build_context가 붙이는 [1], [2]가 그 번호의 출발점입니다.

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

  • show: ========== Part 1. 기본 함수 ==========
  • compare_k: k값을 바꾸며 검색 점수와 임계값 미달 개수를 비교합니다.
  • get_retriever: ========== Part 2. 심화 함수 ==========
  • search: 유사도 검색 후 relevance score 기준을 통과한 문서만 반환합니다.
  • build_context: 문서 목록을 출처와 페이지가 포함된 RAG context 문자열로 변환합니다.

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

← 이전 절14차시 · 2. 비교 실험의 4가지 원칙다음 절 →14차시 · 4. 실습 ② — 기준선 측정