> EulerAgent > 튜토리얼 > 패턴 > Judge 노드와 품질 루프

패턴 04. Judge 노드와 품질 루프 — 만족스러울 때까지 반복하기

학습 목표

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

사전 준비

ls my_first_pattern.yaml
euleragent pattern validate my_first_pattern.yaml

1. 왜 Judge 노드가 필요한가?

03_simple_linear.md의 선형 패턴에서 write 노드는 한 번만 실행됩니다. 결과물의 품질이 낮아도 그냥 종료됩니다. 실제로는 이렇게 동작해야 합니다.

초안 작성 → 품질 평가
                │
                ├── 충분히 좋음 → 완료
                │
                └── 부족함 → 수정 → 다시 평가 → ...

Judge 노드는 이 "평가 → 분기" 역할을 담당합니다. LLM에게 평가를 요청하고, 그 결과에 따라 다른 노드로 라우팅합니다.


2. Judge 노드 이해하기

evaluator_v1 스키마

judge.schema: evaluator_v1은 내장 평가 스키마입니다. 이 스키마를 사용하면 Judge LLM에게 다음 구조화된 JSON 응답을 요청합니다.

{
  "score": 0.87,
  "route": "finalize",
  "reason": "핵심 개념이 명확하게 설명됨. 코드 예시 품질 우수.",
  "suggestions": [
    "도입부를 좀 더 강렬하게 시작하면 좋을 것 같음",
    "결론에 행동 유도(CTA)를 추가하면 더 완결성 있음"
  ]
}

route_values 선언

Judge 노드는 route_values에 가능한 라우팅 값을 선언해야 합니다. 모든 값에 대응하는 엣지가 있어야 합니다.

nodes:
  - id: evaluate
    kind: judge
    judge:
      schema: evaluator_v1
      route_values: [finalize, revise]    # 반드시 양쪽 다 엣지 필요

edges:
  - from: evaluate
    to: finalize
    when: "judge.route == finalize"       # finalize 커버

  - from: evaluate
    to: revise
    when: "judge.route == revise"         # revise 커버

when 조건 문법

Judge 라우팅에 사용되는 when DSL:

# 라우트 값 비교
when: "judge.route == finalize"
when: "judge.route == revise"

# 점수 임계값 (선택적)
when: "judge.score >= 0.85"
when: "judge.score < 0.7"

3. 패턴 설계

앞서 만든 블로그 작성 패턴에 Judge 루프를 추가합니다.

[research] → [draft] → [evaluate] → finalize
                            │
                            └── judge.route == revise
                                    │
                                    ▼
                                 [revise] ──────────────┐
                                                        │
                        ◄─────────────────────────────-┘

완전한 흐름:

┌─────────────────────────────────────────────────────────────────┐
│ blog_with_judge.pattern 흐름도                                    │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  [research]                                                     │
│     │ 주제 조사 (llm/execute, exclude: web.search)               │
│     │ when: true                                                │
│     ▼                                                           │
│  [draft]                                                        │
│     │ 초안 작성 (llm/execute)                                    │
│     │ when: true                                                │
│     ▼                                                           │
│  [evaluate] ──────── when: judge.route == finalize ─────────────┐
│     │ 품질 평가 (judge/evaluator_v1)                             │
│     │ when: judge.route == revise                               │
│     ▼                                                           │
│  [revise]                                                       │
│     │ 초안 개선 (llm/execute)                                    │
│     │ when: true                                                │
│     └──────────────────────────► [evaluate] (루프 최대 3회)       │
│                                                                 │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │ [FINALIZE]  blog_post.md 저장                            │◄──┘
│  └──────────────────────────────────────────────────────────┘   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

4. YAML 작성

blog_with_judge.yaml 파일을 생성합니다.

id: blog.quality_loop
version: 1
category: writing
description: "Judge 노드로 품질 루프를 갖춘 블로그 작성 패턴"

defaults:
  # 사이클이 있으므로 max_iterations는 필수!
  # evaluate → revise → evaluate 루프가 최대 3회 반복됨
  max_iterations: 3

  # 전체 실행에서 도구 호출 최대 횟수
  max_total_tool_calls: 15

  # Judge가 이 점수 이상이면 finalize 라우팅을 선호
  # (런타임 참고값 - Judge LLM의 결정에 영향을 줌)
  pass_threshold: 0.85

nodes:
  # ── 노드 1: research ──
  - id: research
    kind: llm
    runner:
      mode: execute
      exclude_tools: [web.search, web.fetch]
    prompt:
      system_append: |
        당신은 기술 리서처입니다. 주제에 대한 핵심 포인트를 정리하세요.
        출력: 마크다운 구조의 리서치 노트 (500-800 단어)
    artifacts:
      primary: research_notes.md

  # ── 노드 2: draft ──
  - id: draft
    kind: llm
    runner:
      mode: execute
      exclude_tools: [web.search, web.fetch, shell.exec]
    prompt:
      system_append: |
        당신은 기술 블로그 작가입니다.
        리서치 노트를 바탕으로 완성된 블로그 포스트 초안을 작성하세요.

        요구사항:
        - 길이: 800-1200 단어
        - 구성: 도입부 → 본문(3개 섹션) → 결론
        - 독자: 경험 있는 개발자
        - 코드 예시 포함
        - 마크다운 포맷
    artifacts:
      primary: blog_post.md

  # ── 노드 3: evaluate (Judge) ──
  - id: evaluate
    kind: judge             # judge 타입으로 선언
    judge:
      schema: evaluator_v1  # 내장 평가 스키마
      # 가능한 라우팅 값 목록.
      # 반드시 아래 엣지에서 모두 커버해야 함!
      route_values: [finalize, revise]
    prompt:
      system_append: |
        당신은 기술 블로그 편집장입니다. 다음 기준으로 블로그 포스트를 평가하세요.

        평가 기준:
        - 기술적 정확성 (30%): 정보가 정확하고 최신인가?
        - 구성과 가독성 (25%): 논리적 흐름, 섹션 구분이 명확한가?
        - 코드 품질 (25%): 코드 예시가 실행 가능하고 명확한가?
        - 독자 가치 (20%): 독자가 새로운 것을 배울 수 있는가?

        score >= 0.85이면 'finalize', 미만이면 'revise'를 선택하세요.
        suggestions는 구체적이고 실행 가능하게 작성하세요.

  # ── 노드 4: revise ──
  - id: revise
    kind: llm
    runner:
      mode: execute
      exclude_tools: [web.search, web.fetch, shell.exec]
    prompt:
      system_append: |
        당신은 기술 블로그 작가입니다.
        편집장의 피드백을 반영해서 블로그 포스트를 개선하세요.

        중요: 편집장의 suggestions를 모두 반영하되,
        포스트의 전체 구조와 기술적 내용은 유지하세요.
        개선된 전체 포스트를 다시 작성하세요.
    artifacts:
      primary: blog_post.md  # draft와 같은 파일명 — 덮어씀

edges:
  # 선형 흐름
  - from: research
    to: draft
    when: "true"

  - from: draft
    to: evaluate
    when: "true"

  # Judge 라우팅 — route_values의 모든 값을 커버해야 함
  - from: evaluate
    to: finalize
    when: "judge.route == finalize"   # finalize 루트

  - from: evaluate
    to: revise
    when: "judge.route == revise"     # revise 루트

  # 수정 후 다시 평가 (루프)
  - from: revise
    to: evaluate
    when: "true"

finalize:
  artifact: blog_post.md

5. 검증

euleragent pattern validate blog_with_judge.yaml

예상 출력:

Validating pattern: blog_with_judge.yaml

  Stage 1 (Schema)      PASS
  Stage 2 (Structural)  PASS
  Stage 3 (IR Analysis) PASS

  Cycle detected: evaluate → revise → evaluate
  Bounded by: max_iterations = 3  ✓

Validation complete: 0 errors, 0 warnings

6. 컴파일

euleragent pattern compile blog_with_judge.yaml

컴파일 출력에서 사이클 정보를 확인합니다:

{
  "id": "blog.quality_loop",
  "entry_node": "research",
  "cycles": [
    {
      "path": ["evaluate", "revise", "evaluate"],
      "length": 2,
      "bounded_by": "max_iterations",
      "max_iterations": 3
    }
  ],
  "nodes": {
    "evaluate": {
      "kind": "judge",
      "judge": {
        "schema": "evaluator_v1",
        "route_values": ["finalize", "revise"],
        "route_coverage": {
          "finalize": { "covered": true, "edge": "evaluate→finalize" },
          "revise": { "covered": true, "edge": "evaluate→revise" }
        }
      }
    }
  }
}

route_coverage에서 모든 route_values가 커버됐음을 확인합니다.


7. 실행 및 Judge 결과 확인

설치 및 실행

cp blog_with_judge.yaml .euleragent/patterns/
euleragent pattern run blog.quality_loop my-agent \
  --task "Docker 컨테이너 네트워킹 이해하기 — bridge, host, overlay 모드 비교" \
  --project default

예상 출력 (Judge가 revise 결정):

[run:g7b3e1d4] Starting pattern: blog.quality_loop

  ✓ research     Completed (11s)
  ✓ draft        Completed (16s) — 1,023 words
  ✓ evaluate     Completed (8s)
                 score: 0.71 → route: revise
                 reason: "코드 예시가 부족하고 overlay 네트워크 설명이 얕음"
                 suggestions:
                   - "docker network create 명령 예시 추가"
                   - "overlay 네트워크의 실제 사용 사례 (Docker Swarm) 추가"
  ✓ revise       Completed (19s) — 1,187 words (개선됨)
  ✓ evaluate     Completed (7s)
                 score: 0.89 → route: finalize
                 reason: "코드 예시 충실, 구성 명확, 독자 가치 높음"
  ✓ finalize     Completed

Run g7b3e1d4 completed. (2 evaluate iterations)
Artifact: .euleragent/runs/g7b3e1d4/artifacts/blog_post.md

Judge 결과를 이벤트 스트림에서 확인

cat .euleragent/runs/g7b3e1d4/pattern_events.jsonl | grep '"node":"evaluate"'

출력:

{"ts":"2026-02-23T14:32:18Z","event":"node.complete","node":"evaluate","kind":"judge","result":{"score":0.71,"route":"revise","reason":"코드 예시가 부족하고 overlay 네트워크 설명이 얕음","suggestions":["docker network create 명령 예시 추가","overlay 네트워크의 실제 사용 사례 (Docker Swarm) 추가"]},"iteration":1}
{"ts":"2026-02-23T14:32:52Z","event":"node.complete","node":"evaluate","kind":"judge","result":{"score":0.89,"route":"finalize","reason":"코드 예시 충실, 구성 명확, 독자 가치 높음","suggestions":[]},"iteration":2}

8. max_iterations 제거 시 발생하는 에러 시연

의도적으로 에러를 발생시켜봅니다. blog_with_judge.yaml에서 defaults.max_iterations를 제거하거나 주석 처리합니다.

# defaults:
#   max_iterations: 3   ← 이 줄을 제거

검증 실행:

euleragent pattern validate blog_with_judge.yaml

예상 출력:

Validating pattern: blog_with_judge.yaml

  Stage 1 (Schema)      PASS
  Stage 2 (Structural)  PASS
  Stage 3 (IR Analysis) FAIL

  ERROR [UNBOUNDED_CYCLE]
    Cycle detected: evaluate → revise → evaluate
    This cycle has no bound. Set defaults.max_iterations to limit iterations.
    Hint: Add to defaults section:
      max_iterations: 3

Validation complete: 1 error, 0 warnings

max_iterations를 복원하면 에러가 사라집니다.


9. pass_threshold의 역할

defaults.pass_threshold: 0.85는 Judge LLM에게 전달되는 런타임 힌트입니다. Judge의 system_append에 자동으로 주입됩니다.

Judge LLM이 받는 평가 지침:
  pass_threshold: 0.85
  → "score >= 0.85이면 finalize, 미만이면 revise를 선택하세요"

pass_threshold는 Judge의 라우팅을 강제하지 않습니다. Judge LLM이 최종 결정을 내립니다. 이 값은 Judge에게 기대 수준을 알려주는 역할을 합니다.


10. 주요 개념 설명

사이클과 선형 흐름의 차이

선형 패턴 (사이클 없음):

[A] → [B] → [C] → finalize

max_iterations 불필요. 각 노드가 정확히 한 번 실행됩니다.

품질 루프 패턴 (사이클 있음):

[A] → [B] → [C] → finalize
              ↑      |
              └─[D]←─┘ (C가 revise를 선택하면)

max_iterations 필수. C-D-C 루프가 무한히 반복될 수 있습니다.

Judge가 아닌 llm 노드로 평가할 수 없나?

기술적으로는 llm 노드에서 when: "true" 조건으로 분기할 수 있습니다. 하지만 judge 노드는 다음 이점이 있습니다.

  1. evaluator_v1 스키마로 구조화된 응답 (score, route, suggestions)이 보장됨
  2. route_values 선언으로 컴파일 타임에 커버리지 검증 가능
  3. suggestions가 다음 revise 노드에 자동으로 전달됨
  4. 이벤트 스트림에 평가 결과가 구조화되어 기록됨

max_iterations 작동 방식

max_iterations: 3은 사이클 내의 루프 횟수를 제한합니다. 3회 후에도 Judge가 revise를 선택하면, 런타임은 강제로 finalize로 라우팅합니다.

1회: evaluate(score:0.71, revise) → revise
2회: evaluate(score:0.79, revise) → revise
3회: evaluate(score:0.83, revise) → ⚠️ max_iterations 도달 → 강제 finalize

이때 이벤트 스트림에 경고가 기록됩니다.


11. 실습 과제: 더 세밀한 라우팅

현재 패턴은 finalize 또는 revise 두 가지 경로만 있습니다. 이를 확장해서 점수에 따라 다른 개선 강도를 적용해보세요.

과제: 점수 기반 3단계 라우팅

# evaluate 노드 수정
judge:
  schema: evaluator_v1
  route_values: [finalize, light_edit, major_rewrite]

# system_append 수정
system_append: |
  평가 기준:
  - score >= 0.85: 'finalize'
  - score 0.65-0.84: 'light_edit' (가벼운 수정)
  - score < 0.65: 'major_rewrite' (전면 재작성)
# 새 노드 추가
- id: light_edit
  kind: llm
  runner:
    mode: execute
  prompt:
    system_append: |
      편집장의 suggestions 중 상위 2개만 반영하여
      최소한의 수정으로 블로그 포스트를 개선하세요.

- id: major_rewrite
  kind: llm
  runner:
    mode: execute
  prompt:
    system_append: |
      블로그 포스트를 전면 재작성하세요.
      research_notes.md를 다시 읽고 완전히 새로운 접근으로 작성합니다.
# 새 엣지 추가
- from: evaluate
  to: finalize
  when: "judge.route == finalize"

- from: evaluate
  to: light_edit
  when: "judge.route == light_edit"

- from: evaluate
  to: major_rewrite
  when: "judge.route == major_rewrite"

- from: light_edit
  to: evaluate
  when: "true"

- from: major_rewrite
  to: evaluate
  when: "true"
euleragent pattern validate blog_three_routes.yaml

JUDGE_ROUTE_COVERAGE_ERROR가 발생하지 않는지 확인하세요.


다음 단계

Judge 루프를 이해했습니다. 이제 실제 웹 검색을 안전하게 패턴에 통합하는 방법을 배웁니다.

← 이전: 첫 번째 커스텀 패턴 목록으로 다음: 웹 리서치 통합 →