Graph 04. Bounded Loops — Guaranteeing max_iterations
Learning Objectives
After completing this tutorial, you will be able to:
- Implement a draft → evaluate → revise loop in a Graph.
- Understand how to safely bound a loop using
defaults.max_iterations. - Know the conditions that trigger an
UNBOUNDED_CYCLEerror and how to resolve it. - Verify the max_iterations value in the IR defaults section via
graph compile. - Complete a practical code fix loop example (implement → test → fix → test).
Prerequisites
mkdir -p examples/graphs/loops
# 이전 튜토리얼 파일 확인
ls examples/graphs/loops/
What Is a Bounded Loop?
It is a common pattern for agents to iterate on their work until the desired quality is reached. However, infinite
loops lead to token cost overruns, excessive execution time, and resource waste. euleragent uses max_iterations to
place an upper bound on loop iterations.
Loop structure:
draft ──→ evaluate ──[judge.route==revise]──→ revise ──┐
↓ │
[judge.route==finalize] │
↓ ←─────────┘
finalize (repeats up to max_iterations times)
max_iterations limits how many times the evaluation node (evaluate) can execute.
Step-by-Step Hands-On
Step 1: Write a Pattern with a 3-Iteration Loop Limit
# 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
Expected output:
단계 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)
Step 2: Demonstrate the UNBOUNDED_CYCLE Error by Removing defaults.max_iterations
Observe the error that occurs when max_iterations is absent.
# 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
Expected output:
단계 2/3: Pattern 기본 검증...
[✗] 순환 감지됨: evaluate → revise → evaluate
max_iterations가 설정되지 않았습니다!
오류: UNBOUNDED_CYCLE
그래프에 경계 없는 순환이 감지되었습니다:
evaluate → revise → evaluate (무한 루프 가능)
defaults.max_iterations가 없으면 이 루프는 무한히 반복될 수 있습니다.
해결 방법:
defaults:
max_iterations: 3 # 원하는 최대 반복 횟수 설정
결과: 유효하지 않음 (오류 1개)
Step 3: Fix by Adding max_iterations
Fix unbounded_loop.yaml by adding max_iterations.
# 수정된 버전
defaults:
max_iterations: 3 # ← 추가
max_total_tool_calls: 20
# 수정 후 재검증
euleragent graph validate examples/graphs/loops/unbounded_loop.yaml
# 결과: 유효 ✓
Step 4: Inspect the IR defaults via graph compile
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))"
Expected output:
{
"max_iterations": 3,
"max_total_tool_calls": 20,
"max_web_search_calls": 5
}
The IR's defaults are used as runtime guards during LangGraph execution. max_iterations: 3 means that
after the evaluate node has executed 3 times, if the judge still returns "revise," it will be forcibly routed to "finalize."
{
"langgraph_builder": {
"runtime_guards": {
"max_iterations": {
"counter_key": "__iteration_count__",
"target_nodes": ["evaluate"],
"limit": 3,
"on_exceed": "force_route_to_finalize"
}
}
}
}
Step 5: Practical Example — Code Fix Loop
Implement a code fix loop that is useful in real development workflows.
implement (write code) → test (run tests) → fix (fix errors) → test (re-run)
↓ tests pass
finalize (save final code)
# 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
Expected output:
검증 중: 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 ✓ (동일 카운터)
완료
결과: 유효 ✓
Exact Behavior of max_iterations
What max_iterations counts is the number of executions of the loop anchor node.
Typically, the judge node serves as the loop anchor.
Example: max_iterations: 3, test node is the anchor
Run 1: implement → test (1st) → fail → fix → test (2nd) → fail → fix → test (3rd) → fail
↓
test has executed 3 times → max_iterations exceeded
→ judge result ignored → forcibly routed to finalize
Run 2: implement → test (1st) → pass
↓
Normal termination (max_iterations not exhausted)
Important: Forced finalize upon exceeding max_iterations means termination without quality guarantees.
Therefore, setting an appropriate value is crucial.
# 권장: 도메인별 max_iterations 가이드
defaults:
# 문서 작성: 2-3회 반복으로 충분
max_iterations: 3
# 코드 수정: 4-6회 (더 많은 시도 필요)
max_iterations: 5
# 복잡한 분석: 5-8회
max_iterations: 6
Expected Output Summary
| Command | Expected Result |
|---|---|
graph validate bounded_loop.yaml |
Valid (loop boundary confirmed: max_iterations=3) |
graph validate unbounded_loop.yaml |
UNBOUNDED_CYCLE error |
graph compile bounded_loop.yaml |
defaults.max_iterations: 3 in IR |
graph validate code_fix_loop.yaml |
Valid (3-route judge confirmed) |
Key Concepts Summary
| Concept | Description |
|---|---|
max_iterations |
Maximum execution count for the judge loop anchor node |
| UNBOUNDED_CYCLE | Triggered when a loop exists but max_iterations is not set |
| Loop anchor node | Typically the judge node (counts evaluation iterations) |
| Forced finalize | Terminates regardless of quality when max_iterations is exceeded |
max_loops |
Maximum internal execution count for an individual node (llm) -- a separate setting |
max_iterations vs max_loops
| Setting | Scope | What It Counts | Location |
|---|---|---|---|
defaults.max_iterations |
Entire graph | Loop anchor node execution count | defaults section |
runner.max_loops |
Individual node | Internal retry count of that node | nodes[].runner |
defaults:
max_iterations: 3 # ← 그래프 레벨: evaluate가 3번 실행 가능
nodes:
- id: revise
runner:
max_loops: 2 # ← 노드 레벨: revise 내부에서 2번 재시도 가능
The two settings operate independently. With max_iterations: 3 + max_loops: 2, the revise node can
make up to 3 (graph iterations) x 2 (internal retries) = 6 effective attempts.
Common Errors
Error 1: Setting max_iterations on a Graph Without Loops
defaults:
max_iterations: 3 # 루프가 없는 선형 그래프에서 설정
edges:
- from: draft
to: finalize # 단방향, 루프 없음
when: "true"
경고: MAX_ITERATIONS_UNUSED
defaults.max_iterations=3이 설정되어 있지만 그래프에 순환이 없습니다.
이 설정은 무시됩니다.
In this case, only a warning is issued and validation still passes. It is best to remove unnecessary settings.
Error 2: max_iterations Too Small
defaults:
max_iterations: 1 # 1회만 허용
If the judge always returns "revise," execution will be forcibly finalized after just 1 iteration. While this may be intentional, it is generally recommended to set at least 2-3.
경고: MAX_ITERATIONS_TOO_SMALL
max_iterations=1은 루프를 사실상 비활성화합니다.
revise 루프가 필요하면 max_iterations >= 2를 권장합니다.
Error 3: Judge Node Is Not the Loop Anchor
# 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를 설정하세요.
Practice Exercise
Exercise: Report Writing Loop
Write a loop graph with the following specifications.
- research node: Investigate the topic (plan mode + web.search)
- draft node: Write a report draft (execute mode)
- critique node: judge --
excellent/needs_improvement/reject excellent-- finalizeneeds_improvement-- improve (minor fixes)reject-- research (re-investigate from scratch)- improve node: Make improvements (execute mode)
- Maximum iterations: 5
- Final artifact:
report.md
Hint: The reject route going back to research also consumes the max_iterations counter.
# 작성 후 검증 및 컴파일
euleragent graph validate examples/graphs/loops/report_loop.yaml
euleragent graph compile examples/graphs/loops/report_loop.yaml
Previous: 03_judge_route.md | Next: 05_interrupt_hooks.md