> EulerAgent > 튜토리얼 > 그래프 > 그래프 08. 고급 병렬 패턴 — 3+ 브랜치, 복잡한...

그래프 08. 고급 병렬 패턴 — 3+ 브랜치, 복잡한 상태

학습 목표

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


사전 준비

# 07_parallel_basics.md 완료 확인
euleragent graph validate examples/graphs/parallel/my_first_parallel.yaml

mkdir -p examples/graphs/parallel

1. 3-브랜치 리서치 패턴

토폴로지

plan ──┬──→ web_search   ──┬──→ merge → analyze → finalize
       ├──→ local_search ──┤
       └──→ database_search─┘

이 패턴은 3개의 독립적인 데이터 소스에서 동시에 정보를 수집합니다.

단계 1: 독립 state key 설계

각 브랜치가 서로 다른 state key를 쓰는 설계입니다. 이 경우 last_write가 단일 브랜치에서만 사용되므로 안전합니다.

state_schema:
  web_results:
    type: list
    merge: last_write    # web_search 브랜치만 씀 → 안전

  local_results:
    type: list
    merge: last_write    # local_search 브랜치만 씀 → 안전

  db_results:
    type: list
    merge: last_write    # database_search 브랜치만 씀 → 안전

  source_count:
    type: integer
    merge: sum_int       # 3개 브랜치 모두 씀 → sum_int로 합산

장점: 각 소스의 결과를 독립적으로 접근 가능 단점: state_schema 키가 많아지고 조인 노드가 여러 키를 읽어야 함

단계 2: 공유 key 설계 (append_list)

각 브랜치가 같은 state key에 쓰는 설계입니다.

state_schema:
  all_results:
    type: list
    merge: append_list   # 3개 브랜치 모두 씀 → append_list로 결정론적 합산

  source_count:
    type: integer
    merge: sum_int       # 3개 브랜치의 결과 수 합산

장점: 단순한 스키마, 조인 노드가 하나의 키만 읽음 단점: 어느 소스에서 왔는지 출처 추적 어려움

단계 3: 완전한 3-브랜치 YAML 작성

# examples/graphs/parallel/three_branch_research.yaml
id: graph.three_branch_research
version: 1
category: research
description: 3개 소스 병렬 리서치 — web + local + database → merge → analyze

state_schema:
  # 독립 키 전략: 각 소스의 결과를 별도로 보존
  web_results:
    type: list
    merge: last_write      # web_search만 씀

  local_results:
    type: list
    merge: last_write      # local_search만 씀

  db_results:
    type: list
    merge: last_write      # database_search만 씀

  # 공유 합산 키: 전체 결과 수
  source_count:
    type: integer
    merge: sum_int         # 3개 브랜치 모두 씀

  # 조인 노드가 씀
  final_analysis:
    type: string
    merge: last_write      # merge 노드만 씀

defaults:
  max_iterations: 3
  max_total_tool_calls: 50
  max_web_search_calls: 10

parallel_groups:
  - id: research_group
    branches: [web_search, local_search, database_search]
    join: merge_findings

nodes:
  - id: plan
    kind: llm
    runner:
      mode: execute

  - id: web_search
    kind: llm
    runner:
      mode: execute
      exclude_tools: [shell.exec, file.write]  # 병렬 브랜치 안전 설정
    writes_state: [web_results, source_count]

  - id: local_search
    kind: llm
    runner:
      mode: execute
      exclude_tools: [shell.exec, file.write]
    writes_state: [local_results, source_count]

  - id: database_search
    kind: llm
    runner:
      mode: execute
      exclude_tools: [shell.exec, file.write]
    writes_state: [db_results, source_count]

  - id: merge_findings
    kind: llm
    runner:
      mode: execute
    reads_state: [web_results, local_results, db_results, source_count]
    writes_state: [final_analysis]
    artifacts:
      primary: analysis_report.md

  - id: evaluate
    kind: judge
    judge:
      schema: evaluator_v1
      route_values: [finalize, revise]

  - id: revise
    kind: llm
    runner:
      mode: execute
    artifacts:
      primary: analysis_report.md

edges:
  # plan → 팬아웃
  - from: plan
    to: web_search
    when: "true"
  - from: plan
    to: local_search
    when: "true"
  - from: plan
    to: database_search
    when: "true"

  # 팬인 → merge_findings
  - from: web_search
    to: merge_findings
    when: "true"
  - from: local_search
    to: merge_findings
    when: "true"
  - from: database_search
    to: merge_findings
    when: "true"

  # 하류 순차
  - from: merge_findings
    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: analysis_report.md
euleragent graph validate examples/graphs/parallel/three_branch_research.yaml

예상 출력:

단계 3/3: Graph 추가 검증...
  [✓] state_schema 존재 (parallel_groups 사용 시 필수)
  [✓] state_schema 타입+merge 호환성:
        web_results: list + last_write ✓
        local_results: list + last_write ✓
        db_results: list + last_write ✓
        source_count: integer + sum_int ✓
        final_analysis: string + last_write ✓
  [✓] parallel_groups 검증:
        research_group:
          branches: [web_search, local_search, database_search] (3개)
          join: merge_findings ✓
  [✓] 각 브랜치 writes_state:
        web_search: [web_results, source_count] ✓
        local_search: [local_results, source_count] ✓
        database_search: [db_results, source_count] ✓
  [✓] source_count (sum_int): 3개 브랜치가 씀 → 결정론적 ✓
  [✓] 팬아웃 엣지: plan → web_search, local_search, database_search ✓
  [✓] 팬인 엣지: 모든 브랜치 → merge_findings ✓
  완료

결과: 유효 ✓ (오류 없음, 경고 없음)

2. 공유 key append_list 전략 비교

같은 그래프를 append_list 공유 전략으로 재설계합니다.

# examples/graphs/parallel/three_branch_shared.yaml
id: graph.three_branch_shared
version: 1
category: research
description: 3개 브랜치 — 공유 append_list 키 전략

state_schema:
  all_findings:
    type: list
    merge: append_list   # 3개 브랜치 모두 씀 → append_list (결정론적)

  source_count:
    type: integer
    merge: sum_int       # 3개 브랜치 모두 씀 → sum_int (결정론적)

defaults:
  max_iterations: 2
  max_total_tool_calls: 40

parallel_groups:
  - id: research_group
    branches: [web_search, local_search, database_search]
    join: merge_findings

nodes:
  - id: plan
    kind: llm
    runner:
      mode: execute

  - id: web_search
    kind: llm
    runner:
      mode: execute
    writes_state: [all_findings, source_count]

  - id: local_search
    kind: llm
    runner:
      mode: execute
    writes_state: [all_findings, source_count]

  - id: database_search
    kind: llm
    runner:
      mode: execute
    writes_state: [all_findings, source_count]

  - id: merge_findings
    kind: llm
    runner:
      mode: execute
    reads_state: [all_findings, source_count]
    artifacts:
      primary: report.md

edges:
  - from: plan
    to: web_search
    when: "true"
  - from: plan
    to: local_search
    when: "true"
  - from: plan
    to: database_search
    when: "true"
  - from: web_search
    to: merge_findings
    when: "true"
  - from: local_search
    to: merge_findings
    when: "true"
  - from: database_search
    to: merge_findings
    when: "true"
  - from: merge_findings
    to: finalize
    when: "true"

finalize:
  artifact: report.md
euleragent graph validate examples/graphs/parallel/three_branch_shared.yaml
# 결과: 유효 ✓ (all_findings: append_list → 경고 없음)

3. PARALLEL_BRANCH_LIMIT_EXCEEDED 시연 (9개 브랜치)

병렬 브랜치 최대 개수(8개)를 초과하면 어떻게 되는지 확인합니다.

# examples/graphs/parallel/too_many_branches.yaml (에러 학습용)
id: graph.too_many_branches
version: 1
category: demo

state_schema:
  results:
    type: list
    merge: append_list

parallel_groups:
  - id: giant_group
    branches:
      - branch_1
      - branch_2
      - branch_3
      - branch_4
      - branch_5
      - branch_6
      - branch_7
      - branch_8
      - branch_9   # ← 9개! 최대 8개 초과
    join: join_node

nodes:
  - id: branch_1
    kind: llm
    runner: {mode: execute}
    writes_state: [results]
  - id: branch_2
    kind: llm
    runner: {mode: execute}
    writes_state: [results]
  - id: branch_3
    kind: llm
    runner: {mode: execute}
    writes_state: [results]
  - id: branch_4
    kind: llm
    runner: {mode: execute}
    writes_state: [results]
  - id: branch_5
    kind: llm
    runner: {mode: execute}
    writes_state: [results]
  - id: branch_6
    kind: llm
    runner: {mode: execute}
    writes_state: [results]
  - id: branch_7
    kind: llm
    runner: {mode: execute}
    writes_state: [results]
  - id: branch_8
    kind: llm
    runner: {mode: execute}
    writes_state: [results]
  - id: branch_9
    kind: llm
    runner: {mode: execute}
    writes_state: [results]
  - id: join_node
    kind: llm
    runner: {mode: execute}
    reads_state: [results]

edges:
  - {from: branch_1, to: join_node, when: "true"}
  - {from: branch_2, to: join_node, when: "true"}
  - {from: branch_3, to: join_node, when: "true"}
  - {from: branch_4, to: join_node, when: "true"}
  - {from: branch_5, to: join_node, when: "true"}
  - {from: branch_6, to: join_node, when: "true"}
  - {from: branch_7, to: join_node, when: "true"}
  - {from: branch_8, to: join_node, when: "true"}
  - {from: branch_9, to: join_node, when: "true"}
  - {from: join_node, to: finalize, when: "true"}

finalize:
  artifact: output.md
euleragent graph validate examples/graphs/parallel/too_many_branches.yaml

예상 출력:

오류: PARALLEL_BRANCH_LIMIT_EXCEEDED
  parallel_groups 'giant_group'에 9개의 브랜치가 있습니다.
  최대 허용 브랜치 수는 8개입니다.

  현재 브랜치: [branch_1, branch_2, ..., branch_9] (9개)
  허용 최대: 8개

  해결 방법:
  1. 브랜치 수를 8개 이하로 줄이세요.
  2. 여러 그룹으로 분리하고 중간 집계 노드를 추가하세요:
     grp_a: [branch_1..4] → merge_a
     grp_b: [branch_5..8] → merge_b
     (branch_9는 sequential하게 merge_b 다음에)

4. 중첩 parallel_groups 설계 원칙

euleragent Graph는 현재 중첩 parallel_groups를 지원하지 않습니다. flat 구조만 허용됩니다.

# 지원하지 않는 중첩 구조 (개념적)
parallel_groups:
  - id: outer_group
    branches: [inner_group_1, inner_group_2]  # ← inner 그룹이 브랜치?
    join: outer_join

  - id: inner_group_1    # ← 이것이 outer의 브랜치? 미지원
    branches: [a, b]
    join: inner_join_1
오류: PARALLEL_NESTED_GROUP_FORBIDDEN
  parallel_groups는 중첩을 지원하지 않습니다.
  'inner_group_1'이 'outer_group'의 브랜치로 참조됩니다.

중첩 대안 설계

중첩이 필요한 경우, 여러 순차 그룹으로 분리하여 해결합니다.

필요한 구조 (중첩):
  [A1, A2] → join_A ─┬─→ B1 ─→ final
                      └─→ B2 ─┘

대안 구조 1 (순차 병렬 그룹):
  [A1, A2] → join_A (1번째 그룹)
  → [B1, B2] → final (2번째 그룹)

대안 구조 2 (병렬 확장):
  [A1, A2, B1, B2] → join (전부 한 번에)
  join이 A 결과와 B 결과를 모두 처리
# 대안 구조 1: 두 개의 순차 parallel_groups
parallel_groups:
  - id: group_a
    branches: [a1, a2]
    join: join_a

  - id: group_b
    branches: [b1, b2]
    join: join_b

# 실행 순서: [a1, a2] 동시 → join_a → [b1, b2] 동시 → join_b
edges:
  - {from: a1, to: join_a, when: "true"}
  - {from: a2, to: join_a, when: "true"}
  - {from: join_a, to: b1, when: "true"}   # join_a가 b 그룹의 팬아웃
  - {from: join_a, to: b2, when: "true"}
  - {from: b1, to: join_b, when: "true"}
  - {from: b2, to: join_b, when: "true"}

5. PARALLEL_SIDE_EFFECT_FORBIDDEN 시연

병렬 브랜치에서 shell.exec 또는 file.writeforce_tool로 사용하면 발생합니다.

# 위험한 예: 병렬 브랜치에서 file.write force_tool 사용
nodes:
  - id: branch_a
    kind: llm
    runner:
      mode: plan
      force_tool: file.write   # ← 병렬 브랜치에서 file.write 금지!
    writes_state: [results]
# 이 YAML을 검증하면:
euleragent graph validate examples/graphs/parallel/side_effect_test.yaml

예상 출력:

오류: PARALLEL_SIDE_EFFECT_FORBIDDEN
  병렬 브랜치 'branch_a'에서 force_tool: file.write가 사용되었습니다.

  병렬 브랜치에서 다음 도구는 금지됩니다:
    - file.write: 동일 파일 동시 쓰기로 데이터 손상 위험
    - shell.exec: 병렬 셸 실행으로 파일 시스템 충돌 위험

  해결 방법:
  1. force_tool을 제거하고 브랜치는 상태(state)만 사용하세요.
  2. 파일 쓰기가 필요하면 조인 노드 이후 순차 노드에서 수행하세요.

올바른 설계:

# 병렬 브랜치: state에만 씀
- id: branch_a
  kind: llm
  runner:
    mode: execute      # force_tool 없음
  writes_state: [results]

# 조인 이후 순차 노드: 파일 쓰기 가능
- id: finalize_writer
  kind: llm
  runner:
    mode: plan
    force_tool: file.write   # 여기서는 OK (병렬 아님)

예상 출력 요약

명령 예상 결과
graph validate three_branch_research.yaml 유효 ✓, 3개 브랜치, source_count sum_int 확인
graph validate three_branch_shared.yaml 유효 ✓, append_list 공유 전략 확인
graph validate too_many_branches.yaml PARALLEL_BRANCH_LIMIT_EXCEEDED (9개 브랜치)
graph validate side_effect_test.yaml PARALLEL_SIDE_EFFECT_FORBIDDEN (file.write)

독립 key vs 공유 key 전략 비교

항목 독립 key 전략 공유 key 전략
state_schema 복잡도 높음 (key 수 많음) 낮음 (key 수 적음)
출처 추적 가능 여부 가능 (각 소스별 key) 불가 (혼합됨)
merge 전략 위험도 last_write (단일 브랜치) append_list (항상 안전)
조인 노드 복잡도 높음 (여러 key 읽기) 낮음 (적은 key 읽기)
권장 시나리오 소스별 차별화 처리 필요 단순 합산

핵심 개념 요약

개념 설명
독립 key 전략 각 브랜치가 별도 key에 씀 — last_write가 단일 브랜치
공유 key 전략 여러 브랜치가 같은 key에 씀 — append_list/sum_int 필수
source_count 각 브랜치의 결과 수를 sum_int로 합산하는 패턴
PARALLEL_BRANCH_LIMIT_EXCEEDED 브랜치 9개 이상 시 발생
중첩 parallel_groups 미지원 — flat 구조만 허용
PARALLEL_SIDE_EFFECT_FORBIDDEN 병렬 브랜치에서 file.write/shell.exec force_tool 금지
exclude_tools 브랜치에서 특정 도구를 명시적으로 제외하는 안전 설정

흔한 오류

오류 1: 조인 노드가 parallel_groups에 없는데 writes_state 없음

# join_node는 parallel_groups.join이므로 branche가 아님
# 그러나 writes_state는 선언 가능 (조인 노드도 상태를 씀)
- id: merge_findings
  kind: llm
  reads_state: [all_findings]
  writes_state: [final_analysis]   # OK: 조인 노드의 쓰기는 허용

조인 노드는 브랜치가 아니므로 writes_state 미선언 시 에러가 없습니다. 그러나 명시적 선언이 문서화 측면에서 권장됩니다.

오류 2: 두 개의 parallel_groups가 같은 join 노드 공유

parallel_groups:
  - id: group_a
    branches: [a1, a2]
    join: shared_join    # ← 두 그룹이 같은 join을?

  - id: group_b
    branches: [b1, b2]
    join: shared_join    # ← 동일한 join 노드
오류: PARALLEL_JOIN_MULTIPLE
  'shared_join' 노드가 2개의 parallel_groups에서 join으로 참조됩니다:
    group_a, group_b
  각 parallel_groups는 고유한 join 노드를 가져야 합니다.

실습 과제

과제: 시장 조사 4-브랜치 병렬 그래프

다음 구조로 4-브랜치 시장 조사 그래프를 작성하세요.

plan → [trend_research | competitor_research | customer_research | tech_research]
      → synthesize → evaluate ⇄ revise → finalize

state_schema 요구사항: - all_insights: list + append_list (4개 브랜치 공유) - insight_count: integer + sum_int (각 브랜치가 찾은 인사이트 수) - trend_summary: string + last_write (trend_research만 씀) - synthesis_report: string + last_write (synthesize 노드가 씀)

euleragent graph validate examples/graphs/parallel/market_research_4branch.yaml
euleragent graph compile examples/graphs/parallel/market_research_4branch.yaml \
  --out examples/graphs/parallel/market_research_4branch_compiled.json

이전: 07_parallel_basics.md | 다음: 09_parallel_with_quality.md

← 이전 목록으로 다음 →