jongkwan.dev
개발 · Essay №053

LangGraph 에이전트 오케스트레이션: StateGraph 기반 멀티에이전트 시스템 설계

LangGraph StateGraph 기반 멀티에이전트 오케스트레이션과 Durable Execution 설계

이종관2026년 2월 23일23 min read
Contents

멀티에이전트 시스템의 실행 흐름, 상태, 장애 복구를 LangGraph StateGraph로 그래프화한다.

왜 오케스트레이션이 필요한가

멀티에이전트 시스템에서 개별 에이전트의 추론 능력만큼 중요한 것이 에이전트 간 통신, 상태 관리, 실행 흐름 제어다. 9개 에이전트가 순차 파이프라인으로 연결된 단순한 for-loop 구조는 다음과 같은 근본적인 한계를 가진다.

한계설명결과
장애 복구 불가7번째 단계에서 오류 시 처음부터 재실행불필요한 API 호출, 비용 낭비
병렬 실행 불가독립적인 분석 단계도 순차 대기총 지연 시간 증가
조건 분기 불가데이터 품질 불량 시에도 전체 실행오염된 데이터로 의사결정
상태 추적 부재"어디까지 진행됐는가" 기록 없음디버깅, 감사 불가
사람 개입 불가고위험 결정에서 승인 절차 없음자동화의 위험 노출

LangGraph는 이 모든 문제를 그래프 기반 상태기계(StateGraph) 로 해결한다.


LangGraph 핵심 개념

StateGraph: 그래프 기반 워크플로우

LangGraph는 에이전트 워크플로우를 상태 기반의 제어된 실행 그래프(Stateful, controlled execution graph) 로 모델링한다. 노드(Node)는 각 에이전트의 실행 단위이고, 엣지(Edge)는 상태 전이를 정의한다. 그래프는 유향 비순환 그래프(Directed Acyclic Graph, DAG) 형태로 구성한다.

python
from langgraph.graph import StateGraph, END
from langgraph.checkpoint.postgres import PostgresSaver
 
class PipelineState(TypedDict):
    project_id: str
    market: str
    config: dict
    scan_results: list[ScanResult]
    news_items: list[NewsItem]
    position_alerts: list[PositionAlert]
    signals: list[Signal]
    debate_result: dict | None
    final_decision: dict | None
    errors: list[str]
 
graph = StateGraph(PipelineState)

PipelineState는 전체 워크플로우의 공유 상태다. 각 노드는 이 상태를 읽고 업데이트하며, 체크포인터가 매 단계마다 스냅샷을 저장한다.

핵심 구성 요소

구성 요소역할
Node에이전트 실행 단위 (함수)
Edge노드 간 상태 전이
Conditional Edge조건에 따른 분기
State공유 상태 (TypedDict)
Checkpointer매 노드 완료 시 상태 저장 (게임의 세이브 포인트와 같다)
Thread실행 인스턴스 식별자

StateGraph DAG 설계: 트레이딩 파이프라인

Sense-Think-Act 3계층 매핑

9개 에이전트를 Sense-Think-Act 3계층으로 분리하고, LangGraph의 StateGraph로 DAG를 구성한다.

그래프 정의 코드

python
graph = StateGraph(PipelineState)
 
# --- Sense Layer ---
graph.add_node("data_quality_gate", data_quality_gate)
graph.add_node("volume_scanner", volume_scanner)
graph.add_node("news_collector", news_collector)
graph.add_node("position_sensor", position_sensor)
 
# --- Think Layer (Fan-out / Fan-in) ---
graph.add_node("quant_analyst", quant_analyst)
graph.add_node("llm_analyst", llm_analyst)
graph.add_node("sector_rotation", sector_rotation)
graph.add_node("analysis_merge", merge_analysis_results)
graph.add_node("bull_bear_debate", bull_bear_debate)
graph.add_node("ensemble_voter", ensemble_voter)
 
# --- Act Layer ---
graph.add_node("risk_sentinel", risk_sentinel)
graph.add_node("trade_executor", trade_executor)
 
# 엣지 정의
graph.set_entry_point("data_quality_gate")
 
# Data Quality Gate: 조건부 분기
graph.add_conditional_edges(
    "data_quality_gate",
    lambda s: "continue" if not s["errors"] else "end",
    {"continue": "volume_scanner", "end": END},
)
 
# Sense Layer 순차 실행
graph.add_edge("volume_scanner", "news_collector")
graph.add_edge("news_collector", "position_sensor")
 
# Fan-out: 병렬 분석
graph.add_edge("position_sensor", "quant_analyst")
graph.add_edge("position_sensor", "llm_analyst")
graph.add_edge("position_sensor", "sector_rotation")
 
# Fan-in: 결과 합류
graph.add_edge("quant_analyst", "analysis_merge")
graph.add_edge("llm_analyst", "analysis_merge")
graph.add_edge("sector_rotation", "analysis_merge")
 
# Think Layer 후반
graph.add_edge("analysis_merge", "bull_bear_debate")
graph.add_edge("bull_bear_debate", "ensemble_voter")
graph.add_edge("ensemble_voter", "risk_sentinel")
 
# Risk Sentinel: 조건부 분기
graph.add_conditional_edges(
    "risk_sentinel",
    lambda s: "execute" if s["final_decision"].get("approved") else "end",
    {"execute": "trade_executor", "end": END},
)
graph.add_edge("trade_executor", END)

Fan-out / Fan-in 병렬 처리

병렬 처리 이득

의사결정 루프에서 Quant Analyst(p95 < 300ms), Sector Rotation(p95 < 300ms), LLM Analyst(p95 < 8s)는 서로 독립적이다. 여기서 p95는 95 백분위 지연(요청의 95%가 그 시간 안에 끝남)을 뜻한다. 순차 실행하면 총 8.6초 이상 소요되지만, 병렬 실행하면 가장 느린 LLM Analyst 기준 8초로 수렴한다.

LangGraph의 Fan-out 동작 방식

LangGraph에서 하나의 노드가 여러 노드로 엣지를 가지면 자동으로 Fan-out(병렬 분기) 된다. Fan-in은 여러 노드가 동일한 다음 노드를 가리킬 때 발생하며, 모든 선행 노드가 완료될 때까지 대기한다.

python
# position_sensor -> [quant, llm, sector] : Fan-out
graph.add_edge("position_sensor", "quant_analyst")
graph.add_edge("position_sensor", "llm_analyst")
graph.add_edge("position_sensor", "sector_rotation")
 
# [quant, llm, sector] -> analysis_merge : Fan-in
graph.add_edge("quant_analyst", "analysis_merge")
graph.add_edge("llm_analyst", "analysis_merge")
graph.add_edge("sector_rotation", "analysis_merge")

analysis_merge 노드는 세 에이전트의 출력을 하나의 통합 리포트로 합친다. 상태(State) 내에 각 에이전트의 결과가 축적되므로, 합류 노드에서 모든 결과를 읽을 수 있다.


체크포인팅과 내구 실행(Durable Execution)

체크포인터 동작

LangGraph의 체크포인터는 매 super-step(노드 실행 완료)마다 전체 상태의 스냅샷을 저장한다. 이를 통해 다음이 가능해진다.

기능설명
Fault Tolerance장애 발생 시 마지막 체크포인트부터 재개
Human-in-the-Loop특정 단계에서 실행을 중단하고 사람의 승인을 기다림
Time Travel이전 상태로 되돌아가 "만약 다르게 판단했다면?" 시뮬레이션
Audit Trail각 단계별 상태가 기록되어 의사결정 추적 가능

PostgreSQL 기반 체크포인터

python
from langgraph.checkpoint.postgres import PostgresSaver
 
checkpointer = PostgresSaver.from_conn_string(DATABASE_URL)
app = graph.compile(checkpointer=checkpointer)
 
# 실행
result = await app.ainvoke(
    initial_state,
    config={"configurable": {"thread_id": "pipeline-run-uuid"}}
)

thread_id가 체크포인트 조회의 기본 키다. 동일한 thread_id로 재실행하면 마지막 체크포인트 이후 단계부터 자동으로 재개된다.

재시작 시퀀스

Durable Execution 원칙

Durable Execution은 LangGraph가 제공하는 핵심 패턴으로, 다음 원칙을 따른다.

원칙트레이딩 적용
DeterministicRisk Sentinel까지는 동일 입력에 동일 출력 보장
Idempotent주문 제출은 idempotency_key로 중복 방지
Side Effect 격리주문 제출(외부 API 호출)은 task로 래핑

트레이딩에서 "정확히 한 번 실행(exactly-once)"은 브로커/거래소 경계 때문에 완벽하게 보장하기 어렵다. 따라서 idempotency_key를 주문 단위로 부여하고, 재시작 시 동일 키의 주문이 이미 제출되었는지 확인하는 멱등성 검사가 필수다.


Human-in-the-Loop

사람 개입이 필요한 상황

자동매매에서 특정 상황은 기계적 판단보다 사람의 확인이 필요하다.

  • 일 손실 한도 근접 시 추가 거래
  • 시장 레짐 급변 (VIX 급등, 블랙 스완 이벤트)
  • 뉴스 신뢰도가 매우 낮은 상태에서의 대규모 매수
  • 새로운 전략/프롬프트 변경 직후 첫 실거래

LangGraph의 Human-in-the-Loop 구현

Risk Sentinel의 출력에 requires_human_approval 플래그를 포함시키고, LangGraph의 interrupt() 함수로 실행을 중단한다.

python
from langgraph.types import interrupt
 
def risk_sentinel(state: PipelineState) -> PipelineState:
    intent = state["final_decision"]
    portfolio = get_portfolio_snapshot(state["project_id"])
 
    decision = evaluate_risk(intent, portfolio)
 
    if decision.requires_human_approval:
        # interrupt(): 실행 중단, 사람 승인 대기. 반환값은 재개 시 사람이 보낸 값
        approved = interrupt(
            f"고위험 거래 감지: {decision.reasons}"
        )
        state["final_decision"]["approved"] = approved
        return state
 
    state["final_decision"]["approved"] = decision.approved
    return state

interrupt()를 호출한 노드는 그 지점에서 멈추고, 사용자가 Command(resume=값)로 재개하면 해당 노드를 처음부터 다시 실행한다. 과거의 NodeInterrupt를 raise하는 방식은 LangGraph v0.4 이후 interrupt() 함수로 대체되었다. 체크포인트 덕분에 중단 이전 단계는 다시 실행할 필요가 없다.


조건부 엣지(Conditional Edge)를 활용한 분기

Data Quality Gate

파이프라인 진입점에서 데이터 품질을 검증하고, 기준 미달 시 전체 실행을 중단한다.

python
graph.add_conditional_edges(
    "data_quality_gate",
    lambda s: "continue" if not s["errors"] else "end",
    {"continue": "volume_scanner", "end": END},
)

Data Quality Gate 체크리스트:

체크 항목기준실패 시
API 응답 시간p95 < 3초재시도 3회 후 중단
시세 데이터 최신성현재 시각 - 데이터 시각 < 5분경고, 10분 초과 시 중단
가격 이상치전일 종가 대비 30% 이내스플릿/권리락 확인
필수 종목 누락유니버스 90% 이상 조회 성공미달 시 중단
뉴스 API 가용성최소 1개 소스 응답전체 불응답 시 뉴스 없이 진행

Risk Sentinel 분기

리스크 판정 결과에 따라 실행(execute) 또는 종료(end)로 분기한다.

python
graph.add_conditional_edges(
    "risk_sentinel",
    lambda s: "execute" if s["final_decision"].get("approved") else "end",
    {"execute": "trade_executor", "end": END},
)

Risk Sentinel의 판정은 5가지 결과를 가진다.

판정의미후속 동작
APPROVE원안 승인Trade Executor로 전달
RESIZE사이즈 조정 후 승인조정된 Intent로 실행
HOLD조건부 보류조건 충족 시 재평가
REJECT거부로그 기록 후 종료
KILL긴급 중단모든 미체결 취소

이벤트 소스 기반 상태 지속성

3중 저장 구조

트레이딩 시스템의 상태 지속성은 3중 구조로 설계한다.

저장소성격역할
Kafka불변(append-only)모든 이벤트 기록, 재생(replay) 가능, 감사 추적
Redis가변(TTL 기반)실시간 캐시, Pub/Sub, Rate Limit, 에이전트 가중치
PostgreSQL영구 저장LangGraph 체크포인트, 거래 이력, Reflection 메모

이벤트 Envelope 스키마

모든 에이전트 간 메시지는 공통 Envelope로 표준화한다.

json
{
  "event_id": "uuid-v7",
  "ts": "2026-02-21T09:15:00.123+09:00",
  "event_type": "QUANT_DONE",
  "correlation_id": "pipeline-run-uuid",
  "project_id": "project-uuid",
  "agent": "quant_analyst",
  "payload": {
    "symbol": "005930",
    "signal": 0.72,
    "confidence": 0.68,
    "feature_vector": { "rsi_14": 32.5, "macd_signal": 0.003 }
  },
  "schema_version": "v2",
  "idempotency_key": "uuid-for-dedup"
}

idempotency_keycorrelation_id가 핵심이다. 전자는 중복 처리 방지, 후자는 하나의 파이프라인 실행에 속한 모든 이벤트를 추적하는 데 사용된다.


LangGraph vs AutoGen 비교

프레임워크 특성 비교

비교 항목LangGraphAutoGen
모델링 방식그래프/상태기계이벤트 기반 대화
실행 흐름DAG로 명시적 정의에이전트 간 메시징
상태 관리TypedDict 공유 상태대화 히스토리
체크포인팅내장 (PostgreSQL, SQLite)외부 구현 필요
Durable Execution네이티브 지원미지원
Human-in-the-Loopinterrupt 내장UserProxyAgent
병렬 실행Fan-out/Fan-in 내장Teams/GroupChat
디버깅Time Travel (상태 되감기)대화 로그

TradingAgents 프레임워크

TradingAgents는 실제 트레이딩 조직을 모사한 멀티에이전트 구조를 제안한다. 7가지 역할 (Fundamental Analyst, Sentiment Analyst, News Analyst, Technical Analyst, Bull/Bear Researcher, Trader, Risk Manager)이 5단계 파이프라인으로 협업한다.

text
Analyst Team  ->  Researcher Team  ->  Trader  ->  Risk Team  ->  Fund Manager
(4명 병렬)        (Bull/Bear 토론)     (종합)      (3관점 토론)    (최종 승인)

Q1 2024 백테스트에서 AAPL 누적수익률 26.62%(Sharpe 8.21), GOOGL 24.36%(Sharpe 6.39), AMZN 23.21%(Sharpe 5.60)을 기록했다. 이는 논문(arxiv:2412.20138) 보고치이며 3종목/3개월이라는 제한된 범위에서 나온 값이다. Sharpe 8 수준은 실거래에서 재현되기 어려운 수치이므로 실거래 재현은 별개로 본다.

어떤 프레임워크를 선택할 것인가

트레이딩 시스템에서는 LangGraph가 더 적합하다. 이유는 (1) 체크포인팅으로 장애 복구가 가능하고, (2) Conditional Edge로 리스크 게이트를 자연스럽게 모델링할 수 있으며, (3) Human-in-the-Loop가 내장되어 있고, (4) Time Travel로 "다른 판단을 했다면?"을 시뮬레이션할 수 있기 때문이다.

AutoGen의 강점은 에이전트 간 자유로운 대화와 동적 팀 구성에 있어, Bull/Bear Debate 같은 토론 구조에는 AutoGen의 GroupChat 패턴이 유리할 수 있다. 두 프레임워크를 혼용하는 것도 가능하다.


에이전트별 SLO와 실패 모드

에이전트마다 서비스 수준 목표(Service Level Objective, SLO)를 지연 기준으로 정의한다. 표의 p99는 99 백분위 지연(요청의 99%가 그 시간 안에 끝남)을 뜻한다.

지연/처리량 목표

에이전트권장 지연 SLO동시성주요 실패 모드완화책
Volume Scannerp99 < 200ms수백 심볼/회데이터 지연/누락워터마크, 쿨다운
News Collectorp95 < 3s수십 기사/회중복, 루머디듀프, 소스 신뢰도
Position Sensorp99 < 200ms프로젝트당 전종목시세 지연Redis 캐시, DB 정합
Quant Analystp95 < 300ms티켓당 1회피처 오류피처 버전 고정
LLM Analystp95 < 8s동시 10건환각, 과잉확신근거 인용, Critic 루프
Sector Rotationp95 < 300ms주기적레짐 오판히스테리시스
Bull/Bear Debatep95 < 10s동시 수건수렴 실패라운드 제한(2)
Ensemble Voterp95 < 200ms티켓당 1회과적합캘리브레이션
Risk Sentinelp99 < 50ms주문 전 필수한도 미적용하드룰 우선
Trade Executorp99 < 100ms주문/초주문거부, 미체결재호가, 타임아웃

에이전트별 I/O 흐름

Trade Executor의 출력에 있는 IS는 Implementation Shortfall(목표가 대비 실제 체결가의 차이)이며, bps는 basis points(1bps = 0.01%) 단위다.


운영: 테스트, 카나리 배포, 킬 스위치

3단 테스트 체계

단계설명규제 근거
회귀 테스트코드 변경 시 기존 기능 검증MiFID II RTS 6: 적합성 테스트
시뮬레이션백테스트 + 페이퍼 트레이딩SEC Rule 15c3-5
카나리 배포자본 1-5%로 시작 후 확대Google SRE 카나리 릴리스

카나리 배포 원칙

트레이딩에서 "트래픽" 대신 "자본/노출(Exposure)"을 카나리 단위로 삼는다.

  • 신규 전략/모델 변경: 총 자본의 1-5%로 시작 후 성과/리스크/슬리피지 모니터링
  • LLM 프롬프트/토론 규약 변경: 제한된 섹터/소수 심볼로 먼저 적용 후 드리프트 확인
  • 문제 발생 시 즉시 이전 버전으로 롤백 가능하도록 "버전 고정 + 이벤트 로그 기반 재현" 준비

Kill Switch

Kill Switch는 모든 미체결 주문을 즉시 취소하고 파이프라인을 중단하는 비상 장치다.

모든 LangGraph 노드는 실행 전에 Redis의 Kill Switch 플래그를 확인한다. 플래그가 설정되어 있으면 즉시 종료한다.


전체 인프라 스택 요약

기술역할핵심 사용처
LangGraph오케스트레이션StateGraph DAG, 체크포인트, 조건부 분기, Human-in-the-Loop
Kafka이벤트 스트림파이프라인/거래/포지션 이벤트 토픽, 감사 로그
Redis실시간 캐시시세 캐시, Pub/Sub, Rate Limiter, Kill Switch, 에이전트 가중치
Celery스케줄링/비동기Beat(정기 트리거), PositionSensor 폴링, Reflection, 보고서
PostgreSQL영구 저장체크포인트, 거래 이력, Reflection Memo, 가중치 스냅샷

LangGraph는 "에이전트를 어떻게 연결하고 실행할 것인가"에 대한 답이다. 그래프 기반 DAG + 체크포인팅 + 조건 분기 + 병렬 실행 + Human-in-the-Loop를 하나의 프레임워크로 묶어, 순차 파이프라인을 직접 확장하지 않고도 운영 가능한(production-grade) 멀티에이전트 시스템을 구축할 수 있다.


정리

순차 for-loop 구조는 장애 복구, 병렬 실행, 조건 분기, 상태 추적, 사람 개입을 모두 자체 구현해야 한다. LangGraph는 이를 StateGraph 하나로 묶어, 노드와 엣지로 실행 흐름을 명시하고 공유 상태를 TypedDict로 관리한다. 체크포인터가 매 노드 완료 시 상태를 저장하므로 장애 시 마지막 지점부터 재개할 수 있고, interrupt()로 고위험 결정 앞에서 사람 승인을 받을 수 있다. Conditional Edge는 데이터 품질 게이트와 리스크 게이트를 자연스럽게 모델링한다. 체크포인팅, 조건 분기, Human-in-the-Loop가 모두 필요한 트레이딩 같은 도메인에서는 LangGraph가 적합하고, 자유로운 대화형 토론이 핵심이면 AutoGen을 함께 쓰는 선택도 가능하다.


참고 자료