타임 트래블

타임 트래블 (Time Travel)

LangGraph 에이전트의 모든 상태 변화는 **체크포인트(checkpoint)**를 만듭니다. 그 순간의 에이전트 상태를 완전히 담은 스냅샷이죠. 타임 트래블을 쓰면 어떤 체크포인트든 검사하고, 에이전트가 그때 가졌던 정확한 상태를 보고, 그 지점에서 실행을 재개해서 다른 경로를 탐색할 수 있어요. 디버거이자, 실행 취소 버튼이자, 감사 로그가 하나로 합쳐진 것이라고 생각하면 됩니다.

출처: LangChain 공식 문서 — frontend-time-travel

이 기능은 LangGraph 에이전트 서버가 필요해요. 로컬에서는 langgraph dev로 실행하거나 LangSmith에 배포해서 사용할 수 있습니다.

체크포인트는 어떻게 동작하나요?

LangGraph는 모든 노드 실행 후 에이전트 상태를 영속화합니다. 저장된 각 상태는 ThreadState 객체로, 다음을 담아요.

  • checkpoint — 이 특정 스냅샷을 식별하는 메타데이터 (ID, 타임스탬프)
  • values — 이 시점의 전체 에이전트 상태 (메시지, 커스텀 키)
  • tasks — 다음에 실행되도록 예약된 그래프 노드
  • next — 실행 계획에서 앞으로 올 노드의 이름

이것은 에이전트가 내린 모든 결정, 호출한 모든 도구, 만든 모든 응답의 선형 타임라인을 만듭니다. 여러분의 UI는 이 타임라인을 렌더링하고 사용자가 아무 지점으로나 점프하게 할 수 있어요.

useStream 설정하기

에이전트용 스트림을 만든 뒤, LangGraph 클라이언트에서 활성 스레드의 체크포인트 히스토리를 명시적으로 가져옵니다. 체크포인트에서 재개할 때는 forkFrom: { checkpointId }를 사용해요.

아래 코드는 타입 안전한 스트림 상태를 위해 useStream<typeof myAgent>를 사용합니다. Python 또는 JavaScript 백엔드의 타입 추론에 대해서는 프론트엔드 개요의 해당 섹션을 참고하세요.

import { useStream } from "@langchain/react";
import { useEffect, useState } from "react";

const AGENT_URL = "http://localhost:2024";

export function TimeTravelChat() {
  const [threadId, setThreadId] = useState<string | null>(null);
  const [history, setHistory] = useState<ThreadState[]>([]);
  const stream = useStream<typeof myAgent>({
    apiUrl: AGENT_URL,
    assistantId: "time_travel",
    threadId,
    onThreadId: setThreadId,
  });

  useEffect(() => {
    if (!threadId || stream.isLoading) return;
    stream.client.threads.getHistory(threadId).then(setHistory);
  }, [stream.client, threadId, stream.isLoading]);

  function resumeFrom(cp: ThreadState) {
    stream.submit({}, {
      forkFrom: { checkpointId: cp.checkpoint.checkpoint_id },
    });
  }

  return (
    <div className="flex h-screen">
      <ChatPanel messages={stream.messages} />
      <TimelineSidebar history={history} onSelect={resumeFrom} />
    </div>
  );
}

Vue, Svelte, Angular에서도 watch/$effect/effect로 히스토리를 가져오는 같은 구조를 구현합니다.

체크포인트 타임라인 만들기

타임라인 사이드바는 각 체크포인트를 클릭 가능한 항목으로 보여줘요. 각 항목은 실행된 노드와 그 시점의 메시지 수를 표시합니다.

function TimelineSidebar({ history, onSelect }) {
  return (
    <aside className="w-80 overflow-y-auto border-l bg-gray-50 p-4">
      <h2 className="mb-4 text-sm font-semibold uppercase text-gray-500">
        Checkpoint Timeline
      </h2>
      <div className="space-y-2">
        {history.map((cp, i) => {
          const taskName = cp.tasks?.[0]?.name ?? "unknown";
          const msgCount = (cp.values?.messages as unknown[])?.length ?? 0;

          return (
            <button
              key={cp.checkpoint.checkpoint_id}
              onClick={() => onSelect(cp)}
              className="w-full rounded-lg border bg-white p-3 text-left ..."
            >
              <div className="flex items-center justify-between">
                <span className="text-xs text-gray-400">#{i + 1}</span>
                <NodeBadge name={taskName} />
              </div>
              <p className="mt-1 text-sm font-medium">{taskName}</p>
              <p className="text-xs text-gray-500">
                {msgCount} message{msgCount !== 1 ? "s" : ""}
              </p>
            </button>
          );
        })}
      </div>
    </aside>
  );
}

체크포인트 상태 검사하기

체크포인트를 클릭하면 그 시점의 전체 상태를 보여줘야 합니다. JSON 뷰어를 쓰면 개발자가 에이전트가 무엇을 알고 무슨 결정을 내렸는지 완전히 볼 수 있어요. 노드 이름, 다음 실행 노드(next), 메시지 수를 요약해 보여주고, 펼치면 JSON.stringify로 전체 값을 볼 수 있게 합니다.

프로덕션 UI에서는 원시 JSON.stringify 대신 접을 수 있는 노드를 가진 적절한 JSON 뷰어 컴포넌트(react-json-viewreact-json-tree 같은)를 고려하세요.

체크포인트에서 재개하기

타임 트래블의 핵심은 이전 체크포인트에서 실행을 재개하는 능력입니다. 사용자가 체크포인트를 선택하면 null 입력으로 submit을 호출하고 체크포인트 ID를 전달합니다.

stream.submit({}, {
  forkFrom: { checkpointId: selectedCheckpoint.checkpoint.checkpoint_id },
});

이것은 LangGraph에게 다음을 시키죠.

  1. 선택한 체크포인트의 상태로 롤백
  2. 그 지점부터 그래프를 재실행
  3. 새 결과를 클라이언트로 스트리밍

선택한 체크포인트 이후의 기존 메시지는 새 실행 경로로 대체됩니다. 이렇게 하면 대화 타임라인에 사실상 **분기(branch)**가 생겨요.

체크포인트에서 재개해도 원래 타임라인이 삭제되지는 않아요. 이전 체크포인트들은 여전히 히스토리에 남아 있으므로, 사용자는 이전 작업을 잃지 않고 언제든 돌아가 다른 경로를 시도할 수 있습니다.

SplitView 레이아웃

타임 트래블은 왼쪽에 메인 채팅, 오른쪽에 타임라인을 두는 분할(split) 레이아웃에서 가장 잘 동작해요. 사이드바에 선택된 체크포인트의 CheckpointInspector를 함께 배치하면, 사용자가 타임라인에서 항목을 고르고 그 상태를 바로 살펴볼 수 있습니다.

체크포인트 메타데이터 추출하기

원시 체크포인트 데이터를 타임라인 표시에 친숙한 항목으로 변환해서, 원시 ID 대신 의미 있는 라벨로 렌더링할 수 있어요. 인덱스, checkpoint ID, 노드 이름, 메시지 수, 인터럽트 여부, 다음 노드 목록을 추출하는 헬퍼를 만들면 됩니다.

사용 사례

타임 트래블은 다양한 시나리오에서 매우 유용합니다.

  • 에이전트 동작 디버깅 — 에이전트의 결정을 단계별로 살펴 왜 그 경로를 골랐는지 이해합니다.
  • 실행 취소 (Undoing actions) — 에이전트가 잘못된 방향으로 갔다면 이전 체크포인트에서 재개해 다시 시도합니다.
  • 대안 탐색 — 대화 중간의 체크포인트에서 분기(fork)해 서로 다른 입력이 결과를 어떻게 바꾸는지 봅니다.
  • 감사 (Auditing) — 컴플라이언스, 품질 보증, 사후 분석을 위해 에이전트 행동의 전체 이력을 검토합니다.
  • 교육 (Teaching) — 에이전트 실행을 단계별로 따라가며 다단계 추론이 어떻게 동작하는지 설명합니다.

타임 트래블은 인간 검토(human-in-the-loop) 패턴과 함께 쓸 때 특히 강력해요. 인간 검토자가 인터럽트에서 에이전트의 행동을 거부하면, 그 행동이 취해지기 전의 체크포인트에서 재개해 수정 입력을 줄 수 있습니다.

타임라인에서 인터럽트 다루기

인터럽트(인간 검토 일시 중지)를 포함한 체크포인트는 특별한 시각적 처리가 필요해요. 에이전트가 멈춰 인간 입력을 기다렸던 지점을 나타내니까요. 인터럽트가 있는 체크포인트에 "Interrupt" 배지나 강조 색상을 달아 구분할 수 있습니다.

모범 사례

  • 히스토리를 게으르게 로드 — 수백 개의 체크포인트가 있는 스레드에서는 페이지네이션하거나 최근 N개만 로드해 UI를 빠르게 유지하세요.
  • 의미 있는 라벨 표시 — 원시 체크포인트 ID 대신 노드 이름과 메시지 수를 보여주세요. 사용자에게는 UUID가 아니라 맥락이 필요해요.
  • 재개 전 확인 — 이전 체크포인트에서 재개하면 현재 실행 경로가 대체됩니다. 사용자가 대화 상태를 실수로 잃지 않도록 확인 대화상자를 보여주세요.
  • 현재 체크포인트 강조 — 어떤 체크포인트가 대화의 현재 상태에 해당하는지 시각적으로 명확히 하세요.
  • 키보드 탐색 지원 — 파워 유저는 화살표 키로 체크포인트를 탐색하고 싶어 합니다. 원활한 디버깅을 위해 타임라인에 키보드 핸들러를 추가하세요.
  • 체크포인트 간 상태 차이 표시 — 고급 사용자를 위해 연속된 두 체크포인트 사이에 무엇이 바뀌었는지 보여주면, 에이전트 상태가 각 단계에서 어떻게 진화했는지 정확히 드러납니다.

더 알아보기 (Learn more)