29차시. Conditional Edge 실습
4. 실습 ③ — 유형별 처리 노드
출처: 원본 PDF 29차시_ConditionalEdge실습.pdf 9쪽 · 쪽 그림 9쪽
📝 src/nodes/intents.py
import re
import config
def greeting_node(state) -> dict:
return {
"answer": ("안녕하세요! 사내 문서에 관한 질문을 도와드립니다.\n"
"예: 환불 규정, 휴가 신청 방법, 고객센터 운영 시간 등"),
"grade": "pass",
"log": ["greeting 응답"],
}
def calc_node(state) -> dict:
expr = state["question"].strip().rstrip("=?").strip()
# 안전 검사: 숫자와 연산자만 허용 (eval 사용 시 필수)
if not re.fullmatch(r"[\d\s\+\-\*/\(\)\.]+", expr):
return {"answer": "계산할 수 없는 식입니다.",
"grade": "pass", "log": ["calc: 형식 오류"]}
try:
result = eval(expr, {"__builtins__": {}}, {})
except Exception:
return {"answer": "계산할 수 없는 식입니다.",
"grade": "pass", "log": ["calc: 계산 실패"]}
return {"answer": f"{expr} = {result}",
"grade": "pass", "log": [f"calc: {result}"]}
def scope_node(state) -> dict:
return {
"answer": ("죄송합니다. 저는 사내 문서에 관한 질문에만 "
"답변할 수 있습니다.\n"
"문서와 관련된 내용을 물어봐 주세요."),
"grade": "pass",
"log": ["scope: 범위 밖 안내"],
}⚠️ eval을 쓸 때는 반드시 검사부터
eval은 문자열을 파이썬 코드로 실행합니다.
사용자 입력을 그대로 넣으면 심각한 보안 취약점입니다.
· 정규식으로 숫자·연산자만 허용
· __builtins__를 비워 내장 함수 접근 차단
운영 환경이라면 eval 대신
ast.literal_eval이나 전용 계산 라이브러리를 쓰세요.
실습 코드 · 29/intents.py
실습 코드: 실습/src/29/intents.py · 위의 원본 PDF 29차시 9쪽 「4️⃣ 실습 ③ — 유형별 처리 노드」과 이어집니다.
# ==============================================================
# 29차시. PPT 29차시_ConditionalEdge실습. 코드 폴더는 src/29
# PPT: 29차시 질문 분류
# 역할: intent가 greeting, calc, scope일 때 바로 답을 만드는 세 노드. 문서를 찾지 않습니다.
# 읽는 키: question (calc_node만 계산식에 사용)
# 쓰는 키: answer, grade, reason, log
# 그래프:
# greeting_node --> 안내 문장, grade=pass
# calc_node -----> 식 계산, 성공하면 grade=pass, 실패하면 grade=giveup
# scope_node ----> 문서 밖 안내, grade=pass
# --------------------------------------------------------------
# 파이썬만 아는 분을 위한 말
# 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에 적습니다.
# 세 노드 모두 Retriever로 가지 않고 그래프가 END로 끝납니다.
# calc_node는 숫자와 +-*/(). 쉼표만 허용한 뒤 eval로 계산합니다. 내장 함수는 비워 둡니다.
# scope_node는 질문 문장과 상관없이 같은 안내를 반환합니다. 분류는 classifier가 이미 끝낸 상태입니다.
# ==============================================================
# ============================================================
# Intent Nodes
# ============================================================
# 질문 유형에 따라 간단한 답변을 반환하는 노드 모음입니다.
# 인사말, 계산식, 문서 범위 밖 질문을 각각 처리합니다.
# ============================================================
import re
# ============================================================
# 1. 인사말 처리 노드
# ============================================================
def greeting_node(state):
"""
사용자가 인사했을 때 안내 메시지를 반환합니다.
"""
# 사내 문서 질문을 도와줄 수 있다는 안내문입니다.
answer = (
"안녕하세요! 사내 문서에 관한 질문을 도와드립니다.\n"
"예: 환불 규정, 휴가 신청 방법, 고객센터 운영 시간 등"
)
# 답변과 처리 결과를 State에 저장합니다.
return {
"answer": answer,
"grade": "pass",
"reason": "인사말에 대한 안내 완료",
"log": ["인사말 응답"],
}
# ============================================================
# 2. 계산 처리 노드
# ============================================================
def calc_node(state):
"""
사용자가 입력한 간단한 계산식을 계산합니다.
"""
# State에서 사용자의 질문을 가져옵니다.
question = state.get("question", "")
# 질문 앞뒤의 공백을 제거합니다.
expression = question.strip()
# 마지막에 붙은 '=' 또는 '?'를 제거합니다.
expression = expression.rstrip("=?").strip()
# 숫자와 계산 기호만 허용합니다.
allowed_pattern = r"[\d\s+\-*/().,]+"
# 허용되지 않은 문자가 있으면 계산하지 않습니다.
if re.fullmatch(allowed_pattern, expression) is None:
return {
"answer": "계산식을 처리하지 못했습니다.",
"grade": "giveup",
"reason": "허용되지 않은 문자가 있습니다.",
"log": ["계산 실패"],
}
try:
# 천 단위 구분 쉼표를 제거합니다.
clean_expression = expression.replace(",", "")
# 검사를 통과한 계산식을 계산합니다.
result = eval(
clean_expression,
{
"__builtins__": {}
},
{},
)
# 계산 결과를 반환합니다.
return {
"answer": f"{expression} = {result}",
"grade": "pass",
"reason": "계산 완료",
"log": [
f"계산 완료: {expression} = {result}"
],
}
except Exception as error:
# 계산 중 오류가 발생하면 안내 메시지를 반환합니다.
return {
"answer": "계산식을 처리하지 못했습니다.",
"grade": "giveup",
"reason": f"계산 중 오류 발생: {type(error).__name__}",
"log": ["계산 실패"],
}
# ============================================================
# 3. 문서 범위 밖 질문 처리 노드
# ============================================================
def scope_node(state):
"""
사내 문서와 관련 없는 질문에 안내 메시지를 반환합니다.
"""
# 문서 질문만 처리한다는 안내문입니다.
answer = (
"죄송합니다.\n"
"저는 사내 문서에 관한 질문에만 답변할 수 있습니다.\n"
"문서와 관련된 내용을 물어봐 주세요."
)
# 범위 밖 질문에 대한 결과를 반환합니다.
return {
"answer": answer,
"grade": "pass",
"reason": "문서 범위 밖 질문에 대한 안내 완료",
"log": ["문서 범위 밖 질문"],
}
# ============================================================
# 단독 실행 테스트
# ============================================================
if __name__ == "__main__":
# 각 노드를 테스트할 질문입니다.
test_cases = [
("greeting", "안녕하세요"),
("calc", "1,000 + 2,000 * 3"),
("scope", "오늘 날씨 어때요?"),
]
# 질문을 하나씩 테스트합니다.
for intent, question in test_cases:
# 노드에 전달할 State를 만듭니다.
state = {
"question": question
}
# 의도에 따라 실행할 노드를 선택합니다.
if intent == "greeting":
result = greeting_node(state)
elif intent == "calc":
result = calc_node(state)
else:
result = scope_node(state)
# 실행 결과를 출력합니다.
print(f"\n질문: {question}")
print(f"답변: {result['answer']}")
print(f"판정: {result['grade']}")
print(f"로그: {result['log']}")실행 결과 샘플 (교안 기록. API 키가 필요한 코드는 키 없이 맞춘 예시이고, 모델의 답은 실행할 때마다 달라집니다)
===== 실행 결과 샘플 =====
실제 실행 (API 호출 없음)
이 파일 __main__ 을 실행한 출력입니다.
질문: 안녕하세요
답변: 안녕하세요! 사내 문서에 관한 질문을 도와드립니다.
예: 환불 규정, 휴가 신청 방법, 고객센터 운영 시간 등
판정: pass
로그: ['인사말 응답']
질문: 1,000 + 2,000 * 3
답변: 1,000 + 2,000 * 3 = 7000
판정: pass
로그: ['계산 완료: 1,000 + 2,000 * 3 = 7000']
질문: 오늘 날씨 어때요?
답변: 죄송합니다.
저는 사내 문서에 관한 질문에만 답변할 수 있습니다.
문서와 관련된 내용을 물어봐 주세요.
판정: pass
로그: ['문서 범위 밖 질문']
같은 calc_node에 기록된 질문 '10 + 20'을 넣어 확인한 반환:
answer='10 + 20 = 30' grade=pass log=['계산 완료: 10 + 20 = 30']
그래프 로그: 10 + 20 intent=calc grade=pass elapsed=0.0
그래프 로그: 안녕하세요 intent=greeting grade=pass elapsed=0.01
그래프 로그: 오늘 날씨 intent=scope grade=pass코드 해설
intent가 greeting, calc, scope일 때 바로 답을 만드는 세 노드. 문서를 찾지 않습니다.
코드 앞에서 고정해 두는 말입니다.
- 읽는 키: question (calc_node만 계산식에 사용)
- 쓰는 키: answer, grade, reason, log
- 그래프:
- greeting_node --> 안내 문장, grade=pass
- calc_node -----> 식 계산, 성공하면 grade=pass, 실패하면 grade=giveup
- scope_node ----> 문서 밖 안내, grade=pass
- 파이썬만 아는 분을 위한 말
- State는 노드들이 함께 쓰는 딕셔너리입니다. 함수는 state를 읽고, 바꿀 키만 return 합니다.
함수와 클래스로 나누어 읽습니다.
greeting_node: 이름 그대로 한 가지 일만 합니다. 본문 주석을 같이 읽습니다.calc_node: 이름 그대로 한 가지 일만 합니다. 본문 주석을 같이 읽습니다.scope_node: 이름 그대로 한 가지 일만 합니다. 본문 주석을 같이 읽습니다.
파일 맨 아래 실행 결과 샘플 주석은 실행되지 않습니다. 화면 출력은 다음 절에 다시 적어 두었습니다.