📚 Agentic AI - 기업용 자율 에이전트 개발 7장 · 기억하는 에이전트 Agentic AI란

체크포인터 한 줄이면 기억이 생긴다

한 줄 요약

compile(checkpointer=InMemorySaver()) 한 줄이 2장의 history.append(...) 를 대신합니다. 그리고 저장 위치를 바꾸려면 이 한 줄만 바꾸면 됩니다.


1. 체크포인터가 하는 일

아래는 code/ch07_memory.py 의 일부입니다 — 위에서 이미 붙여 넣었으니 다시 넣지 마세요.

chatbot = graph.compile(checkpointer=InMemorySaver())

체크포인터(checkpointer) 는 그래프가 실행될 때마다 상태를 저장하고, 다음 실행 때 불러오는 역할을 합니다.

[체크포인터 없음]invoke ① {"messages": ["안녕"]}→ 답변invoke ② {"messages": ["뭐라고 했지?"]}→ 이전 대화를 모른다[체크포인터 있음]invoke ① {"messages": ["안녕"]}└ 저장: [안녕, 답변1]invoke ② {"messages": ["뭐라고 했지?"]}└ 불러옴: [안녕, 답변1] + 새 메시지└ 저장: [안녕, 답변1, 뭐라고 했지?, 답변2]우리가 보내는 것은 '새 메시지' 하나뿐입니다. 기존 대화는 체크포인터가 불러오고 다시 저장합니다.
우리가 보내는 것은 '이번 말' 하나뿐이고, 나머지는 체크포인터가 맡는다

우리가 넣는 것은 이번 말 하나뿐인데, 체크포인터가 앞의 것을 붙여 줍니다.

invoke 한 번 안에서 일어나는 순서

언제 꺼내고 언제 저장하는지를 시간 순서로 보세요.

invoke({"messages": [이번말]}, {"thread_id": "손님-오지우"})① 서랍에서 꺼낸다 [사람1, AI1]② 새 메시지을 붙인다 [사람1, AI1, 이번말]thread_id로찾는다 (1번)노드 실행③ 서랍에 다시 저장한다 [사람1, AI1, 이번말, AI2]노드가끝날 때마다①과 ③ 은 우리가 짜는 코드가 아닙니다. checkpointer를 설정하면 그래프가 알아서 합니다.
불러오기 → 노드 실행 → 다시 저장하기. ①과 ③ 은 우리가 짜는 코드가 아니다

꺼내기는 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가 하는 것과 사람이 정하는 것 — 라이브러리는 저장과 복원을 대신합니다. 무엇을 얼마나 기억할지는 여전히 설계입니다. 너무 적게 남기면 "그거"가 안 통하고, 너무 많이 남기면 비용이 늡니다.


스스로 해 보기

  1. get_state() 로 상태를 찍어 보고, 말을 한 번 더 건 뒤 다시 찍어 보세요. 몇 개씩 늘어나나요?
  2. checkpointer=InMemorySaver()빼고 컴파일해 보세요.
    python chatbot = graph.compile()
    [1] 의 결과가 어떻게 달라지나요?
  3. 위의 최근 10개만 보내기를 넣고, 짧은 대화를 여러 번 이어 가 보세요. 몇 번째부터 앞말을 잊나요?
  4. 같은 thread_id프로그램을 두 번 실행해 보세요. 첫 실행의 대화를 기억하나요? 왜 그럴까요?

4번이 InMemorySaver 의 한계를 몸으로 확인하는 실험입니다.


핵심 정리

  • compile(checkpointer=...) 한 줄이 2장의 history.append(...) 를 대신합니다.
  • 우리는 이번 말 하나만 넣고, 체크포인터가 앞의 것을 붙여 줍니다.
  • InMemorySaver 는 메모리에 저장합니다. 설정이 없어 편하지만 프로그램을 끄면 사라집니다.
  • 저장 위치를 바꾸려면 이 한 줄만 바꿉니다 — SqliteSaver, PostgresSaver. 나머지 코드는 그대로입니다.
  • get_state(config).values 로 쌓인 상태를 직접 볼 수 있습니다.
  • 저장은 노드가 끝날 때마다 일어납니다. 그래서 "체크포인트"입니다.
  • 길이 관리는 여전히 우리 일입니다. 오래된 것을 자르거나 요약하는 것은 사람이 정합니다.
← 이전 절03. 따라하기 상태를 정의하고 노드로 잇기다음 절 →05. 따라하기 CSV 분석 에이전트에 기억 붙이기