그래프 04. 경계 있는 루프 — max_iterations 보장
학습 목표
이 튜토리얼을 마치면 다음을 할 수 있습니다.
- draft → evaluate → revise 루프를 Graph에서 구현할 수 있다.
defaults.max_iterations로 루프를 안전하게 경계 짓는 방법을 이해한다.UNBOUNDED_CYCLE에러가 발생하는 조건과 해결 방법을 안다.graph compile로 IR의 defaults 섹션을 확인하고 max_iterations 값을 검증한다.- 코드 수정 루프 (implement → test → fix → test) 실용 예제를 완성한다.
사전 준비
mkdir -p examples/graphs/loops
# 이전 튜토리얼 파일 확인
ls examples/graphs/loops/
경계 있는 루프란?
에이전트가 원하는 품질에 도달할 때까지 작업을 반복하는 것은 흔한 패턴입니다. 그러나 무한
루프는 토큰 비용, 실행 시간, 자원 낭비를 초래합니다. euleragent는 max_iterations로 루프
횟수에 상한을 둡니다.
루프 구조:
draft ──→ evaluate ──[judge.route==revise]──→ revise ──┐
↓ │
[judge.route==finalize] │
↓ ←─────────┘
finalize (최대 max_iterations회 반복)
max_iterations는 평가 노드(evaluate)가 최대 몇 번 실행될 수 있는지를 제한합니다.
단계별 실습
단계 1: 3회 루프 제한 패턴 작성
# examples/graphs/loops/bounded_loop.yaml
id: graph.bounded_loop
version: 1
category: demo
description: draft → evaluate → revise 루프, 최대 3회 반복
defaults:
max_iterations: 3 # evaluate 노드 최대 3회 실행
max_total_tool_calls: 20
max_web_search_calls: 5
nodes:
- id: draft
kind: llm
runner:
mode: execute
artifacts:
primary: output.md
- id: evaluate
kind: judge
judge:
schema: evaluator_v1
route_values: [finalize, revise]
- id: revise
kind: llm
runner:
mode: execute
max_loops: 3 # revise 노드 자체도 최대 3회
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/bounded_loop.yaml
예상 출력:
단계 1/3: YAML 파싱... 완료
단계 2/3: Pattern 기본 검증...
[✓] 노드 ID 유일성
[✓] 엣지 소스/타겟 존재
[✓] finalize 도달 가능성
[✓] judge route_values 커버리지
[✓] 순환 감지됨: evaluate → revise → evaluate
max_iterations: 3 ✓ (경계 있음)
완료
단계 3/3: Graph 추가 검증...
[✓] 병렬 그룹 없음 — state_schema 불필요
완료
결과: 유효 ✓ (루프 경계 확인됨: max_iterations=3)
단계 2: defaults.max_iterations 제거 시 UNBOUNDED_CYCLE 에러 시연
max_iterations가 없을 때 어떤 에러가 발생하는지 확인합니다.
# examples/graphs/loops/unbounded_loop.yaml
id: graph.unbounded_loop
version: 1
category: demo
description: 의도적 에러 — max_iterations 없는 루프
defaults:
max_total_tool_calls: 20
# max_iterations 없음! ← 의도적 오류
nodes:
- id: draft
kind: llm
runner:
mode: execute
- id: evaluate
kind: judge
judge:
schema: evaluator_v1
route_values: [finalize, revise]
- id: revise
kind: llm
runner:
mode: execute
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/unbounded_loop.yaml
예상 출력:
단계 2/3: Pattern 기본 검증...
[✗] 순환 감지됨: evaluate → revise → evaluate
max_iterations가 설정되지 않았습니다!
오류: UNBOUNDED_CYCLE
그래프에 경계 없는 순환이 감지되었습니다:
evaluate → revise → evaluate (무한 루프 가능)
defaults.max_iterations가 없으면 이 루프는 무한히 반복될 수 있습니다.
해결 방법:
defaults:
max_iterations: 3 # 원하는 최대 반복 횟수 설정
결과: 유효하지 않음 (오류 1개)
단계 3: max_iterations 추가로 수정
unbounded_loop.yaml에 max_iterations를 추가하여 수정합니다.
# 수정된 버전
defaults:
max_iterations: 3 # ← 추가
max_total_tool_calls: 20
# 수정 후 재검증
euleragent graph validate examples/graphs/loops/unbounded_loop.yaml
# 결과: 유효 ✓
단계 4: graph compile로 IR의 defaults 확인
euleragent graph compile examples/graphs/loops/bounded_loop.yaml \
--out /tmp/bounded_loop_ir.json
python -m json.tool /tmp/bounded_loop_ir.json | \
python -c "import sys,json; d=json.load(sys.stdin); \
print(json.dumps(d['defaults'], indent=2))"
예상 출력:
{
"max_iterations": 3,
"max_total_tool_calls": 20,
"max_web_search_calls": 5
}
IR의 defaults는 LangGraph 실행 시 런타임 가드로 사용됩니다. max_iterations: 3은 evaluate
노드가 3번 실행된 후 judge가 여전히 "revise"를 반환하면 강제로 "finalize"로 라우팅합니다.
{
"langgraph_builder": {
"runtime_guards": {
"max_iterations": {
"counter_key": "__iteration_count__",
"target_nodes": ["evaluate"],
"limit": 3,
"on_exceed": "force_route_to_finalize"
}
}
}
}
단계 5: 실용 예제 — 코드 수정 루프
실제 개발 워크플로우에서 유용한 코드 수정 루프를 구현합니다.
implement (코드 작성) → test (테스트 실행) → fix (오류 수정) → test (재실행)
↓ 테스트 통과
finalize (최종 코드 저장)
# examples/graphs/loops/code_fix_loop.yaml
id: graph.code_fix_loop
version: 1
category: engineering
description: 코드 작성 → 테스트 → 수정 루프, 최대 4회
defaults:
max_iterations: 4
max_total_tool_calls: 40
max_web_search_calls: 5
nodes:
- id: implement
kind: llm
runner:
mode: execute
artifacts:
primary: solution.py
secondary: [tests.py]
- id: test
kind: judge
judge:
schema: evaluator_v1
route_values: [pass, fail, critical_fail]
- id: fix
kind: llm
runner:
mode: execute
max_loops: 2
artifacts:
primary: solution.py
- id: escalate
kind: llm
runner:
mode: plan
force_tool: web.search
artifacts:
primary: research_notes.md
edges:
- from: implement
to: test
when: "true"
- from: test
to: finalize
when: "judge.route == pass"
- from: test
to: fix
when: "judge.route == fail"
- from: test
to: escalate
when: "judge.route == critical_fail"
- from: fix
to: test
when: "true"
- from: escalate
to: fix
when: "approvals_resolved"
finalize:
artifact: solution.py
# 검증
euleragent graph validate examples/graphs/loops/code_fix_loop.yaml
# 컴파일
euleragent graph compile examples/graphs/loops/code_fix_loop.yaml \
--out examples/graphs/loops/code_fix_loop_compiled.json
예상 출력:
검증 중: examples/graphs/loops/code_fix_loop.yaml
단계 2/3: Pattern 기본 검증...
[✓] judge route_values 커버리지
test: [pass, fail, critical_fail]
엣지: pass → finalize ✓
엣지: fail → fix ✓
엣지: critical_fail → escalate ✓
[✓] 순환 감지됨: test → fix → test
max_iterations: 4 ✓
[✓] 순환 감지됨: test → escalate → fix → test
max_iterations: 4 ✓ (동일 카운터)
완료
결과: 유효 ✓
max_iterations의 정확한 동작 방식
max_iterations가 카운트하는 것은 루프 기준 노드(loop anchor node)의 실행 횟수입니다.
일반적으로 judge 노드가 루프의 기준이 됩니다.
예: max_iterations: 3, test 노드가 기준
실행 1: implement → test (1회) → fail → fix → test (2회) → fail → fix → test (3회) → fail
↓
test가 3회 실행됨 → max_iterations 초과
→ judge 결과 무시 → 강제로 finalize 라우팅
실행 2: implement → test (1회) → pass
↓
정상 종료 (max_iterations 미소진)
중요: max_iterations 초과 시 강제 finalize는 품질 보장 없이 종료를 의미합니다.
따라서 적절한 값 설정이 중요합니다.
# 권장: 도메인별 max_iterations 가이드
defaults:
# 문서 작성: 2-3회 반복으로 충분
max_iterations: 3
# 코드 수정: 4-6회 (더 많은 시도 필요)
max_iterations: 5
# 복잡한 분석: 5-8회
max_iterations: 6
예상 출력 요약
| 명령 | 예상 결과 |
|---|---|
graph validate bounded_loop.yaml |
유효 ✓ (루프 경계 확인됨: max_iterations=3) |
graph validate unbounded_loop.yaml |
UNBOUNDED_CYCLE 오류 |
graph compile bounded_loop.yaml |
defaults.max_iterations: 3 in IR |
graph validate code_fix_loop.yaml |
유효 ✓ (3-route judge 확인됨) |
핵심 개념 요약
| 개념 | 설명 |
|---|---|
max_iterations |
judge 루프 기준 노드의 최대 실행 횟수 |
| UNBOUNDED_CYCLE | 루프가 있지만 max_iterations가 없을 때 발생 |
| 루프 기준 노드 | 일반적으로 judge 노드 (평가 횟수 카운트) |
| 강제 finalize | max_iterations 초과 시 품질과 무관하게 종료 |
max_loops |
개별 노드(llm)의 내부 최대 실행 횟수 (별개 설정) |
max_iterations vs max_loops 차이
| 설정 | 범위 | 카운트 대상 | 위치 |
|---|---|---|---|
defaults.max_iterations |
그래프 전체 | 루프 기준 노드 실행 횟수 | defaults 섹션 |
runner.max_loops |
개별 노드 | 해당 노드의 내부 재시도 횟수 | nodes[].runner |
defaults:
max_iterations: 3 # ← 그래프 레벨: evaluate가 3번 실행 가능
nodes:
- id: revise
runner:
max_loops: 2 # ← 노드 레벨: revise 내부에서 2번 재시도 가능
두 설정은 독립적으로 작동합니다. max_iterations: 3 + max_loops: 2이면 revise는
최대 3(그래프 반복) × 2(내부 재시도) = 6번의 실질 시도를 할 수 있습니다.
흔한 오류
오류 1: 루프가 없는데 max_iterations 설정
defaults:
max_iterations: 3 # 루프가 없는 선형 그래프에서 설정
edges:
- from: draft
to: finalize # 단방향, 루프 없음
when: "true"
경고: MAX_ITERATIONS_UNUSED
defaults.max_iterations=3이 설정되어 있지만 그래프에 순환이 없습니다.
이 설정은 무시됩니다.
이 경우 경고만 발생하고 검증은 통과합니다. 불필요한 설정은 제거하는 것이 좋습니다.
오류 2: max_iterations가 너무 작음
defaults:
max_iterations: 1 # 1회만 허용
judge가 항상 "revise"를 반환하면 1번 실행 후 강제 finalize됩니다. 이는 의도한 것일 수 있으나, 일반적으로 최소 2~3으로 설정하는 것을 권장합니다.
경고: MAX_ITERATIONS_TOO_SMALL
max_iterations=1은 루프를 사실상 비활성화합니다.
revise 루프가 필요하면 max_iterations >= 2를 권장합니다.
오류 3: judge 노드가 루프 기준 노드가 아닌 경우
# evaluate는 루프 외부에 있고, 실제 루프는 fix → retest → fix
nodes:
- id: evaluate # 한 번만 실행됨
- id: retest # 루프 기준 노드
- id: fix # 루프의 일부
edges:
- from: evaluate
to: fix
when: "judge.route == fix"
- from: fix
to: retest
when: "true"
- from: retest
to: finalize
when: "judge.route == pass"
- from: retest
to: fix
when: "judge.route == fail"
오류: UNBOUNDED_CYCLE
루프 감지됨: fix → retest → fix
루프 기준 노드는 'retest'입니다.
max_iterations를 설정하세요.
실습 과제
과제: 보고서 작성 루프
다음 스펙으로 루프 그래프를 작성하세요.
- research 노드: 주제 조사 (plan mode + web.search)
- draft 노드: 보고서 초안 작성 (execute mode)
- critique 노드: judge —
excellent/needs_improvement/reject excellent→ finalizeneeds_improvement→ improve (작은 수정)reject→ research (처음부터 재조사)- improve 노드: 개선 (execute mode)
- 최대 반복: 5회
- 최종 결과물:
report.md
힌트: reject가 research로 돌아가는 루프도 max_iterations 카운터를 소모합니다.
# 작성 후 검증 및 컴파일
euleragent graph validate examples/graphs/loops/report_loop.yaml
euleragent graph compile examples/graphs/loops/report_loop.yaml
이전: 03_judge_route.md | 다음: 05_interrupt_hooks.md