RAG2026-5일29차시 · Conditional Edge 실습10 / 19← 이전목차다음 →MetaContents

29차시. Conditional Edge 실습

7. 실습 ⑥ — 경로별 동작 확인

출처: 원본 PDF 29차시_ConditionalEdge실습.pdf 15~16쪽 · 쪽 그림 15쪽 16쪽

📝 src/test_routing.py

출력
from graph import app
from state import init_state
import time

CASES = [
    ("안녕하세요",                  "greeting"),
    ("고맙습니다",                  "greeting"),
    ("125 + 340 * 2",              "calc"),
    ("오늘 날씨 어때요?",           "scope"),
    ("환불은 며칠 이내인가요?",     "document"),
    ("휴가 신청은 어떻게 하나요?",  "document"),
]

for q, expected in CASES:
    t0 = time.time()
    nodes = []
    for step in app.stream(init_state(q), {"recursion_limit": 25}):
        nodes.extend(step.keys())
    sec = time.time() - t0

    final = app.invoke(init_state(q), {"recursion_limit": 25})
    mark = "✓" if final.get("intent") == expected else "✗"

    print(f"\n{mark} Q: {q}")
    print(f"   유형: {final.get('intent')} (기대: {expected})")
    print(f"   경로: {' -> '.join(nodes)}")
    print(f"   시간: {sec:.2f}초")
    print(f"   A: {final['answer'][:60]}")

실행 결과
─────────────────────────────────────────────
─────────
✓ Q: 안녕하세요
   유형: greeting (기대: greeting)
   경로: classify -> greeting
   시간: 0.01초                       ← LLM 호출 0회
   A: 안녕하세요! 사내 문서에 관한 질문을 도와드립니다...

✓ Q: 125 + 340 * 2
   유형: calc (기대: calc)
   경로: classify -> calc
   시간: 0.01초
   A: 125 + 340 * 2 = 805
✓ Q: 오늘 날씨 어때요?
   유형: scope (기대: scope)
   경로: classify -> scope
   시간: 0.01초
   A: 죄송합니다. 저는 사내 문서에 관한 질문에만...

✓ Q: 환불은 며칠 이내인가요?
   유형: document (기대: document)
   경로: classify -> retrieve -> generate -> verify
   시간: 4.12초                       ← 정상 RAG 경로
   A: 환불은 상품 수령 후 7일 이내에 신청하실 수 있습니다[1]...

🎯 확인할 것
단축 경로 세 개는 0.01초에 끝났습니다.
LLM을 한 번도 부르지 않았습니다.
27차시 시스템이었다면
세 질문 모두 검색 → fallback으로 4초씩 걸렸을 겁니다.

실습 코드 · 29/test_routing.py

실습 코드: 실습/src/29/test_routing.py · 위의 원본 PDF 29차시 15~16쪽 「7️⃣ 실습 ⑥ — 경로별 동작 확인」과 이어집니다.

Python
# ==============================================================
# 29차시. PPT 29차시_ConditionalEdge실습. 코드 폴더는 src/29
# PPT: 29차시 질문 분류
# 역할: graph29에 질문을 넣어 실제 intent가 예상 intent와 같은지 표시합니다.
# 읽는 키: question
# 쓰는 키: 화면 출력. State는 바꾸지 않고 결과만 읽습니다.
# 그래프:
#   classify --> greeting | calc | scope | retrieve --> ...
# --------------------------------------------------------------
# 파이썬만 아는 분을 위한 말
# 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에 적습니다.
# 기대값과 같으면 체크 표시, 다르면 엑스 표시를 출력합니다.
# 답은 앞에서 60자만 출력합니다.
# ==============================================================

# ============================================================
# Classifier Test
# ============================================================
# 여러 질문을 그래프에 전달하여 분류 결과를 확인합니다.
# 실제 분류 결과와 예상 분류 결과를 비교하여
# 분류 노드가 정상적으로 작동하는지 테스트합니다.
# ============================================================

import os
import sys

# ============================================================
# 모듈 경로 설정
# ============================================================

# 현재 테스트 파일이 있는 폴더입니다.
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))

# 현재 폴더의 상위 src 폴더를 찾습니다.
SRC_DIR = os.path.abspath(
    os.path.join(CURRENT_DIR, "..")
)

# src 폴더에서 graph29와 State 모듈을 찾을 수 있도록 추가합니다.
sys.path.insert(0, SRC_DIR)

# ============================================================
# 그래프와 초기 State 가져오기
# ============================================================

# 분류 그래프를 가져옵니다.
from graph29 import app

# 질문을 초기 State로 만들어 주는 함수를 가져옵니다.
from graph_state2 import make_initial_state

# ============================================================
# 테스트 질문과 예상 결과
# ============================================================

# 각 질문과 기대하는 분류 결과를 저장합니다.
TEST_CASES = [
    ("안녕하세요", "greeting"),
    ("고맙습니다", "greeting"),
    ("125 + 340 * 2", "calc"),
    ("오늘 날씨 어때요?", "scope"),
    ("환불은 며칠 이내인가요?", "document"),
    ("휴가 신청은 어떻게 하나요?", "document"),
]

# ============================================================
# 테스트 실행 함수
# ============================================================

def run_tests():
    """
    테스트 질문을 그래프에 전달하고 결과를 출력합니다.
    """

    # 테스트 질문을 하나씩 확인합니다.
    for question, expected_intent in TEST_CASES:

        # 질문을 초기 State로 변환합니다.
        initial_state = make_initial_state(question)

        # 그래프를 실행합니다.
        result = app.invoke(
            initial_state,
            {
                "recursion_limit": 25,
            },
        )

        # 실제 분류 결과를 가져옵니다.
        actual_intent = result.get("intent", "")

        # 실제 결과와 예상 결과를 비교합니다.
        if actual_intent == expected_intent:
            mark = "✓"
        else:
            mark = "✗"

        # 답변이 없어도 오류가 발생하지 않도록 기본값을 사용합니다.
        answer = result.get("answer", "")

        # 질문별 테스트 결과만 출력합니다.
        print()
        print(f"{mark} 질문: {question}")
        print(f"   실제 분류: {actual_intent}")
        print(f"   예상 분류: {expected_intent}")
        print(f"   답변: {answer[:60]}")

if __name__ == "__main__":
    run_tests()

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

출력
===== 실행 결과 샘플 =====
경로: classify → greeting | calc | scope | retrieve → END
예시 출력 (API 키 필요, 실행하지 않음)
테스트 표의 기대 intent는 코드에 적혀 있습니다.
안녕하세요 -> greeting
고맙습니다 -> greeting
125 + 340 * 2 -> calc
오늘 날씨 어때요? -> scope
환불은 며칠 이내인가요? -> document
휴가 신청은 어떻게 하나요? -> document
실행 통과 여부와 답 앞부분은 기록에 없습니다.
기록된 같은 종류:
안녕하세요 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

코드 해설

graph29에 질문을 넣어 실제 intent가 예상 intent와 같은지 표시합니다.

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

  • 읽는 키: question
  • 쓰는 키: 화면 출력. State는 바꾸지 않고 결과만 읽습니다.
  • 그래프:
  • classify --> greeting | calc | scope | retrieve --> ...
  • 파이썬만 아는 분을 위한 말
  • State는 노드들이 함께 쓰는 딕셔너리입니다. 함수는 state를 읽고, 바꿀 키만 return 합니다.
  • return에 적지 않은 키는 이전 값이 남습니다.
  • log와 tried_queries는 리스트라서, 새로 return한 항목이 뒤에 이어 붙습니다.

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

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

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

← 이전 절29차시 · 6. 실습 ⑤ — 검색 실패 사유별 세부 라우팅다음 절 →29차시 · 8. 개선 효과 측정