> EulerAgent > 튜토리얼 > 그래프 > 그래프 04. 경계 있는 루프 — max_iterati...

그래프 04. 경계 있는 루프 — max_iterations 보장

학습 목표

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


사전 준비

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.yamlmax_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를 설정하세요.

실습 과제

과제: 보고서 작성 루프

다음 스펙으로 루프 그래프를 작성하세요.

힌트: rejectresearch로 돌아가는 루프도 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

← 이전 목록으로 다음 →