AI 에이전트 — 모델이 아니라 그래프 · 체크포인트·재개·시간여행 · 이론
어제 그 자리로 돌아갈 수 있는가
한 줄 요약
체크포인터를 붙이면 그래프는 단계마다 상태를 통째로 남긴다. 그래서 이어서 돌릴 수 있고, 과거로 되감을 수 있고, 거기서 갈래를 칠 수 있다. 그리고 같은 이유로 상태의 크기가 곧 저장 비용이 된다.
왜 이게 필요했나
에이전트가 열 단계를 도는 도중 여덟 번째에서 실패했다고 하자. 체크포인터가 없으면 방법은 하나다 — 처음부터 다시. 앞의 일곱 단계가 파일을 쓰고 메일을 보냈다면 그것도 다시 일어난다.
더 흔한 쪽은 이것이다. 사용자가 답을 받아 보고 "세 번째 단계에서 다른 자료를 썼으면 어땠을까" 라고 묻는다. 상태가 남아 있지 않으면 대답할 방법이 없다. 입력을 고쳐 처음부터 다시 돌리는 수밖에 없고, 그러면 앞의 두 단계가 만든 결과까지 새로 만들어진다. 같은 조건에서 비교해야 하는데 조건이 달라져 버린다.
체크포인터는 이 두 문제를 한 가지 방법으로 푼다. 단계가 끝날 때마다 그 시점의 상태 전체를 한 벌 저장해 둔다. 저장된 시점 하나하나에 이름표(checkpoint_id)가 붙어 있고, 그 이름표를 들고 가면 언제든 그 자리로 돌아갈 수 있다.
어떻게 동작하나
쓰는 법은 두 줄이다. 컴파일할 때 체크포인터를 주고, 돌릴 때 thread_id 를 준다.
from langgraph.checkpoint.memory import MemorySaverapp = graph.compile(checkpointer=MemorySaver())config = {"configurable": {"thread_id": "user-42"}}app.invoke({"topic": "가을"}, config)thread_id 는 대화 한 벌을 가리키는 이름이다. 같은 값을 주면 저장된 상태 위에 이어서 돌고, 다른 값을 주면 아무것도 없는 데서 새로 시작한다. [Persistence](https://docs.langchain.com/oss/python/langgraph/persistence) 문서가 이것을 단기 기억이라고 부른다 — 대화 한 벌 안에서만 이어지는 기억이라는 뜻이다.
여기서 중요한 것은 저장되는 시점이 몇 개인가 다. 노드 셋을 줄줄이 이은 그래프를 이 실습 환경(langgraph 0.2.60)에서 한 번 돌리고 get_state_history(config) 를 세어 보면 다섯 개가 나왔다. 입력을 받은 자리 하나, 노드가 끝날 때마다 하나씩 셋, 그리고 다 끝난 자리 하나다. 노드 하나가 늘면 체크포인트도 하나가 는다.
목록은 최신 것부터 나온다. 시간 순으로 읽고 싶으면 뒤집어야 한다. 스냅샷 하나에는 그 시점의 값(values), 다음에 돌 노드(next), 그리고 그 자리를 가리키는 config 가 들어 있다.
되감기 — 저장된 자리에서 다시 돈다
과거 스냅샷의 config 를 그대로 쓰고 입력을 None 으로 주면, 그 자리에서부터 다시 돈다.
snapshot = ... # next 가 ("write",) 인 스냅샷app.invoke(None, snapshot.config)입력을 None 으로 주는 것이 핵심이다. 새 입력을 주면 새로 시작하는 것이고, None 은 "저장된 그 상태로 이어서 돌라" 는 뜻이 된다. 실제로 재어 보니 write 앞으로 되감으면 체크포인트가 둘 늘었다(write 와 review). plan 앞이면 셋, review 앞이면 하나다. 다시 돈 만큼만 늘어난다.
분기 — 값을 고치고 다른 길로 간다
같은 좌표에서 값 하나를 바꿔 넣으면 거기서부터 다른 갈래가 생긴다.
forked = app.update_state(snapshot.config, {"angle": "비교"})app.invoke(None, forked)update_state 는 그 자리에 체크포인트를 하나 더 얹고 새 checkpoint_id 가 든 config 를 돌려준다. 그것으로 이어 돌리면 새 갈래가 만들어진다.
여기서 흔히 오해하는 부분이 하나 있다. 원래 갈래는 지워지지 않는다. 체크포인트가 그대로 남아 있으므로 그 시점의 config 로 언제든 읽을 수 있다. 하지만 thread_id 만 주고 get_state(config) 를 부르면 돌아오는 것은 새 갈래의 끝이다. 스레드의 "지금" 이 옮겨 갔기 때문이다. 원래 결과와 새 결과를 나란히 두고 비교하려면, 분기하기 전에 그 시점의 config 를 손에 들고 있어야 한다. 이 실습에서 재어 보니 한 번 돌린 뒤 write 앞에서 갈래를 치면 체크포인트는 다섯에서 여덟이 되었다 — 갈래를 만든 자리 하나에 다시 돈 노드 둘이다. [Use time-travel](https://docs.langchain.com/oss/python/langgraph/use-time-travel) 이 이 두 가지를 재생과 분기라는 이름으로 나눠 설명한다.
저장되는 것은 상태 전체다
체크포인트에 무엇이 들어가는지 헷갈릴 필요가 없다. 상태 전체다. 일부가 아니다.
그래서 상태에 큰 값을 담으면 체크포인트가 함께 커진다. 시간으로 이야기하면 기계와 부하에 따라 달라지지만, 크기는 같은 상태면 늘 같으니 재어 보면 된다. 이 실습의 그래프에서 상태를 직렬화해 바이트를 세어 봤다. 아무것도 담지 않았을 때 체크포인트 다섯 개의 합은 399바이트였다. 상태에 1,500바이트짜리 값 하나를 담고 같은 그래프를 돌렸더니 합이 6,447바이트가 되었다. 그 값을 실제로 들고 있는 체크포인트는 다섯 중 넷이었다. 한 번 담은 값이 네 번 저장된 것이다.
이 비율은 노드 수가 늘수록 커진다. 노드가 서른 개면 한 번 담은 값이 서른 번 가까이 저장된다. 그래서 큰 원문이나 표는 상태 바깥(파일·저장소)에 두고 상태에는 가리키는 값만 담는 편이 낫다.
현장에서 만나는 모습
첫째, 대화가 이어지지 않는다. 체크포인터를 안 붙였거나, 붙여 놓고 thread_id 를 매번 새로 만들고 있는 것이다. 오류는 나지 않는다. 그냥 매번 처음처럼 군다. 실제로 재어 보니 체크포인터 없는 그래프에 같은 config 로 두 번 돌려도 두 번 다 새로 시작했다.
둘째, 분기했더니 원래 결과가 안 보인다. 지워진 것이 아니라 스레드의 "지금" 이 옮겨 간 것이다. 원래 시점의 config 를 들고 있으면 그대로 읽힌다.
셋째, 저장량이 눈에 띄게 는다. 상태에 원문을 통째로 담아 두고 노드를 많이 도는 그래프가 그렇게 된다. 원인을 찾으려면 시간이 아니라 크기를 재야 한다.
넷째, 파드가 죽으면 다 사라진다. MemorySaver 는 이름 그대로 프로세스 안 메모리에 담는다. 새 프로세스가 뜨면 같은 thread_id 로 물어봐도 남은 것이 하나도 없다. 실습이나 시험에는 알맞지만 운영에는 맞지 않는다. 오래 남겨야 하면 바깥 저장소에 쓰는 체크포인터를 쓴다 — 어떤 것이 있는지는 [Persistence](https://docs.langchain.com/oss/python/langgraph/persistence) 와 [그래프 API 레퍼런스](https://reference.langchain.com/python/langgraph/graphs/)에 적혀 있다.
실무에서 진짜 중요한 것
thread_id를 무엇으로 정할지 먼저 정한다. 사용자별인가, 건별인가, 세션별인가. 이 선택이 "무엇이 이어지는가" 를 통째로 정한다.- 되감기와 분기를 말로 구분한다. 되감기는 같은 값으로 다시, 분기는 값을 고쳐 다른 길로. 코드에서는
update_state하나가 그 둘을 가른다. - 원래 갈래의 좌표를 손에 들고 분기한다. 나중에 비교하려면 그
config가 필요하다. - 상태에 넣기 전에 "이걸 매 단계 저장해도 되나" 를 묻는다. 답이 망설여지면 바깥에 두고 가리키는 값만 담는다.
- 재는 것은 시간이 아니라 크기다. 직렬화해서 바이트를 세면 같은 상태에서 늘 같은 답이 나온다.
다음 실습에서 할 것
/root/work/agckpt/ckpt.py 를 한 단계씩 키운다. 먼저 MemorySaver 와 thread_id 로 같은 대화가 이어지고 다른 대화가 따로 남는 것을 확인하고, 체크포인트를 세어 목록으로 읽는다. 이어 되감을 좌표를 찾는 함수를 만들고, 그 좌표로 되감고, 값을 고쳐 갈래를 친 뒤 원래 갈래를 다시 읽어 낸다. 그다음 상태를 직렬화해 바이트를 세어 큰 값 하나가 몇 군데에 실리는지 숫자로 확인하고, 마지막으로 장부를 새로 만들면 무엇이 사라지는지 자기 손으로 재현한다. 채점기는 여러분의 모듈을 실제로 불러 실행하고, 매번 다른 주제와 다른 크기의 값으로 두드려 본다.