체크포인터 (Checkpointers)

체크포인터 (Checkpointers)

LangGraph의 체크포인터는 그래프 실행의 각 단계에서 그래프 상태를 체크포인트로 저장해요. 이를 통해 지속성(persistence), human-in-the-loop(사람 개입), 장애에 강한(fault-tolerant) 실행이 가능해집니다.

Tip: 체크포인트된 상태를 추적하고 에이전트가 세션을 넘어 어떻게 이어지는지 디버깅하려면 LangSmith를 활용하세요. tracing quickstart를 따라 설정할 수 있어요.

왜 체크포인터를 쓰나요

  • Human-in-the-loop: 체크포인터는 사람이 그래프 단계를 검사·중단·승인할 수 있게 해주는 human-in-the-loop 워크플로를 가능하게 해요. 이런 워크플로에는 체크포인터가 꼭 필요합니다. 사람은 언제든지 그래프의 현재 상태를 볼 수 있어야 하고, 그래프는 사람이 상태를 수정한 뒤 실행을 이어갈 수 있어야 하기 때문이에요. 예시는 Interrupts에서 확인할 수 있습니다.
  • 대화 메모리를 체크포인터로 추가하고 관리하는 방법은 Add memory를 참고하세요.
  • Time travel(시간 여행): 체크포인터는 "time travel"을 지원해요. 사용자가 과거 그래프 실행을 재생(replay)해서 특정 그래프 단계를 검토하거나 디버깅할 수 있습니다.

핵심 개념

스레드 (Threads)

스레드는 체크포인터가 저장한 각 체크포인트에 부여되는 고유 ID(스레드 식별자)예요. 스레드는 일련의 runs가 쌓아온 누적 상태를 담고 있습니다. 실행(run)이 발생하면 어시스턴트의 그래프 state가 스레드에 영속됩니다.

체크포인터가 있는 그래프를 호출할 때는 반드시 config의 configurable 부분에 thread_id를 지정해야 해요:

{"configurable": {"thread_id": "1"}}

스레드의 현재 상태와 과거 상태는 모두 조회할 수 있어요. 상태를 영속하려면 실행 전에 스레드가 먼저 생성되어 있어야 합니다. LangSmith API는 스레드와 스레드 상태를 만들고 관리하는 여러 엔드포인트를 제공해요. 자세한 내용은 API reference에서 확인할 수 있습니다.

체크포인트 (Checkpoints)

특정 시점의 스레드 상태를 체크포인트라고 해요. 체크포인트는 각 super-step마다 저장된 그래프 상태의 스냅샷이며, StateSnapshot 객체로 표현됩니다(전체 필드 참조는 StateSnapshot fields 참고).

Super-steps

from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import InMemorySaver
from langchain_core.runnables import RunnableConfig
from typing import Annotated
from typing_extensions import TypedDict
graph = workflow.compile(checkpointer=checkpointer)

config: RunnableConfig = {"configurable": {"thread_id": "1"}}
graph.invoke({"foo": "", "bar":[]}, config)

상태 가져오기와 갱신하기

상태 이력 가져오기

이 예시에서 get_state_history의 출력은 다음과 같은 형태가 돼요.

재생 (Replay)

과거 실행을 재생하는 전체 예시와 코드는 Time travel에서 확인할 수 있어요.

상태 갱신 (Update state)

update_state를 사용해 그래프 상태를 수정할 수 있어요. 이 메서드는 갱신된 값으로 새 체크포인트를 생성하지, 원래 체크포인트를 수정하지는 않습니다.

갱신은 노드 갱신과 동일하게 처리돼요: reducer 함수가 정의되어 있으면 값을 reducer에 통과시키므로, reducer가 있는 채널은 값을 덮어쓰지 않고 누적(accumulate) 합니다.

선택적으로 as_node를 지정해 이 갱신이 어느 노드에서 온 것처럼 취급할지 제어할 수 있는데, 이는 다음에 어떤 노드가 실행될지에 영향을 줍니다. 자세한 내용은 Time travel: as_node를 참고하세요.

지속성 모드 (Durability modes)

LangGraph는 성능과 데이터 일관성의 균형을 조절할 수 있게 세 가지 지속성 모드를 지원해요. 그래프 실행 메서드를 호출할 때 지속성 모드를 지정할 수 있습니다:

graph.stream(
    {"input": "test"},
    durability="sync"
)
  • "exit": LangGraph는 그래프 실행이 끝날 때(성공, 오류, 또는 human-in-the-loop 인터럽트로 인해)만 변경 사항을 영속합니다.

체크포인트 저장 최적화

Warning: DeltaChannellanggraph>=1.2가 필요하며 현재 베타 상태예요. API는 향후 릴리스에서 변경될 수 있습니다.

체크포인터 라이브러리

내부적으로 체크포인팅은 BaseCheckpointSaver 인터페이스를 따르는 체크포인터 객체로 구현돼요. LangGraph는 여러 체크포인터 구현을 제공하는데, 모두 독립적으로 설치 가능한 라이브러리로 구현되어 있습니다.

  • langgraph-checkpoint: 체크포인터 saver의 기본 인터페이스(BaseCheckpointSaver)와 직렬화/역직렬화 인터페이스입니다.

체크포인터 인터페이스

각 체크포인터는 BaseCheckpointSaver 인터페이스를 따르며 다음 메서드들을 구현합니다.

AsyncSqliteSaver / AsyncPostgresSaver 체크포인터에 대해 자세히 알아볼 수 있어요.

직렬화 도구 (Serializer)

langgraph_checkpoint는 직렬화 도구를 구현하기 위한 protocol을 정의하고, 기본 구현으로 langchain과 LangGraph 프리미티브, datetime, enum 등 다양한 타입을 처리하는 JsonPlusSerializer를 제공합니다.

pickle을 이용한 직렬화

from langgraph.checkpoint.memory import InMemorySaver
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer

# ... 그래프 정의 ...
graph.compile(
    checkpointer=InMemorySaver(serde=JsonPlusSerializer(pickle_fallback=True))
)

암호화 (Encryption)

체크포인터는 저장되는 모든 상태를 선택적으로 암호화할 수 있어요. 활성화하려면 EncryptedSerializer 인스턴스를 아무 BaseCheckpointSaver 구현의 serde 인자로 전달하면 됩니다.

커스텀 체크포인터 만들기

기본 계약 (Base contract)

from collections.abc import AsyncIterator, Iterator, Sequence
from typing import Any
from langchain_core.runnables import RunnableConfig
from langgraph.checkpoint.base import (
    BaseCheckpointSaver,
    ChannelVersions,
    Checkpoint,
)

put_writes / aput_writes

langgraph.checkpoint.base에서 WRITES_IDX_MAP을 import하세요. 이 매핑은 __error__, __interrupt__ 같은 특수 채널을 예약된 음수 인덱스로 연결해서, 일반 write 인덱스와 충돌하지 않게 해줍니다.

직렬화

JsonPlusSerializer는 모든 LangGraph 네이티브 타입을 자동으로 처리해요:

  • _DeltaSnapshot — delta 채널이 사용하는 sentinel blob (msgpack ext code 7)
  • Pydantic v2 모델, dataclass, numpy 배열, datetime, enum 등

Delta 채널 지원

Info: DeltaChannel은 베타 상태입니다. 디자인이 안정되기 전까지 API와 디스크 표현이 변경될 수 있어요.

런타임에 필요한 것

channel_values에 delta 채널이 없는 체크포인트를 로드할 때, LangGraph는 saver.get_delta_channel_history(config=config, channels=[...])를 호출합니다. 이는 각 채널에 대해 다음을 반환해요.

기본 구현

                collected[write[1]].append(write)
        for ch in list(remaining):
            if ch in tup.checkpoint["channel_values"]:
                seed[ch] = tup.checkpoint["channel_values"][ch]
                remaining.discard(ch)
        cursor = tup.parent_config

    return {
        ch: {"writes": list(reversed(collected[ch])), **({"seed": seed[ch]} if ch in seed else {})}
        for ch in channels
    }