1. 이번 단계에서 달라지는 것
한 번 질의하면 잊어버리던 에이전트에, 대화를 이어 기억하는 단기기억을 추가함.
②편의 에이전트는 매 질의가 "새 채팅창"이었음. 이전 대화를 기억하지 못해 같은 맥락을 반복 설명해야 했음. 이번에는 Checkpointer를 붙여 대화 상태를 저장하고, thread_id로 사용자별 기억을 관리함.
- MemorySaver — 상태를 저장하는 단기기억 저장소. 현재는 RAM 기반(프로그램 종료 시 삭제)임.
- checkpointer 옵션 — compile 시 기억 공간을 그래프에 연결함.
- thread_id — 대화 세션을 구분하는 식별자. 같은 id면 기억이 누적됨.
그래프 구조 자체는 ②편과 동일하다. 바뀐 건 "상태를 저장하고 다음 호출에 다시 불러온다"는 점뿐이다.
2. 메모리 생성
`MemorySaver` 객체 하나를 만들어 두면 됨.
from langgraph.checkpoint.memory import MemorySaver # 단기기억용. 종료 시 삭제
# 메모리 생성 -> 현재는 RAM 저장. 실제 운영은 외부 벡터/영속 DB로 대체
memory = MemorySaver()
인메모리라 재시작하면 사라짐. 실서비스에서는 물리적 DB(예: Redis, Postgres 등)로 교체하는 게 일반적임.
3. 모델·도구·노드 (②편과 동일)
기억 추가만이 목적이므로 LLM·도구·판단 노드는 그대로 가져감.
from langgraph.graph import StateGraph, END, MessagesState, START
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage
from langchain_aws import ChatBedrockConverse
from langgraph.prebuilt import ToolNode, tools_condition
from dotenv import load_dotenv
import os, boto3
load_dotenv()
llm = ChatBedrockConverse(
model=os.getenv('MODEL_ID'),
client=boto3.client('bedrock-runtime', region_name=os.getenv('AWS_REGION'))
)
@tool
def multiply(a: int, b: int) -> int:
'''두 수를 곱한 후 반환'''
print(f' [TOOL 실행] {a}x{b} 계산중..')
return a * b
tools = [multiply]
llm_with_tools = llm.bind_tools(tools)
def chatbot_node(state: MessagesState):
res = llm_with_tools.invoke(state['messages'])
return {"messages": [res]}
핵심 로직은 변하지 않았음. 기억은 그래프 외부(체크포인터)에서 관리되기 때문임.
4. 그래프 구성 — 컴파일에 checkpointer 연결
`compile(checkpointer=memory)` 한 줄이 단기기억을 켜는 스위치임.
workflow = StateGraph(MessagesState)
workflow.add_node('chatbot', chatbot_node) # 판단 노드
workflow.add_node('tools', ToolNode(tools)) # 행동 노드
workflow.add_edge(START, 'chatbot')
workflow.add_conditional_edges(
'chatbot', # 텍스트 응답 -> END
tools_condition # 도구 필요 -> tools 노드
)
workflow.add_edge('tools', 'chatbot')
# 컴파일 옵션으로 단기기억 공간 제공
app = workflow.compile(checkpointer=memory)
`checkpointer`가 붙으면, 그래프는 매 실행 후 상태를 저장하고 다음 호출 때 같은 `thread_id`의 상태를 자동으로 복원함.
5. 실행 — thread_id로 세션 고정
`config`에 `thread_id`를 담아 넘기면 대화가 누적됨.
if __name__ == '__main__':
# 사용자별 기억 관리. 여기서는 "user-1"로 고정
config = {"configurable": {"thread_id": "user-1"}}
while True:
user_input = input('\n유저: ').lower()
if user_input == 'q':
break
prompt = {"messages": [HumanMessage(content=user_input)]}
# 실행 시 config를 함께 전달해야 기억이 이어짐
for evt in app.stream(prompt, stream_mode='values', config=config):
msg = evt['messages'][-1]
print("Agent", msg.content)
`config` 없이 호출하면 체크포인터가 어떤 세션인지 알 수 없어 기억이 이어지지 않음.
6. 동작 확인
이전 질의를 기억하는지 3턴에 걸쳐 확인함.
- 1차 — 100 곱하기 2는 → 200
- 2차 — 100 곱하기 3는 → 300
- 3차 — 그간 물어봤던 질문들을 간단하게 정리해줘
3차 응답에서 에이전트가 앞선 두 질문을 스스로 정리해 냈음.
Agent 지금까지 물어보신 질문들을 정리하면 다음과 같습니다:
1. 100 곱하기 2는? → 답: 200
2. 100 곱하기 3은? → 답: 300
두 번 모두 곱셈 계산을 요청하셨습니다.
질문을 거듭할수록 이전 대화가 상태에 누적되어 프롬프트로 함께 전달된다. 그래서 "정리해줘" 같은 맥락 의존 질의가 가능해진다.
요약
그래프 로직은 그대로 둔 채 체크포인터만 추가해, 맥락을 기억하는 멀티턴 에이전트로 발전시켰음.
| 항목 | LangGraph 2 | LangGraph 3 |
| 대화 기억 | 없음(매번 초기화) | 있음(누적) |
| 추가 요소 | — | MemorySaver + thread_id |
| 변경 지점 | — | compile(checkpointer=...), config 전달 |
| 저장 위치 | — | RAM(향후 외부 DB로 확장) |
이제 에이전트는 "기억"한다. 다음 단계에서는 LLM이 모르는 외부·사내 데이터를 RAG로 끌어와 할루시네이션을 막는다.
'SK플래닛 ai활용 데이터엔지니어 과정 2기 > ML & DL' 카테고리의 다른 글
| LangGraph 5 - 멀티 에이전트 협업 (0) | 2026.06.04 |
|---|---|
| LangGraph 4 - RAG 에이전트 (0) | 2026.06.02 |
| LangGraph 2 - Tool + LLM (0) | 2026.06.02 |
| LangGraph 1 - 상태 그래프 기초 (0) | 2026.06.02 |
| LLM 2 - 서비스 구축 실습 (0) | 2026.05.29 |