그래프 08. 고급 병렬 패턴 — 3+ 브랜치, 복잡한 상태
학습 목표
이 튜토리얼을 마치면 다음을 할 수 있습니다.
- 3개 브랜치 병렬 리서치 패턴을 설계하고 구현할 수 있다.
- 각 브랜치가 독립적인 state key를 쓰는 설계와 공유 key를 쓰는 설계를 비교할 수 있다.
source_count(integer+sum_int)로 각 브랜치의 결과 수를 합산하는 패턴을 구현한다.PARALLEL_BRANCH_LIMIT_EXCEEDED에러를 직접 시연할 수 있다.- 중첩 parallel_groups의 제약을 이해하고 대안을 설계할 수 있다.
PARALLEL_SIDE_EFFECT_FORBIDDEN에러와 그 이유를 설명할 수 있다.
사전 준비
# 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개의 독립적인 데이터 소스에서 동시에 정보를 수집합니다.
web_search: 인터넷 검색 (execute mode —web.search를 force_tool로 지정하면 HITL 필요)local_search: 로컬 문서 검색 (execute mode)database_search: 데이터베이스 조회 (execute mode)
단계 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.write를 force_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