본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 1. · 14 Views
LangGraph 단기 메모리 완벽 가이드
LangGraph에서 대화 상태를 유지하고 관리하는 단기 메모리 시스템을 다룹니다. Checkpointer의 개념부터 프로덕션 환경의 PostgresSaver 설정, 그리고 효율적인 메시지 관리 전략까지 실무에 필요한 모든 내용을 담았습니다.
목차
- Checkpointer 개념 이해
- InMemorySaver 사용하기
- thread_id로 대화 세션 관리
- PostgresSaver 프로덕션 설정
- 메시지 트리밍 전략
- 메시지 요약 패턴
1. Checkpointer 개념 이해
어느 날 김개발 씨가 LangGraph로 챗봇을 만들다가 이상한 현상을 발견했습니다. 분명히 방금 전에 이름을 알려줬는데, 챗봇이 "이름이 뭐예요?"라고 다시 묻는 것이었습니다.
대화 내용을 전혀 기억하지 못하는 챗봇, 어떻게 해결할 수 있을까요?
Checkpointer는 한마디로 그래프 실행 중간의 상태를 저장하고 복원하는 장치입니다. 마치 게임의 세이브 포인트와 같습니다.
각 노드가 실행될 때마다 상태를 저장해두면, 나중에 그 지점부터 다시 시작하거나 이전 대화 맥락을 이어갈 수 있습니다.
다음 코드를 살펴봅시다.
from langgraph.graph import StateGraph, MessagesState
from langgraph.checkpoint.memory import MemorySaver
# Checkpointer 생성 - 상태 저장의 핵심
checkpointer = MemorySaver()
# 그래프 정의
graph_builder = StateGraph(MessagesState)
# 노드 추가 (챗봇 로직)
graph_builder.add_node("chatbot", chatbot_node)
graph_builder.set_entry_point("chatbot")
graph_builder.set_finish_point("chatbot")
# 핵심: checkpointer를 연결하여 상태 저장 활성화
graph = graph_builder.compile(checkpointer=checkpointer)
김개발 씨는 입사 6개월 차 주니어 개발자입니다. 최근 회사에서 LangGraph를 활용한 AI 챗봇 프로젝트를 맡게 되었습니다.
열심히 기본 구조를 만들었는데, 테스트를 해보니 챗봇이 대화 내용을 전혀 기억하지 못했습니다. "안녕하세요, 저는 김개발입니다."라고 인사했는데, 바로 다음 질문에서 "성함이 어떻게 되시나요?"라고 되묻는 것이었습니다.
마치 금붕어처럼 3초 전 기억도 없는 셈이었습니다. 선배 개발자 박시니어 씨가 다가와 코드를 살펴봅니다.
"아, Checkpointer를 연결하지 않았네요. 그래서 매번 새로운 대화처럼 시작하는 거예요." 그렇다면 Checkpointer란 정확히 무엇일까요?
쉽게 비유하자면, Checkpointer는 마치 비서가 회의록을 작성하는 것과 같습니다. 회의 중간중간 중요한 내용을 기록해두면, 나중에 "아까 뭐라고 했더라?"라고 물어볼 때 바로 찾아볼 수 있습니다.
Checkpointer도 마찬가지로 그래프의 각 단계가 실행될 때마다 상태를 기록해둡니다. LangGraph에서 그래프는 여러 노드로 구성됩니다.
사용자 입력을 받는 노드, AI가 응답을 생성하는 노드, 도구를 호출하는 노드 등이 순차적으로 또는 조건에 따라 실행됩니다. Checkpointer가 없으면 각 실행이 독립적으로 이루어져서 이전 대화 내용을 알 수 없습니다.
Checkpointer를 연결하면 상황이 달라집니다. 각 노드가 실행될 때마다 현재 상태가 자동으로 저장됩니다.
여기서 상태란 대화 메시지 목록, 사용자 정보, 중간 계산 결과 등 그래프가 관리하는 모든 데이터를 의미합니다. 위의 코드를 살펴보겠습니다.
먼저 MemorySaver를 임포트하고 인스턴스를 생성합니다. 이것이 가장 기본적인 Checkpointer입니다.
그 다음 일반적인 방식으로 그래프를 정의합니다. 핵심은 마지막 줄입니다.
compile 메서드에 checkpointer 인자를 전달하면 됩니다. 이렇게 간단한 한 줄 추가로 그래프는 모든 실행 상태를 기억하게 됩니다.
실제 현업에서 Checkpointer는 다양하게 활용됩니다. 가장 기본적으로는 대화 맥락 유지에 사용합니다.
또한 에러가 발생했을 때 마지막 정상 상태로 복구하거나, 특정 시점으로 되돌아가 다른 선택지를 시도해볼 수도 있습니다. 하지만 Checkpointer만 연결한다고 끝이 아닙니다.
어떤 대화인지 구분할 수 있는 식별자가 필요합니다. 이것이 바로 다음에 배울 thread_id입니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 조언대로 Checkpointer를 연결하니, 드디어 챗봇이 대화를 기억하기 시작했습니다.
"오, 진짜 기억하네요!"라며 김개발 씨는 감탄했습니다.
실전 팁
💡 - Checkpointer는 compile 시점에 한 번만 연결하면 이후 모든 실행에 자동 적용됩니다
- 개발 단계에서는 MemorySaver로 빠르게 테스트하고, 프로덕션에서는 영구 저장소를 사용하세요
2. InMemorySaver 사용하기
김개발 씨가 Checkpointer의 개념을 이해하고 나니, 이제 실제로 적용해볼 차례입니다. 가장 먼저 접하게 되는 것이 InMemorySaver인데, 이름 그대로 메모리에 상태를 저장합니다.
빠르고 간편하지만, 그만큼 주의할 점도 있습니다.
InMemorySaver는 파이썬 딕셔너리를 사용하여 상태를 메모리에 저장하는 가장 간단한 Checkpointer입니다. 별도의 데이터베이스 설정 없이 즉시 사용할 수 있어서 개발과 테스트에 이상적입니다.
다만 프로그램이 종료되면 모든 데이터가 사라진다는 점을 기억해야 합니다.
다음 코드를 살펴봅시다.
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import StateGraph, MessagesState
# InMemorySaver 인스턴스 생성
memory = MemorySaver()
def chatbot_node(state: MessagesState):
# 상태에서 메시지 히스토리 접근
messages = state["messages"]
response = llm.invoke(messages)
return {"messages": [response]}
# 그래프 빌드
builder = StateGraph(MessagesState)
builder.add_node("chatbot", chatbot_node)
builder.set_entry_point("chatbot")
graph = builder.compile(checkpointer=memory)
# 대화 실행 - config에 thread_id 필수
config = {"configurable": {"thread_id": "user-123"}}
result = graph.invoke({"messages": [("user", "안녕하세요")]}, config)
박시니어 씨가 김개발 씨에게 물었습니다. "일단 로컬에서 테스트해볼 건데, 어떤 Checkpointer를 쓰면 좋을까요?" 김개발 씨는 문서를 뒤적이다가 MemorySaver를 발견했습니다.
"이거 괜찮아 보이는데요? 설정도 간단하고요." 박시니어 씨가 고개를 끄덕였습니다.
"맞아요. 개발할 때는 InMemorySaver가 제일 편해요.
데이터베이스 연결하고 테이블 만들고 할 필요가 없으니까요." InMemorySaver는 마치 책상 위 메모지와 같습니다. 필요할 때 빠르게 적고 바로 읽을 수 있습니다.
하지만 퇴근할 때 메모지를 버리면 다음 날 아무것도 남아있지 않죠. InMemorySaver도 프로그램이 종료되면 저장된 모든 상태가 사라집니다.
사용법은 매우 간단합니다. MemorySaver 클래스를 임포트하고 인스턴스를 만들면 됩니다.
그게 전부입니다. 데이터베이스 URL도, 연결 설정도 필요 없습니다.
위 코드에서 주목할 부분은 config 딕셔너리입니다. Checkpointer를 사용할 때는 반드시 thread_id를 지정해야 합니다.
이 ID가 있어야 "이 대화는 여기에 저장하고, 저 대화는 저기에 저장"하는 식으로 구분할 수 있습니다. MessagesState는 LangGraph가 제공하는 기본 상태 스키마입니다.
내부에 messages라는 리스트를 가지고 있어서 대화 히스토리를 자동으로 관리해줍니다. 새 메시지를 추가하면 기존 메시지 뒤에 자동으로 붙습니다.
chatbot_node 함수를 보면 **state["messages"]**로 지금까지의 대화 내용에 접근합니다. LLM에게 전체 대화 히스토리를 전달하면, AI는 맥락을 이해하고 적절한 응답을 생성합니다.
InMemorySaver가 적합한 상황은 명확합니다. 로컬 개발 환경에서 빠르게 기능을 테스트할 때, 단위 테스트를 작성할 때, 또는 임시로 프로토타입을 만들 때 사용하면 됩니다.
서버가 재시작되어도 대화가 유지될 필요가 없는 상황이죠. 반면 프로덕션 환경에서는 적합하지 않습니다.
서버가 여러 대인 경우 각 서버의 메모리가 분리되어 있어서 데이터를 공유할 수 없습니다. 또한 서버가 재시작되면 모든 대화 기록이 날아갑니다.
김개발 씨가 궁금해했습니다. "그럼 실제 서비스에서는 뭘 써야 해요?" "그건 조금 있다가 PostgresSaver를 배울 때 알려줄게요.
일단 지금은 InMemorySaver로 기본기를 익히는 게 중요해요."
실전 팁
💡 - InMemorySaver는 테스트 코드에서 특히 유용합니다. 매 테스트마다 새로운 인스턴스를 만들면 깨끗한 상태로 시작합니다
- 메모리 사용량에 주의하세요. 대화가 많아지면 메모리가 계속 증가합니다
3. thread id로 대화 세션 관리
김개발 씨가 만든 챗봇이 잘 작동하는 것 같았습니다. 그런데 문제가 생겼습니다.
A 사용자와 대화하던 중 B 사용자가 접속했더니, B 사용자에게 A 사용자의 대화 내용이 보이는 것이었습니다. 여러 사용자의 대화를 어떻게 분리할 수 있을까요?
thread_id는 각 대화 세션을 고유하게 식별하는 키입니다. 마치 호텔 객실 번호처럼, 같은 thread_id를 사용하면 같은 대화방에 들어가고, 다른 thread_id를 사용하면 완전히 새로운 대화방이 됩니다.
Checkpointer는 이 thread_id를 기준으로 상태를 저장하고 불러옵니다.
다음 코드를 살펴봅시다.
from langgraph.checkpoint.memory import MemorySaver
memory = MemorySaver()
graph = builder.compile(checkpointer=memory)
# 사용자별 대화 세션 분리
user_a_config = {"configurable": {"thread_id": "user-a-session"}}
user_b_config = {"configurable": {"thread_id": "user-b-session"}}
# User A의 대화
graph.invoke({"messages": [("user", "내 이름은 철수야")]}, user_a_config)
graph.invoke({"messages": [("user", "내 이름이 뭐였지?")]}, user_a_config)
# User B의 대화 - 완전히 독립적
graph.invoke({"messages": [("user", "안녕하세요")]}, user_b_config)
# 상태 조회
state_a = graph.get_state(user_a_config)
print(state_a.values["messages"]) # 철수 관련 대화만 출력
김개발 씨는 고민에 빠졌습니다. 혼자 테스트할 때는 문제가 없었는데, 팀원들과 함께 테스트하니 대화가 뒤섞이는 것이었습니다.
박시니어 씨에게 도움을 요청했습니다. "thread_id를 어떻게 설정하고 있어요?" 김개발 씨가 코드를 보여줬습니다.
모든 요청에 같은 thread_id를 사용하고 있었습니다. 박시니어 씨가 웃으며 말했습니다.
"이러면 모든 사람이 같은 대화방에 들어가는 거예요. 호텔에서 손님마다 다른 객실을 배정하듯이, 사용자마다 다른 thread_id를 줘야 해요." thread_id의 역할을 정확히 이해해봅시다.
Checkpointer는 데이터를 저장할 때 thread_id를 키로 사용합니다. 마치 사물함에 물건을 넣을 때 번호표를 받는 것과 같습니다.
나중에 같은 번호표를 가져오면 내 물건을 찾을 수 있고, 다른 번호표를 가져오면 다른 사람의 사물함이 열리겠죠. config 딕셔너리는 configurable이라는 중첩 구조를 사용합니다.
이 안에 thread_id를 넣어야 합니다. 이 구조는 LangGraph의 규칙이므로 반드시 따라야 합니다.
thread_id는 어떤 문자열이든 될 수 있습니다. 실무에서는 보통 사용자 ID와 세션 ID를 조합하여 사용합니다.
예를 들어 "user-123-session-456" 같은 형식입니다. 이렇게 하면 같은 사용자라도 새 대화를 시작할 때 새로운 세션을 만들 수 있습니다.
위 코드에서 get_state 메서드를 주목해주세요. 이 메서드로 특정 thread_id의 현재 상태를 조회할 수 있습니다.
디버깅할 때 매우 유용합니다. 대화가 제대로 저장되고 있는지, 어떤 메시지들이 쌓여있는지 확인할 수 있습니다.
thread_id 설계 시 고려할 점이 있습니다. 너무 단순하면 충돌이 발생할 수 있고, 너무 복잡하면 관리가 어려워집니다.
UUID를 사용하면 충돌 걱정 없이 고유성을 보장할 수 있습니다. 또한 thread_id를 데이터베이스에 저장해두면 사용자가 나중에 이전 대화를 이어갈 수 있습니다.
웹 브라우저의 로컬 스토리지나 쿠키에 저장해두는 것도 방법입니다. 김개발 씨가 사용자별로 다른 thread_id를 부여하니, 드디어 대화가 깔끔하게 분리되었습니다.
"이제 서로 대화가 안 섞이네요!" 박시니어 씨가 덧붙였습니다. "잘했어요.
그런데 지금은 메모리에 저장하고 있으니까, 서버 재시작하면 다 날아가요. 프로덕션에서는 PostgresSaver를 써야 해요."
실전 팁
💡 - thread_id는 URL이나 쿠키에 저장하여 사용자가 브라우저를 닫았다 열어도 대화를 이어갈 수 있게 하세요
- 보안이 중요한 경우 thread_id에 민감한 정보를 직접 넣지 말고, 서버에서 매핑 테이블을 관리하세요
4. PostgresSaver 프로덕션 설정
드디어 김개발 씨의 챗봇이 베타 서비스를 앞두고 있습니다. 그런데 운영팀에서 연락이 왔습니다.
"서버 점검 때문에 재시작하면 대화 기록이 다 날아가는 거 아니에요?" InMemorySaver의 한계를 넘어서, 영구적으로 데이터를 저장할 방법이 필요합니다.
PostgresSaver는 PostgreSQL 데이터베이스에 상태를 저장하는 프로덕션급 Checkpointer입니다. 서버가 재시작되어도 데이터가 유지되고, 여러 서버가 동일한 데이터베이스를 공유할 수 있습니다.
실제 서비스를 운영할 때 반드시 필요한 구성입니다.
다음 코드를 살펴봅시다.
from langgraph.checkpoint.postgres import PostgresSaver
import psycopg
# 데이터베이스 연결 문자열
DB_URI = "postgresql://user:password@localhost:5432/chatbot_db"
# 동기 방식 연결
with psycopg.connect(DB_URI) as conn:
# 필요한 테이블 자동 생성
checkpointer = PostgresSaver(conn)
checkpointer.setup() # 최초 1회 실행
graph = builder.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "prod-user-123"}}
result = graph.invoke({"messages": [("user", "안녕하세요")]}, config)
# 비동기 방식 (권장)
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
import psycopg_pool
pool = psycopg_pool.AsyncConnectionPool(DB_URI)
checkpointer = AsyncPostgresSaver(pool)
김개발 씨는 긴장되기 시작했습니다. 지금까지는 혼자 개발하면서 테스트했는데, 이제 실제 사용자들이 접속할 예정입니다.
InMemorySaver로는 절대 안 된다는 것을 알고 있었습니다. 박시니어 씨가 차근차근 설명을 시작했습니다.
"프로덕션에서는 PostgresSaver를 써야 해요. 세 가지 이유가 있어요." 첫 번째는 영속성입니다.
메모리와 달리 데이터베이스에 저장하면 서버가 재시작되어도 데이터가 남아있습니다. 사용자가 어제 나눈 대화를 오늘 이어갈 수 있습니다.
두 번째는 확장성입니다. 서버가 한 대일 때는 문제가 없지만, 사용자가 늘어나면 서버를 여러 대 운영해야 합니다.
메모리는 서버마다 분리되어 있지만, 데이터베이스는 모든 서버가 공유할 수 있습니다. 세 번째는 안정성입니다.
PostgreSQL은 수십 년간 검증된 데이터베이스입니다. 트랜잭션, 백업, 복구 등 엔터프라이즈급 기능을 모두 갖추고 있습니다.
코드를 살펴보겠습니다. 먼저 psycopg 라이브러리로 데이터베이스에 연결합니다.
psycopg는 PostgreSQL의 공식 파이썬 드라이버입니다. 연결 문자열에는 사용자 이름, 비밀번호, 호스트, 포트, 데이터베이스 이름이 포함됩니다.
setup 메서드가 중요합니다. 이 메서드는 Checkpointer가 필요로 하는 테이블을 자동으로 생성합니다.
최초 한 번만 실행하면 되고, 이미 테이블이 있으면 아무 일도 하지 않습니다. 실제 서비스에서는 비동기 방식을 권장합니다.
동기 방식은 데이터베이스 요청이 끝날 때까지 다른 요청을 처리하지 못합니다. 비동기 방식을 사용하면 대기 시간 동안 다른 요청을 처리할 수 있어서 성능이 크게 향상됩니다.
커넥션 풀도 필수입니다. 매 요청마다 새로운 연결을 만들면 오버헤드가 큽니다.
풀을 사용하면 미리 만들어둔 연결을 재사용하여 성능을 높일 수 있습니다. 환경 변수로 연결 정보를 관리하는 것이 좋습니다.
코드에 비밀번호를 직접 쓰면 보안 문제가 발생할 수 있습니다. 또한 개발, 스테이징, 프로덕션 환경마다 다른 데이터베이스를 사용해야 하므로 환경 변수가 편리합니다.
김개발 씨가 PostgresSaver로 전환하고 테스트를 마쳤습니다. "이제 서버 재시작해도 대화가 살아있네요!" 박시니어 씨가 마지막으로 당부했습니다.
"잘했어요. 그런데 대화가 계속 쌓이면 데이터베이스도 커지고 응답도 느려져요.
다음에는 메시지 트리밍에 대해 알아봐요."
실전 팁
💡 - 프로덕션에서는 반드시 커넥션 풀을 사용하세요. 풀 크기는 서버 스펙과 동시 접속자 수를 고려하여 설정합니다
- 데이터베이스 연결 정보는 환경 변수나 시크릿 매니저로 관리하세요
5. 메시지 트리밍 전략
서비스 런칭 후 한 달이 지났습니다. 김개발 씨는 이상한 현상을 발견했습니다.
초반에는 빠르게 응답하던 챗봇이 점점 느려지고 있었습니다. 데이터베이스를 확인해보니, 한 사용자와의 대화가 500개가 넘는 메시지를 가지고 있었습니다.
어떻게 해결할 수 있을까요?
메시지 트리밍은 대화 히스토리에서 오래된 메시지를 제거하여 컨텍스트 크기를 관리하는 전략입니다. LLM은 처리할 수 있는 토큰 수에 제한이 있고, 토큰이 많을수록 비용과 응답 시간이 증가합니다.
트리밍을 통해 최근의 관련성 높은 메시지만 유지할 수 있습니다.
다음 코드를 살펴봅시다.
from langgraph.graph import MessagesState
from langchain_core.messages import trim_messages, SystemMessage
# 트리밍 함수 정의
def trim_conversation(messages):
# 시스템 메시지는 항상 유지
system_message = None
if messages and isinstance(messages[0], SystemMessage):
system_message = messages[0]
messages = messages[1:]
# 최근 10개 메시지만 유지
trimmed = trim_messages(
messages,
max_tokens=4000,
strategy="last",
token_counter=len, # 실제로는 tiktoken 사용
allow_partial=False,
)
if system_message:
return [system_message] + trimmed
return trimmed
def chatbot_node(state: MessagesState):
trimmed = trim_conversation(state["messages"])
response = llm.invoke(trimmed)
return {"messages": [response]}
박시니어 씨가 모니터링 대시보드를 보며 말했습니다. "API 비용이 예상보다 세 배나 나왔어요.
원인을 찾아봐야겠어요." 조사 결과, 문제는 대화 히스토리였습니다. 열성적인 사용자 한 명이 매일 챗봇과 대화하면서 메시지가 수백 개로 불어났습니다.
매 요청마다 500개의 메시지를 LLM에 보내고 있었던 것입니다. "LLM은 보내는 토큰 수에 따라 과금돼요.
500개 메시지면 수만 토큰이에요. 게다가 대부분의 LLM은 컨텍스트 윈도우 제한도 있어요." 해결책은 메시지 트리밍입니다.
마치 채팅 앱에서 오래된 메시지는 '더 보기'를 눌러야 보이는 것처럼, 최근 메시지만 LLM에게 전달하는 것입니다. LangChain은 trim_messages 유틸리티를 제공합니다.
이 함수는 다양한 전략으로 메시지를 자를 수 있습니다. **strategy="last"**는 가장 최근 메시지부터 유지합니다.
반대로 **strategy="first"**를 쓰면 초반 메시지를 유지합니다. max_tokens 파라미터가 핵심입니다.
이 값을 초과하지 않도록 메시지를 잘라냅니다. GPT-4의 경우 컨텍스트 윈도우가 128k 토큰이지만, 비용과 속도를 고려하면 4000~8000 토큰 정도가 적당합니다.
token_counter는 토큰 수를 계산하는 함수입니다. 예제에서는 간단히 len을 사용했지만, 실제로는 tiktoken 라이브러리로 정확하게 계산해야 합니다.
모델마다 토크나이저가 다르기 때문입니다. 주의할 점이 있습니다.
시스템 메시지는 항상 유지해야 합니다. 시스템 메시지에는 챗봇의 역할과 규칙이 정의되어 있는데, 이것이 잘리면 챗봇의 성격이 변할 수 있습니다.
코드에서 시스템 메시지를 먼저 분리하고 트리밍 후 다시 붙이는 이유입니다. 트리밍 전략은 서비스 특성에 따라 다릅니다.
고객 상담 챗봇이라면 최근 대화가 중요하므로 last 전략이 적합합니다. 반면 튜토리얼 챗봇이라면 처음 설명한 내용이 중요할 수 있습니다.
또 다른 고려사항은 allow_partial입니다. False로 설정하면 메시지 중간에서 자르지 않습니다.
대화의 맥락이 끊기는 것을 방지할 수 있습니다. 김개발 씨가 트리밍을 적용한 후, API 비용이 절반으로 줄었습니다.
"이렇게 간단한 방법이 있었네요!" 박시니어 씨가 덧붙였습니다. "좋아요.
그런데 정말 긴 대화에서는 트리밍만으로 부족할 수 있어요. 중요한 내용이 잘려나가면 맥락을 잃거든요.
다음에는 메시지 요약 패턴을 알아봐요."
실전 팁
💡 - tiktoken 라이브러리를 사용하여 정확한 토큰 수를 계산하세요. 대략적인 계산은 예산 초과로 이어질 수 있습니다
- 트리밍 후에도 대화의 맥락이 유지되는지 테스트하세요. 너무 공격적으로 자르면 챗봇이 엉뚱한 답변을 할 수 있습니다
6. 메시지 요약 패턴
트리밍으로 비용 문제는 해결했지만, 새로운 문제가 생겼습니다. 사용자가 "아까 추천해준 책 제목이 뭐였죠?"라고 물었는데, 그 대화가 이미 잘려나가서 챗봇이 대답하지 못한 것입니다.
중요한 정보는 유지하면서 컨텍스트를 줄이는 방법이 있을까요?
메시지 요약 패턴은 오래된 대화를 삭제하는 대신 요약본으로 압축하는 전략입니다. LLM을 사용하여 이전 대화의 핵심 내용을 짧은 요약문으로 만들고, 이를 새 대화의 앞부분에 배치합니다.
이렇게 하면 토큰은 절약하면서도 중요한 맥락을 유지할 수 있습니다.
다음 코드를 살펴봅시다.
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage
async def summarize_messages(messages, llm):
"""이전 대화를 요약합니다"""
if len(messages) < 10:
return None
# 요약할 메시지 선택 (최근 5개 제외)
to_summarize = messages[:-5]
recent = messages[-5:]
summary_prompt = """다음 대화의 핵심 내용을 3문장으로 요약하세요.
사용자의 이름, 선호도, 중요한 결정 사항을 반드시 포함하세요.
대화:
{conversation}"""
conversation = "\n".join([f"{m.type}: {m.content}" for m in to_summarize])
summary = await llm.ainvoke(summary_prompt.format(conversation=conversation))
# 요약을 시스템 메시지로 변환
summary_message = SystemMessage(
content=f"[이전 대화 요약]\n{summary.content}"
)
return [summary_message] + recent
def chatbot_with_summary(state: MessagesState):
messages = state["messages"]
# 메시지가 많으면 요약 적용
processed = summarize_messages(messages, llm) or messages
response = llm.invoke(processed)
return {"messages": [response]}
김개발 씨는 당황했습니다. 사용자가 불만을 토로하고 있었습니다.
"분명히 아까 말했잖아요. 왜 기억을 못 하는 거예요?" 트리밍이 너무 공격적이었던 것입니다.
10분 전 대화가 이미 잘려나가서, 챗봇은 아무것도 모르는 상태였습니다. 박시니어 씨가 해결책을 제시했습니다.
"단순히 자르지 말고, 요약을 만들어봐요. 책 한 권을 읽은 후에 핵심만 메모해두는 것처럼요." 메시지 요약 패턴의 아이디어는 간단합니다.
오래된 메시지를 버리는 대신, LLM에게 요약을 요청합니다. 100개의 메시지도 잘 요약하면 몇 문장으로 줄일 수 있습니다.
위 코드를 단계별로 살펴보겠습니다. 먼저 메시지가 일정 개수 이상인지 확인합니다.
메시지가 적으면 요약할 필요가 없습니다. 여기서는 10개를 기준으로 삼았습니다.
다음으로 메시지를 두 그룹으로 나눕니다. 최근 5개는 그대로 유지하고, 나머지는 요약 대상입니다.
최근 대화는 맥락상 중요하므로 원본을 유지합니다. 요약 프롬프트가 중요합니다.
단순히 "요약해줘"라고 하면 중요한 정보가 빠질 수 있습니다. 사용자 이름, 선호도, 결정 사항 등 반드시 포함해야 할 정보를 명시하세요.
요약 결과는 SystemMessage로 변환합니다. 시스템 메시지는 대화의 맨 앞에 위치하므로, LLM이 항상 이 맥락을 인지한 상태로 응답합니다.
실제 구현에서는 몇 가지 최적화가 필요합니다. 매 요청마다 요약을 만들면 비용이 많이 듭니다.
대신 일정 주기로 요약을 만들고 저장해두면 효율적입니다. 요약의 품질도 중요합니다.
요약이 너무 짧으면 정보가 손실되고, 너무 길면 트리밍의 의미가 없어집니다. 적절한 길이를 찾는 것이 핵심입니다.
또한 요약을 누적하는 방식도 있습니다. 이전 요약에 새 내용을 추가하여 요약을 업데이트합니다.
이렇게 하면 아주 긴 대화에서도 핵심 맥락을 유지할 수 있습니다. 김개발 씨가 요약 패턴을 적용한 후, 사용자 불만이 크게 줄었습니다.
"아, 아까 말씀하신 그 책이요. '클린 코드'였죠?"라고 챗봇이 대답하자, 사용자도 만족했습니다.
박시니어 씨가 마무리했습니다. "이제 기본적인 메모리 관리는 마스터했어요.
트리밍과 요약을 상황에 맞게 조합하면 대부분의 경우를 커버할 수 있어요." 김개발 씨는 뿌듯했습니다. Checkpointer의 개념부터 시작해서 프로덕션 설정, 그리고 효율적인 메모리 관리까지.
LangGraph의 단기 메모리 시스템을 완전히 이해하게 된 것입니다.
실전 팁
💡 - 요약 주기를 적절히 설정하세요. 너무 자주 요약하면 비용이 증가하고, 너무 드물면 컨텍스트가 커집니다
- 중요한 정보(사용자 이름, 선호도, 결정 사항)는 요약 프롬프트에 명시적으로 포함하도록 지시하세요
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
Helm 마이크로서비스 패키징 완벽 가이드
Kubernetes 환경에서 마이크로서비스를 효율적으로 패키징하고 배포하는 Helm의 핵심 기능을 실무 중심으로 학습합니다. Chart 생성부터 릴리스 관리까지 체계적으로 다룹니다.
보안 아키텍처 구성 완벽 가이드
프로젝트의 보안을 처음부터 설계하는 방법을 배웁니다. AWS 환경에서 VPC부터 WAF, 암호화, 접근 제어까지 실무에서 바로 적용할 수 있는 보안 아키텍처를 단계별로 구성해봅니다.
AWS Organizations 완벽 가이드
여러 AWS 계정을 체계적으로 관리하고 통합 결제와 보안 정책을 적용하는 방법을 실무 스토리로 쉽게 배워봅니다. 초보 개발자도 바로 이해할 수 있는 친절한 설명과 실전 예제를 제공합니다.
AWS KMS 암호화 완벽 가이드
AWS KMS(Key Management Service)를 활용한 클라우드 데이터 암호화 방법을 초급 개발자를 위해 쉽게 설명합니다. CMK 생성부터 S3, EBS 암호화, 봉투 암호화까지 실무에 필요한 모든 내용을 담았습니다.
AWS Secrets Manager 완벽 가이드
AWS에서 데이터베이스 비밀번호, API 키 등 민감한 정보를 안전하게 관리하는 Secrets Manager의 핵심 개념과 실무 활용법을 배워봅니다. 초급 개발자도 쉽게 따라할 수 있도록 실전 예제와 함께 설명합니다.