체크포인터 한 줄이면 기억이 생긴다
한 줄 요약
compile(checkpointer=InMemorySaver()) 한 줄이 2장의 history.append(...) 를 대신합니다. 그리고 저장 위치를 바꾸려면 이 한 줄만 바꾸면 됩니다.
1. 체크포인터가 하는 일
아래는 code/ch07_memory.py 의 일부입니다 — 위에서 이미 붙여 넣었으니 다시 넣지 마세요.
chatbot = graph.compile(checkpointer=InMemorySaver())
체크포인터(checkpointer) 는 그래프가 실행될 때마다 상태를 저장하고, 다음 실행 때 불러오는 역할을 합니다.
우리가 넣는 것은 이번 말 하나뿐인데, 체크포인터가 앞의 것을 붙여 줍니다.
invoke 한 번 안에서 일어나는 순서
언제 꺼내고 언제 저장하는지를 시간 순서로 보세요.
꺼내기는 invoke 당 한 번, 저장은 노드가 끝날 때마다입니다. 우리 그래프는 노드가 하나라 ③이 한 번이지만, 8장의 그래프는 노드가 셋(계획→실행→종합)이라 ③이 세 번 일어납니다.
8장 : 꺼내기 1번 ──▶ [계획] ──▶ [실행] ──▶ [종합]
저장③ 저장③ 저장③
2. 2장과 나란히
아래는 개념 설명용 코드입니다 — 실습 파일에 넣지 않습니다.
# 2장 — 손으로
history = []
history.append(say("user", message)) # ← 저장
answer = chat_multi(SYSTEM, history) # ← 전체를 넘김
history.append(say("model", answer)) # ← 저장
# 7장 — 체크포인터
chatbot.invoke({"messages": [{"role": "user", "content": message}]}, config)
# └─ 이번 말만. 나머지는 체크포인터가.
| 2장 | 7장 |
|---|---|
history 변수를 우리가 관리 |
체크포인터가 관리 |
append 를 우리가 호출 |
add_messages 가 자동 |
| 전체를 매번 넘김 | 이번 말만 넘김 |
| 손님마다 리스트 따로 만들기 | thread_id |
3. InMemorySaver — 메모리에 저장
아래는 code/ch07_memory.py 의 일부입니다 — 위에서 이미 붙여 넣었으니 다시 넣지 마세요.
from langgraph.checkpoint.memory import InMemorySaver
chatbot = graph.compile(checkpointer=InMemorySaver())
이름 그대로 메모리에 저장합니다.
장점 — 설치할 것도, 설정할 것도 없습니다. 실습에 딱 맞습니다.
단점 — 프로그램을 끄면 사라집니다.
터미널에서 실행 — 프로젝트 폴더
agentic_ai에서,(agentic)표시를 확인한 뒤.
python code/ch07_memory.py # 대화가 쌓임
# 프로그램 종료 → 다 사라짐
python code/ch07_memory.py # 처음부터 다시
2장 6절에서 말한 한계 ②(프로그램을 끄면 사라진다)가 아직 남아 있습니다.
4. 저장 위치를 바꾸려면
이 한 줄만 바꾸면 됩니다.
아래는 개념 설명용 코드입니다 — 실습 파일에 넣지 않습니다.
# 메모리 (실습용)
from langgraph.checkpoint.memory import InMemorySaver
chatbot = graph.compile(checkpointer=InMemorySaver())
# SQLite 파일 (개인 프로젝트)
import sqlite3
from langgraph.checkpoint.sqlite import SqliteSaver
conn = sqlite3.connect("chat.db", check_same_thread=False)
chatbot = graph.compile(checkpointer=SqliteSaver(conn))
그래프·노드·호출부는 한 글자도 안 바뀝니다. 바뀌는 건 체크포인터를 만드는 줄뿐입니다.
문서에서 보게 될
from_conn_string(...)은 조심하세요. 그건with블록에서 쓰는 형태라,compile()에 그냥 넣으면 동작하지 않습니다.
python with SqliteSaver.from_conn_string("chat.db") as cp: chatbot = graph.compile(checkpointer=cp) ... # 여기 안에서만 쓸 수 있다
위처럼sqlite3.connect(...)로 연결을 직접 만들어 넘기는 편이 코드 구조를 안 바꿔서 편합니다. PostgreSQL(PostgresSaver)은 첫 실행에.setup()을 한 번 불러 표를 만들어야 합니다.이게 라이브러리를 쓰는 이점입니다. 2장 방식으로 직접 짰다면 "메모리에 쌓기 → 파일에 저장하기 → DB에 저장하기"로 옮길 때마다 코드를 다시 짜야 했을 겁니다. 여기서는 한 줄 교체입니다.
다만 SQLite·PostgreSQL 용은 별도 패키지 설치가 필요합니다 (
langgraph-checkpoint-sqlite등). 이 과정에서는InMemorySaver만 씁니다.
5. 상태를 직접 꺼내 보기
직접 고쳐 보기 —
agentic_ai/code/ch07_memory.py를 열어 아래처럼 바꾸거나 덧붙인 뒤 다시 실행하세요.
config = {"configurable": {"thread_id": "손님-오지우"}}
snapshot = chatbot.get_state(config)
print("쌓인 메시지:", len(snapshot.values["messages"]))
for m in snapshot.values["messages"]:
print(f" {type(m).__name__:14s} {str(m.content)[:45]}")
실제 실행 결과입니다.
쌓인 메시지: 6
HumanMessage 제 이름은 오지우입니다. 스마트워치를 샀어요.
AIMessage 오지우 고객님, 스마트워치 구매 건으로 연락 주셨군요...
HumanMessage 제가 뭘 샀다고 했죠?
AIMessage 고객님께서는 스마트워치를 구매하셨다고 말씀해주셨습니다...
HumanMessage 제 이름도 기억하세요?
AIMessage 네, 고객님의 성함은 오지우님이십니다...
세 번 말했더니 6개입니다. 3절 5번에서 본 그 목록이죠. 2장에서 손으로 쌓았던 것과 똑같은 모양입니다.
thread_id는[1]에서 쓴 것과 같은"손님-오지우"여야 합니다. 없는 이름을 넣으면 빈 서랍이라 아무것도 안 나옵니다.
get_state() 의 항목 |
무엇 |
|---|---|
.values |
상태 딕셔너리 ({"messages": [...]}) |
.next |
다음에 실행될 노드. 끝났으면 비어 있다 |
.config |
이 상태의 식별 정보 |
.values 만 알면 충분합니다.
6. 저장은 "매 노드마다" 일어납니다
이름이 체크포인트(checkpoint, 중간 저장) 인 이유가 있습니다.
START ──▶ [노드 A] ──▶ [노드 B] ──▶ [노드 C] ──▶ END
저장 저장 저장
노드가 끝날 때마다 상태를 저장합니다. 우리 그래프는 노드가 하나라 한 번이지만, 8장의 그래프는 노드가 셋이라 세 번 저장됩니다.
이게 왜 유용한가 — 중간에 실패해도 그 지점부터 다시 시작할 수 있습니다. 그리고 "3번째 노드까지 실행됐을 때 상태가 어땠는지" 를 되짚어 볼 수 있습니다.
이 과정에서는 여기까지만 씁니다. LangGraph에는 중간에 멈추고 사람의 승인을 받은 뒤 이어 가는 기능(human-in-the-loop)도 있는데, 그것도 이 체크포인트 위에 만들어져 있습니다.
7. 대화가 길어지면 — 여전히 남는 문제
2장 7절의 그 이야기가 그대로 적용됩니다.
1턴 입력 70 토큰
2턴 입력 93 토큰
3턴 입력 111 토큰
...
50턴 입력 수천 토큰
체크포인터는 저장을 대신해 줄 뿐, 길이를 관리해 주지는 않습니다. 쌓인 것을 전부 모델에게 보냅니다.
대책
| 방법 | 어떻게 |
|---|---|
| 오래된 것 잘라내기 | 최근 N개만 모델에게 보낸다 |
| 요약해서 넣기 | 오래된 대화를 한 문단으로 요약해 대체 |
| turn 수 제한 | 일정 횟수가 지나면 새 thread_id 로 |
가장 단순한 방법은 노드에서 자르는 것입니다.
개선안 — 원하면 고쳐 보세요 ·
agentic_ai/code/ch07_memory.py
실습 기본 코드에는 없는 내용입니다. 지금 안 고쳐도 다음 절로 넘어갈 수 있습니다.
def counselor(state: State) -> dict:
recent = state["messages"][-10:] # 최근 10개만
answer = llm.invoke([{"role": "system", "content": SYSTEM}] + recent)
return {"messages": [answer]}
한 줄이면 됩니다. 다만 몇 개를 남길지는 사람이 정합니다.
AI가 하는 것과 사람이 정하는 것 — 라이브러리는 저장과 복원을 대신합니다. 무엇을 얼마나 기억할지는 여전히 설계입니다. 너무 적게 남기면 "그거"가 안 통하고, 너무 많이 남기면 비용이 늡니다.
스스로 해 보기
get_state()로 상태를 찍어 보고, 말을 한 번 더 건 뒤 다시 찍어 보세요. 몇 개씩 늘어나나요?checkpointer=InMemorySaver()를 빼고 컴파일해 보세요.
python chatbot = graph.compile()
[1]의 결과가 어떻게 달라지나요?- 위의 최근 10개만 보내기를 넣고, 짧은 대화를 여러 번 이어 가 보세요. 몇 번째부터 앞말을 잊나요?
- 같은
thread_id로 프로그램을 두 번 실행해 보세요. 첫 실행의 대화를 기억하나요? 왜 그럴까요?
4번이
InMemorySaver의 한계를 몸으로 확인하는 실험입니다.
핵심 정리
compile(checkpointer=...)한 줄이 2장의history.append(...)를 대신합니다.- 우리는 이번 말 하나만 넣고, 체크포인터가 앞의 것을 붙여 줍니다.
InMemorySaver는 메모리에 저장합니다. 설정이 없어 편하지만 프로그램을 끄면 사라집니다.- 저장 위치를 바꾸려면 이 한 줄만 바꿉니다 —
SqliteSaver,PostgresSaver. 나머지 코드는 그대로입니다. get_state(config).values로 쌓인 상태를 직접 볼 수 있습니다.- 저장은 노드가 끝날 때마다 일어납니다. 그래서 "체크포인트"입니다.
- 길이 관리는 여전히 우리 일입니다. 오래된 것을 자르거나 요약하는 것은 사람이 정합니다.