장애 허용
장애 허용 (Fault tolerance)
LangGraph에서 노드별 타임아웃, 재시도, 오류 핸들러를 구성해요.
노드가 실패할 때 — 느린 외부 API, 일시적 네트워크 오류, 처리되지 않은 예외 때문이든 — LangGraph는 대응할 수 있는 세 가지 조합 가능한(composable) 메커니즘을 제공합니다:
- 재시도(Retries): 예외 타입과 백오프 설정에 따라 실패한 시도를 자동으로 재실행
- 타임아웃(Timeouts): 단일 시도가 실행될 수 있는 시간 상한
- 오류 처리(Error handling): 모든 재시도가 소진된 후 복구 함수 실행
set_node_defaults를 사용해 모든 노드에 이 메커니즘을 한 번에 구성해서, 매 add_node 호출마다 반복하지 않을 수 있어요.
이것들은 고정된 순서로 조합됩니다. 노드 시도가 어떤 예외를 일으키면(타임아웃의 NodeTimeoutError 포함) 재시도 정책이 재시도할지 결정해요. 오류 핸들러는 재시도가 모두 소진된 후에만 실행됩니다.
슈퍼스텝 경계에서 런을 깔끔하게 멈추고 나중에 재개하려면 Graceful shutdown을 참고하세요.
%%{init:{'theme':'base','themeVariables':{'lineColor':'#40668D','primaryColor':'#E5F4FF','primaryTextColor':'#030710','primaryBorderColor':'#006DDD'}}}%%
flowchart LR
start([Attempt starts]) --> exec[Run node]
exec -->|"success"| done([Continue graph])
exec -->|"any exception<br/>including NodeTimeoutError"| retry{retry_policy<br/>matches?}
retry -->|"yes, attempts left"| exec
retry -->|"exhausted or absent"| handler{error_handler?}
handler -->|"yes"| run_handler["Invoke handler<br/>with NodeError"]
run_handler --> route([Update state +<br/>Command goto])
handler -->|"no"| bubble([Exception<br/>bubbles up])
classDef process fill:#E5F4FF,stroke:#006DDD,stroke-width:2px,color:#030710
classDef decision fill:#FDF3FF,stroke:#7E65AE,stroke-width:2px,color:#504B5F
classDef alert fill:#F8E8E6,stroke:#B27D75,stroke-width:2px,color:#634643
classDef output fill:#EBD0F0,stroke:#885270,stroke-width:2px,color:#441E33
class exec,run_handler process
class retry,handler decision
class bubble alert
class done,route,start output
출처: 문서
본문
재시도 (Retries)
재시도 정책은 예외 타입과 백오프 설정에 따라 실패한 노드 시도를 자동으로 재실행해요.
add_node에 retry_policy=를 전달하세요:
from langgraph.types import RetryPolicy
builder.add_node(
"call_api",
call_api,
retry_policy=RetryPolicy(max_attempts=3),
)
기본 동작 (Default behavior)
기본적으로 retry_on은 default_retry_on을 사용하며, 다음 예외(및 그 서브클래스)를 제외한 모든 예외에 대해 재시도합니다:
ValueErrorTypeErrorArithmeticErrorImportErrorLookupErrorNameErrorSyntaxErrorRuntimeErrorReferenceErrorStopIterationStopAsyncIterationOSError
requests, httpx 같은 인기 HTTP 라이브러리의 예외에는 5xx 상태 코드에서만 재시도합니다. NodeTimeoutError는 기본적으로 재시도 가능합니다.
파라미터 (Parameters)
| 파라미터 | 타입 | 기본값 | 설명 |
|---|---|---|---|
max_attempts |
int |
3 |
첫 시도를 포함한 최대 시도 횟수. |
initial_interval |
float |
0.5 |
첫 재시도 전까지의 초. |
backoff_factor |
float |
2.0 |
각 재시도 후 간격에 적용되는 배수. |
max_interval |
float |
128.0 |
재시도 사이 최대 초. |
jitter |
bool |
True |
간격에 무작위 지터(jitter) 추가. |
retry_on |
type[Exception] | Sequence[type[Exception]] | Callable[[Exception], bool] |
default_retry_on |
재시도할 예외, 또는 재시도 가능 예외에 True를 반환하는 콜러블. |
커스텀 재시도 로직 (Custom retry logic)
retry_on에 콜러블 또는 예외 타입을 전달하세요. default_retry_on을 import해 기본 동작을 확장할 수 있어요:
from langgraph.types import RetryPolicy, default_retry_on
def custom_retry_on(exc: BaseException) -> bool:
if isinstance(exc, MyCustomError):
return False
return default_retry_on(exc)
builder.add_node(
"call_api",
call_api,
retry_policy=RetryPolicy(max_attempts=3, retry_on=custom_retry_on),
)
재시도 상태 점검 (Inspect retry state)
노드 안에서 실행 정보를 사용해 현재 시도 번호를 확인할 수 있어요. 기본 호출이 계속 실패할 때 폴백으로 전환하는 데 유용합니다:
from langgraph.graph import StateGraph, START, END
from langgraph.runtime import Runtime
from langgraph.types import RetryPolicy
from typing_extensions import TypedDict
class State(TypedDict):
result: str
def my_node(state: State, runtime: Runtime) -> State:
if runtime.execution_info.node_attempt > 1: # [!code highlight]
return {"result": call_fallback_api()}
return {"result": call_primary_api()}
builder = StateGraph(State)
builder.add_node("my_node", my_node, retry_policy=RetryPolicy(max_attempts=3))
builder.add_edge(START, "my_node")
builder.add_edge("my_node", END)
execution_info는 다음 필드를 노출합니다:
| 속성 | 타입 | 설명 |
|---|---|---|
node_attempt |
int |
현재 시도 번호(1부터 시작). 첫 시도 1, 첫 재시도 2, ... |
node_first_attempt_time |
float | None |
첫 시도가 시작된 Unix 타임스탬프. 재시도 전체에 걸쳐 일정. |
thread_id |
str | None |
현재 실행의 스레드 ID. 체크포인터 없으면 None. |
run_id |
str | None |
현재 실행의 런 ID. config에 제공되지 않으면 None. |
checkpoint_id |
str |
현재 실행의 체크포인트 ID. |
task_id |
str |
현재 실행의 태스크 ID. |
execution_info는 재시도 정책 없이도 사용할 수 있습니다 — node_attempt는 기본적으로 1이에요.
타임아웃 (Timeouts)
add_node의 timeout= 파라미터는 단일 노드 시도가 실행될 수 있는 시간을 제한합니다. 숫자(초), timedelta, 또는 실행·유휴 한도를 분리하는 TimeoutPolicy를 전달하세요:
from datetime import timedelta
from langgraph.types import TimeoutPolicy
# Simple wall-clock cap
builder.add_node("call_model", call_model, timeout=60)
builder.add_node("call_model", call_model, timeout=timedelta(minutes=2))
# Separate run and idle limits
builder.add_node(
"call_model",
call_model,
timeout=TimeoutPolicy(run_timeout=120, idle_timeout=30),
)
실행 타임아웃 (Run timeout)
run_timeout은 단일 시도의 하드 벽시계(wall-clock) 상한이에요. 노드 활동과 무관하게 절대 갱신되지 않습니다:
from langgraph.types import TimeoutPolicy
builder.add_node(
"call_model",
call_model,
timeout=TimeoutPolicy(run_timeout=120),
)
한도가 초과되면 LangGraph는 NodeTimeoutError를 일으키고, 실패한 시도의 모든 쓰기를 지우며, 재시도 정책이 재시도할지 결정하게 합니다.
유휴 타임아웃 (Idle timeout)
idle_timeout은 진행 상황에 따라 리셋되는 상한이에요. 노드가 지정된 시간 동안 관찰 가능한 진행을 멈출 때만 발동합니다 — run_timeout과 달리 노드가 진행 신호를 낼 때마다 시계가 리셋됩니다:
builder.add_node(
"call_model",
call_model,
timeout=TimeoutPolicy(idle_timeout=30),
)
run_timeout과 idle_timeout을 함께 설정할 수 있습니다. 먼저 발동하는 쪽이 시도를 취소합니다.
진행 신호 (Progress signals)
기본 refresh_on="auto"에서는 다음 중 하나라도 발생하면 유휴 시계가 리셋됩니다:
CONFIG_KEY_SEND를 통한 상태 쓰기- 스트림 출력(생성된 async 스트림 청크)
- 하위 태스크 스케줄링
- 런타임 스트림 작성자(writer) 호출
- 노드 또는 그 하위 항목의 모든 LangChain 콜백 이벤트(LLM 토큰, 도구 호출, 체인 시작/끝 등)
하트비트 모드 (Heartbeat mode)
refresh_on="heartbeat"를 설정하면 갱신 소스를 명시적인 runtime.heartbeat() 호출로만 좁힐 수 있어요. 수다스러운 하위 개체가 시계를 리셋하지 않는 엄격한 유휴 정의를 원할 때 유용합니다:
builder.add_node(
"call_model",
call_model,
timeout=TimeoutPolicy(idle_timeout=30, refresh_on="heartbeat"),
)
수동 하트비트 (Manual heartbeats)
진행 신호를 자연스럽게 내지 않는 장기 실행 작업은 runtime.heartbeat()를 호출해 유휴 시계를 수동으로 리셋하세요:
from langgraph.graph import StateGraph, START, END
from langgraph.runtime import Runtime
from langgraph.types import TimeoutPolicy
from typing_extensions import TypedDict
class State(TypedDict):
result: str
async def long_running_node(state: State, runtime: Runtime) -> State:
for batch in fetch_batches():
process(batch)
runtime.heartbeat() # [!code highlight]
return {"result": "done"}
builder = StateGraph(State)
builder.add_node(
"long_running_node",
long_running_node,
timeout=TimeoutPolicy(idle_timeout=30, refresh_on="heartbeat"),
)
builder.add_edge(START, "long_running_node")
builder.add_edge("long_running_node", END)
runtime.heartbeat()는 유휴 제한 시도 밖에서는 no-op이므로 조건 없이 호출해도 됩니다.
NodeTimeoutError
타임아웃이 발동하면 LangGraph는 어떤 한도가 걸렸는지에 대한 구조화된 컨텍스트와 함께 NodeTimeoutError를 일으킵니다:
| 속성 | 타입 | 설명 |
|---|---|---|
node |
str |
실행이 타임아웃된 노드 이름. |
elapsed |
float |
타임아웃 발동까지 경과한 초. |
kind |
Literal["idle", "run"] |
어떤 타임아웃이 발동했는지. |
idle_timeout |
float | None |
구성된 유휴 타임아웃(초), 있으면. |
run_timeout |
float | None |
구성된 실행 타임아웃(초), 있으면. |
NodeTimeoutError는 기본적으로 재시도 가능해요. timeout을 재시도 정책과 결합하면 즉시 동작합니다 — 타임아웃 시계는 각 새 시도에서 리셋되고, 타임아웃된 시도의 쓰기는 다음 재시도 전에 지워집니다:
from langgraph.types import RetryPolicy, TimeoutPolicy
builder.add_node(
"call_model",
call_model,
timeout=TimeoutPolicy(idle_timeout=30),
retry_policy=RetryPolicy(max_attempts=3),
)
Send로 동적 타임아웃 (Dynamic timeouts with Send)
Send로 노드를 동적으로 발송할 때(예: map-reduce 패턴), Send에 직접 타임아웃을 전달해 그 특정 push에 대한 대상 노드의 정적 타임아웃을 오버라이드할 수 있어요:
from langgraph.types import Send, TimeoutPolicy
def fan_out(state: OverallState):
return [
Send("process_item", {"item": item}, timeout=TimeoutPolicy(idle_timeout=15))
for item in state["items"]
]
Send에 타임아웃을 생략하면 대상 노드의 타임아웃(add_node 시점에 설정)이 적용됩니다. 이렇게 하면 노드에 기본 타임아웃을 설정하고 개별 호출에서는 더 엄격하게 조일 수 있어요.
오류 처리 (Error handling)
오류 핸들러는 노드가 실패하고 모든 재시도가 소진된 후 실행됩니다. 현재 상태를 받아 상태를 갱신하거나 Command로 다른 노드로 라우팅할 수 있어요. 전체 그래프를 중단하는 대신 우아하게 복구하려는 보상 흐름(Saga 패턴)에 유용합니다.
add_node에 error_handler=를 전달하세요:
from langgraph.errors import NodeError
from langgraph.types import Command, RetryPolicy
from langgraph.graph import StateGraph, START
from typing_extensions import TypedDict
class State(TypedDict):
status: str
def charge_payment(state: State) -> State:
raise RuntimeError("payment gateway timeout")
def payment_error_handler(state: State, error: NodeError) -> Command:
return Command(
update={"status": f"compensated: {error.error}"},
goto="finalize",
)
def finalize(state: State) -> State:
return state
graph = (
StateGraph(State)
.add_node(
"charge_payment",
charge_payment,
retry_policy=RetryPolicy(max_attempts=3, retry_on=ConnectionError),
error_handler=payment_error_handler,
)
.add_node("finalize", finalize)
.add_edge(START, "charge_payment")
.compile()
)
핸들러는 재시도 정책이 소진된 후에만, 또는 재시도 정책이 구성되지 않았다면 즉시 발동합니다. 재시도 정책과 오류 핸들러는 분리되어 유지됩니다. 언제 재시도하고 언제 보상할지는 독립적으로 구성하세요.
NodeError
오류 핸들러는 타입 있는 error: NodeError 파라미터로 실패 컨텍스트를 받습니다. 이는 타입 어노테이션으로 주입됩니다(runtime: Runtime과 같은 패턴):
from langgraph.errors import NodeError
def my_handler(state: State, error: NodeError) -> Command:
print(f"Node {error.node} failed with: {error.error}")
return Command(update={"status": "recovered"}, goto="next_step")
NodeError는 두 필드를 가진 frozen dataclass입니다:
| 속성 | 타입 | 설명 |
|---|---|---|
node |
str |
실행이 실패한 노드 이름. |
error |
BaseException |
실패한 노드가 일으킨 예외. |
error: NodeError 파라미터는 선택입니다. 실패 컨텍스트가 필요 없는 핸들러는 (state) 또는 (state, runtime) 같은 더 단순한 시그니처를 사용할 수 있어요.
Command로 라우팅 (Route with Command)
오류 핸들러는 Command를 반환해 상태를 갱신하고 특정 노드로 라우팅할 수 있어서, Saga/보상 패턴을 가능하게 합니다:
from langgraph.errors import NodeError
from langgraph.types import Command, RetryPolicy
from langgraph.graph import StateGraph, START
from typing_extensions import TypedDict
class State(TypedDict):
status: str
def reserve_inventory(state: State) -> State:
return {"status": "reserved"}
def charge_payment(state: State) -> State:
raise RuntimeError("payment timeout")
def payment_error_handler(state: State, error: NodeError) -> Command:
return Command(
update={"status": f"compensated_after_{error.node}: {error.error}"},
goto="finalize",
)
def finalize(state: State) -> State:
return state
graph = (
StateGraph(State)
.add_node("reserve_inventory", reserve_inventory)
.add_node(
"charge_payment",
charge_payment,
retry_policy=RetryPolicy(max_attempts=3, retry_on=ConnectionError),
error_handler=payment_error_handler,
)
.add_node("finalize", finalize)
.add_edge(START, "reserve_inventory")
.add_edge("reserve_inventory", "charge_payment")
.compile()
)
charge_payment는 ConnectionError에 대해 최대 3회 재시도합니다. 재시도가 소진되면(또는 오류가 ConnectionError가 아니면), 핸들러가 상태를 갱신하고 finalize로 라우팅해 그래프를 중단하는 대신 보상합니다.
재개 안전 실패 (Resume-safe failures)
interrupt()와의 동작 (Behavior with interrupt())
서브그래프 실패 (Subgraph failures)
노드가 서브그래프를 감싸고 그 서브그래프가 처리되지 않은 예외를 일으키면, 그 예외는 부모 노드로 표면화됩니다. 부모 노드에 오류 핸들러가 있으면 그 핸들러가 서브그래프 예외를 error.error에 담아 발동합니다.
그래프 기본값 (Graph defaults)
매 add_node 호출마다 같은 retry_policy=, error_handler=, timeout=, cache_policy=를 반복하는 대신, set_node_defaults로 그래프 전역 기본값을 한 곳에서 구성하세요:
from langgraph.errors import NodeError
from langgraph.types import RetryPolicy, TimeoutPolicy
from langgraph.graph import StateGraph, START
from typing_extensions import TypedDict
class State(TypedDict):
status: str
def default_error_handler(state: State, error: NodeError) -> State:
return {"status": f"handled: {error.error}"}
graph = (
StateGraph(State)
.set_node_defaults(
retry_policy=RetryPolicy(max_attempts=3),
error_handler=default_error_handler,
timeout=TimeoutPolicy(run_timeout=30),
)
.add_node("step_a", step_a)
.add_node("step_b", step_b)
.add_edge(START, "step_a")
.compile()
)
이제 step_a와 step_b 모두 중복 없이 같은 재시도 정책, 오류 핸들러, 타임아웃을 공유합니다.
우선순위 (Precedence)
add_node()에 직접 전달한 노드별 값은 항상 set_node_defaults()가 설정한 기본값을 오버라이드합니다. 기본값은 compile() 시점에 해석되므로, set_node_defaults()를 add_node() 전후 어느 순서로든 호출할 수 있어요:
graph = (
StateGraph(State)
.set_node_defaults(error_handler=default_error_handler)
.add_node("step_a", step_a) # uses default_error_handler
.add_node("step_b", step_b, error_handler=custom_error_handler) # uses custom_error_handler
.add_edge(START, "step_a")
.compile()
)
기본 오류 핸들러 (Default error handler)
error_handler 기본값은 모든 그래프 실행이 외부 프로세스(예: 백그라운드 작업 행)에 매핑되고, 처리되지 않은 노드 실패가 그 프로세스를 실패로 표시해야 할 때 특히 유용해요. 매 add_node마다 error_handler=를 반복할 필요가 없습니다. 단계가 자체 로직이 필요하면 노드별 핸들러가 여전히 우선합니다:
from langgraph.errors import NodeError
from langgraph.graph import StateGraph, START
from langgraph.types import Command, RetryPolicy
from typing_extensions import TypedDict
class State(TypedDict):
process_id: str
status: str
def fetch_data(state: State) -> State:
return {"status": "fetched"}
def charge_payment(state: State) -> State:
raise RuntimeError("payment timeout")
def finalize(state: State) -> State:
return state
def mark_process_failed(state: State, error: NodeError) -> State:
# Persist failure on the external process row keyed by process_id.
return {"status": f"failed at {error.node}: {error.error}"}
def refund_payment(state: State, error: NodeError) -> Command:
return Command(
update={"status": f"compensated after {error.node}"},
goto="finalize",
)
graph = (
StateGraph(State)
.set_node_defaults(
retry_policy=RetryPolicy(max_attempts=3),
error_handler=mark_process_failed,
)
.add_node("fetch_data", fetch_data) # uses mark_process_failed
.add_node(
"charge_payment",
charge_payment,
error_handler=refund_payment, # overrides the graph-wide default
)
.add_node("finalize", finalize)
.add_edge(START, "fetch_data")
.add_edge("fetch_data", "charge_payment")
.compile()
)
fetch_data가 재시도 후 실패하면 mark_process_failed가 실행됩니다. charge_payment가 재시도 후 실패하면 노드별 핸들러가 기본값을 오버라이드하므로 refund_payment가 실행됩니다.
핸들러는 Error handling에서 설명한 것과 같은 (state, error: NodeError) 시그니처를 받습니다. thread_id 같은 config 값을 접근해야 한다면 선택적인 세 번째 인자로 RunnableConfig도 받습니다:
from langchain_core.runnables import RunnableConfig
def mark_process_failed(
state: State, error: NodeError, config: RunnableConfig
) -> State:
thread_id = config["configurable"].get("thread_id")
return {"status": f"failed on thread {thread_id}: {error.error}"}
적용 가능성 매트릭스 (Applicability matrix)
모든 기본값이 모든 노드 타입에 적용되는 것은 아닙니다. 오류 핸들러 노드(add_node(error_handler=...)로 등록된 것)는 안전하지 않은 동작을 방지하기 위해 특정 기본값에서 제외됩니다:
set_node_defaults 파라미터 |
일반 노드에 적용 | 오류 핸들러 노드에 적용 | 이유 |
|---|---|---|---|
retry_policy |
✅ | ✅ | 일시적 실패 시 핸들러도 재시도되어야 함 |
timeout |
✅ | ✅ | 멈춘 핸들러는 멈춘 일반 노드처럼 취소되어야 함 |
error_handler |
✅ | ❌ | 핸들러는 절대 자기 자신을 포착해선 안 됨 |
cache_policy |
✅ | ❌ | 핸들러 결과 캐싱은 안전하지 않음 |
범위 (Scope)
부모 그래프에 설정된 기본값은 서브그래프가 상속하지 않습니다. 각 그래프는 자체 기본값을 유지합니다.
Functional API
같은 timeout=와 retry_policy= 파라미터를 functional API의 @task와 @entrypoint에서도 사용할 수 있어요:
from langgraph.func import entrypoint, task
from langgraph.types import RetryPolicy, TimeoutPolicy
@task(
timeout=TimeoutPolicy(idle_timeout=30),
retry_policy=RetryPolicy(max_attempts=3),
)
async def call_api(url: str) -> str:
response = await fetch(url)
return response.text
@entrypoint(timeout=60)
async def my_workflow(inputs: dict) -> str:
result = await call_api("https://api.example.com/data")
return result
동작은 add_node와 동일합니다. 타임아웃 시 NodeTimeoutError가 일어나고, 버퍼링된 쓰기가 지워지며, 재시도 정책이 재시도할지 결정해요.
우아한 종료 (Graceful shutdown)
협력적 종료(cooperative shutdown)를 사용하면 현재 슈퍼스텝이 완료된 뒤 진행 중인 그래프 런을 멈추고 재개 가능한 체크포인트를 저장할 수 있어요. 이는 SIGTERM 신호를 처리하거나, 작업을 잃지 않고 리소스를 회수해야 하는 외부 감독자에 유용합니다.
RunControl을 만들고 invoke 또는 stream에 control=로 전달하세요. 어떤 스레드에서든 request_drain()을 호출해 런이 멈춰야 한다는 신호를 보낼 수 있습니다:
from langgraph.runtime import RunControl
from langgraph.errors import GraphDrained
control = RunControl()
# In a signal handler or supervisor:
# control.request_drain("sigterm")
try:
result = graph.invoke(inputs, config, control=control)
except GraphDrained as e:
# The graph stopped early and saved a checkpoint.
# Resume later with the same config.
print(f"Drained: {e.reason}")
의미 (Semantics)
드레인은 협력적이며 슈퍼스텝 사이에서 동작합니다. 실행 중인 작업을 선점하지 않습니다:
| 시나리오 | 동작 |
|---|---|
| 실행 중인 노드 | 완료까지 실행. 드레인은 다음 슈퍼스텝에 적용됩니다. |
| 현재 재시도 중인 재시도 정책 있는 노드 | 재시도 루프가 소진 또는 성공까지 실행. 드레인은 그 후 적용됩니다. |
| 그래프가 드레인과 같은 틱에서 자연 완료 | 정상 반환. control.drain_requested를 검사해 정상 런과 구분합니다. |
| 더 많은 슈퍼스텝이 남음 | GraphDrained(reason) 발생. 체크포인트 저장, 재개 가능. |
| 서브그래프가 드레인 요청 | GraphDrained이 부모를 통해 올라가 자체 다음 슈퍼스텝 경계에서 멈춥니다. |
드레인 후 재개 (Resume after drain)
invoke(None, config)로 같은 thread_id를 사용해 드레인된 런을 재개하세요:
result = graph.invoke(None, config)
노드 안에서 드레인 상태 읽기 (Read drain state inside a node)
runtime 파라미터로 드레인 상태에 접근해 슈퍼스텝 경계에 도달하기 전에 노드 동작을 조정할 수 있어요:
from langgraph.runtime import Runtime
async def my_node(state: State, runtime: Runtime) -> State:
if runtime.drain_requested:
# Skip expensive work and return a minimal result
return {"status": "skipped", "reason": runtime.drain_reason}
return {"status": await do_work()}
SIGTERM 훅 패턴 (SIGTERM hook pattern)
프로세스 종료를 처리하는 권장 패턴:
import signal
from langgraph.runtime import RunControl
from langgraph.errors import GraphDrained
control = RunControl()
signal.signal(signal.SIGTERM, lambda *_: control.request_drain("sigterm"))
try:
result = graph.invoke(inputs, config, control=control)
except GraphDrained as e:
log.info("graph drained: %s", e.reason)
# Resume on next startup with the same config
제한사항 (Limitations)
- 타임아웃은 async 전용:
timeout이 있는 동기 노드는 컴파일 시점에 거부됩니다. - 노드당 핸들러 하나: 각 노드는 최대 하나의
error_handler를 가집니다. - 핸들러 실패는 위로 올라감: 오류 핸들러 자신이 예외를 일으키면, 노드에 핸들러가 없는 것처럼 그 예외가 전파됩니다.
set_node_defaults는 서브그래프가 상속하지 않음: 각 그래프는 자체 기본값을 독립적으로 관리합니다.
더 알아보기 (Learn more)
- checkpointers — 체크포인트 저장·복구.
- persistence — 스레드 영속화.
- interrupts — human-in-the-loop.