그래프 06. State Schema 상세 — 타입, 리듀서, 설계
학습 목표
이 튜토리얼을 마치면 다음을 할 수 있습니다.
state_schema가 필요한 이유와 병렬 쓰기 충돌을 설명할 수 있다.- 5가지 타입(string, integer, float, list, dict)의 용도를 구분할 수 있다.
- 5가지 merge 전략(append_list, sum_int, concat_str, last_write, first_write)의 LangGraph 매핑을 설명할 수 있다.
- 14개 금지 조합을 포함한 타입+전략 호환성 표를 활용할 수 있다.
STATE_SCHEMA_MERGE_TYPE_MISMATCH에러를 의도적으로 발생시키고 수정할 수 있다.PARALLEL_NONDETERMINISTIC_MERGE경고를 이해하고 안전한 설계를 선택할 수 있다.- 실제 병렬 그래프를 위한 state_schema 설계 체크리스트를 적용할 수 있다.
사전 준비
mkdir -p examples/graphs/state
# state_schema는 parallel_groups와 함께 사용됩니다
# 이 튜토리얼은 07_parallel_basics.md의 준비 단계이기도 합니다
euleragent graph --help
1. state_schema가 필요한 이유
병렬 쓰기 충돌 시나리오
병렬 브랜치가 없는 선형 그래프에서는 노드가 순차적으로 실행되므로 상태 충돌이 없습니다.
그러나 parallel_groups를 사용하면 여러 노드가 동시에 같은 상태 키에 접근할 수 있습니다.
선형 실행 (충돌 없음):
research → findings 씀 → draft → findings 읽음 → finalize
병렬 실행 (충돌 가능):
branch_a → findings 씀 ─┐
branch_b → findings 씀 ─┤ → 동시에 같은 키에 쓰기 → 어느 값이 최종?
branch_c → findings 씀 ─┘
LangGraph는 이 충돌을 리듀서(reducer) 함수로 해결합니다. 리듀서가 없으면
INVALID_CONCURRENT_GRAPH_UPDATE 예외가 발생합니다.
euleragent의 state_schema는 이 리듀서를 선언적으로 정의하는 방법입니다.
LangGraph 리듀서 내부 동작
# state_schema의 append_list가 LangGraph 내부에서 변환되는 방식 (개념적)
from typing import Annotated, TypedDict
import operator
class GraphState(TypedDict):
# merge: append_list → Annotated[list, operator.add]
findings: Annotated[list, operator.add]
# 병렬 실행 시 리듀서 적용:
# branch_a가 쓴 값: ["결과 A"]
# branch_b가 쓴 값: ["결과 B"]
# 리듀서 적용: ["결과 A"] + ["결과 B"] = ["결과 A", "결과 B"]
# 최종 state["findings"] = ["결과 A", "결과 B"]
2. 5가지 타입
type: list
여러 항목을 순서 있게 수집하는 용도입니다. 병렬 브랜치의 결과 목록 합산에 가장 많이 사용됩니다.
state_schema:
findings:
type: list
merge: append_list # 권장: 병렬 결과 수집 시
type: string
텍스트 데이터를 저장합니다. 요약, 설명, 경로명 등에 사용됩니다.
state_schema:
summary:
type: string
merge: last_write # 단일 브랜치가 쓸 때
# 또는
combined_text:
type: string
merge: concat_str # 여러 브랜치 텍스트 연결 시
type: integer
정수 카운터, 횟수, 개수 저장에 사용됩니다.
state_schema:
result_count:
type: integer
merge: sum_int # 권장: 각 브랜치의 결과 수 합산
type: float
실수 점수, 확률, 비율 저장에 사용됩니다.
state_schema:
confidence_score:
type: float
merge: last_write # 단일 브랜치 (float은 sum/append 없음)
type: dict
구조화된 데이터 저장에 사용됩니다. 메타데이터, 설정, 복잡한 결과 등입니다.
state_schema:
metadata:
type: dict
merge: last_write # dict는 last_write 또는 first_write만 가능
3. 5가지 merge 전략
append_list — 리스트 연결 (list 전용)
여러 브랜치의 리스트 결과를 모두 수집합니다. 병렬 브랜치 결과 수집의 황금 표준입니다.
findings:
type: list
merge: append_list
# 동작: operator.add (리스트 연결)
["결과 A", "결과 B"] + ["결과 C"] = ["결과 A", "결과 B", "결과 C"]
- 장점: 결정론적, 모든 결과 수집
- 단점: 순서 비보장 (브랜치 완료 순서에 따름)
- LangGraph:
Annotated[list, operator.add]
sum_int — 정수 합산 (integer 전용)
여러 브랜치의 카운터를 합산합니다.
result_count:
type: integer
merge: sum_int
# 동작: operator.add (정수 덧셈)
branch_a 기여: 3
branch_b 기여: 5
합산 결과: 3 + 5 = 8
- 장점: 결정론적, 완전한 합산
- LangGraph:
Annotated[int, operator.add]
concat_str — 문자열 연결 (string 전용)
여러 브랜치의 텍스트를 이어 붙입니다. 구분자 없이 단순 연결이므로 주의가 필요합니다.
combined_notes:
type: string
merge: concat_str
# 동작: operator.add (문자열 연결)
branch_a: "첫 번째 메모. "
branch_b: "두 번째 메모."
결과: "첫 번째 메모. 두 번째 메모."
- 주의: 구분자가 없으므로 각 브랜치가 끝에 공백이나 줄바꿈을 포함해야 합니다.
- LangGraph:
Annotated[str, operator.add]
last_write — 마지막 쓰기 승리 (모든 타입)
가장 마지막으로 완료된 브랜치의 값이 최종값이 됩니다. 비결정론적입니다.
summary:
type: string
merge: last_write
위험 시나리오: 2개 브랜치가 같은 last_write 키를 씀
branch_a 완료: state["summary"] = "A의 요약"
branch_b 완료: state["summary"] = "B의 요약" ← 마지막 완료이므로 승리
결과: "B의 요약" (하지만 다음 실행에서는 A가 마지막일 수도 있음)
- 단일 브랜치: 안전 (하나만 쓰므로 결정론적)
- 복수 브랜치: 위험 (비결정론적) —
PARALLEL_NONDETERMINISTIC_MERGE경고 발생
first_write — 첫 번째 쓰기 승리 (모든 타입)
가장 먼저 완료된 브랜치의 값이 최종값이 됩니다. 역시 비결정론적입니다.
primary_result:
type: string
merge: first_write
- 용도: 여러 브랜치 중 가장 빠른 결과만 필요할 때
- 주의: 어느 브랜치가 먼저 완료될지 보장 없음
4. 타입+전략 호환성 표
euleragent는 다음 조합만 허용합니다. 나머지는 STATE_SCHEMA_MERGE_TYPE_MISMATCH 에러입니다.
| 타입 | append_list | sum_int | concat_str | last_write | first_write |
|---|---|---|---|---|---|
| list | O | X | X | O | O |
| string | X | X | O | O | O |
| integer | X | O | X | O | O |
| float | X | X | X | O | O |
| dict | X | X | X | O | O |
강조 표시(굵게): 해당 타입의 권장 merge 전략
금지 조합 (14개): 1. list + sum_int 2. list + concat_str 3. string + append_list 4. string + sum_int 5. integer + append_list 6. integer + concat_str 7. float + append_list 8. float + sum_int 9. float + concat_str 10. dict + append_list 11. dict + sum_int 12. dict + concat_str 13. (예약) 향후 추가될 수 있음 14. (예약) 향후 추가될 수 있음
단계별 실습
단계 1: 완전한 state_schema 작성 (3개 키)
# examples/graphs/state/schema_demo.yaml
id: graph.schema_demo
version: 1
category: demo
description: state_schema 상세 데모 — findings + summary + error_count
state_schema:
findings:
type: list
merge: append_list # 각 브랜치가 수집한 결과 목록 합산
summary:
type: string
merge: last_write # 단일 노드가 최종 요약을 씀
error_count:
type: integer
merge: sum_int # 각 브랜치에서 발견한 오류 수 합산
defaults:
max_iterations: 2
max_total_tool_calls: 30
parallel_groups:
- id: search_group
branches: [web_branch, local_branch]
join: merge_results
nodes:
- id: start
kind: llm
runner:
mode: execute
- id: web_branch
kind: llm
runner:
mode: execute
writes_state: [findings, error_count]
- id: local_branch
kind: llm
runner:
mode: execute
writes_state: [findings, error_count]
- id: merge_results
kind: llm
runner:
mode: execute
reads_state: [findings, error_count]
writes_state: [summary]
artifacts:
primary: summary.md
edges:
- from: start
to: web_branch
when: "true"
- from: start
to: local_branch
when: "true"
- from: web_branch
to: merge_results
when: "true"
- from: local_branch
to: merge_results
when: "true"
- from: merge_results
to: finalize
when: "true"
finalize:
artifact: summary.md
euleragent graph validate examples/graphs/state/schema_demo.yaml
예상 출력:
단계 3/3: Graph 추가 검증...
[✓] state_schema 존재 (parallel_groups 사용)
[✓] state_schema 타입+merge 호환성:
findings: list + append_list ✓
summary: string + last_write ✓
error_count: integer + sum_int ✓
[✓] 모든 브랜치 writes_state 선언:
web_branch: [findings, error_count] ✓
local_branch: [findings, error_count] ✓
[✓] merge_results reads_state 선언 ✓
완료
결과: 유효 ✓
단계 2: STATE_SCHEMA_MERGE_TYPE_MISMATCH 에러 시연
type: string에 merge: append_list를 지정하면 어떻게 되는지 확인합니다.
# examples/graphs/state/schema_mismatch.yaml
id: graph.schema_mismatch
version: 1
category: demo
state_schema:
findings:
type: string # ← string 타입
merge: append_list # ← 하지만 append_list는 list 전용! 불일치!
count:
type: integer
merge: concat_str # ← integer에 concat_str? 불일치!
score:
type: float
merge: sum_int # ← float에 sum_int? 불일치!
parallel_groups:
- id: grp
branches: [branch_a, branch_b]
join: join_node
nodes:
- id: branch_a
kind: llm
runner: {mode: execute}
writes_state: [findings, count, score]
- id: branch_b
kind: llm
runner: {mode: execute}
writes_state: [findings, count, score]
- id: join_node
kind: llm
runner: {mode: execute}
reads_state: [findings, count, score]
edges:
- {from: branch_a, to: join_node, when: "true"}
- {from: branch_b, to: join_node, when: "true"}
- {from: join_node, to: finalize, when: "true"}
finalize:
artifact: output.md
euleragent graph validate examples/graphs/state/schema_mismatch.yaml
예상 출력:
단계 3/3: Graph 추가 검증...
[✗] state_schema 타입+merge 호환성 검사 실패:
오류: STATE_SCHEMA_MERGE_TYPE_MISMATCH (3개)
1. 키 'findings': type=string, merge=append_list
append_list는 type=list에서만 사용 가능합니다.
해결: merge를 last_write, first_write, 또는 concat_str로 변경하세요.
2. 키 'count': type=integer, merge=concat_str
concat_str는 type=string에서만 사용 가능합니다.
해결: merge를 last_write, first_write, 또는 sum_int로 변경하세요.
3. 키 'score': type=float, merge=sum_int
sum_int는 type=integer에서만 사용 가능합니다.
해결: merge를 last_write 또는 first_write로 변경하세요.
결과: 유효하지 않음 (오류 3개)
단계 3: 수정 후 재검증
에러를 수정합니다.
# 수정된 state_schema
state_schema:
findings:
type: string # string이라면
merge: concat_str # ← concat_str 사용 (또는 타입을 list로 변경)
count:
type: integer
merge: sum_int # ← integer에는 sum_int
score:
type: float
merge: last_write # ← float에는 last_write 또는 first_write
euleragent graph validate examples/graphs/state/schema_mismatch.yaml
# 결과: 유효 ✓ (수정 후)
단계 4: PARALLEL_NONDETERMINISTIC_MERGE 시연
2개 브랜치가 동일한 last_write 키에 쓸 때 발생하는 경고를 확인합니다.
# examples/graphs/state/schema_nondeterministic.yaml
id: graph.schema_nondeterministic
version: 1
category: demo
description: 비결정론적 merge 경고 시연
state_schema:
summary:
type: string
merge: last_write # ← 두 브랜치가 이 키를 씀 → 비결정론적!
parallel_groups:
- id: grp
branches: [branch_a, branch_b]
join: join_node
nodes:
- id: branch_a
kind: llm
runner: {mode: execute}
writes_state: [summary] # ← summary에 씀
- id: branch_b
kind: llm
runner: {mode: execute}
writes_state: [summary] # ← branch_a와 같은 키에 씀 → 충돌!
- id: join_node
kind: llm
runner: {mode: execute}
reads_state: [summary]
edges:
- {from: branch_a, to: join_node, when: "true"}
- {from: branch_b, to: join_node, when: "true"}
- {from: join_node, to: finalize, when: "true"}
finalize:
artifact: output.md
euleragent graph validate examples/graphs/state/schema_nondeterministic.yaml
예상 출력:
단계 3/3: Graph 추가 검증...
경고: PARALLEL_NONDETERMINISTIC_MERGE
상태 키 'summary' (type=string, merge=last_write)가 2개 이상의 브랜치에서 쓰입니다:
branch_a: writes_state=[summary]
branch_b: writes_state=[summary]
last_write 리듀서는 마지막으로 완료된 브랜치의 값을 선택합니다.
브랜치 완료 순서는 실행마다 달라질 수 있으므로 결과가 비결정론적입니다.
해결 방법:
1. summary 타입을 list로 변경하고 merge를 append_list로 변경하세요.
2. 또는 summary를 쓰는 브랜치를 하나로 줄이세요.
3. 또는 concat_str로 변경하여 두 브랜치 텍스트를 이어 붙이세요.
결과: 유효 (오류 없음, 경고 1개)
경고는 있지만 검증은 통과합니다. 하지만 비결정론적 결과를 수용할 수 없다면 해결 방법을 따르십시오.
5. state_schema 설계 체크리스트
병렬 그래프의 state_schema를 설계할 때 다음 체크리스트를 사용하세요.
체크리스트
1. parallel_groups가 있으면 → state_schema 필수
□ state_schema 최상위에 선언됨
2. 각 브랜치의 writes_state 파악
□ 모든 브랜치의 writes_state 목록화
□ 각 키를 몇 개 브랜치가 쓰는지 확인
3. 공유 키 식별 (2개 이상 브랜치가 쓰는 키)
□ 공유 키에 last_write 사용 시 → 비결정론적 경고 확인
□ 공유 키가 list 타입 → append_list 사용
□ 공유 키가 integer 타입 → sum_int 사용
□ 공유 키가 string 타입 → concat_str 고려 (또는 list로 변경)
4. 타입+merge 호환성
□ list 키: append_list, last_write, first_write 중 선택
□ string 키: last_write, first_write, concat_str 중 선택
□ integer 키: last_write, first_write, sum_int 중 선택
□ float 키: last_write, first_write 중 선택
□ dict 키: last_write, first_write 중 선택
5. 조인 노드의 reads_state
□ 조인 노드가 필요한 모든 키를 reads_state에 선언
6. 최종 검증
□ euleragent graph validate 통과
□ 경고(PARALLEL_NONDETERMINISTIC_MERGE) 검토 후 수용 또는 수정
설계 예시: 올바른 state_schema
# 3개 브랜치, 여러 키를 사용하는 올바른 설계
state_schema:
# 3개 브랜치 모두 쓰는 공유 키 → append_list (결정론적)
all_findings:
type: list
merge: append_list
# 3개 브랜치 모두 숫자를 더하는 키 → sum_int (결정론적)
total_sources:
type: integer
merge: sum_int
# branch_a만 쓰는 키 → last_write (단일 브랜치이므로 안전)
web_summary:
type: string
merge: last_write
# branch_b만 쓰는 키 → last_write (단일 브랜치이므로 안전)
local_summary:
type: string
merge: last_write
# 조인 노드만 쓰는 키 → last_write (병렬 아님)
final_report:
type: string
merge: last_write
예상 출력 요약
| 명령 | 예상 결과 |
|---|---|
graph validate schema_demo.yaml |
유효 ✓, 3개 키 모두 호환성 확인 |
graph validate schema_mismatch.yaml |
STATE_SCHEMA_MERGE_TYPE_MISMATCH 3개 |
graph validate schema_nondeterministic.yaml |
유효 (경고 1개: PARALLEL_NONDETERMINISTIC_MERGE) |
핵심 개념 요약
| 개념 | 설명 |
|---|---|
| state_schema | 공유 상태 키의 타입과 리듀서 정의 |
| 리듀서(reducer) | 병렬 쓰기 충돌 해결 함수 |
| append_list | 리스트 연결 — list 타입 전용, 결정론적 |
| sum_int | 정수 합산 — integer 타입 전용, 결정론적 |
| concat_str | 문자열 연결 — string 타입 전용, 결정론적 |
| last_write | 마지막 쓰기 승리 — 모든 타입, 비결정론적 가능 |
| first_write | 첫 번째 쓰기 승리 — 모든 타입, 비결정론적 가능 |
| STATE_SCHEMA_MERGE_TYPE_MISMATCH | 타입+merge 금지 조합 사용 |
| PARALLEL_NONDETERMINISTIC_MERGE | 2개+ 브랜치가 same last_write 키 사용 |
흔한 오류
오류 1: state_schema 키 이름에 하이픈 사용
state_schema:
my-findings: # ← 하이픈 불가! 언더스코어 사용
type: list
merge: append_list
오류: STATE_SCHEMA_INVALID_KEY_NAME
'my-findings': 키 이름에 하이픈(-)을 사용할 수 없습니다.
언더스코어(_) 또는 camelCase를 사용하세요: my_findings
오류 2: writes_state에 state_schema 없는 키 선언
state_schema:
findings:
type: list
merge: append_list
nodes:
- id: branch_a
writes_state: [findings, nonexistent_key] # ← nonexistent_key는 schema에 없음
오류: WRITES_STATE_KEY_NOT_IN_SCHEMA
노드 'branch_a'의 writes_state에 'nonexistent_key'가 있지만
state_schema에 해당 키가 없습니다.
해결: state_schema에 nonexistent_key를 추가하거나
writes_state에서 제거하세요.
오류 3: float에 sum_int 사용
state_schema:
score:
type: float
merge: sum_int # float은 sum_int 불가!
오류: STATE_SCHEMA_MERGE_TYPE_MISMATCH
키 'score': type=float, merge=sum_int
sum_int는 type=integer에서만 사용 가능합니다.
float 타입에서 합산이 필요하면 last_write 또는 first_write를 사용하세요.
(float sum 리듀서는 현재 미지원)
실습 과제
과제: 경쟁사 분석 state_schema 설계
다음 스펙으로 3개 브랜치 병렬 경쟁사 분석 그래프의 state_schema를 설계하세요.
브랜치 구성:
- price_research 브랜치: 가격 정보 수집
- feature_research 브랜치: 기능 비교 수집
- review_research 브랜치: 사용자 리뷰 수집
요구사항: - 각 브랜치의 수집 결과를 하나의 리스트에 합산 - 각 브랜치가 찾은 경쟁사 수를 합산 (integer) - price_research만 평균 가격을 기록 (float) - merge_findings 조인 노드가 최종 분석 보고서 작성 (string)
# 설계 후 검증
euleragent graph validate examples/graphs/state/competitor_analysis.yaml
이전: 05_interrupt_hooks.md | 다음: 07_parallel_basics.md