LangChain 메모리
LangChain에서 메모리는 이전 대화 내용을 기억하여 대화의 문맥을 유지하는 역할을 합니다. 0.3.x 버전부터는 LangChain Expression Language (LCEL) 기반으로 체인을 구성하고, ConversationChain이나 ConversationBufferMemory 등의 레거시 클래스는 사용하지 않는 방향으로 바뀌었습니다. 대신 RunnableWithMessageHistory, ChatMessageHistory 등의 컴포넌트를 활용하여 세션별 대화 기록을 관리하는 최신 메모리 구조를 사용합니다. 또한 대화가 장기화될 경우 요약 메모리를 도입하여 과거 대화를 LLM으로 요약하고 축약된 형태로 저장함으로써 프롬프트 길이 문제를 해결할 수 있습니다. 이번 자료에서는 LCEL 기반 체인 구성과 메모리 관리, 세션별 대화 기록 유지, Redis를 이용한 영속적 메모리, 요약 메모리 구현까지 이론과 코드 실습을 통해 학습해보겠습니다.
LCEL 기반 체인 구성과 새로운 메모리 시스템
**LangChain Expression Language (LCEL)**는 체인, 프롬프트, LLM 등을 파이프라인으로 연결하여 구성하는 새로운 문법입니다. 예를 들어 prompt | llm | parser 형태로 | 연산자를 통해 간단히 체인을 연결할 수 있습니다. 이러한 LCEL 체인은 LangChain 0.3 버전대에서 도입되었으며, 과거에 ConversationChain 등으로 메모리를 관리하던 방식에서 벗어나 보다 유연하게 컴포넌트를 조립할 수 있게 해줍니다.
과거에는 ConversationChain + ConversationBufferMemory 등을 사용하여 대화형 체인을 만들었지만, 0.2.7 버전부터 ConversationChain은 폐기(deprecated) 되었고 1.0 에서 제거될 예정입니다. 대신 **RunnableWithMessageHistory**라는 래퍼를 사용하여 체인에 대화 메모리를 주입하는 것이 권장됩니다. RunnableWithMessageHistory를 활용하면 다음과 같은 이점이 있습니다:
- 스트리밍, 배치, 비동기 등 다양한 호출 방식 지원 (출력 스트림 처리 등 가능)
- 메모리를 체인 외부에서 관리할 수 있는 유연성 (여러 세션 관리에 용이)
- 멀티스레드 안전성 및 동시성 지원으로 다중 사용자 환경에 대응
즉, 체인의 실행 전후로 메시지 기록을 자동으로 불러오고 저장해 주기 때문에, 개발자는 메모리 관리 로직을 직접 구현할 부담을 크게 덜 수 있습니다.
LCEL 체인 구성은 먼저 프롬프트와 LLM을 정의하고 이를 파이프로 연결하는 방식으로 이뤄집니다. 여기에 메모리를 통합하기 위해 RunnableWithMessageHistory로 한 번 더 감싸게 됩니다. 전체적인 흐름은 다음과 같습니다:
RunnableWithMessageHistory의 내부 동작 개요. 기존 체인 (runnable)과 대화 기록(BaseChatMessageHistory)을 연결하여, 체인 실행 전에 과거 메시지를 프롬프트에 주입하고 실행 후에는 새 메시지를 기록한다.
- 프롬프트 템플릿 설정: 대화의 시스템 지침, 대화 기록 자리표시자, 사용자 입력 등을 포함하는 ChatPromptTemplate 작성
- LLM 설정: OpenAI GPT-4, GPT-3.5 등 사용할 언어 모델을 ChatOpenAI 등으로 초기화 (또는 기타 LLM)
- 체인 결합: prompt | llm | 출력파서 형태로 LLMChain 구성 (LCEL 문법 사용)
- 메모리 통합: 위 체인을 RunnableWithMessageHistory로 감싸고, 세션별로 ChatMessageHistory를 반환하는 함수 (get_session_history)를 제공
이런 방식으로 구성된 체인은 입력 시 세션 ID에 해당하는 대화 내역을 자동으로 불러와 프롬프트를 구성하고, 출력 후에는 해당 대화 내역을 갱신합니다.
다음 섹션부터 이 과정을 단계별로 구현해보겠습니다.
ChatMessageHistory를 통한 대화 기록 관리
LangChain에서는 대화 기록을 추상화한 BaseChatMessageHistory 클래스가 있습니다. 이를 구현한 InMemoryChatMessageHistory, RedisChatMessageHistory 등의 구현체를 통해 메모리를 유지합니다. 기본적으로 LangChain의 메모리 구성 요소는 자체적인 **영속성(persistence)**을 내장하고 있지 않지만, chat_memory를 활용해 파일, 데이터베이스 등 외부 저장소에 기록을 남길 수 있습니다. 즉, 메모리 저장소는 크게 두 종류로 구분됩니다:
- 휘발성 메모리 (In-Memory): 세션 동안만 유효한 메모리로, 프로세스가 종료되면 사라집니다. 예) InMemoryChatMessageHistory (파이썬 객체 내 리스트 저장). 설정이 간단하고 속도가 빠르지만, 세션 종료 후 기록이 유지되지 않음. 개발/테스트나 임시 봇에 적합.
- 영속성 메모리 (Persistent): 외부 데이터베이스나 캐시를 이용해 대화 기록을 저장하므로, 어플리케이션 재시작 후에도 유지됩니다. 예) RedisChatMessageHistory, PostgresChatMessageHistory, FileChatMessageHistory 등. 멀티 프로세스/서버 환경이나 장기 세션에 적합하며, 데이터베이스 연결 설정이 필요.
LangChain 0.3 버전대에서는 BaseChatMessageHistory를 활용한 메모리 관리가 권장되며, InMemoryChatMessageHistory를 사용한 예제를 먼저 살펴보겠습니다.
InMemoryChatMessageHistory 사용 예제
간단한 예로, InMemoryChatMessageHistory 객체를 직접 사용해 사용자/AI 메시지를 추가하고 조회할 수 있습니다:
from langchain_core.chat_history import InMemoryChatMessageHistory
# 새 메모리 객체 생성
history = InMemoryChatMessageHistory()
history.add_user_message("안녕하세요, 제 이름은 철수입니다.")
history.add_ai_message("안녕하세요 철수님, 무엇을 도와드릴까요?")
# 현재까지의 대화 내용 확인
for msg in history.messages:
print(f"{msg.type}: {msg.content}")
위 코드에서는 history.messages 리스트에 HumanMessage와 AIMessage 객체가 순서대로 저장됩니다. add_user_message는 사용자의 대화 발화를, add_ai_message는 AI의 응답을 추가하는 메서드입니다. 출력 결과는 다음과 비슷하게 나타납니다:
human: 안녕하세요, 제 이름은 철수입니다.
ai: 안녕하세요 철수님, 무엇을 도와드릴까요?
이처럼 In-Memory 메모리는 파이썬 객체 내에 대화 내역을 리스트로 유지하므로, 간편하게 사용할 수 있습니다. 하지만 앞서 언급한 대로 프로세스가 재시작되면 내용이 휘발됩니다. 따라서 다중 사용자나 서버 재기동 후에도 기록이 필요한 경우, Redis와 같은 영속적 저장소를 사용하는 것이 좋습니다.
RedisChatMessageHistory 사용 예제
Redis를 사용하면 각 세션의 대화 내용을 Redis DB에 저장하여 앱을 재시작해도 기록을 복원할 수 있습니다. langchain-redis 패키지의 RedisChatMessageHistory 클래스를 통해 손쉽게 연동할 수 있습니다.
사용을 위해 먼저 Redis 서버가 실행 중이어야 하며, 접속 URL을 환경변수로 지정하거나 코드에 명시합니다. 예를 들어 Docker로 로컬 Redis를 실행하거나(redis://localhost:6379), Upstash 등의 관리형 Redis URL을 받을 수 있습니다.
아래는 Redis 기반 채팅 기록 저장소를 설정하는 코드 예시입니다:
!pip install langchain-redis redis # 패키지 설치 (처음 한 번만 실행)
import os
from langchain_redis import RedisChatMessageHistory
REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379")
session_id = "user_123"
# 세션별 Redis 기반 채팅 기록 가져오기/생성
history = RedisChatMessageHistory(session_id=session_id, redis_url=REDIS_URL)
history.add_user_message("Redis에 이 대화 기록을 저장하고 있나요?")
history.add_ai_message("네, 이 대화는 Redis에 저장됩니다. 세션이 유지되는 한 기록을 복원할 수 있어요.")
print(f"현재 Redis에 저장된 '{session_id}' 대화 수: {len(history.messages)}")
위 코드에서 RedisChatMessageHistory(session_id, redis_url)로 특정 세션의 메모리 객체를 생성하면, 내부적으로 해당 키(prefix 포함)에 연결된 Redis 리스트에 대화가 기록됩니다. add_user_message, add_ai_message 사용법은 InMemory와 동일하며, .messages를 조회하면 현재까지 Redis에 저장된 메시지 객체 목록을 돌려줍니다.
이처럼 RedisChatMessageHistory를 사용하면 여러 어플리케이션 인스턴스 간에도 대화 기록을 공유할 수 있고, 대화 기록의 **내구성(durability)**을 높일 수 있습니다. 다만, 별도의 Redis 설치/운영이 필요하므로 시스템 복잡도가 다소 증가할 수 있으며, 네트워크 지연이 약간 발생할 수 있습니다. InMemory vs Redis 선택은 애플리케이션의 요구사항에 따라 결정하면 됩니다 (속도 vs 지속성, 단일 인스턴스 vs 다중 인스턴스 등).
세션 기반 다중 사용자 메모리 구조 구현
이제 앞서 살펴본 ChatMessageHistory를 실제 RunnableWithMessageHistory에 통합하여 다중 사용자 챗봇을 구현해보겠습니다. 핵심은 session_id를 키로 하는 메모리 저장소를 만들고, 각 사용자의 대화는 고유한 session_id로 분리하는 것입니다. 이렇게 하면 여러 사용자가 동시에 챗봇을 이용해도 서로의 대화 내용이 섞이지 않고, 각 세션별로 독립적인 문맥을 유지할 수 있습니다.
우선 실습을 위한 환경을 설정합니다. OpenAI API를 이용할 것이므로, API 키를 .env 파일에 저장해 두었다고 가정합니다 (예: OPENAI_API_KEY=<YOUR_KEY>). 또한 필요한 LangChain 패키지를 설치/임포트하고 API 키를 로드합니다:
%%bash
pip install --upgrade langchain-core langchain-openai python-dotenv
python - <<'PYCODE'
import dotenv, os
dotenv.load_dotenv() # .env 파일 로드 (OPENAI_API_KEY 등)
# 환경 변수 확인 (있다면 키의 일부만 출력)
api_key = os.getenv("OPENAI_API_KEY")
print(f"OpenAI API Key Loaded: {api_key[:8] + '...' if api_key else 'NOT FOUND'}")
PYCODE
이제 프롬프트 템플릿과 LLM을 정의해보겠습니다. 한국어로 질문/응답하는 챗봇 시나리오로, 시스템 프롬프트에는 역할과 스타일을 지정하고, 과거 대화는 history 플레이스홀더로 넣습니다. LCEL 스타일로 체인을 구성하기 위해 ChatPromptTemplate.from_messages를 사용합니다:
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_openai import ChatOpenAI
# 시스템 프롬프트와 메모리 플레이스홀더를 포함한 템플릿
prompt = ChatPromptTemplate.from_messages([
("system", "당신은 뛰어난 한국어 상담 챗봇입니다. 질문에 친절하고 자세히 답변해주세요."),
MessagesPlaceholder(variable_name="history"), # 과거 대화 내용이 여기에 삽입됨
("human", "{input}") # 사용자의 현재 입력
])
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
위에서 MessagesPlaceholder("history")는 history 키로 전달된 메시지 목록을 체인 실행 시 해당 위치에 넣겠다는 뜻입니다. ("human", "{input}") 부분은 사용자의 입력 프롬프트 자리를 정의한 것입니다. 이제 prompt와 llm을 파이프라인으로 연결하여 기본 체인을 만들 수 있습니다:
# LCEL 기반 체인 (프롬프트 -> LLM -> 출력 문자열)
from langchain_core.output_parsers import StrOutputParser
chain = prompt | llm | StrOutputParser() # 최종 출력은 문자열로 파싱
다음으로, 세션별 대화 기록을 제공하는 함수와 메모리 통합 체인을 정의합니다. 앞서 만든 chain을 RunnableWithMessageHistory로 감싸고, 두 가지 중요한 파라미터를 지정해야 합니다:
- get_session_history: session_id를 받아 해당하는 BaseChatMessageHistory 객체를 돌려주는 함수
- input_messages_key: 입력 딕셔너리 중 어떤 키의 값이 대화에서 "사용자 메시지"인지 명시 (우리 예제에서는 "input"이 현재 사용자 메시지)
- history_messages_key: 입력 딕셔너리 중 어떤 키가 "메시지 기록 리스트"인지 명시 (우리 체인에서는 "history" 키에 과거 대화가 들어감)
이제 세션별 메모리 저장소 (store)를 딕셔너리로 만들고, 존재하지 않는 세션 ID가 들어오면 새로운 InMemoryChatMessageHistory를 생성하도록 get_session_history를 구현합니다:
from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
# 세션 ID -> 대화 기록 객체 매핑
store = {}
def get_session_history(session_id: str) -> InMemoryChatMessageHistory:
"""세션 ID에 대응하는 대화 기록 객체를 반환 (없으면 새로 생성)"""
if session_id not in store:
store[session_id] = InMemoryChatMessageHistory()
return store[session_id]
# 메모리를 통합한 체인 래퍼 생성
chatbot = RunnableWithMessageHistory(
chain,
get_session_history,
input_messages_key="input",
history_messages_key="history"
)
이렇게 구성된 chatbot 객체는 입출력 인터페이스가 함수와 비슷하게 동작합니다. chatbot.invoke({"input": 질문}, config={"configurable": {"session_id": 세션}}) 형태로 호출하면, 해당 세션의 history가 자동으로 로드되어 프롬프트를 생성하고, LLM이 응답을 생성한 뒤 그 응답을 다시 메모리에 저장합니다.
이제 실제로 여러 사용자가 교대로 대화하는 시나리오를 시뮬레이션해보겠습니다. 두 개의 session_id (user_a, user_b)를 사용하여 번갈아 질문을 던지고, 각기 다른 문맥이 유지되는지 확인합니다:
# 두 개의 세션을 번갈아가며 대화 시뮬레이션
sessions = ["user_a", "user_b"]
queries = [
"사과 10개를 사고 2개를 먹었어 몇 개가 남았을까?", # user_a 첫 질문
"안녕하세요. 저는 개인 재무 상담을 받고 싶어요.", # user_b 첫 질문
"남은 사과에 3개를 더하면 몇 개가 될까?", # user_a 두번째 질문 (이전 대화 맥락 있음)
"지출을 줄일 수 있는 방법이 있을까요?" # user_b 두번째 질문
]
for i, q in enumerate(queries):
session = sessions[i % 2] # 세션을 번갈아 선택
result = chatbot.invoke({"input": q}, config={"configurable": {"session_id": session}})
print(f"[{session}] 사용자: {q}")
print(f"[{session}] 챗봇: {result}\n")
위 코드에서는 user_a와 user_b가 교대로 질문을 하는 것처럼 시뮬레이션했습니다. RunnableWithMessageHistory 덕분에 각 세션별로 대화 기록이 관리되므로, user_a의 두 번째 질문에는 첫 번째 질문에 대한 답변 내용이 문맥으로 활용됩니다. 반면 user_b는 자신의 재무 상담 맥락만 참고하죠.
실제 출력 결과를 살펴보면 (모델 응답 내용은 예시입니다):
[user_a] 사용자: 사과 10개를 사고 2개를 먹었어 몇 개가 남았을까?
[user_a] 챗봇: 사과 10개를 사고 2개를 먹었다면, 남은 사과의 개수는 다음과 같이 계산할 수 있습니다.
10개 - 2개 = 8개
따라서, 사과는 8개가 남았습니다!
[user_b] 사용자: 안녕하세요. 저는 개인 재무 상담을 받고 싶어요.
[user_b] 챗봇: 안녕하세요. 재무 상담을 도와드리겠습니다. 우선 현재 어떤 재무 상황인지 알려주실 수 있나요?
[user_a] 사용자: 남은 사과에 3개를 더하면 몇 개가 될까?
[user_a] 챗봇: 남은 사과가 8개이고, 여기에 3개를 더하면 다음과 같이 계산할 수 있습니다.
8개 + 3개 = 11개
따라서, 남은 사과는 총 11개가 됩니다!
[user_b] 사용자: 지출을 줄일 수 있는 방법이 있을까요?
[user_b] 챗봇: 지출을 줄이기 위해서는 우선 소비 내역을 점검해야 합니다. 불필요한 구독이나 지출 항목을 찾아보고 예산을 세워보는 것이 좋습니다...
user_a 세션의 챗봇 답변을 보면 첫 질문 응답 내용을 기억하고 두 번째 질문에 활용하고 있음을 알 수 있습니다. 반면 user_b 세션은 재무 상담 맥락으로만 대화가 진행됩니다. 이처럼 session_id를 키로 한 메모리 구조를 사용하면 여러 사용자의 대화를 분리하고, 각자의 문맥을 유지할 수 있습니다.
Tip: RunnableWithMessageHistory를 사용할 때 session_id는 config={"configurable": {"session_id": 값}} 형태로 반드시 넘겨야 합니다. 이를 깜빡하면 어떤 세션의 기록을 불러와야 할지 몰라 에러가 발생하므로 유의하세요. 또한 RunnableWithMessageHistory는 내부적으로 메시지 추가/읽기 작업을 thread-safe하게 처리하므로 동시 요청에도 안전합니다.
요약 메모리 구현 (대화 내용 자동 요약)
긴 대화를 이어가다 보면 과거 모든 메시지를 그대로 프롬프트에 포함하는 것이 비효율적이며, 프롬프트 길이 제한에 걸릴 위험이 있습니다. 이를 해결하는 기법이 **대화 요약 메모리 (Conversation Summary Memory)**입니다. LangChain에서는 과거에 ConversationSummaryMemory 등의 클래스가 제공되었으나, 0.3.x 버전에서는 직접 요약용 체인을 만들어 ChatMessageHistory에 적용하는 방식을 권장합니다.
요약 메모리의 아이디어는 다음과 같습니다:
- 일정 길이 이상으로 대화가 누적되면, 과거 대화를 요약하여 핵심 내용만 남김
- 요약 결과를 메모리에 시스템 메시지 등으로 저장하고, 상세 메시지들은 제거하여 메모리 용량을 줄임
- 새로운 사용자 입력 시 요약된 맥락 + 최근 몇 메시지만 참고하여 LLM에 전달
이 방식을 통해 LLM이 장기간 대화를 하더라도 이전 내용을 맥락으로 유지하되, 필요한 요약 정보만 전달하여 토큰 사용량을 절약할 수 있습니다.
요약 체인 준비
우선 LLM을 이용해 대화 내용을 요약하는 요약 체인을 준비합니다. ChatPromptTemplate를 사용하여 요약 지시 프롬프트를 만들고, 이를 기존 LLM에 적용해서 간단한 요약 전용 체인을 구성하겠습니다:
# 요약용 프롬프트 템플릿 (대화 내용을 한국어로 요약)
summary_prompt = ChatPromptTemplate.from_messages([
("system", "당신은 대화 요약 도우미입니다. 대화의 주요 내용을 간결하게 요약하세요."),
("human", "{conversation}") # 전체 대화 내용을 하나의 문자열로 제공
])
summary_chain = summary_prompt | llm | StrOutputParser()
위 프롬프트는 system 역할에서 요약 태스크를 부여하고, human 역할로 요약 대상이 되는 대화 내용을 통째로 넣는 구조입니다. summary_chain은 주어진 conversation 텍스트를 받아 LLM이 요약 결과를 문자열로 출력하게 됩니다. 이제 실제 대화 요약을 적용하는 로직을 작성해보겠습니다.
요약 적용 및 메모리 업데이트
일례로, 한 사용자가 긴 대화를 이어가는 상황을 가정하여 몇 차례 질문을 던져보고, 대화 내용이 누적된 후 요약을 진행해보겠습니다. 여기서는 요약의 편의상 user_a 세션을 사용하여 연속 질문을 시뮬레이션합니다:
# user_a 세션에 다수의 질문을 순차적으로 입력하여 긴 대화 생성
long_queries = [
"안녕, 오늘 우리는 무엇을 할 예정이었지?", # 대화 맥락 시작
"아, 맞다. 내일 회의 자료 준비해야 했지. 회의는 몇 시였어?",
"그 회의에 누가 참석하는지도 기억나?",
"프로젝트 X의 진행 상황도 공유해야 하나?",
"최근에 이야기 나눴던 새로운 디자인에 대한 업데이트는 있어?" # 여러 차례 연속 질문
]
session = "user_a"
for q in long_queries:
answer = chatbot.invoke({"input": q}, config={"configurable": {"session_id": session}})
# (반복문 완료 후 user_a의 대화 기록에 5차례 Q&A가 쌓였다고 가정)
print(f"요약 전 user_a 메모리 메시지 개수: {len(store[session].messages)}")
print(store[session]) # 요약 전 대화 내용 출력
위 코드 실행이 끝나면 user_a의 메모리에는 다섯 차례의 질문과 답변이 축적되어 있을 것입니다. print(store[session])로 내부 내용을 확인하면 (예시):
Human: 안녕, 오늘 우리는 무엇을 할 예정이었지?
AI: 안녕하세요! 오늘 저녁에 팀 프로젝트 회의가 예정되어 있었어요.
Human: 아, 맞다. 내일 회의 자료 준비해야 했지. 회의는 몇 시였어?
AI: 내일 회의는 오전 10시로 일정되어 있어요. 자료 준비를 미리 해두면 좋겠습니다.
... (이하 생략)
이제 대화 요약을 수행해보겠습니다. 요약 대상은 모든 대화 내용을 하나의 문자열로 결합한 것입니다. ChatMessageHistory의 메시지 리스트를 사용하여 이를 구성한 뒤 summary_chain에 전달합니다. 요약 결과를 받은 뒤에는 메모리 객체를 재구성할 것입니다: 요약 내용을 SystemMessage로 추가하고, 필요한 경우 최근 몇 개의 대화를 원본 그대로 유지합니다. 이번 예시에서는 전체 대화를 요약하고, 마지막 사용자 질문-답변 쌍만 원본 유지해 보겠습니다:
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage
# 요약 대상 대화 내용 추출 (마지막 Q&A 쌍 제외한 이전 내용)
messages = store[session].messages
if len(messages) > 2:
original_dialog = "\n".join([f"{msg.type.upper()}: {msg.content}" for msg in messages[:-2]])
else:
original_dialog = "\n".join([f"{msg.type.upper()}: {msg.content}" for msg in messages])
# LLM으로 요약 생성
summary_text = summary_chain.invoke({"conversation": original_dialog})
print("=== 요약 내용 ===")
print(summary_text)
# 기존 메모리를 요약으로 교체: 이전 내용 요약본 + 최근 Q&A 유지
new_history = InMemoryChatMessageHistory()
new_history.messages.append(SystemMessage(content=f"요약: {summary_text}"))
# 최근 대화의 마지막 Q&A 쌍 복원
if len(messages) >= 2:
last_user_msg = messages[-2]
last_ai_msg = messages[-1]
if isinstance(last_user_msg, HumanMessage):
new_history.add_user_message(last_user_msg.content)
else:
new_history.messages.append(last_user_msg) # 혹시 시스템 메시지 등일 경우 직접 추가
if isinstance(last_ai_msg, AIMessage):
new_history.add_ai_message(last_ai_msg.content)
else:
new_history.messages.append(last_ai_msg)
# 메모리 교체
store[session] = new_history
위 코드에서는 messages[:-2]를 요약 대상으로 삼고, 마지막 두 메시지(HumanMessage, AIMessage)는 요약 후에도 그대로 보존했습니다. LLM이 생성한 summary_text는 하나의 문자열이며, 이를 SystemMessage로 새 메모리에 추가했습니다. 마지막으로 store[session]을 새로운 history로 교체하여 요약 적용을 완료했습니다.
요약 결과가 잘 적용되었는지 메모리 내용을 확인해봅시다:
print(f"요약 후 user_a 메모리 메시지 개수: {len(store[session].messages)}")
print(store[session])
출력 예시는 다음과 같습니다:
요약 후 user_a 메모리 메시지 개수: 3
System: 요약: 팀 프로젝트 회의 준비에 대해 논의함. 회의는 내일 오전 10시이고, 자료를 미리 준비해야 함.
Human: 최근에 이야기 나눴던 새로운 디자인에 대한 업데이트는 있어?
AI: 네, 최근 디자인 시안이 업데이트되어 공유되었습니다. 주요 변경 사항은 색상 팔레트와 레이아웃 개선입니다.
위 결과에서 확인할 수 있듯이, 메모리 내 메시지 수가 크게 줄어들고, 가장 앞에 요약 System 메시지가 들어갔습니다. 이 요약에는 이전 대화들의 핵심이 담겨 있으며, 그 뒤로 마지막 사용자 발화와 챗봇 응답이 원형 그대로 남아 있습니다. 이제 새로운 질문이 들어오면 챗봇은 요약된 맥락과 바로 직전의 상세 맥락을 모두 참고하여 답할 수 있게 됩니다.
요약 적용 후 user_a 세션에 추가 질문을 해보면, 요약 내용에 기반한 응답이 이루어지는 것을 볼 수 있습니다:
follow_up = "그럼 회의 전에 무엇을 더 준비해야 할까?"
response = chatbot.invoke({"input": follow_up}, config={"configurable": {"session_id": session}})
print(f"[{session}] 사용자: {follow_up}")
print(f"[{session}] 챗봇: {response}")
예상 응답 (모델 및 프롬프트에 따라 다를 수 있음):
[user_a] 사용자: 그럼 회의 전에 무엇을 더 준비해야 할까?
[user_a] 챗봇: 요약해드린 내용에 따르면, 회의 자료를 미리 준비하는 것이 중요합니다. 자료 외에도 프로젝트 최신 진행 상황을 점검하고, 필요한 경우 팀원들과 사전 공유하면 좋겠습니다.
챗봇이 답변을 생성할 때, 메모리에 남아있는 요약 정보를 바탕으로 **이전 대화 주제(회의 준비)**를 잊지 않고 언급하는 것을 기대할 수 있습니다. 이렇게 요약 메모리를 활용하면 긴 대화도 효율적으로 유지하면서 사용자의 맥락을 추적할 수 있습니다.
주의: 요약 내용은 사용자의 실제 발화가 아닌 LLM이 생성한 것이므로, 간혹 부정확하거나 맥락을 완전히 담지 못할 수 있습니다. 따라서 요약 빈도나 시점을 조절하고, 필요한 경우 요약 내용을 검증하거나 보완하는 로직을 추가해야 합니다. 또한, RunnableWithMessageHistory 자체에는 요약 기능이 내장되어 있지 않으므로, 위에서 한 것처럼 사용자 정의로 요약 단계를 삽입해야 합니다 (예: 대화 턴수가 N 이상이면 자동 요약 후 메모리 교체 등).
마무리: 최신 메모리 구성 정리 및 활용 팁
정리하면, LangChain v0.3.x에서의 최신 메모리 관리 방법은 메시지 기록 객체와 Runnable 체인의 조합으로 요약될 수 있습니다:
- 메시지 히스토리 관리: ChatMessageHistory 구현체를 활용 (InMemory 또는 Redis 등)하여 대화 내용을 구조화된 형태로 저장
- 체인과 메모리 통합: RunnableWithMessageHistory를 사용하여 체인 실행 시 자동으로 과거 메시지를 프롬프트에 삽입하고, 응답 후 새로운 메시지를 기록
- 세션별 분리: session_id를 통해 여러 사용자의 대화 맥락을 분리 관리 (딕셔너리나 DB 키를 활용)
- 대화 요약 적용: 별도의 요약 LLMChain을 통해 오래된 대화 내용을 요약하고 메모리에 반영하여 컨텍스트 길이를 관리
이러한 구성은 LangChain Expression Language(LCEL) 덕분에 간결한 문법으로 구현할 수 있었으며, 과거의 ConversationChain 방식보다 유연하고 강력합니다. 예를 들어, RunnableWithMessageHistory를 쓰면 체인 외부에서 메모리를 관리할 수 있으므로 메모리 객체를 자유롭게 조작하거나 교체할 수 있고, 스트리밍 응답 처리 등과도 호환됩니다. 실제로 공식 문서에서도 ConversationChain 대신 새로운 메모리 래퍼 사용을 권장하고 있으며, 1.0 버전에서 legacy 메모리 클래스들은 제거될 예정입니다.
마지막으로, 실전 응용 시 도움이 될 몇 가지 팁을 공유합니다:
- 환경변수 관리: .env 파일과 python-dotenv를 사용해 API 키나 Redis URL 등을 관리하면 코드에 민감한 정보를 하드코딩하지 않아도 됩니다. 실습 초반에 dotenv.load_dotenv()로 불러오는 패턴을 활용하세요.
- Redis 등의 외부 자원 사용: Redis를 사용할 때는 서버 연결 오류에 대비한 예외 처리를 넣거나, redis_url을 환경변수로 쉽게 바꿀 수 있게 해 두면 유용합니다. 또한 RedisChatMessageHistory에 ttl(time-to-live)이나 key_prefix 등을 설정하여 데이터 관리 정책을 정할 수 있습니다 (예: 오래된 세션 데이터 자동 삭제 등).
- 메모리 초기화/삭제: 필요에 따라 특정 세션의 store[session_id]를 지워서 대화를 초기화할 수 있습니다. 예컨대 사용자가 “대화 초기화”를 요구하면 해당 세션의 메모리를 InMemoryChatMessageHistory()의 새 인스턴스로 교체하면 됩니다.
- 요약 주기 조절: 요약은 대화 흐름의 자연스러움에 영향을 주므로, 너무 자주 하지 않도록 임계치를 설정하는 것이 좋습니다. 예를 들어 10턴 이상 대화가 지속되거나, 메모리 토큰량이 일정 수준 이상일 때 요약을 수행하는 방식입니다. LangChain에서는 토큰 계산 유틸리티를 제공하므로 이를 활용해 임계치를 토큰 기준으로 잡을 수도 있습니다.
- 다른 요약 기법: 간략한 요약 외에도 벡터 DB에 대화 내용 저장 후 필요시 유사도 검색하여 컨텍스트로 넣는 방법, 또는 중요 정보 추출 및 키-값 형태로 기억하는 등의 기법도 있습니다. 복잡한 시나리오에서는 요약 메모리와 다른 메모리 방식들을 **결합(CombinedMemory)**할 수도 있습니다.
이번 실습을 통해 LangChain 최신 메모리 컴포넌트의 개념과 활용법을 익혔습니다. 잘 구성된 메모리 시스템은 챗봇이 사용자와 맥락이 끊기지 않는 대화를 이어나가는 데 필수적입니다. 배운 내용을 토대로 다양한 응용에 창의적으로 적용해 보세요!
'HRDI_AI > [인공지능] LangChain과 RAG를 활용한 AI 서비스 개발 Ⅰ' 카테고리의 다른 글
| 7. LangChain 도구 (4) | 2025.05.30 |
|---|---|
| 6. LangChain LCEL (0) | 2025.05.30 |
| 4. LangChain 출력 파서(OutputParser) (2) | 2025.05.26 |
| 3. LangChain 프롬프트 템플릿 (0) | 2025.05.26 |
| 2. LangChain을 활용한 모델 사용, 비용 모니터링 및 캐싱 전략 (0) | 2025.05.26 |