RAG2026-5일34차시 · RAG 평가 개요14 / 18← 이전목차다음 →MetaContents

34차시. RAG 평가 개요

실습 1 — RAG 평가 개요 실습 따라 하기

관련 수업 자료: 원본 PDF 34차시_RAG평가개요.pdf · 실습 코드 32/test_regression.py

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

수업 자료와 이어지는 곳

실습 코드원본 PDF이어지는 내용
32/test_regression.py-원본 PDF 에 짝이 되는 쪽이 없어 이 페이지 아래에 실었습니다

실행 흐름

출력
질문 세트 → 정답 조각이 k안에
정답 조각이 k안에 → 검색 수정  (아니오)
정답 조각이 k안에 → 키워드와 무환각  (예)
키워드와 무환각 → 생성 검증 수정  (아니오)
키워드와 무환각 → 통과  (예)

실습 순서

질문 8개를 검색 성공이 목표인 것과 답변 문장이 목표인 것으로 나눕니다. test_regression.py가 키워드를 어떻게 검사하는지 읽고, 없는 질문을 성공으로 세지 않는지 확인합니다.

스스로 확인

  • 검색 실패를 답변 점수로만 보면 왜 진단이 꼬이는가?
  • 없는 질문의 정답이 거절인 이유를 아는가?
  • 한 세트에서 100퍼센트여도 새 질문에는 실패할 수 있다는 점을 말하는가?

이어지는 실습 코드

실습 코드 · 32/test_regression.py

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

Python
# ==============================================================
# 32차시. PPT 32차시_고도화ModularRAG. 코드 폴더는 src/32
# PPT: 32차시 경로 점검과 회귀 테스트
# 역할: graph32.ask로 기존 질문을 다시 물어 키워드, pass 수, 재생성 수, 재검색 수를 집계합니다.
# 읽는 키: answer, grade, retries, rewrites
# 쓰는 키: 화면의 질문별 표시와 마지막 집계
# 그래프:
#   각 질문 --> ask --> 키워드 포함 여부
#   문서에 없는 대표이사 질문은 안내 문장 키워드가 있으면 정답
# --------------------------------------------------------------
# 파이썬만 아는 분을 위한 말
# State는 노드들이 함께 쓰는 딕셔너리입니다. 함수는 state를 읽고, 바꿀 키만 return 합니다.
# return에 적지 않은 키는 이전 값이 남습니다.
# log와 tried_queries는 리스트라서, 새로 return한 항목이 뒤에 이어 붙습니다.
# Retriever: 검색 담당. 질문과 비슷한 문서 조각만 고릅니다. 답 문장은 만들지 않습니다.
# Generator: 고른 문서를 근거로 답 문장을 만듭니다. 문서가 없으면 안내 문장만 돌려줍니다.
# Verifier: 답이 근거와 맞는지 검사하고 grade를 적습니다.
#   grade=pass     답을 확정하고 그래프를 끝냅니다.
#   grade=retry    같은 문서로 답 문장만 다시 씁니다. retries가 1 늘어납니다.
#   grade=research 문서가 부족합니다. 검색어를 바꾼 뒤 다시 찾습니다. rewrites가 늘어납니다.
#   grade=giveup   더 진행하지 않고 fallback으로 안내 문장만 돌려줍니다.
# fallback: 실패했을 때 답을 지어내지 않고 안내 문장을 반환하는 노드입니다.
# intent: 질문 종류. greeting 인사, calc 계산, scope 문서 밖, document 문서 검색.
# rewrite: 사용자가 친 question은 유지하고, 검색에 쓰는 query만 바꿉니다.
# safe_node: 노드를 try로 감쌉니다. 오류가 나도 그래프는 멈추지 않고 node_error에 적습니다.
# check_answer는 keys 중 하나라도 답 문자열에 있으면 참입니다.
# 정답률, 검증통과 수, 재생성 건수, 재검색 건수는 실행 결과라 기록에 없는 값은 적지 않습니다.
# ==============================================================

# ============================================================
# test_regression.py
# graph32가 기존 질문에 여전히 올바르게 답하는지 검사합니다.
# 각 답변에 기대한 핵심 키워드가 포함되어 있는지 확인합니다.
# ============================================================

from graph32 import ask

# ============================================================
# 테스트 질문
# ============================================================
# q    : RAG에 입력할 질문
# keys : 정답으로 인정할 핵심 키워드

TESTS = [
    {
        "q": "환불은 며칠 이내인가요?",
        "keys": ["7일"],
    },
    {
        "q": "교환 기간은 얼마인가요?",
        "keys": ["14일"],
    },
    {
        "q": "고객센터 운영 시간은?",
        "keys": ["09:00", "18:00", "9시", "18시"],
    },
    {
        "q": "반품하고 싶은데 언제까지?",
        "keys": ["7일"],
    },
    {
        "q": "물건 바꾸려면 택배비 누가?",
        "keys": ["배송비", "택배비", "고객"],
    },
    {
        "q": "환불 방법과 수수료는?",
        "keys": ["환불", "수수료"],
    },
    {
        "q": "대표이사가 누구인가요?",
        "keys": [
            "관련 자료를 찾지 못했습니다",
            "확인할 수 없습니다",
            "정보가 없습니다",
        ],
    },
]

# ============================================================
# 답변 확인 함수
# ============================================================

def check_answer(answer, keys):
    """답변에 기대한 키워드가 하나라도 있는지 확인합니다."""

    # 키워드를 하나씩 확인
    for key in keys:

        # 답변에 키워드가 있으면 정답
        if key in answer:
            return True

    # 모든 키워드가 없으면 오답
    return False

# ============================================================
# 통계 변수 초기화
# ============================================================

# 정답 개수
correct = 0

# Verifier를 통과한 질문 개수
pass_count = 0

# 답변을 재생성한 질문 개수
retry_count = 0

# 질문을 고쳐 재검색한 질문 개수
rewrite_count = 0

# ============================================================
# 회귀 테스트 실행
# ============================================================

for test in TESTS:

    # 테스트 질문과 정답 키워드 가져오기
    question = test["q"]
    keys = test["keys"]

    # --------------------------------------------------------
    # 1. Modular RAG 실행
    # --------------------------------------------------------

    result = ask(question)

    # --------------------------------------------------------
    # 2. 최종 답변 가져오기
    # --------------------------------------------------------

    answer = result.get("answer", "")

    # --------------------------------------------------------
    # 3. 답변이 맞는지 확인
    # --------------------------------------------------------

    ok = check_answer(
        answer,
        keys
    )

    if ok:

        correct += 1
        mark = "✓"

    else:

        mark = "✗"

    # --------------------------------------------------------
    # 4. Verifier 통과 여부 확인
    # --------------------------------------------------------

    grade = result.get("grade", "")

    if grade == "pass":
        pass_count += 1

    # --------------------------------------------------------
    # 5. 재생성 여부 확인
    # --------------------------------------------------------

    retries = result.get("retries", 0)

    if retries > 0:
        retry_count += 1

    # --------------------------------------------------------
    # 6. 재검색 여부 확인
    # --------------------------------------------------------

    rewrites = result.get("rewrites", 0)

    if rewrites > 0:
        rewrite_count += 1

    # --------------------------------------------------------
    # 7. 질문별 결과 출력
    # --------------------------------------------------------

    print(
        f"{mark} {question} | "
        f"판정={grade} | "
        f"재생성={retries} | "
        f"재검색={rewrites}"
    )

# ============================================================
# 전체 테스트 결과
# ============================================================

# 전체 질문 개수
count = len(TESTS)

print()
print("=" * 50)

# 정답률 계산
accuracy = correct / count * 100

print(
    f"정답률   : "
    f"{correct}/{count} "
    f"({accuracy:.0f}%)"
)

print(
    f"검증통과 : "
    f"{pass_count}/{count}"
)

print(
    f"재생성   : "
    f"{retry_count}건"
)

print(
    f"재검색   : "
    f"{rewrite_count}건"
)

print("=" * 50)

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

출력
===== 실행 결과 샘플 =====
경로: classify → retrieve → generate → verify → END
예시 출력 (API 키 필요, 실행하지 않음)
일곱 질문의 정답률과 건수 집계는 기록에 없습니다.
기록과 맞는 질문만 적습니다.
환불은 며칠 이내에 신청해야 하나요? (테스트 문구는 '환불은 며칠 이내인가요?') grade=pass retries=0 rewrites=0 elapsed=6.69
  다른 실행 elapsed=2.39 n_docs=2 top_score=0.056
대표이사가 누구인가요? grade=giveup rewrites=2 elapsed=3.41 fail_reason=low_score top_score=-0.219

코드 해설

graph32.ask로 기존 질문을 다시 물어 키워드, pass 수, 재생성 수, 재검색 수를 집계합니다.

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

  • 읽는 키: answer, grade, retries, rewrites
  • 쓰는 키: 화면의 질문별 표시와 마지막 집계
  • 그래프:
  • 각 질문 --> ask --> 키워드 포함 여부
  • 문서에 없는 대표이사 질문은 안내 문장 키워드가 있으면 정답
  • 파이썬만 아는 분을 위한 말
  • State는 노드들이 함께 쓰는 딕셔너리입니다. 함수는 state를 읽고, 바꿀 키만 return 합니다.
  • return에 적지 않은 키는 이전 값이 남습니다.

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

  • check_answer: 답변에 기대한 키워드가 하나라도 있는지 확인합니다.

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

코드 설명

  • check_answer(answer, keys) 는 키워드가 하나라도 들어 있으면 True 입니다. 1-1 절에서 본 키워드 채점과 같은 방식이라, "7일이 아니라 30일입니다" 같은 답도 정답으로 셉니다. 오늘 배운 한계를 이 코드에서 직접 확인할 수 있습니다.
  • 대표이사 질문의 keys 는 "관련 자료를 찾지 못했습니다" 같은 안내 문장입니다. 문서에 없는 질문은 거절하는 것이 정답이라서, 지어낸 이름이 나오면 오답이 됩니다.
  • correct 와 pass_count 는 다른 숫자입니다. correct 는 키워드로 본 최종 답의 정답 수이고, pass_count 는 Verifier 가 pass 를 준 수입니다. 둘을 같은 것으로 읽는 분이 많습니다.
  • retry_count 와 rewrite_count 는 횟수의 합이 아니라, 재생성이나 재검색이 한 번이라도 일어난 질문의 수입니다. if retries > 0: 으로 질문당 1만 더하기 때문입니다.
  • from graph32 import ask 가 FAISS 와 OpenAI 를 준비하므로 API 키가 있어야 돕니다. 여기서 나온 정답률도 이 일곱 질문에서의 결과일 뿐입니다.
← 이전 절34차시 · 체크리스트와 과제다음 절 →34차시 · 퀴즈 1 — Precision@k와 Recall@k 계산하기