> EulerAgent > 튜토리얼 > 그래프 > 그래프 06. State Schema 상세 — 타입, ...

그래프 06. 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"]

sum_int — 정수 합산 (integer 전용)

여러 브랜치의 카운터를 합산합니다.

result_count:
  type: integer
  merge: sum_int
# 동작: operator.add (정수 덧셈)
branch_a 기여: 3
branch_b 기여: 5
합산 결과: 3 + 5 = 8

concat_str — 문자열 연결 (string 전용)

여러 브랜치의 텍스트를 이어 붙입니다. 구분자 없이 단순 연결이므로 주의가 필요합니다.

combined_notes:
  type: string
  merge: concat_str
# 동작: operator.add (문자열 연결)
branch_a: "첫 번째 메모. "
branch_b: "두 번째 메모."
결과: "첫 번째 메모. 두 번째 메모."

last_write — 마지막 쓰기 승리 (모든 타입)

가장 마지막으로 완료된 브랜치의 값이 최종값이 됩니다. 비결정론적입니다.

summary:
  type: string
  merge: last_write
위험 시나리오: 2개 브랜치가 같은 last_write 키를 씀
  branch_a 완료: state["summary"] = "A의 요약"
  branch_b 완료: state["summary"] = "B의 요약"  ← 마지막 완료이므로 승리

  결과: "B의 요약" (하지만 다음 실행에서는 A가 마지막일 수도 있음)

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: stringmerge: 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

← 이전 목록으로 다음 →