RAG2026-5일31차시 · 예외 처리 설계8 / 19← 이전목차다음 →MetaContents

31차시. 예외 처리 설계

5. 실습 ① — 노드별 예외 처리 표준화

출처: 원본 PDF 31차시_방어적코딩실습.pdf 11~12쪽 · 쪽 그림 11쪽 12쪽

모든 노드가 같은 방식으로 실패하도록 데코레이터를 만듭니다.

📝 src/nodes/safe.py

Python
import functools
import traceback

def safe_node(default_updates=None, name=None):
    # 노드에서 예외가 나도 그래프가 중단되지 않게 감싼다
    def deco(fn):
        node_name = name or fn.__name__

        @functools.wraps(fn)
        def wrapper(state):
            try:
                return fn(state)
            except Exception as e:
                err = f"{type(e).__name__}: {str(e)[:80]}"
                print(f"[ERROR] {node_name}: {err}")
                print(traceback.format_exc()[:400])

                updates = dict(default_updates or {})
                updates.setdefault("node_error", err)
                updates.setdefault("log",
                                   [f"{node_name} 오류: {err}"])
                return updates
        return wrapper
    return deco

📝 적용

Python
from nodes.safe import safe_node

@safe_node({"documents": [], "retrieval_ok": False,
            "fail_reason": "search_error"})
def retriever_node(state):
    ...        # 기존 코드 그대로

@safe_node({"answer": "", "insufficient": True,
            "gen_error": "unknown"})
def generator_node(state):
    ...
@safe_node({"grade": "pass", "reason": "검증 불가 - 통과 처리"})
def verifier_node(state):
    ...

🛡️ 각 노드의 실패 시 기본값이 다릅니다
검색 실패 → retrieval_ok=False
→ 라우터가 재작성이나 fallback으로 보냅니다
생성 실패 → gen_error 표시
→ 검증 노드가 즉시 giveup 판정합니다
검증 실패 → grade="pass"
→ 검증 때문에 서비스가 막히면 안 되니 통과시킵니다
실패했을 때 어디로 흘러가야 하는지를
기본값으로 표현하는 것이 핵심입니다.

실습 코드 · 31/safe.py

실습 코드: 실습/src/31/safe.py · 위의 원본 PDF 31차시 11~12쪽 「5️⃣ 실습 ① — 노드별 예외 처리 표준화」과 이어집니다.

Python
# ==============================================================
# 31차시. PPT 31차시_예외처리설계. 코드 폴더는 src/31
# PPT: 31차시 안전 실행과 운영 로그
# 역할: safe_node. 노드 함수를 실행하고, 예외가 나면 node_error와 log로 바꿔 반환합니다.
# 읽는 키: 호출자가 넘기는 state 전체
# 쓰는 키: 성공 시 노드의 return 그대로. 실패 시 node_error, log
# 그래프:
#   safe_node(fn, state)
#     |-- fn(state) 성공 --> 그 dict
#     +-- 예외 ----------> {node_error, log: ['노드 실행 중 오류 발생']}
# --------------------------------------------------------------
# 파이썬만 아는 분을 위한 말
# 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에 적습니다.
# 그래프 함수 시그니처는 fn(state) -> dict 입니다. safe_node는 그 함수를 한 번 감쌉니다.
# 오류 객체는 print로도 찍고, 문자열로 node_error에 넣습니다.
# 이 파일은 OpenAI와 FAISS를 부르지 않습니다.
# ==============================================================

# ============================================================
# Safe Node
# LangGraph 노드를 안전하게 실행합니다.
# 노드에서 오류가 발생해도 그래프가 중단되지 않도록 합니다.
# ============================================================

def safe_node(fn, state):
    """
    노드 함수를 실행하고,
    오류가 발생하면 오류 정보를 State로 반환합니다.
    """

    try:
        # 원래 노드를 실행합니다.
        result = fn(state)

        # 정상 실행 결과를 그대로 반환합니다.
        return result

    except Exception as e:

        # 발생한 오류를 화면에 출력합니다.
        print("[ERROR]", e)

        # 오류를 State 정보로 바꿔 반환합니다.
        return {
            "node_error": str(e),
            "log": ["노드 실행 중 오류 발생"]
        }

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

출력
===== 실행 결과 샘플 =====
실제 실행 (API 호출 없음)
함수만 호출해 확인했습니다. 파일 자체에는 __main__이 없어 직접 실행하면 출력이 없습니다.

정상 함수가 {'grade':'pass','answer':'10 + 20'} 를 반환했을 때:
{'grade': 'pass', 'answer': '10 + 20'}

함수가 RuntimeError('일부러 발생시킨 오류')를 냈을 때:
[ERROR] 일부러 발생시킨 오류
{'node_error': '일부러 발생시킨 오류', 'log': ['노드 실행 중 오류 발생']}

코드 해설

safe_node. 노드 함수를 실행하고, 예외가 나면 node_error와 log로 바꿔 반환합니다.

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

  • 읽는 키: 호출자가 넘기는 state 전체
  • 쓰는 키: 성공 시 노드의 return 그대로. 실패 시 node_error, log
  • 그래프:
  • safe_node(fn, state)
  • |-- fn(state) 성공 --> 그 dict
  • +-- 예외 ----------> {node_error, log: ['노드 실행 중 오류 발생']}
  • 파이썬만 아는 분을 위한 말
  • State는 노드들이 함께 쓰는 딕셔너리입니다. 함수는 state를 읽고, 바꿀 키만 return 합니다.

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

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

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

← 이전 절31차시 · 4. 왜 안내 문구를 나누는가다음 절 →31차시 · 6. 실습 ② — 타임아웃과 재시도