> EulerAgent > 튜토리얼 > 그래프 > 그래프 01. Graph vs Pattern — 무엇이...

그래프 01. Graph vs Pattern — 무엇이 다른가?

학습 목표

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


사전 준비

# 환경 확인
euleragent --version
pip show langgraph | grep Version
euleragent graph --help

1. Pattern과 Graph의 관계

1.1 Graph는 Pattern의 완전한 상위 집합

euleragent의 Pattern과 Graph는 별개의 시스템이 아닙니다. Graph는 Pattern의 완전한 상위 집합(superset)입니다. 이것이 의미하는 바는 다음과 같습니다.

Pattern YAML ──────────────────────────────────────────
  ┌─────────────────────────────────────────────────┐
  │  id, version, category, description             │
  │  defaults (max_iterations, ...)                 │
  │  nodes (llm, judge, finalize kinds)             │
  │  edges (from, to, when conditions)              │
  │  finalize                                       │
  └─────────────────────────────────────────────────┘

Graph YAML ─────────────────────────────────────────────
  ┌─────────────────────────────────────────────────┐
  │  [Pattern의 모든 필드 동일하게 사용 가능]        │
  │                                                 │
  │  + state_schema      ← 신규 (타입+리듀서 정의)  │
  │  + parallel_groups   ← 신규 (팬아웃/팬인)       │
  │  + interrupt_before  ← 신규 (노드 전 일시 정지) │
  │  + interrupt_after   ← 신규 (노드 후 일시 정지) │
  └─────────────────────────────────────────────────┘

따라서 유효한 Pattern YAML은 대부분 euleragent graph validate도 통과합니다. 단, Graph 모듈은 LangGraph StateGraph를 통해 실행되므로 실행 엔진이 다릅니다.

1.2 실행 엔진의 차이

Pattern 실행 흐름:
  YAML → euleragent 순차 실행 엔진 → 결과

Graph 실행 흐름:
  YAML → IR 컴파일 → LangGraph StateGraph → 결과

Pattern은 euleragent 내부의 단순한 순차 상태 머신으로 실행됩니다. Graph는 LangGraph의 StateGraph를 통해 실행되며, 이를 통해 병렬 실행, 체크포인트, 조건부 엣지 등을 활용합니다.


2. 무엇이 추가되었나

2.1 state_schema — 공유 상태 정의

state_schema는 그래프 전체가 공유하는 상태 딕셔너리의 구조를 정의합니다. 병렬 브랜치가 서로 같은 상태 키에 접근할 때 충돌을 방지하기 위해 리듀서(reducer)를 선언합니다.

state_schema:
  findings:          # 상태 키 이름
    type: list       # 데이터 타입
    merge: append_list   # 리듀서 전략
  score:
    type: integer
    merge: sum_int
  summary:
    type: string
    merge: last_write

state_schema가 없으면 LangGraph는 기본값으로 last_write를 적용하여, 병렬 브랜치에서 같은 키에 쓸 때 결과가 비결정론적이 됩니다.

2.2 parallel_groups — 팬아웃/팬인

parallel_groups는 여러 노드를 동시에 실행하는 팬아웃(fan-out)과, 모든 브랜치가 완료된 후 하나의 노드로 합치는 팬인(fan-in)을 선언합니다.

parallel_groups:
  - id: research_group
    branches: [web_search, local_search, doc_search]
    join: merge_results

이 선언은 web_search, local_search, doc_search가 동시에 실행되고, 모두 완료된 후 merge_results 노드가 실행됨을 의미합니다.

2.3 interrupt_before / interrupt_after — 노드 수준 일시 정지

LangGraph의 체크포인트(checkpoint) 메커니즘을 활용하여, 특정 노드 실행 전/후에 그래프를 일시 정지할 수 있습니다.

checkpointer: memory   # ← interrupt 사용 시 최상위에 반드시 선언 (INTERRUPT_REQUIRES_CHECKPOINTER)

nodes:
  - id: publish
    kind: llm
    interrupt_before: true   # publish 실행 전 일시 정지
    runner:
      mode: execute

필수: interrupt_before/interrupt_after를 사용하는 그래프는 최상위에 checkpointer: 필드를 선언해야 합니다. 선언하지 않으면 graph validateINTERRUPT_REQUIRES_CHECKPOINTER 오류가 발생합니다.

이는 Pattern의 HITL(Human-In-The-Loop) force_tool 승인 큐와는 다른 메커니즘입니다. interrupt는 LangGraph 체크포인트 기반이고, force_tool 승인은 euleragent 파일 기반 JSONL 큐를 사용합니다.

2.4 LangGraph 통합

Graph 모듈은 langgraph.graph.StateGraph를 사용하여 그래프를 컴파일하고 실행합니다. 이를 통해 LangGraph의 모든 기능(스트리밍, 체크포인팅, 시각화 등)을 활용할 수 있습니다.


3. LangGraph StateGraph 아키텍처

3.1 공유 상태 모델

LangGraph StateGraph의 핵심 아이디어는 모든 노드가 하나의 공유 상태 딕셔너리를 읽고 쓴다는 것입니다.

# LangGraph가 내부적으로 생성하는 상태 타입 (개념적 표현)
from typing import TypedDict, Annotated
import operator

class GraphState(TypedDict):
    findings: Annotated[list, operator.add]     # append_list → operator.add
    score: Annotated[int, operator.add]         # sum_int → operator.add
    summary: str                                # last_write → 기본값

Annotated[list, operator.add]는 LangGraph에서 "이 키에 여러 노드가 동시에 쓸 때, operator.add(즉, 리스트 연결)로 합치라"는 의미입니다.

3.2 브랜치 격리 vs 공유 상태

Pattern의 노드들은 순차적으로 실행되므로 상태 충돌이 없습니다. Graph의 병렬 브랜치는 격리되어 실행되지만 공유 상태를 통해 통신합니다.

잘못된 이해:
  브랜치 A의 상태 ──→ (격리됨) ──→ 독립 상태
  브랜치 B의 상태 ──→ (격리됨) ──→ 독립 상태

올바른 이해:
  브랜치 A ──→ 공유 상태 딕셔너리 ←── 브랜치 B
                     ↕
  리듀서가 충돌을 해결함 (operator.add 등)

3.3 조건부 엣지

LangGraph는 add_conditional_edges를 사용하여 Judge 노드의 라우팅을 구현합니다.

# euleragent가 내부적으로 생성하는 LangGraph 코드 (개념적 표현)
def route_judge(state: GraphState) -> str:
    return state.get("route", "finalize")

graph.add_conditional_edges(
    "evaluate",
    route_judge,
    {"finalize": "finalize", "revise": "revise"}
)

4. YAML → IR → LangGraph 컴파일 파이프라인

Graph 모듈의 컴파일 파이프라인은 3단계로 구성됩니다.

┌─────────────────────────────────────────────────────────────────┐
│ 단계 1: YAML 파싱                                               │
│   your_graph.yaml                                               │
│        ↓ (PyYAML 파싱)                                          │
│   Python 딕셔너리                                               │
└─────────────────────────────────────────────────────────────────┘
         ↓
┌─────────────────────────────────────────────────────────────────┐
│ 단계 2: 검증 (euleragent graph validate)                        │
│   - Pattern 기본 검증 (노드 존재, 엣지 연결 등)                 │
│   - Graph 추가 검증 (병렬 에러 코드 14개 확인)                  │
│   - state_schema 타입+merge 호환성 검사                         │
└─────────────────────────────────────────────────────────────────┘
         ↓
┌─────────────────────────────────────────────────────────────────┐
│ 단계 3: IR 생성 (euleragent graph compile)                      │
│   - Graph IR JSON 생성                                          │
│   - graph_type: "graph" 마킹                                    │
│   - LangGraph StateGraph 빌더 코드 포함                         │
│   - parallel_groups → LangGraph 병렬 엣지 변환                  │
│   - state_schema → Annotated 타입 변환                          │
└─────────────────────────────────────────────────────────────────┘
         ↓
┌─────────────────────────────────────────────────────────────────┐
│ 실행 시: LangGraph StateGraph 실행                              │
│   - IR을 LangGraph StateGraph로 구체화                          │
│   - .compile() 호출                                             │
│   - .invoke() / .stream() 실행                                  │
└─────────────────────────────────────────────────────────────────┘

5. 단계별 실습

단계 1: euleragent doctor로 환경 확인

euleragent doctor

예상 출력:

euleragent doctor 실행 중...
  Python          3.11.4   ✓
  euleragent      0.9.0    ✓
  langgraph       1.0.9    ✓  (graph 모듈 사용 가능)
  pyyaml          6.0.1    ✓
  click           8.1.7    ✓

모든 진단 항목 통과.

LangGraph 버전이 1.0.9 미만이거나 없으면 다음을 실행하세요.

pip install "langgraph>=1.0.9"

단계 2: graph validate 실행 (예제 파일 사용)

# 예제 디렉터리에 있는 병렬 그래프 검증
euleragent graph validate examples/graphs/parallel/02_research_parallel.yaml

예상 출력:

검증 중: examples/graphs/parallel/02_research_parallel.yaml

단계 1/3: YAML 파싱... 완료
단계 2/3: Pattern 기본 검증...
  [✓] 노드 ID 유일성
  [✓] 엣지 소스/타겟 존재
  [✓] finalize 노드 도달 가능성
  [✓] judge route_values 커버리지
  완료
단계 3/3: Graph 추가 검증...
  [✓] state_schema 존재 (parallel_groups 사용)
  [✓] 모든 브랜치 writes_state 선언
  [✓] 조인 노드 존재 및 단일성
  [✓] 브랜치 수 제한 (≤8)
  [✓] 부작용 도구 미사용 (shell.exec, file.write)
  [✓] 모든 브랜치 조인 노드로 수렴
  [✓] state_schema merge 타입 호환성
  완료

결과: 유효 (오류 없음)

단계 3: graph compile 실행

# 단순 선형 그래프 컴파일
euleragent graph compile examples/graphs/linear/01_simple_pipeline.yaml

예상 출력 (IR JSON 발췌):

{
  "graph_type": "graph",
  "id": "graph.simple_pipeline",
  "version": 1,
  "compiled_at": "2026-02-23T10:00:00Z",
  "langgraph_version": "1.0.9",
  "state_schema": null,
  "parallel_groups": [],
  "nodes": [
    {
      "id": "research",
      "kind": "llm",
      "runner": { "mode": "plan", "force_tool": "web.search" },
      "interrupt_before": false,
      "interrupt_after": false
    },
    {
      "id": "draft",
      "kind": "llm",
      "runner": { "mode": "execute" }
    },
    {
      "id": "finalize",
      "kind": "finalize"
    }
  ],
  "edges": [
    { "from": "research", "to": "draft", "when": "approvals_resolved" },
    { "from": "draft", "to": "finalize", "when": "true" }
  ],
  "langgraph_builder": {
    "add_nodes": ["research", "draft", "finalize"],
    "add_edges": [
      ["research", "draft"],
      ["draft", "finalize"]
    ],
    "add_conditional_edges": [],
    "interrupt_before": [],
    "interrupt_after": []
  }
}

graph_type: "graph" 필드가 Pattern IR(graph_type: "pattern")과 구별해 줍니다. langgraph_builder 섹션은 LangGraph StateGraph가 실행 시 어떻게 구축될지를 미리 보여줍니다.

단계 4: 컴파일 결과를 파일로 저장

euleragent graph compile examples/graphs/linear/01_simple_pipeline.yaml \
  --out /tmp/compiled_linear.json

# 내용 확인
python -m json.tool /tmp/compiled_linear.json | head -60

단계 5: JSON 형식 검증 결과 확인

euleragent graph validate examples/graphs/parallel/02_research_parallel.yaml \
  --format json | python -m json.tool

예상 출력:

{
  "valid": true,
  "errors": [],
  "warnings": [
    {
      "code": "PARALLEL_NONDETERMINISTIC_MERGE",
      "severity": "warning",
      "message": "노드 'summary_branch'가 last_write 키 'summary'에 씁니다. 2개 이상의 브랜치가 이 키를 쓰면 비결정론적 결과가 발생합니다.",
      "node": "summary_branch"
    }
  ],
  "graph_type": "graph",
  "parallel_groups_count": 1,
  "branch_count": 2
}

6. INVALID_CONCURRENT_GRAPH_UPDATE 이해하기

이 에러는 LangGraph가 리듀서 없이 동일 키에 병렬 쓰기를 감지할 때 발생합니다. euleragent의 graph validate는 이를 미리 감지하여 PARALLEL_STATE_KEY_MERGE_MISSING 에러로 보고합니다.

발생 시나리오

# 위험한 패턴 — state_schema 없이 parallel_groups 사용
parallel_groups:
  - id: grp
    branches: [branch_a, branch_b]
    join: join_node

nodes:
  - id: branch_a
    kind: llm
    writes_state: [findings]   # ← 'findings'에 쓰려고 함
  - id: branch_b
    kind: llm
    writes_state: [findings]   # ← 같은 키 'findings'에 쓰려고 함

# state_schema가 없으므로 LangGraph는 'findings'의 리듀서를 모름
# 런타임에 INVALID_CONCURRENT_GRAPH_UPDATE 발생!

수정 방법

# 올바른 패턴 — state_schema에 리듀서 명시
state_schema:
  findings:
    type: list
    merge: append_list   # ← 리듀서 선언으로 충돌 해결

parallel_groups:
  - id: grp
    branches: [branch_a, branch_b]
    join: join_node

graph validate를 실행하면 이 문제를 실행 전에 잡을 수 있습니다.

# 에러 감지 예시
euleragent graph validate bad_example.yaml

# 출력:
# [오류] PARALLEL_STATE_SCHEMA_MISSING
#   parallel_groups가 선언되어 있지만 state_schema가 없습니다.
#   병렬 실행 시 INVALID_CONCURRENT_GRAPH_UPDATE 예외가 발생합니다.
#   해결: 최상위에 state_schema를 선언하고 각 브랜치의 writes_state
#   키에 대한 리듀서를 정의하세요.

7. 리듀서(Reducer)의 역할

리듀서는 "여러 브랜치가 같은 상태 키에 값을 쓸 때 어떻게 합칠 것인가"를 정의합니다.

euleragent merge LangGraph 타입 어노테이션 동작
append_list Annotated[list, operator.add] 리스트 연결 ([a, b] + [c] = [a, b, c])
sum_int Annotated[int, operator.add] 정수 합산 (3 + 2 = 5)
concat_str Annotated[str, operator.add] 문자열 연결 ("hello" + " world")
last_write str / list / int (기본값) 마지막 쓰기 승리 (비결정론적)
first_write 커스텀 리듀서 첫 번째 쓰기 유지, 이후 무시
# 개념적 Python 코드 — euleragent가 내부적으로 생성
import operator
from typing import Annotated, TypedDict

class GraphState(TypedDict):
    findings: Annotated[list, operator.add]   # append_list
    score: Annotated[int, operator.add]       # sum_int
    notes: Annotated[str, operator.add]       # concat_str
    summary: str                              # last_write (기본)

8. 언제 Pattern 대신 Graph를 쓸까?

결정 가이드

질문 1: 진정한 병렬 실행이 필요한가?
  → 예: 여러 독립적인 데이터 소스를 동시에 조회해야 함
  → 아니오: 순차 실행으로 충분함 → Pattern 사용

질문 2: 노드 수준 일시 정지(interrupt)가 필요한가?
  → 예: 특정 노드 전/후에 사람이 상태를 검사해야 함
  → 아니오: HITL force_tool로 충분 → Pattern 사용

질문 3: LangGraph의 체크포인팅/스트리밍이 필요한가?
  → 예: 그래프 상태를 중간에 저장하고 재개해야 함
  → 아니오: Pattern 사용

모두 아니오라면 → Pattern을 사용하십시오 (더 단순하고 안정적)
하나 이상 예라면 → Graph를 고려하십시오 (00_disclaimer.md 먼저 읽기)

구체적 시나리오

시나리오 권장
리서치 → 초안 → 검토 (순차) Pattern
웹 검색 + 로컬 검색 동시 실행 Graph
루프 있는 단순 반복 개선 Pattern
병렬 멀티 에이전트 리서치 Graph
단순 judge 라우팅 Pattern
병렬 데이터 수집 + judge 평가 Graph
긴급 배포 (안정성 최우선) Pattern

예상 출력

이 튜토리얼에서 실행한 명령들의 요약된 예상 출력입니다.

# euleragent doctor
Python 3.11.4  | euleragent 0.9.0  | langgraph 1.0.9 # euleragent graph validate (유효한 경우)
결과: 유효 (오류 없음)

# euleragent graph compile
{
  "graph_type": "graph",
  "id": "...",
  "langgraph_builder": { ... }
}

핵심 개념 요약

개념 설명
Graph ⊃ Pattern Graph는 Pattern의 모든 기능을 포함한다
state_schema 공유 상태 키의 타입과 리듀서 정의
parallel_groups 팬아웃/팬인 동시 실행 선언
interrupt_before/after LangGraph 체크포인트 기반 노드 일시 정지
리듀서(reducer) 병렬 쓰기 충돌을 해결하는 함수
IR JSON YAML에서 컴파일된 중간 표현
INVALID_CONCURRENT_GRAPH_UPDATE 리듀서 없이 병렬 쓰기 시 LangGraph 런타임 오류

흔한 오류

오류 1: LangGraph 버전 너무 낮음

오류: LANGGRAPH_COMPILE_FAILED
  langgraph 버전 0.2.x가 감지되었습니다. 1.0.9 이상이 필요합니다.
해결: pip install "langgraph>=1.0.9"

오류 2: graph 명령을 찾을 수 없음

오류: No such command 'graph'.
해결: pip install -e . 로 재설치 후 euleragent graph --help 확인

오류 3: YAML 파싱 오류

오류: yaml.scanner.ScannerError: mapping values are not allowed here
해결: YAML 들여쓰기 확인 (스페이스 2칸 또는 4칸, 탭 혼용 금지)

오류 4: PARALLEL_STATE_SCHEMA_MISSING

오류: parallel_groups가 선언되어 있지만 state_schema가 없습니다.
해결: 최상위에 state_schema 섹션을 추가하고 각 브랜치의 writes_state
      키에 대한 type과 merge를 정의하세요.

이전: 00_disclaimer.md | 다음: 02_linear_graph.md

← 이전 목록으로 다음 →