패턴 04. Judge 노드와 품질 루프 — 만족스러울 때까지 반복하기
학습 목표
이 튜토리얼을 마치면 다음을 할 수 있습니다.
- Judge 노드를 선언하고
evaluator_v1스키마를 올바르게 설정할 수 있다 route_values와 엣지의 관계를 이해하고JUDGE_ROUTE_COVERAGE_ERROR를 예방할 수 있다evaluate → revise → evaluate루프 패턴을 설계하고 사이클을 올바르게 경계 지을 수 있다max_iterations와UNBOUNDED_CYCLE에러의 관계를 설명할 수 있다pattern_events.jsonl에서 Judge 결과를 읽고 해석할 수 있다
사전 준비
03_simple_linear.md완료 (선형 패턴 작성 경험)my_first_pattern.yaml존재 (03 튜토리얼에서 작성)
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)를 추가하면 더 완결성 있음"
]
}
score: 0.0~1.0.pass_threshold와 비교에 사용 (런타임 참고값)route:route_values중 하나. 실제 라우팅 결정에 사용reason: 평가 근거 (로그에 기록됨)suggestions: 다음 revise 노드에 자동으로 전달됨
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 노드는 다음 이점이 있습니다.
evaluator_v1스키마로 구조화된 응답 (score, route, suggestions)이 보장됨route_values선언으로 컴파일 타임에 커버리지 검증 가능suggestions가 다음 revise 노드에 자동으로 전달됨- 이벤트 스트림에 평가 결과가 구조화되어 기록됨
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 루프를 이해했습니다. 이제 실제 웹 검색을 안전하게 패턴에 통합하는 방법을 배웁니다.
- 다음 튜토리얼: 05_web_research.md —
force_tool: web.search로 웹 검색을 HITL 승인 하에 사용합니다 - 인간 검토: 06_human_gate.md — Judge 대신 사람이 직접 평가하는 게이트를 만듭니다
- 3-way 라우팅: 07_multi_route.md — 위의 실습 과제를 더 깊이 탐구합니다