LangGraph 결함 허용 (Fault Tolerance): 재시도(Retries), 타임아웃(Timeouts), 오류 처리기(Error
요약
LangGraph의 새로운 결함 허용(Fault Tolerance) 기능을 통해 회복 탄력적인 에이전트를 구축하는 방법을 설명합니다. 체크포인팅의 한계를 넘어 @retry, TimeoutPolicy, ErrorHandler를 활용해 API 속도 제한이나 타임아웃 같은 운영 문제를 능동적으로 해결하는 가이드를 제공합니다.
핵심 포인트
- 체크포인팅은 상태 저장에는 유용하지만 능동적 복구 기능은 부족함
- @retry 데코레이터를 통한 자동 재시도 메커니즘 지원
- TimeoutPolicy 클래스로 에이전트 실행 시간 제어 가능
- ErrorHandler 노드를 활용한 우아한 오류 처리 및 폴백 구현
LangGraph 결함 허용 (Fault Tolerance): 재시도(Retries), 타임아웃(Timeouts), 오류 처리기(Error Handlers)를 통한 회복 탄력적인 에이전트 구축
당신의 에이전트가 복잡한 연구 작업의 90%를 완료하고, 14번의 API 호출에 성공한 뒤, 15번째 호출에서 일시적인 속도 제한(rate limit)에 걸렸습니다. 이제 에이전트는 멈춰버렸습니다. 체크포인트(Checkpoints)는 여기서 당신을 구해주지 못합니다. 체크포인트는 에이전트가 어디서 멈췄는지는 알려주지만, 어떻게 우아하게 복구할지(recover gracefully)는 알려주지 않기 때문입니다. 상태 지속성(state persistence)과 능동적 복구(active recovery) 사이의 이러한 간극은 프로덕션 에이전트를 운영하는 팀들에게 운영 부담의 가장 큰 원인이 되어 왔으며, LangGraph의 새로운 결함 허용(fault tolerance) 프리미티브(primitives)가 마침내 이 간극을 메워줍니다.
타이밍이 중요합니다. 조직이 개념 증명(proof-of-concept) 에이전트에서 매일 수천 건의 호출을 처리하는 프로덕션 배포로 전환함에 따라, 수동 개입의 경제성은 유지될 수 없게 됩니다. 15%의 확률로 인간의 재시작이 필요한 지원 에이전트는 생산성 향상이 아니라 부채(liability)입니다. 새로운 @retry 데코레이터(decorator), TimeoutPolicy 클래스, 그리고 ErrorHandler 노드는 2026년의 에이전트 워크로드(agentic workloads)의 운영 현실을 다루면서, 프레임워크의 기존 회복 탄력적 에이전트 아키텍처 (resilient agent architecture)를 기반으로 이 과제에 대해 LangGraph가 내놓은 첫 번째 종합적인 해답입니다.
문제점: 왜 체크포인팅(Checkpointing)만으로는 충분하지 않은가
PostgresSaver, MemorySaver 또는 최신 분산 옵션을 사용하든 관계없이, LangGraph의 체크포인팅(checkpointing) 시스템은 한 가지 작업에 탁월합니다. 바로 실행의 정의된 시점에서 에이전트의 완전한 상태(state)를 캡처하는 것입니다. 에이전트가 충돌하면, 정확히 어떤 일이 일어났는지 검사하고 해당 상태에서 재개할 수 있습니다. 이는 모든 진지한 에이전트 시스템의 기본 요건이며, LangGraph는 이를 잘 수행해 왔습니다.
하지만 체크포인팅은 근본적으로 수동적(passive)입니다. 이는 "우리가 어디서 멈췄는가?"라는 질문에는 답하지만, "다시 시도해야 하는가?", "얼마나 기다려야 하는가?", 또는 "이 실패가 계속될 경우 우리의 폴백(fallback)은 무엇인가?"라는 질문에는 답하지 못합니다.
실제 운영 환경의 에이전트 배포에서 지배적으로 나타나는 실패 모드(failure modes)를 고려해 보십시오. 도구 API(tool APIs)의 속도 제한(Rate limits)이 가장 흔한 사례입니다. OpenAI, Anthropic, 그리고 모든 제3자 데이터 제공업체는 속도 제한을 적용하며, 이는 일시적인(transient) 현상이 되도록 설계되어 있습니다. 오후 2시 15분에 발생한 429 응답은 오후 2시 16분에는 성공할 가능성이 높습니다. 외부 서비스에서 발생하는 일시적인 5xx 오류도 유사한 패턴을 따릅니다. LLM 제공업체의 타임아웃(timeouts)은 트래픽이 몰리는 시간대에 급증하며, 만약 에이전트가 피크 시간대에 작동한다면 이를 정기적으로 목격하게 될 것입니다. 에이전트와 외부 서비스 간의 네트워크 분할(Network partitions)은 누구도 인정하고 싶지 않겠지만 생각보다 자주 발생합니다.
멀티 에이전트 워크플로우 (multi-agent workflows)와 최신 Deep Agents 아키텍처에서는 추가적인 과제에 직면하게 됩니다. 바로 서브 에이전트의 중단(sub-agent hangs) 문제입니다. 계획(planning) 에이전트가 조사(research) 서브 에이전트에게 작업을 위임했는데, 서브 에이전트가 영원히 오지 않을 응답을 기다리며 멈춰버리는 상황입니다. 타임아웃(timeouts) 설정이 없다면 전체 워크플로우가 얼어붙게 됩니다.
진정한 비용은 기술적인 것이 아니라 운영적인 측면에서 발생합니다. 모든 수동 재시작은 인간의 주의력, 컨텍스트 스위칭(context switching), 그리고 의사결정을 요구합니다. 고객 대응 에이전트를 운영하는 팀들은 결함 허용(fault tolerance) 패턴을 도입하기 전에는, 일시적인 오류로 인해 멈춘 에이전트를 단순히 재시작하는 데 온콜(on-call) 교대 근무 시간의 상당 부분을 소비했다고 보고합니다. 에이전트 개발 라이프사이클 (agent development lifecycle)은 배포를 훨씬 넘어 확장되며, 적절한 회복 메커니즘이 없다면 모니터링은 단순히 불을 끄러 다니는 소방 활동(firefighting)이 되어버립니다.
개념적 차이는 명확합니다. 체크포인팅(checkpointing)은 어디서 재개할지를 정의하는 반면, 결함 허용(fault tolerance)은 포기하기 전에 _재시도할지 여부와 그 방법_을 정의합니다. 당신에게는 이 두 가지가 모두 필요합니다.
핵심 API: @retry 데코레이터
@retry 데코레이터는 이전처럼 모든 외부 API 호출에 번거로운 상용구 코드(boilerplate)를 채워 넣지 않고도, 노드 함수에 운영 수준의 재시도 로직(retry logic)을 부여합니다. 기본 시그니처는 다음과 같이 간단합니다:
@retry(max_attempts=3, backoff="exponential", retryable_exceptions=[RateLimitError, TimeoutError])
def call_external_api(state: AgentState) -> AgentState:
...
설정 옵션들은 재시도 시나리오의 전체 스펙트럼을 다룹니다. max_attempts는 최초 시도를 포함하는 정수입니다. 따라서 max_attempts=3은 최초 1회 시도와 2회의 재시도를 의미합니다. backoff 파라미터는 `
NVIDIA의 병렬 실행 강화 기능 (parallel execution enhancements)을 사용하는 팀에게 중요한 구현 세부 사항이 하나 있습니다. @retry를 @independent(병렬화 가능한 노드를 위한 데코레이터)와 함께 결합할 때, @retry는 반드시 가장 안쪽의 데코레이터여야 합니다. 이는 재시도 로직이 병렬화 래퍼(parallelization wrapper)가 아닌 실제 노드 실행을 감싸도록 보장하기 위함입니다.
타임아웃 정책 (Timeout Policies): 무한 작업의 경계 설정
재시도(Retries)가 예외(exceptions)를 통해 자신을 알리는 실패를 처리한다면, 타임아웃(Timeouts)은 아예 반환되지 않는 작업으로부터 시스템을 보호합니다. TimeoutPolicy 클래스는 개별 노드, 서브그래프(subgraphs), 그리고 전체 그래프 호출(graph invocations)이라는 세 가지 수준에서 세밀한 제어를 제공합니다.
설정 계층 구조는 에이전트가 실제로 실패하는 방식을 반영합니다. node_timeout은 단일 노드 실행의 최대 지속 시간을 설정하며, 이는 특정 API 호출이 30초 이상 걸리지 않아야 한다는 것을 알고 있을 때 유용합니다. tool_timeout은 노드 자체의 계산 시간과는 별개로, 노드 내의 모든 도구 호출(tool calls)에 일괄 적용됩니다. graph_timeout은 전체 호출에 대한 실제 시간(wall-clock) 제한을 설정하여, 무한 루프에 빠지거나 재귀적인 계획 사이클(recursive planning cycles)에 갇혀 통제 불능 상태가 되는 에이전트를 방지합니다.
설정 패턴은 그래프 컴파일(graph compilation) 시점에 적용됩니다:
from langgraph.timeout import TimeoutPolicy
policy = TimeoutPolicy(
...
타임아웃 동작은 on_timeout 파라미터를 통해 구성할 수 있습니다. 기본값인 `
구현 시 비동기(async) 노드에는 내부적으로 asyncio.timeout()을 사용합니다. 동기(Synchronous) 노드는 동일한 동작을 수행하도록 자동으로 래핑(wrapped)되지만, 비동기 구현이 더 효율적입니다. 이는 프로덕션 환경에서 비동기 노드 함수를 선호해야 하는 또 다른 이유이기도 합니다.
LangGraph의 멀티 에이전트(multi-agent) 기능을 사용하는 팀의 경우, 타임아웃 정책은 오케스트레이션(orchestration) 수준에서 에이전트 개발 스택 (agent development stack)과 통합됩니다. 서브 에이전트(Sub-agent)의 타임아웃을 독립적으로 구성할 수 있어, 오작동하는 서브 에이전트가 부모 에이전트의 전체 타임아웃 예산을 모두 소모하는 것을 방지할 수 있습니다.
LangSmith는 다른 관측성(observability) 데이터와 함께 타임아웃 메트릭을 제공합니다. 노드당 timeout_rate는 호출 중 타임아웃이 발생한 비율을 보여주며, p99_duration은 타임아웃 임계값이 겹쳐진 지연 시간(latency) 분포를 표시합니다. 이를 통해 추측이 아닌 실제 프로덕션 동작을 기반으로 타임아웃을 간편하게 조정할 수 있습니다.
오류 처리기 노드 (Error Handler Nodes): 중앙 집중식 복구 로직
재시도(Retries)와 타임아웃(Timeouts)은 특정 유형의 실패를 처리하지만, 프로덕션 에이전트에는 복구 결정을 내릴 수 있는 통합된 장소가 필요합니다. ErrorHandler 노드는 이러한 중앙 집중화를 제공하며, 여기저기 흩어져 있는 try-except 블록을 일관된 오류 복구 아키텍처로 대체합니다.
등록은 범위(scope) 기반 설정을 사용합니다:
graph.add_error_handler(
handler_node,
scope="global" # 또는 "subgraph" 또는 ["node_a", "node_b"]
...
전역(Global) 핸들러는 어떤 노드에서든 발생하는 처리되지 않은 예외(unhandled exception)를 포착합니다. 서브그래프(Subgraph) 핸들러는 특정 서브그래프 범위로 제한되며, 에이전트의 서로 다른 부분에 서로 다른 복구 전략이 필요할 때 유용합니다. 노드 리스트(Node-list) 범위 지정은 특정 노드들을 대상으로 하며, 연관된 API 호출 클러스터에서 발생하는 오류를 처리하는 데 이상적입니다.
핸들러 노드는 지능적인 복구 결정을 내리는 데 필요한 모든 정보를 포함하는 ErrorContext 객체를 전달받습니다:
class ErrorContext:
exception: Exception # 포착된 예외
failed_node: str # 예외를 발생시킨 노드의 이름
...
attempt_history 필드는 특히 가치가 높습니다. 이 필드는 단순히 노드가 실패했다는 사실뿐만 아니라, 몇 번이나 실패했는지, 그리고 각 시도에서 _어떤 예외 (exceptions)_가 발생했는지를 알려줍니다. 타임아웃 (timeout)으로 한 번 실패한 노드는 속도 제한 (rate limit) 오류로 다섯 번의 재시도 (retries)를 모두 소진한 노드와는 다릅니다.
핸들러 (Handler) 반환 값은 Command 패턴을 통해 실행 흐름을 제어합니다:
def error_handler(context: ErrorContext) -> Command:
if isinstance(context.exception, RateLimitError):
# 성능 저하 모드 (degraded-mode) 노드로 라우팅
...
Command(resume=True) 옵션은 특히 강력합니다. 이는 재시도 카운터를 초기화하여 실패한 노드를 다시 시도합니다. 이를 통해 핸들러가 먼저 속도 제한 백오프 (rate limit backoff)를 시도한 다음, API 키를 교체하고, 마지막으로 포기하는 식의 "에스컬레이션 및 재시도 (escalate and retry)" 패턴을 구현할 수 있습니다.
라우팅 전 상태 수정은 Command(update={...})를 통해 지원됩니다. 이를 통해 데이터 소스를 사용 불가능한 것으로 상태 (state)에 표시한 뒤, 부분적인 데이터로 작동해야 하는 합성 (synthesis) 노드로 라우팅하는 등의 패턴을 사용할 수 있습니다.
실제 운영 환경에서는 두 가지 패턴이 특히 유용하게 나타납니다. "서킷 브레이커 (circuit breaker)" 패턴은 (상태 또는 외부 저장소를 사용하여) 시간에 따른 실패율을 추적하고 임계값에 도달하면 성능 저하 모드 (degraded mode)로 전환합니다. 이는 기본 데이터 소스를 사용할 수 없는 상황에서도 계속 작동해야 하는 에이전트에게 유용합니다. "에스컬레이션 (escalation)" 패턴은 일상적인 실패는 자동으로 처리하면서 특정 오류 유형에 대해서는 인간 참여형 (human-in-the-loop) 중단을 생성합니다. 이는 에이전트 시스템 (agentic systems)이 인간의 의사결정을 완전히 대체하기보다는 증강해야 한다는 원칙을 존중하는 방식입니다.
실습: 코드 워크스루 (Code Walkthrough)
세 가지 결함 허용 (fault tolerance) 기본 요소를 모두 보여주는 연구 에이전트를 구축해 보겠습니다. 이 에이전트는 세 개의 외부 API (arXiv, Wikipedia, 뉴스 서비스)를 쿼리하고, 결과를 합성하며, 보고서를 생성합니다. 이는 운영 환경의 에이전트에서 흔히 볼 수 있는 패턴이며, 결함 허용이 해결하고자 하는 실패 모드 (failure modes)를 정확히 보여줍니다.
from typing import TypedDict, List, Optional
from langgraph.graph import StateGraph, START, END
from langgraph.retry import retry
from langgraph.timeout import TimeoutPolicy
from langgraph.errors import ErrorContext, Command
from langsmith import traceable
import httpx
import asyncio
State definition captures both data and operational metadata
class ResearchState(TypedDict):
query: str
arxiv_results: Optional[List[dict]]
wikipedia_results: Optional[List[dict]]
news_results: Optional[List[dict]]
unavailable_sources: List[str] # Track which sources failed
synthesis: Optional[str]
final_report: Optional[str]
Custom exceptions for clear retry targeting
class RateLimitError(Exception):
pass
class SourceUnavailableError(Exception):
pass
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기