14차시. 성능 비교 실습
3. 실습 ① — 자동 측정 도구 만들기
출처: 원본 PDF 14차시_체계적성능평가.pdf 7쪽 · 쪽 그림 7쪽
손으로 하나씩 돌리면 지칩니다. 자동으로 돌리고 표를 뽑는 도구를 만듭니다.
📝 src/benchmark.py
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자)📝 측정 함수
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 문자열로 변환합니다.
파일 맨 아래 실행 결과 샘플 주석은 실행되지 않습니다. 화면 출력은 다음 절에 다시 적어 두었습니다.