31차시. 예외 처리 설계
5. 실습 ① — 노드별 예외 처리 표준화
출처: 원본 PDF 31차시_방어적코딩실습.pdf 11~12쪽 · 쪽 그림 11쪽 12쪽
모든 노드가 같은 방식으로 실패하도록 데코레이터를 만듭니다.
📝 src/nodes/safe.py
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📝 적용
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️⃣ 실습 ① — 노드별 예외 처리 표준화」과 이어집니다.
# ==============================================================
# 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: 이름 그대로 한 가지 일만 합니다. 본문 주석을 같이 읽습니다.
파일 맨 아래 실행 결과 샘플 주석은 실행되지 않습니다. 화면 출력은 다음 절에 다시 적어 두었습니다.