> EulerAgent > 튜토리얼 > 그래프 > 그래프 03. Graph에서 Judge 라우팅

그래프 03. Graph에서 Judge 라우팅

학습 목표

이 튜토리얼을 마치면 다음을 할 수 있습니다.


사전 준비

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 패턴을 작성하세요.

# 작성 후 검증
euleragent graph validate examples/graphs/loops/judge_4route.yaml
euleragent graph compile examples/graphs/loops/judge_4route.yaml

과제 2: 두 개의 judge 노드

한 그래프에 두 개의 독립적인 judge 노드를 사용하는 패턴을 작성하세요.

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

← 이전 목록으로 다음 →