그래프 03. Graph에서 Judge 라우팅
학습 목표
이 튜토리얼을 마치면 다음을 할 수 있습니다.
- 2-route judge 패턴을 Graph YAML로 작성할 수 있다.
- 3-route judge 패턴을 Graph YAML로 작성할 수 있다.
JUDGE_ROUTE_COVERAGE_ERROR를 의도적으로 발생시키고 수정할 수 있다.graph compile로 judge AST가 IR에 어떻게 표현되는지 확인한다.- judge 라우팅이 LangGraph의
add_conditional_edges로 컴파일됨을 이해한다. - 런타임에
state["route"]값으로 분기가 결정되는 원리를 설명한다.
사전 준비
mkdir -p examples/graphs/loops
# 이전 튜토리얼 완료 확인
euleragent graph validate examples/graphs/linear/my_linear.yaml
Judge 라우팅 기본 개념
Judge 노드는 이전 노드의 출력 품질을 평가하고 다음 단계를 결정합니다. Pattern과 Graph
모두 judge 라우팅을 지원하지만, Graph에서는 이 라우팅이 LangGraph의 add_conditional_edges
API로 컴파일됩니다.
Judge 노드 실행 흐름:
1. LLM이 evaluator_v1 스키마로 이전 노드 출력 평가
2. 평가 결과에서 route 값 추출 (예: "finalize" 또는 "revise")
3. state["route"] = "finalize" 설정 (LangGraph 공유 상태 업데이트)
4. add_conditional_edges의 path_function이 state["route"] 반환
5. path_map에 따라 다음 노드로 이동
단계별 실습
단계 1: 2-route Judge 패턴 작성
가장 일반적인 2-route judge (finalize 또는 revise)를 작성합니다.
# examples/graphs/loops/judge_2route.yaml
id: graph.judge_2route
version: 1
category: demo
description: 2-route judge — finalize 또는 revise
defaults:
max_iterations: 3
max_total_tool_calls: 20
nodes:
- id: draft
kind: llm
runner:
mode: execute
artifacts:
primary: output.md
- id: evaluate
kind: judge
judge:
schema: evaluator_v1
route_values: [finalize, revise] # 2개 route
- id: revise
kind: llm
runner:
mode: execute
max_loops: 2
artifacts:
primary: output.md
edges:
- from: draft
to: evaluate
when: "true"
- from: evaluate
to: finalize
when: "judge.route == finalize"
- from: evaluate
to: revise
when: "judge.route == revise"
- from: revise
to: evaluate
when: "true"
finalize:
artifact: output.md
euleragent graph validate examples/graphs/loops/judge_2route.yaml
예상 출력:
단계 2/3: Pattern 기본 검증...
[✓] judge 노드 route_values 커버리지
evaluate: route_values=[finalize, revise]
엣지: judge.route==finalize → finalize ✓
엣지: judge.route==revise → revise ✓
완료
결과: 유효 ✓
단계 2: 3-route Judge 패턴 작성
더 세밀한 3-route judge (finalize / revise / escalate)를 작성합니다.
# examples/graphs/loops/judge_3route.yaml
id: graph.judge_3route
version: 1
category: demo
description: 3-route judge — finalize, revise, 또는 escalate
defaults:
max_iterations: 4
max_total_tool_calls: 30
nodes:
- id: draft
kind: llm
runner:
mode: execute
artifacts:
primary: output.md
- id: evaluate
kind: judge
judge:
schema: evaluator_v1
route_values: [finalize, revise, escalate] # 3개 route
- id: revise
kind: llm
runner:
mode: execute
artifacts:
primary: output.md
- id: escalate
kind: llm
runner:
mode: plan # 에스컬레이션은 HITL 승인 필요
force_tool: web.search
artifacts:
primary: escalation_report.md
edges:
- from: draft
to: evaluate
when: "true"
- from: evaluate
to: finalize
when: "judge.route == finalize"
- from: evaluate
to: revise
when: "judge.route == revise"
- from: evaluate
to: escalate
when: "judge.route == escalate"
- from: revise
to: evaluate
when: "true"
- from: escalate
to: evaluate
when: "approvals_resolved"
finalize:
artifact: output.md
euleragent graph validate examples/graphs/loops/judge_3route.yaml
예상 출력:
단계 2/3: Pattern 기본 검증...
[✓] judge 노드 route_values 커버리지
evaluate: route_values=[finalize, revise, escalate]
엣지: judge.route==finalize → finalize ✓
엣지: judge.route==revise → revise ✓
엣지: judge.route==escalate → escalate ✓
완료
결과: 유효 ✓
단계 3: JUDGE_ROUTE_COVERAGE_ERROR 의도적 발생
route_values에 선언된 route에 대한 엣지가 없을 때 발생하는 에러를 직접 확인합니다.
# examples/graphs/loops/judge_broken.yaml
id: graph.judge_broken
version: 1
category: demo
description: 의도적으로 broken된 judge — 에러 학습용
defaults:
max_iterations: 3
nodes:
- id: draft
kind: llm
runner:
mode: execute
- id: evaluate
kind: judge
judge:
schema: evaluator_v1
route_values: [finalize, revise, escalate] # 3개 선언
edges:
- from: draft
to: evaluate
when: "true"
- from: evaluate
to: finalize
when: "judge.route == finalize"
- from: evaluate
to: revise
when: "judge.route == revise"
# escalate 엣지 없음! ← 의도적 오류
finalize:
artifact: output.md
euleragent graph validate examples/graphs/loops/judge_broken.yaml
예상 출력:
단계 2/3: Pattern 기본 검증...
[✗] judge 노드 route_values 커버리지
evaluate: route_values=[finalize, revise, escalate]
엣지: judge.route==finalize → finalize ✓
엣지: judge.route==revise → revise ✓
엣지: judge.route==escalate → ✗ (엣지 없음!)
오류: JUDGE_ROUTE_COVERAGE_ERROR
노드 'evaluate'의 route_values에 'escalate'가 선언되어 있지만
'judge.route == escalate' 조건의 엣지가 없습니다.
해결 방법:
1. 'escalate' 목적지 노드와 엣지를 추가하세요:
- id: escalate
kind: llm
...
edges:
- from: evaluate
to: escalate
when: "judge.route == escalate"
2. 또는 route_values에서 'escalate'를 제거하세요:
route_values: [finalize, revise]
결과: 유효하지 않음 (오류 1개)
이 에러는 런타임에 judge가 "escalate"를 반환했을 때 어디로 가야 할지 모르는 상황을 미리 방지합니다.
단계 4: graph compile로 judge AST 확인
유효한 2-route judge를 컴파일하고 IR에서 judge 표현을 확인합니다.
euleragent graph compile examples/graphs/loops/judge_2route.yaml \
--out /tmp/judge_2route_ir.json
python -m json.tool /tmp/judge_2route_ir.json | grep -A 30 '"judge"'
예상 출력 (IR 발췌):
{
"id": "evaluate",
"kind": "judge",
"judge": {
"schema": "evaluator_v1",
"route_values": ["finalize", "revise"],
"ast": {
"type": "conditional_router",
"source_node": "evaluate",
"state_key": "route",
"branches": [
{
"route_value": "finalize",
"target_node": "finalize",
"condition": "judge.route == finalize"
},
{
"route_value": "revise",
"target_node": "revise",
"condition": "judge.route == revise"
}
]
}
}
}
"ast" 섹션이 judge 라우팅의 추상 구문 트리(Abstract Syntax Tree)입니다. 이 AST가
LangGraph 컴파일 시 add_conditional_edges로 변환됩니다.
IR의 langgraph_builder 섹션도 확인합니다.
python -m json.tool /tmp/judge_2route_ir.json | \
python -c "import sys,json; d=json.load(sys.stdin); \
print(json.dumps(d['langgraph_builder']['add_conditional_edges'], indent=2))"
예상 출력:
[
{
"source": "evaluate",
"path_function": "route_evaluate",
"path_map": {
"finalize": "finalize",
"revise": "revise"
}
}
]
단계 5: LangGraph에서 conditional_edges로 컴파일됨 이해
IR의 add_conditional_edges 정보는 실행 시 다음 LangGraph 코드로 구체화됩니다.
# euleragent가 내부적으로 생성하는 개념적 LangGraph 코드
from langgraph.graph import StateGraph, END
from typing import TypedDict
class GraphState(TypedDict):
route: str # judge가 설정하는 라우팅 값
def route_evaluate(state: GraphState) -> str:
"""judge가 state["route"]에 설정한 값으로 분기 결정"""
return state.get("route", "finalize")
# StateGraph 생성 및 노드 추가
graph = StateGraph(GraphState)
graph.add_node("draft", draft_fn)
graph.add_node("evaluate", evaluate_fn)
graph.add_node("revise", revise_fn)
# 일반 엣지
graph.add_edge("draft", "evaluate")
graph.add_edge("revise", "evaluate")
# judge 라우팅 → conditional_edges
graph.add_conditional_edges(
"evaluate", # 소스 노드
route_evaluate, # 경로 결정 함수
{
"finalize": END, # finalize는 종료 노드
"revise": "revise"
}
)
graph.set_entry_point("draft")
compiled = graph.compile()
핵심 포인트:
- state["route"]는 evaluate 노드(judge)가 실행될 때 설정됨
- route_evaluate 함수는 이 값을 읽어 다음 노드를 결정
- path_map은 route_values 각각을 노드 이름으로 매핑
단계 6: 3-route judge IR 확인
3-route judge도 컴파일하여 비교합니다.
euleragent graph compile examples/graphs/loops/judge_3route.yaml \
--out /tmp/judge_3route_ir.json
python -m json.tool /tmp/judge_3route_ir.json | \
python -c "import sys,json; d=json.load(sys.stdin); \
print(json.dumps(d['langgraph_builder']['add_conditional_edges'], indent=2))"
예상 출력:
[
{
"source": "evaluate",
"path_function": "route_evaluate",
"path_map": {
"finalize": "finalize",
"revise": "revise",
"escalate": "escalate"
}
}
]
2-route와 3-route의 차이는 path_map에 항목이 하나 더 있다는 것뿐입니다. LangGraph는
임의 개수의 route를 지원합니다.
예상 출력 요약
| 명령 | 예상 결과 |
|---|---|
graph validate judge_2route.yaml |
유효 ✓ |
graph validate judge_3route.yaml |
유효 ✓ |
graph validate judge_broken.yaml |
JUDGE_ROUTE_COVERAGE_ERROR |
graph compile judge_2route.yaml |
IR에 add_conditional_edges 확인 |
graph compile judge_3route.yaml |
path_map에 3개 항목 확인 |
핵심 개념 요약
| 개념 | 설명 |
|---|---|
| route_values | judge 노드가 반환할 수 있는 라우팅 값 목록 |
judge.route == X |
엣지의 when 조건 — judge가 X를 선택했을 때 |
state["route"] |
LangGraph 공유 상태의 route 키 |
add_conditional_edges |
LangGraph에서 judge 라우팅을 구현하는 API |
path_function |
state["route"]를 읽어 다음 노드를 반환하는 함수 |
path_map |
route_value → 다음 노드 이름 매핑 |
| JUDGE_ROUTE_COVERAGE_ERROR | route_values 항목에 대한 엣지 누락 |
Pattern vs Graph의 Judge 처리 차이
| 항목 | Pattern | Graph |
|---|---|---|
| judge 선언 방식 | 동일 | 동일 |
| route_values 처리 | 런타임 조건 평가 | add_conditional_edges로 컴파일 |
| route 값 저장 | 내부 실행 컨텍스트 | state["route"] (공유 상태) |
| 런타임 분기 결정 | 순차 실행 엔진 | LangGraph path_function |
| 검증 방식 | Pattern 검증기 | Graph 검증기 (동일 규칙) |
Graph에서 judge 라우팅의 동작 방식은 Pattern과 완전히 동일합니다. 차이는 내부 구현뿐입니다.
흔한 오류
오류 1: when 조건 오타
edges:
- from: evaluate
to: finalize
when: "judge.route == Finalize" # ← 대문자 F — route_values와 불일치
오류: JUDGE_ROUTE_COVERAGE_ERROR
'Finalize'에 대한 엣지가 있지만 route_values=['finalize', 'revise']에
'Finalize'가 없습니다. route_values는 소문자를 권장합니다.
해결: when: "judge.route == finalize" (소문자로 통일)
오류 2: 같은 route에 여러 엣지
edges:
- from: evaluate
to: finalize
when: "judge.route == finalize"
- from: evaluate
to: publish # ← 같은 route에 두 번째 엣지
when: "judge.route == finalize"
오류: JUDGE_ROUTE_DUPLICATE_EDGE
'evaluate' → route 'finalize'에 대한 엣지가 2개입니다:
finalize, publish
하나의 route는 하나의 목적지만 허용됩니다.
오류 3: judge 노드에 runner 선언
- id: evaluate
kind: judge
runner: # ← judge 노드에 runner는 불필요/무시됨
mode: execute
judge:
schema: evaluator_v1
route_values: [finalize, revise]
경고: JUDGE_RUNNER_IGNORED
judge 노드 'evaluate'에 runner가 선언되어 있습니다.
judge 노드는 자체 실행 방식이 있으므로 runner는 무시됩니다.
오류 4: 모든 엣지가 같은 when 조건
edges:
- from: evaluate
to: finalize
when: "true" # ← judge.route 조건 없이 항상 진행
- from: evaluate
to: revise
when: "true" # ← 둘 다 "true" — 어디로 가야 할지 모름
오류: AMBIGUOUS_EDGE_CONDITION
노드 'evaluate'에서 나가는 엣지가 2개 있는데 모두 when="true"입니다.
judge 노드의 엣지는 judge.route 조건을 사용해야 합니다.
실습 과제
과제 1: 4-route judge 작성
다음 4개 route를 가진 judge 패턴을 작성하세요.
excellent→ 즉시 finalizegood→ 약간의 polish 후 finalizeneeds_work→ 전면 revise 후 재평가reject→ 처음부터 다시 draft
# 작성 후 검증
euleragent graph validate examples/graphs/loops/judge_4route.yaml
euleragent graph compile examples/graphs/loops/judge_4route.yaml
과제 2: 두 개의 judge 노드
한 그래프에 두 개의 독립적인 judge 노드를 사용하는 패턴을 작성하세요.
quality_judge: 품질 평가 → high_quality / low_qualitysafety_judge: 안전성 평가 → safe / unsafe
draft → quality_judge → [low_quality: revise] → quality_judge
→ [high_quality: safety_judge]
→ [unsafe: sanitize] → safety_judge
→ [safe: finalize]
이전: 02_linear_graph.md | 다음: 04_bounded_loop.md