Home > EulerAgent > Tutorials > Graph > Graph 04. Bounded Loops — Guaranteeing max_iterations

Graph 04. Bounded Loops — Guaranteeing max_iterations

Learning Objectives

After completing this tutorial, you will be able to:


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.

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

← Prev Back to List Next →