이미지 로딩 중...
AI Generated
2025. 11. 12. · 4 Views
Python으로 AI 에이전트 만들기 7편 - 대화 메모리와 컨텍스트 관리
AI 에이전트가 이전 대화를 기억하고 맥락을 이해하도록 만드는 방법을 배웁니다. 메모리 시스템 구현부터 컨텍스트 윈도우 관리까지, 실무에서 바로 적용 가능한 대화형 AI 구축 기법을 다룹니다.
목차
- 대화 메모리의 기본 개념과 필요성 - AI가 대화를 기억하는 방법
- ConversationBufferWindowMemory - 최근 N개 대화만 기억하기
- ConversationSummaryMemory - 대화를 요약하여 저장하기
- 컨텍스트 윈도우와 토큰 관리 전략 - 효율적인 메모리 활용
- 시스템 메시지와 메모리 결합 - 페르소나 유지하기
- 세션별 메모리 관리 - 다중 사용자 처리하기
- 메모리 영속화 - Redis와 데이터베이스 연동
- 컨텍스트 압축 기법 - 중요한 정보만 선택하기
1. 대화 메모리의 기본 개념과 필요성 - AI가 대화를 기억하는 방법
시작하며
여러분이 챗봇을 만들었는데 사용자가 "내 이름은 철수야"라고 말한 직후 "내 이름이 뭐였지?"라고 물어봤을 때 "모르겠어요"라고 답한다면 어떨까요? 사용자는 금방 실망하고 챗봇을 떠나게 될 겁니다.
이런 문제는 실제 개발 현장에서 AI 에이전트를 처음 만들 때 가장 흔하게 발생합니다. LLM 자체는 상태를 유지하지 않기 때문에 이전 대화 내용을 전혀 기억하지 못합니다.
매번 새로운 요청이 들어올 때마다 완전히 새로운 대화처럼 처리하는 것이죠. 바로 이럴 때 필요한 것이 대화 메모리(Conversation Memory)입니다.
메모리 시스템을 구현하면 AI가 이전 대화를 기억하고 맥락을 이해하여 자연스러운 대화를 이어갈 수 있습니다.
개요
간단히 말해서, 대화 메모리는 AI 에이전트가 이전 대화 내용을 저장하고 활용할 수 있게 해주는 시스템입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하면, 사용자는 AI와 대화할 때 자연스러운 대화 흐름을 기대합니다.
"그것"이나 "아까 말한 것"처럼 맥락을 참조하는 표현을 쓰고, AI가 이전 대화를 기억하기를 원하죠. 예를 들어, 고객 지원 챗봇에서 사용자가 "결제 문제가 있어요"라고 한 후 "어떻게 해결하나요?"라고 물었을 때, AI는 "결제 문제"를 기억하고 있어야 합니다.
기존에는 개발자가 직접 대화 히스토리를 관리하고 프롬프트에 수동으로 포함시켰다면, 이제는 LangChain 같은 프레임워크를 사용해 자동으로 메모리를 관리할 수 있습니다. 대화 메모리의 핵심 특징은 첫째, 대화 히스토리를 구조화된 형태로 저장하고, 둘째, 적절한 컨텍스트만 선택적으로 LLM에 전달하며, 셋째, 토큰 제한을 고려한 스마트한 관리가 가능하다는 점입니다.
이러한 특징들이 사용자 경험을 크게 향상시키고 AI 에이전트를 실용적으로 만들어줍니다.
코드 예제
from langchain.memory import ConversationBufferMemory
from langchain.chains import ConversationChain
from langchain_openai import ChatOpenAI
# 대화 히스토리를 저장할 메모리 객체 생성
memory = ConversationBufferMemory()
# LLM과 메모리를 연결한 대화 체인 생성
llm = ChatOpenAI(temperature=0.7, model="gpt-4")
conversation = ConversationChain(
llm=llm,
memory=memory,
verbose=True # 디버깅을 위해 내부 동작 출력
)
# 첫 번째 대화: 사용자 정보 입력
response1 = conversation.predict(input="내 이름은 철수고, 파이썬 개발자야")
print(response1)
# 두 번째 대화: 이전 정보를 기억하고 있는지 확인
response2 = conversation.predict(input="내 이름이 뭐였지?")
print(response2) # "철수"라고 정확히 답변
설명
이것이 하는 일: 위 코드는 LangChain의 ConversationBufferMemory를 사용하여 대화 내용을 자동으로 저장하고, 새로운 질문이 들어올 때마다 이전 대화를 포함하여 LLM에 전달하는 시스템을 구축합니다. 첫 번째로, ConversationBufferMemory 객체를 생성합니다.
이 객체는 내부적으로 리스트 형태로 대화 메시지들을 순서대로 저장합니다. 사용자 입력(Human)과 AI 응답(AI)을 쌍으로 기록하여 전체 대화 흐름을 유지하죠.
왜 이렇게 하냐면, LLM 자체는 상태가 없기 때문에 우리가 명시적으로 이전 대화를 전달해야 하기 때문입니다. 그 다음으로, ConversationChain을 생성할 때 LLM과 메모리를 연결합니다.
이렇게 하면 predict() 메서드를 호출할 때마다 자동으로 메모리에서 이전 대화를 가져와 프롬프트에 포함시킵니다. verbose=True 옵션을 켜면 내부적으로 LLM에 전달되는 전체 프롬프트를 볼 수 있어 디버깅에 매우 유용합니다.
세 번째로, 첫 번째 대화에서 "내 이름은 철수"라는 정보를 입력하면 이것이 메모리에 저장됩니다. 그리고 두 번째 대화에서 "내 이름이 뭐였지?"라고 물어보면, ConversationChain이 자동으로 이전 대화를 포함한 프롬프트를 만들어 LLM에 전달합니다.
결과적으로 AI는 "철수"라고 정확히 답변할 수 있게 됩니다. 여러분이 이 코드를 사용하면 별도의 복잡한 상태 관리 없이도 자연스러운 대화형 AI를 만들 수 있습니다.
고객 지원 챗봇, 개인 비서, 교육용 튜터 등 다양한 실무 프로젝트에서 즉시 활용할 수 있으며, 사용자 만족도를 크게 높일 수 있습니다. 또한 코드가 간결해지고 유지보수가 쉬워지는 이점도 있습니다.
실전 팁
💡 메모리 객체는 여러 종류가 있으니 상황에 맞게 선택하세요. ConversationBufferMemory는 모든 대화를 저장하지만, 대화가 길어지면 토큰 제한에 걸릴 수 있습니다.
💡 verbose=True 옵션을 개발 중에는 항상 켜두세요. LLM에 실제로 전달되는 프롬프트를 확인하면 메모리가 제대로 작동하는지 쉽게 디버깅할 수 있습니다.
💡 프로덕션 환경에서는 메모리를 데이터베이스나 Redis에 저장하세요. 서버가 재시작되면 메모리가 초기화되므로 영속성 있는 저장소가 필요합니다.
💡 사용자별로 메모리를 분리 관리하세요. session_id나 user_id를 키로 사용하여 각 사용자의 대화를 독립적으로 유지해야 합니다.
💡 메모리 초기화 기능을 제공하세요. memory.clear() 메서드를 사용하면 대화를 새로 시작할 수 있어 사용자에게 유용합니다.
2. ConversationBufferWindowMemory - 최근 N개 대화만 기억하기
시작하며
여러분이 AI 챗봇을 운영하다가 갑자기 "토큰 제한 초과" 에러를 만난 적 있나요? 사용자가 100번 넘게 대화를 주고받았는데, 이 모든 대화를 매번 LLM에 전달하다 보니 비용도 엄청나고 응답도 느려지는 상황 말이죠.
이런 문제는 ConversationBufferMemory를 사용할 때 필연적으로 발생합니다. 모든 대화를 저장하고 전달하면 대화가 길어질수록 토큰 사용량이 기하급수적으로 증가하고, 결국 LLM의 컨텍스트 윈도우 제한에 부딪히게 됩니다.
비용도 문제지만 응답 속도도 크게 느려져 사용자 경험이 나빠집니다. 바로 이럴 때 필요한 것이 ConversationBufferWindowMemory입니다.
최근 N개의 대화만 선택적으로 기억함으로써 토큰 사용량을 일정하게 유지하면서도 중요한 맥락은 잃지 않을 수 있습니다.
개요
간단히 말해서, ConversationBufferWindowMemory는 가장 최근의 N개 대화 턴만 메모리에 유지하는 슬라이딩 윈도우 방식의 메모리 시스템입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하면, 실제 서비스에서는 토큰 비용과 응답 속도가 매우 중요합니다.
GPT-4를 사용할 경우 토큰당 비용이 상당하므로 불필요한 이전 대화를 계속 전달하는 것은 비효율적이죠. 예를 들어, 음식 주문 챗봇에서 30분 전의 "날씨가 어때?"라는 대화는 현재 주문 과정에 전혀 필요하지 않습니다.
기존의 ConversationBufferMemory가 모든 대화를 무제한으로 저장했다면, ConversationBufferWindowMemory는 k 파라미터로 지정한 개수만큼만 최신 대화를 유지하고 오래된 것은 자동으로 제거합니다. 이 메모리의 핵심 특징은 첫째, 토큰 사용량을 예측 가능하고 일정하게 유지하며, 둘째, 최근 대화의 맥락은 충분히 유지하고, 셋째, 자동으로 오래된 대화를 제거하여 관리 부담이 없다는 점입니다.
이러한 특징들이 실무에서 안정적이고 비용 효율적인 AI 서비스를 만들 수 있게 해줍니다.
코드 예제
from langchain.memory import ConversationBufferWindowMemory
from langchain.chains import ConversationChain
from langchain_openai import ChatOpenAI
# 최근 3개 턴(6개 메시지)만 기억하는 메모리 생성
# k=3은 user-assistant 쌍 3개를 의미
memory = ConversationBufferWindowMemory(k=3)
llm = ChatOpenAI(temperature=0.7, model="gpt-4")
conversation = ConversationChain(
llm=llm,
memory=memory,
verbose=True
)
# 여러 번 대화 진행
conversation.predict(input="내 이름은 철수야")
conversation.predict(input="내 나이는 25살이야")
conversation.predict(input="나는 파이썬을 좋아해")
conversation.predict(input="취미는 게임이야")
# 이 시점에서 첫 번째 대화("내 이름은 철수야")는 메모리에서 제거됨
response = conversation.predict(input="내 이름이 뭐였지?")
print(response) # "내 이름"은 기억 못할 수 있음
설명
이것이 하는 일: 위 코드는 ConversationBufferWindowMemory를 사용하여 최근 3개의 대화 턴만 유지하고, 그보다 오래된 대화는 자동으로 제거하여 메모리와 토큰 사용량을 효율적으로 관리합니다. 첫 번째로, ConversationBufferWindowMemory(k=3)으로 메모리를 생성합니다.
여기서 k=3은 사용자-AI 대화 쌍 3개를 의미하므로 실제로는 6개의 메시지(사용자 3개 + AI 응답 3개)를 저장합니다. 내부적으로는 deque 자료구조를 사용하여 새로운 대화가 추가되면 가장 오래된 대화가 자동으로 제거되는 FIFO 방식으로 동작합니다.
그 다음으로, 4번의 대화를 진행합니다. 처음 3개 대화("이름은 철수", "나이는 25살", "파이썬을 좋아해")까지는 모두 메모리에 저장됩니다.
하지만 4번째 대화("취미는 게임")가 추가되는 순간, k=3 제한에 따라 가장 오래된 첫 번째 대화("이름은 철수")가 메모리에서 자동으로 삭제됩니다. 마지막으로, "내 이름이 뭐였지?"라고 물어봤을 때 AI는 첫 번째 대화가 이미 메모리에서 제거되었기 때문에 정확한 답변을 하지 못할 수 있습니다.
대신 최근 3개 대화("나이는 25살", "파이썬을 좋아해", "취미는 게임")는 여전히 기억하고 있어 이 정보들에 대해서는 정확히 답변할 수 있습니다. 여러분이 이 코드를 사용하면 대화가 아무리 길어져도 토큰 사용량이 일정하게 유지되어 비용을 예측하고 관리하기 쉽습니다.
또한 LLM의 컨텍스트 윈도우 제한을 걱정할 필요가 없어 안정적인 서비스 운영이 가능합니다. 단기 대화 맥락만 필요한 고객 지원, 주문 시스템, 간단한 질의응답 봇 등에 매우 적합합니다.
실전 팁
💡 k 값은 도메인에 따라 조정하세요. 주문 시스템은 k=35면 충분하지만, 복잡한 상담은 k=1015가 필요할 수 있습니다.
💡 중요한 정보는 별도로 저장하세요. 사용자 이름, ID 같은 핵심 정보는 메모리가 아닌 세션 변수나 데이터베이스에 저장하여 영구 보존해야 합니다.
💡 k 값을 너무 작게 설정하면 맥락이 끊기는 느낌을 줍니다. 최소 3 이상으로 설정하여 자연스러운 대화 흐름을 유지하세요.
💡 메모리 내용을 주기적으로 로깅하세요. memory.load_memory_variables({})로 현재 저장된 대화를 확인하면 디버깅과 품질 관리에 유용합니다.
💡 사용자에게 대화 제한을 알려주세요. "최근 5개 대화만 기억합니다"라고 안내하면 사용자가 중요한 정보를 반복해서 제공할 수 있습니다.
3. ConversationSummaryMemory - 대화를 요약하여 저장하기
시작하며
여러분이 고객 상담 챗봇을 운영하는데, 30분간 진행된 긴 대화 내용을 모두 기억해야 하지만 토큰 제한 때문에 불가능한 상황을 겪어본 적 있나요? BufferMemory는 너무 많은 토큰을 쓰고, WindowMemory는 중요한 초반 정보를 잃어버리는 딜레마에 빠지게 됩니다.
이런 문제는 장시간 지속되는 상담, 복잡한 문제 해결 과정, 여러 주제를 오가는 대화에서 특히 심각합니다. 사용자가 처음에 언급한 문제의 배경이나 중요한 세부사항을 나중에 참조해야 하는데, 메모리에서 이미 사라진 경우가 많죠.
그렇다고 모든 대화를 유지하면 비용과 성능 문제가 발생합니다. 바로 이럴 때 필요한 것이 ConversationSummaryMemory입니다.
LLM을 활용하여 대화를 지능적으로 요약함으로써 핵심 정보는 유지하면서 토큰 사용량은 최소화할 수 있습니다.
개요
간단히 말해서, ConversationSummaryMemory는 대화가 진행될 때마다 LLM을 사용하여 이전 대화를 요약하고, 이 요약문을 컨텍스트로 활용하는 스마트한 메모리 시스템입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하면, 실제 비즈니스 환경에서는 긴 대화를 처리해야 하는 경우가 많습니다.
의료 상담, 기술 지원, 법률 자문 등은 10분 이상 지속되며 초반 정보도 계속 중요하죠. 예를 들어, 고객이 처음에 "윈도우 10을 사용 중"이라고 말했다면, 30분 후에도 이 정보는 문제 해결에 필수적입니다.
기존의 WindowMemory가 단순히 오래된 대화를 삭제했다면, SummaryMemory는 삭제 대신 요약하여 핵심 정보를 압축된 형태로 보존합니다. 이 메모리의 핵심 특징은 첫째, LLM이 문맥을 이해하고 중요한 정보만 추출하여 요약하며, 둘째, 대화가 길어져도 토큰 사용량이 급격히 증가하지 않고, 셋째, 초반 대화의 핵심 정보를 장기간 유지할 수 있다는 점입니다.
이러한 특징들이 복잡하고 긴 대화가 필요한 고급 AI 서비스를 가능하게 만듭니다.
코드 예제
from langchain.memory import ConversationSummaryMemory
from langchain.chains import ConversationChain
from langchain_openai import ChatOpenAI
# LLM을 사용하여 대화를 요약하는 메모리 생성
llm = ChatOpenAI(temperature=0, model="gpt-4")
memory = ConversationSummaryMemory(
llm=llm,
return_messages=True # 메시지 형식으로 반환
)
conversation = ConversationChain(
llm=llm,
memory=memory,
verbose=True
)
# 긴 대화 진행
conversation.predict(input="나는 윈도우 10에서 파이썬 3.11을 사용 중이야")
conversation.predict(input="pandas 라이브러리 설치 중 에러가 발생했어")
conversation.predict(input="에러 메시지는 'Microsoft Visual C++ 14.0 required'야")
# 메모리에 저장된 요약 확인
print(memory.load_memory_variables({}))
# "사용자는 윈도우 10, 파이썬 3.11 환경에서 pandas 설치 중
# Visual C++ 관련 에러를 겪고 있음" 형태로 요약됨
설명
이것이 하는 일: 위 코드는 ConversationSummaryMemory를 사용하여 대화가 누적될 때마다 LLM에게 요약을 요청하고, 원본 대화 대신 압축된 요약문을 메모리에 저장하여 토큰 효율성과 정보 보존을 동시에 달성합니다. 첫 번째로, ConversationSummaryMemory를 생성할 때 반드시 LLM 객체를 전달해야 합니다.
이 LLM은 대화를 요약하는 용도로 사용되며, temperature=0으로 설정하여 일관되고 정확한 요약을 생성하도록 합니다. return_messages=True 옵션은 요약을 메시지 형식으로 반환하여 다른 메모리 타입과 호환되게 만듭니다.
그 다음으로, 대화가 진행되면서 메모리가 자동으로 업데이트됩니다. 처음 몇 개의 대화는 그대로 저장되지만, 일정 길이를 넘어가면 LLM에게 "다음 대화를 간결하게 요약해주세요"라는 프롬프트와 함께 이전 대화를 전달합니다.
LLM은 문맥을 이해하고 "윈도우 10 환경", "파이썬 3.11", "pandas 설치 에러", "Visual C++ 필요" 같은 핵심 정보만 추출합니다. 마지막으로, 새로운 질문이 들어오면 원본 대화 대신 이 요약문이 프롬프트에 포함됩니다.
"사용자는 윈도우 10, 파이썬 3.11 환경에서 pandas 설치 중 Visual C++ 관련 에러를 겪고 있음"이라는 한 문장으로 3개 대화의 핵심을 전달하는 것이죠. 이렇게 하면 100단어가 20단어로 압축되어 토큰을 80% 절약하면서도 필요한 모든 정보를 유지합니다.
여러분이 이 코드를 사용하면 고객 지원, 의료 상담, 교육 튜터링처럼 긴 대화가 필요한 서비스에서 초반 정보를 잃지 않으면서도 비용 효율적으로 운영할 수 있습니다. 또한 요약 과정에서 중요하지 않은 잡담이나 반복적인 내용이 제거되어 오히려 LLM이 핵심에 집중하기 쉬워지는 장점도 있습니다.
다만 요약을 위해 추가 LLM 호출이 발생하므로 이 비용도 고려해야 합니다.
실전 팁
💡 요약용 LLM은 저렴한 모델을 사용하세요. gpt-3.5-turbo로 요약하고 메인 대화는 gpt-4를 사용하면 비용을 크게 절감할 수 있습니다.
💡 요약 빈도를 조절하세요. max_token_limit 파라미터로 일정 토큰 이상일 때만 요약하도록 설정하면 불필요한 요약 호출을 방지할 수 있습니다.
💡 요약 품질을 모니터링하세요. 중요한 정보가 누락되는지 주기적으로 확인하고, 필요하면 요약 프롬프트를 커스터마이징하세요.
💡 중요 대화는 요약하지 마세요. 결제 정보, 개인정보 같은 민감한 대화는 ConversationSummaryBufferMemory를 사용하여 최근 대화는 원문을 유지하세요.
💡 요약 실패에 대비하세요. LLM 호출 실패 시 폴백(fallback) 로직을 구현하여 서비스 중단을 방지하세요.
4. 컨텍스트 윈도우와 토큰 관리 전략 - 효율적인 메모리 활용
시작하며
여러분이 GPT-4를 사용하는 AI 서비스를 운영하다가 월말에 청구서를 받고 깜짝 놀란 적 있나요? 예상보다 10배 많은 비용이 청구되었는데, 알고 보니 매 요청마다 불필요하게 긴 대화 히스토리를 전달하고 있었던 거죠.
이런 문제는 컨텍스트 윈도우와 토큰 관리에 대한 이해 부족에서 발생합니다. LLM은 입력 토큰과 출력 토큰 모두에 비용을 청구하는데, 긴 대화 히스토리를 매번 전달하면 입력 토큰이 기하급수적으로 증가합니다.
100회 대화 시 첫 대화가 100번 중복 전송되는 셈이죠. 또한 모델마다 컨텍스트 윈도우 제한이 다르고, 이를 초과하면 에러가 발생합니다.
바로 이럴 때 필요한 것이 체계적인 토큰 관리 전략입니다. 토큰을 계산하고, 제한을 모니터링하며, 필요한 정보만 선택적으로 전달하는 기법을 배워야 합니다.
개요
간단히 말해서, 컨텍스트 윈도우는 LLM이 한 번에 처리할 수 있는 최대 토큰 수를 의미하며, 효율적인 토큰 관리는 이 제한 내에서 최대한 유용한 정보를 전달하는 기술입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하면, 비용과 성능이 직접적으로 연결되기 때문입니다.
GPT-4의 경우 입력 토큰 1,000개당 약 $0.03를 청구하는데, 10,000회 요청에 각각 1,000토큰을 전달하면 $300가 발생합니다. 하지만 불필요한 토큰을 제거하여 500토큰으로 줄이면 비용이 절반으로 감소하죠.
예를 들어, 스타트업이 하루 10만 요청을 처리한다면 토큰 최적화로 월 수천 달러를 절감할 수 있습니다. 기존에는 개발자가 수동으로 토큰을 세고 관리했다면, 이제는 tiktoken 라이브러리와 LangChain의 도구들을 사용해 자동으로 토큰을 측정하고 제한할 수 있습니다.
토큰 관리의 핵심 특징은 첫째, 실시간으로 토큰 사용량을 모니터링하고, 둘째, 컨텍스트 윈도우 제한을 초과하기 전에 자동으로 처리하며, 셋째, 비용과 성능의 균형을 최적화할 수 있다는 점입니다. 이러한 특징들이 상업적으로 성공 가능한 AI 서비스를 만드는 필수 요소입니다.
코드 예제
import tiktoken
from langchain.memory import ConversationTokenBufferMemory
from langchain.chains import ConversationChain
from langchain_openai import ChatOpenAI
# 토큰을 세는 인코더 생성 (GPT-4 기준)
encoding = tiktoken.encoding_for_model("gpt-4")
# 토큰 수 계산 예제
text = "안녕하세요! 파이썬으로 AI 에이전트를 만들어봅시다."
tokens = encoding.encode(text)
print(f"토큰 수: {len(tokens)}") # 약 15-20 토큰
# 최대 토큰 수를 제한하는 메모리 생성
llm = ChatOpenAI(temperature=0.7, model="gpt-4")
memory = ConversationTokenBufferMemory(
llm=llm,
max_token_limit=500, # 메모리가 500토큰을 초과하면 오래된 대화 제거
return_messages=True
)
conversation = ConversationChain(llm=llm, memory=memory, verbose=True)
# 대화가 500토큰을 넘으면 자동으로 오래된 내용 제거
response = conversation.predict(input="긴 질문을 입력...")
설명
이것이 하는 일: 위 코드는 tiktoken 라이브러리로 정확한 토큰 수를 계산하고, ConversationTokenBufferMemory를 사용하여 메모리가 지정된 토큰 제한을 초과하지 않도록 자동으로 관리하는 시스템을 구축합니다. 첫 번째로, tiktoken 라이브러리를 사용하여 텍스트의 정확한 토큰 수를 계산합니다.
encoding_for_model("gpt-4")로 GPT-4의 토크나이저를 가져오고, encode() 메서드로 텍스트를 토큰으로 변환합니다. 한국어는 영어보다 토큰을 더 많이 사용하는데, "안녕하세요"가 3-4개 토큰으로 인코딩되는 것을 확인할 수 있습니다.
이 정보로 프롬프트 설계 시 토큰 사용량을 미리 예측할 수 있습니다. 그 다음으로, ConversationTokenBufferMemory를 생성할 때 max_token_limit을 설정합니다.
이 메모리는 내부적으로 매번 대화가 추가될 때마다 전체 메모리의 토큰 수를 계산합니다. 만약 500토큰을 초과하면 가장 오래된 대화부터 하나씩 제거하여 제한 내로 유지합니다.
WindowMemory와 달리 "대화 개수"가 아닌 "토큰 수"를 기준으로 하므로 더 정밀한 제어가 가능합니다. 세 번째로, 실제 대화를 진행하면 메모리가 자동으로 토큰을 관리합니다.
예를 들어 현재 메모리가 450토큰인 상태에서 100토큰짜리 새 대화가 추가되면 총 550토큰이 되어 제한을 초과합니다. 이때 메모리는 자동으로 가장 오래된 대화(예: 80토큰)를 제거하여 470토큰으로 만듭니다.
이 과정이 매 대화마다 자동으로 수행되어 개발자는 신경 쓸 필요가 없습니다. 여러분이 이 코드를 사용하면 LLM의 컨텍스트 윈도우 제한을 절대 초과하지 않아 에러 없는 안정적인 서비스를 제공할 수 있습니다.
또한 토큰 사용량이 일정하게 유지되어 비용을 정확히 예측하고 관리할 수 있으며, 불필요한 토큰 전송을 방지하여 응답 속도도 빨라집니다. 상용 서비스에서는 필수적으로 적용해야 하는 기법입니다.
실전 팁
💡 모델별 컨텍스트 윈도우를 확인하세요. GPT-4는 8K/32K, GPT-3.5-turbo는 4K/16K 버전이 있으니 적절한 모델을 선택하세요.
💡 안전 마진을 두세요. max_token_limit을 컨텍스트 윈도우의 70-80%로 설정하여 출력 토큰과 시스템 프롬프트를 위한 공간을 확보하세요.
💡 토큰 사용량을 로깅하세요. 각 요청의 토큰 수를 기록하여 비용 분석과 최적화 지점을 찾을 수 있습니다.
💡 한국어는 토큰 효율이 낮습니다. 가능하면 간결한 표현을 사용하고, 불필요한 조사나 어미를 줄여 토큰을 절약하세요.
💡 프롬프트를 최적화하세요. "다음 질문에 답변해주세요" 대신 "답변:"처럼 짧게 작성하면 매 요청마다 토큰을 절약할 수 있습니다.
5. 시스템 메시지와 메모리 결합 - 페르소나 유지하기
시작하며
여러분이 고객 지원 챗봇을 만들었는데, 대화가 길어질수록 AI가 점점 자신이 누구인지 잊어버리고 일반적인 응답을 하는 상황을 본 적 있나요? 처음에는 "저는 ABC 회사의 고객 지원 전문가입니다"라고 했다가, 20분 후에는 "저는 AI 언어모델입니다"라고 답변하는 거죠.
이런 문제는 메모리 관리 과정에서 시스템 메시지(System Message)가 제대로 유지되지 않기 때문에 발생합니다. 메모리가 오래된 대화를 제거하거나 요약할 때 AI의 역할과 행동 규칙을 정의한 시스템 메시지까지 영향을 받을 수 있습니다.
결과적으로 AI가 정체성을 잃고 일관성 없는 응답을 하게 됩니다. 바로 이럴 때 필요한 것이 시스템 메시지와 메모리를 올바르게 결합하는 기법입니다.
시스템 메시지를 항상 유지하면서 대화 메모리만 관리하여 AI의 페르소나와 행동 규칙을 일관되게 유지할 수 있습니다.
개요
간단히 말해서, 시스템 메시지는 AI의 역할, 성격, 응답 스타일을 정의하는 특별한 메시지이며, 이것을 메모리 관리와 독립적으로 유지하여 일관된 AI 페르소나를 만드는 것입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하면, 상업적 AI 서비스는 명확한 브랜드 아이덴티티와 일관된 응답 스타일이 필수입니다.
은행 챗봇은 항상 정중하고 보수적이어야 하고, 게임 캐릭터 AI는 특정 성격을 유지해야 하죠. 예를 들어, "당신은 친절하고 전문적인 법률 상담 AI입니다.
항상 법적 근거를 제시하세요"라는 시스템 메시지가 대화 중간에 사라지면 품질이 크게 저하됩니다. 기존에는 시스템 메시지를 일반 대화처럼 취급하여 메모리에서 제거되는 경우가 있었다면, 이제는 ChatPromptTemplate을 사용하여 시스템 메시지를 독립적으로 관리할 수 있습니다.
이 기법의 핵심 특징은 첫째, 시스템 메시지가 절대 제거되지 않고 항상 프롬프트에 포함되며, 둘째, 메모리 관리와 독립적으로 AI의 행동 규칙을 유지하고, 셋째, 브랜드 일관성과 품질을 보장할 수 있다는 점입니다. 이러한 특징들이 전문적이고 신뢰할 수 있는 AI 서비스를 만드는 데 필수적입니다.
코드 예제
from langchain.memory import ConversationBufferWindowMemory
from langchain_openai import ChatOpenAI
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain.chains import LLMChain
# 시스템 메시지와 메모리를 결합한 프롬프트 템플릿
prompt = ChatPromptTemplate.from_messages([
("system", """당신은 ABC 테크 회사의 고객 지원 전문가입니다.
항상 다음 규칙을 따르세요:
1. 정중하고 전문적인 톤을 유지하세요
2. 기술적 용어는 쉽게 풀어 설명하세요
3. 모든 답변 끝에 '추가 질문이 있으시면 언제든 물어보세요'를 추가하세요"""),
MessagesPlaceholder(variable_name="history"), # 메모리가 여기에 삽입됨
("human", "{input}")
])
# 메모리 생성
memory = ConversationBufferWindowMemory(
k=5,
return_messages=True,
memory_key="history" # 프롬프트의 MessagesPlaceholder와 일치
)
# LLM 체인 구성
llm = ChatOpenAI(temperature=0.7, model="gpt-4")
chain = LLMChain(llm=llm, prompt=prompt, memory=memory, verbose=True)
# 대화 진행 - 시스템 메시지는 항상 유지됨
response = chain.predict(input="파이썬 설치가 안 돼요")
print(response) # 항상 정중한 톤 + 쉬운 설명 + 마무리 멘트 포함
설명
이것이 하는 일: 위 코드는 ChatPromptTemplate을 사용하여 시스템 메시지를 최상단에 고정시키고, MessagesPlaceholder로 메모리 내용을 중간에 삽입하며, 사용자 입력을 마지막에 배치하는 구조화된 프롬프트를 만들어 일관된 AI 페르소나를 유지합니다. 첫 번째로, ChatPromptTemplate.from_messages()로 프롬프트 구조를 정의합니다.
첫 번째 요소인 ("system", "...")은 AI의 역할과 행동 규칙을 정의하는 시스템 메시지입니다. 이것은 템플릿에 하드코딩되어 있어 절대 변경되거나 제거되지 않습니다.
"정중하고 전문적인 톤", "쉬운 설명", "특정 마무리 멘트" 같은 규칙이 모든 대화에서 일관되게 적용되도록 보장합니다. 그 다음으로, MessagesPlaceholder(variable_name="history")가 메모리의 대화 내용이 삽입될 위치를 지정합니다.
메모리가 관리하는 이전 대화들이 이 자리에 동적으로 들어갑니다. memory_key="history"로 설정하여 메모리와 플레이스홀더를 연결하는 것이 중요합니다.
이렇게 하면 메모리가 아무리 변경되어도 시스템 메시지는 영향을 받지 않습니다. 마지막으로, LLMChain으로 모든 것을 연결합니다.
대화가 진행될 때마다 실제 LLM에 전달되는 프롬프트는 다음과 같은 구조입니다: [시스템 메시지] + [이전 대화 5개] + [현재 질문]. 시스템 메시지는 항상 첫 번째에 위치하여 AI가 자신의 역할을 명확히 인식하게 합니다.
20분간 대화가 이어져도, 100번째 응답에서도 여전히 "ABC 테크 회사의 전문가"로 행동합니다. 여러분이 이 코드를 사용하면 브랜드 일관성이 필요한 기업용 챗봇, 특정 캐릭터를 구현하는 게임 NPC, 교육용 튜터 등에서 전문적이고 일관된 사용자 경험을 제공할 수 있습니다.
또한 시스템 메시지에 안전 규칙("절대 개인정보를 요구하지 마세요")을 넣으면 보안과 컴플라이언스를 보장할 수 있습니다. 프롬프트 구조가 명확해져 디버깅과 유지보수도 훨씬 쉬워집니다.
실전 팁
💡 시스템 메시지는 구체적으로 작성하세요. "친절하게"보다는 "고객님이라고 호칭하고, 이모티콘 사용 금지, 존댓말 사용"처럼 명확한 규칙을 제시하세요.
💡 Few-shot 예제를 시스템 메시지에 포함하세요. "예: 질문: 환불 어떻게 하나요? 답변: 고객님, 환불은..."처럼 예시를 제공하면 품질이 크게 향상됩니다.
💡 역할과 제약사항을 명시하세요. "당신은 의사가 아닙니다. 의학적 조언 대신 병원 방문을 권유하세요"처럼 AI가 하지 말아야 할 것도 명확히 하세요.
💡 시스템 메시지를 A/B 테스트하세요. 다양한 버전을 테스트하여 가장 효과적인 프롬프트를 찾으세요.
💡 정기적으로 업데이트하세요. 사용자 피드백을 반영하여 시스템 메시지를 개선하면 AI 품질이 지속적으로 향상됩니다.
6. 세션별 메모리 관리 - 다중 사용자 처리하기
시작하며
여러분이 웹 기반 AI 챗봇을 서비스하는데, 사용자 A와 사용자 B가 동시에 접속했을 때 서로의 대화가 섞여서 나오는 황당한 상황을 경험한 적 있나요? A가 "내 이름은 철수야"라고 했는데 B에게 "안녕하세요 철수님"이라고 인사하는 거죠.
이런 문제는 메모리를 전역(global)으로 관리할 때 발생합니다. 모든 사용자가 하나의 메모리 객체를 공유하면 대화가 뒤섞이고, 개인정보가 노출되며, 사용자 경험이 완전히 망가집니다.
특히 FastAPI, Flask 같은 웹 프레임워크에서 메모리를 잘못 관리하면 동시 접속자 간 데이터 오염이 심각한 문제가 됩니다. 바로 이럴 때 필요한 것이 세션별 메모리 관리입니다.
각 사용자에게 독립적인 메모리를 할당하고 세션 ID로 구분하여 완전히 격리된 대화 환경을 제공해야 합니다.
개요
간단히 말해서, 세션별 메모리 관리는 각 사용자마다 독립적인 메모리 객체를 생성하고 세션 ID(또는 사용자 ID)를 키로 사용하여 관리하는 멀티 테넌시(multi-tenancy) 패턴입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하면, 실제 서비스는 수백~수천 명의 사용자가 동시에 접속합니다.
각 사용자는 자신만의 대화 컨텍스트를 가져야 하고, 다른 사용자의 정보가 노출되어서는 안 되죠. 예를 들어, 온라인 쇼핑몰 챗봇에서 A의 장바구니 정보가 B에게 보이면 심각한 보안 문제가 됩니다.
기존에는 개발자가 수동으로 딕셔너리를 만들어 세션별 메모리를 관리했다면, 이제는 구조화된 패턴과 데이터베이스 백엔드를 사용하여 확장 가능하고 안전하게 관리할 수 있습니다. 이 패턴의 핵심 특징은 첫째, 각 사용자의 대화가 완전히 격리되어 보안과 프라이버시가 보장되며, 둘째, 세션 ID를 통해 효율적으로 메모리를 검색하고 관리하고, 셋째, 확장 가능하여 수천 명의 동시 사용자를 처리할 수 있다는 점입니다.
이러한 특징들이 상용 수준의 멀티 유저 AI 서비스를 가능하게 만듭니다.
코드 예제
from langchain.memory import ConversationBufferWindowMemory
from langchain.chains import ConversationChain
from langchain_openai import ChatOpenAI
from typing import Dict
import uuid
# 세션별 메모리를 저장하는 딕셔너리
session_memories: Dict[str, ConversationBufferWindowMemory] = {}
def get_memory_for_session(session_id: str) -> ConversationBufferWindowMemory:
"""세션 ID에 해당하는 메모리를 반환하거나 새로 생성"""
if session_id not in session_memories:
# 새 세션이면 메모리 생성
session_memories[session_id] = ConversationBufferWindowMemory(
k=5,
return_messages=True
)
return session_memories[session_id]
# 사용자 A의 세션
session_a = str(uuid.uuid4()) # 예: "a1b2c3d4-..."
memory_a = get_memory_for_session(session_a)
llm = ChatOpenAI(temperature=0.7, model="gpt-4")
conversation_a = ConversationChain(llm=llm, memory=memory_a)
conversation_a.predict(input="내 이름은 철수야")
# 사용자 B의 세션 - 완전히 독립적
session_b = str(uuid.uuid4())
memory_b = get_memory_for_session(session_b)
conversation_b = ConversationChain(llm=llm, memory=memory_b)
conversation_b.predict(input="내 이름은 영희야")
# A와 B는 서로의 정보를 모름
print(conversation_a.predict(input="내 이름이 뭐였지?")) # "철수"
print(conversation_b.predict(input="내 이름이 뭐였지?")) # "영희"
설명
이것이 하는 일: 위 코드는 세션 ID를 키로 하는 딕셔너리를 사용하여 각 사용자의 메모리를 독립적으로 생성하고 관리하며, 동시 접속자 간 데이터 오염을 방지하는 멀티 테넌시 시스템을 구현합니다. 첫 번째로, session_memories라는 전역 딕셔너리를 생성하여 모든 세션의 메모리를 저장합니다.
키는 세션 ID(문자열)이고, 값은 ConversationBufferWindowMemory 객체입니다. 이 딕셔너리는 서버가 실행되는 동안 메모리에 유지되며, 수천 개의 세션을 동시에 관리할 수 있습니다.
파이썬 딕셔너리의 O(1) 검색 성능 덕분에 사용자 수가 증가해도 성능 저하가 거의 없습니다. 그 다음으로, get_memory_for_session() 함수는 lazy initialization 패턴을 구현합니다.
세션 ID를 받아서 해당 메모리가 이미 존재하면 그것을 반환하고, 없으면 새로 생성하여 딕셔너리에 추가한 후 반환합니다. 이렇게 하면 첫 요청 시 자동으로 메모리가 생성되고, 이후 요청에서는 기존 메모리를 재사용하여 대화가 이어집니다.
uuid.uuid4()로 전역적으로 유니크한 세션 ID를 생성하여 충돌을 방지합니다. 마지막으로, 각 사용자의 대화를 독립적으로 처리합니다.
사용자 A와 B는 완전히 다른 session_id를 가지므로 서로 다른 메모리 객체를 사용합니다. A가 "내 이름은 철수"라고 해도 B의 메모리에는 전혀 영향을 주지 않습니다.
각 ConversationChain은 독립된 메모리를 참조하므로 A와 B가 동시에 대화해도 문제없이 작동합니다. 이것은 스레드 안전(thread-safe)하며 비동기 환경에서도 안전하게 사용할 수 있습니다.
여러분이 이 코드를 사용하면 FastAPI, Flask, Django 같은 웹 프레임워크에서 안전한 멀티 유저 챗봇을 구축할 수 있습니다. 실제로는 세션 ID를 쿠키나 JWT 토큰에서 추출하여 사용하면 됩니다.
다만 메모리에만 저장하면 서버 재시작 시 모든 대화가 사라지므로, 프로덕션에서는 Redis나 데이터베이스에 메모리를 영속화해야 합니다. 또한 오래된 세션을 주기적으로 정리(cleanup)하여 메모리 누수를 방지하는 것도 중요합니다.
실전 팁
💡 세션 만료 정책을 구현하세요. 30분 동안 활동이 없으면 메모리를 삭제하여 서버 자원을 절약할 수 있습니다.
💡 Redis를 사용하여 메모리를 저장하세요. 분산 환경에서 여러 서버가 동일한 세션에 접근할 수 있고, TTL 기능으로 자동 만료도 가능합니다.
💡 세션 ID는 절대 클라이언트에서 생성하지 마세요. 서버에서 생성하고 검증하여 보안을 강화하세요.
💡 동시성 제어를 고려하세요. asyncio.Lock이나 threading.Lock을 사용하여 동일 세션의 동시 요청을 직렬화하세요.
💡 메모리 사용량을 모니터링하세요. 세션 수가 급증하면 서버 메모리가 부족할 수 있으니 알림을 설정하세요.
7. 메모리 영속화 - Redis와 데이터베이스 연동
시작하며
여러분이 AI 챗봇 서버를 업데이트하려고 재시작했는데, 수천 명의 사용자가 진행 중이던 모든 대화가 사라져버린 상황을 상상해보세요. 사용자들은 "아까 내가 뭐라고 했더라?"를 다시 설명해야 하고, 불만이 폭주하겠죠.
이런 문제는 메모리를 서버의 RAM에만 저장할 때 필연적으로 발생합니다. 서버 재시작, 크래시, 배포 등 어떤 이유로든 프로세스가 종료되면 모든 대화 히스토리가 영구적으로 손실됩니다.
또한 로드 밸런서를 사용하는 분산 환경에서는 같은 사용자의 요청이 다른 서버로 가면 대화 맥락이 끊기는 문제도 있죠. 바로 이럴 때 필요한 것이 메모리 영속화(persistence)입니다.
Redis나 데이터베이스에 대화 히스토리를 저장하여 서버 상태와 무관하게 대화를 유지하고, 분산 환경에서도 일관된 경험을 제공할 수 있습니다.
개요
간단히 말해서, 메모리 영속화는 대화 히스토리를 휘발성 메모리(RAM)가 아닌 영구 저장소(Redis, PostgreSQL 등)에 저장하여 서버 재시작 후에도 대화가 유지되도록 하는 기법입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하면, 상용 서비스는 99.9% 이상의 가용성이 요구됩니다.
배포, 스케일링, 장애 복구 등으로 서버가 재시작되는 것은 일상적인 일이며, 이때마다 사용자 경험이 손상되어서는 안 됩니다. 예를 들어, 고객이 복잡한 기술 문제를 30분간 상담하다가 서버 재시작으로 처음부터 다시 설명해야 한다면 고객 만족도가 급격히 떨어집니다.
기존의 메모리 기반 저장이 빠르지만 휘발성이었다면, Redis나 데이터베이스를 사용하면 약간의 성능을 희생하여 영속성과 확장성을 얻을 수 있습니다. 영속화의 핵심 특징은 첫째, 서버 재시작 후에도 모든 대화가 보존되며, 둘째, 분산 환경에서 여러 서버가 동일한 저장소를 공유하여 일관성을 유지하고, 셋째, 감사(audit)와 분석을 위해 대화 히스토리를 장기 보관할 수 있다는 점입니다.
이러한 특징들이 엔터프라이즈급 AI 서비스의 필수 요구사항을 충족시킵니다.
코드 예제
from langchain.memory import RedisChatMessageHistory
from langchain.memory import ConversationBufferMemory
from langchain.chains import ConversationChain
from langchain_openai import ChatOpenAI
import redis
# Redis 연결 설정
redis_client = redis.Redis(
host='localhost',
port=6379,
db=0,
decode_responses=True
)
def get_conversation_chain(session_id: str):
"""세션별로 Redis에 저장되는 대화 체인 생성"""
# Redis에 메시지 히스토리 저장
message_history = RedisChatMessageHistory(
session_id=session_id,
url="redis://localhost:6379/0",
ttl=3600 # 1시간 후 자동 삭제 (선택적)
)
# Redis 기반 메모리 생성
memory = ConversationBufferMemory(
chat_memory=message_history,
return_messages=True
)
llm = ChatOpenAI(temperature=0.7, model="gpt-4")
chain = ConversationChain(llm=llm, memory=memory, verbose=True)
return chain
# 첫 번째 대화
session_id = "user_12345"
conversation = get_conversation_chain(session_id)
conversation.predict(input="내 이름은 철수야")
# 서버 재시작 시뮬레이션 (변수 초기화)
del conversation
# 새로운 인스턴스 생성 - Redis에서 이전 대화 복원됨
conversation = get_conversation_chain(session_id)
response = conversation.predict(input="내 이름이 뭐였지?")
print(response) # "철수"라고 정확히 기억함!
설명
이것이 하는 일: 위 코드는 RedisChatMessageHistory를 사용하여 모든 대화 메시지를 Redis에 자동으로 저장하고, 서버가 재시작되어도 세션 ID로 이전 대화를 복원하여 끊김 없는 대화 경험을 제공합니다. 첫 번째로, Redis 클라이언트를 설정합니다.
localhost:6379는 기본 Redis 설정이며, 프로덕션에서는 Redis Cloud나 AWS ElastiCache 같은 관리형 서비스를 사용합니다. decode_responses=True는 바이트 대신 문자열로 데이터를 받아 처리를 간편하게 만듭니다.
Redis는 인메모리 데이터베이스로 매우 빠르면서도(밀리초 단위) 디스크에 주기적으로 저장되어 영속성을 제공합니다. 그 다음으로, RedisChatMessageHistory를 생성할 때 session_id를 전달합니다.
이것은 Redis의 키 접두사로 사용되며, "message_store:user_12345" 같은 형태로 저장됩니다. ttl=3600은 1시간 후 자동으로 대화를 삭제하는 옵션으로, GDPR 같은 개인정보 보호 규정을 준수하거나 저장소 용량을 관리하는 데 유용합니다.
이 히스토리 객체는 메시지가 추가될 때마다 자동으로 Redis에 저장합니다. 마지막으로, ConversationBufferMemory에 chat_memory 파라미터로 Redis 히스토리를 전달합니다.
이제 conversation.predict()를 호출할 때마다 내부적으로 두 가지 일이 발생합니다: (1) Redis에서 이전 메시지를 읽어와 프롬프트에 포함시키고, (2) AI 응답을 받은 후 새 메시지를 Redis에 저장합니다. 서버가 재시작되어 conversation 객체가 사라져도, 동일한 session_id로 새 객체를 만들면 Redis에서 모든 대화를 자동으로 복원합니다.
여러분이 이 코드를 사용하면 24/7 운영되는 상용 서비스에서 안정적인 대화 연속성을 제공할 수 있습니다. 무중단 배포(blue-green deployment)가 가능해지고, 서버를 여러 대로 확장해도 모든 서버가 Redis를 공유하므로 사용자는 어느 서버에 연결되든 동일한 경험을 받습니다.
또한 Redis의 복제(replication)와 센티널(sentinel) 기능을 사용하면 고가용성도 확보할 수 있습니다. 감사 로그가 필요하면 Redis 데이터를 주기적으로 PostgreSQL로 백업하는 것도 좋은 전략입니다.
실전 팁
💡 TTL을 적절히 설정하세요. 고객 지원은 24시간, 캐주얼 챗봇은 1시간 같이 도메인에 맞게 조정하여 저장소 비용을 절감하세요.
💡 Redis Cluster를 사용하세요. 데이터가 많으면 단일 Redis로는 부족하므로 클러스터로 샤딩하여 확장성을 확보하세요.
💡 백업 전략을 수립하세요. Redis RDB 스냅샷이나 AOF 로그를 정기적으로 백업하여 재해 복구에 대비하세요.
💡 민감한 정보는 암호화하세요. 대화에 개인정보가 포함되면 Redis에 저장하기 전에 암호화하여 보안을 강화하세요.
💡 PostgreSQL도 고려하세요. 복잡한 쿼리나 장기 보관이 필요하면 RDB가 더 적합할 수 있습니다. LangChain은 SQL 기반 히스토리도 지원합니다.
8. 컨텍스트 압축 기법 - 중요한 정보만 선택하기
시작하며
여러분이 50회 이상 이어진 긴 대화에서 사용자가 처음에 언급한 중요한 정보(예: "파이썬 3.9 사용 중")는 기억해야 하지만, 중간에 나눈 "날씨 어때요?" 같은 잡담은 굳이 필요 없는 상황을 생각해보세요. 모든 대화를 유지하면 토큰이 낭비되고, WindowMemory를 쓰면 중요한 초반 정보를 잃어버리는 딜레마에 빠집니다.
이런 문제는 모든 메시지를 동등하게 취급하기 때문에 발생합니다. 실제로는 일부 메시지는 현재 질문과 매우 관련이 깊고, 어떤 것은 전혀 관련이 없습니다.
관련 없는 정보를 계속 전달하면 LLM이 중요한 맥락에 집중하지 못하고, 비용도 낭비되며, 응답 품질도 떨어집니다. 바로 이럴 때 필요한 것이 컨텍스트 압축(Context Compression) 기법입니다.
현재 질문과 관련된 대화만 지능적으로 선택하여 전달함으로써 토큰 효율과 응답 품질을 동시에 향상시킬 수 있습니다.
개요
간단히 말해서, 컨텍스트 압축은 임베딩이나 LLM을 활용하여 현재 질문과 의미적으로 관련이 높은 대화만 선택적으로 추출하여 프롬프트에 포함시키는 스마트한 메모리 관리 기법입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하면, 긴 대화에서 모든 내용이 똑같이 중요한 것은 아닙니다.
기술 지원 챗봇에서 사용자가 "파이썬 버전은 3.9"라고 했다면 이것은 계속 중요하지만, "오늘 날씨 좋네요"는 문제 해결과 무관합니다. 예를 들어, 100회 대화 중 현재 질문과 관련된 5개만 선택하면 토큰을 95% 절약하면서도 필요한 맥락은 모두 유지할 수 있습니다.
기존의 WindowMemory가 시간 순서로만 선택했다면, 컨텍스트 압축은 의미적 유사도(semantic similarity)를 기준으로 스마트하게 선택합니다. 이 기법의 핵심 특징은 첫째, 현재 질문과 관련 없는 대화를 자동으로 필터링하여 노이즈를 제거하고, 둘째, 관련도 높은 정보만 전달하여 LLM의 집중력을 향상시키며, 셋째, 대화가 아무리 길어도 필요한 토큰만 사용하여 비용을 최적화한다는 점입니다.
이러한 특징들이 장시간 대화에서도 고품질 응답을 유지하면서 비용을 절감할 수 있게 합니다.
코드 예제
from langchain.memory import VectorStoreRetrieverMemory
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import FAISS
from langchain.chains import ConversationChain
from langchain_openai import ChatOpenAI
# 임베딩 모델 생성
embeddings = OpenAIEmbeddings()
# 벡터 저장소 생성 (대화를 임베딩으로 변환하여 저장)
vectorstore = FAISS.from_texts(
["초기 더미 텍스트"], # 초기화용
embedding=embeddings
)
# 벡터 검색 기반 메모리 생성
retriever = vectorstore.as_retriever(search_kwargs={"k": 3}) # 상위 3개만 검색
memory = VectorStoreRetrieverMemory(
retriever=retriever,
memory_key="history"
)
llm = ChatOpenAI(temperature=0.7, model="gpt-4")
conversation = ConversationChain(llm=llm, memory=memory, verbose=True)
# 다양한 주제로 대화
conversation.predict(input="나는 파이썬 3.9를 사용해")
conversation.predict(input="오늘 날씨 정말 좋다")
conversation.predict(input="pandas 라이브러리가 필요해")
conversation.predict(input="저녁으로 뭐 먹을까")
# 파이썬 관련 질문 - 관련된 대화만 자동 검색됨
response = conversation.predict(input="내 파이썬 버전이 뭐였지?")
# "3.9" 관련 대화만 검색되어 전달됨 (날씨, 저녁 대화는 제외)
설명
이것이 하는 일: 위 코드는 VectorStoreRetrieverMemory를 사용하여 모든 대화를 임베딩 벡터로 변환하여 FAISS에 저장하고, 새 질문이 들어오면 벡터 유사도 검색으로 가장 관련성 높은 대화만 선택하여 LLM에 전달하는 스마트 메모리 시스템을 구축합니다. 첫 번째로, OpenAIEmbeddings를 사용하여 임베딩 모델을 생성합니다.
이 모델은 텍스트를 1536차원의 숫자 벡터로 변환하는데, 의미가 비슷한 텍스트는 벡터 공간에서 가까운 위치에 놓입니다. 예를 들어 "파이썬 3.9"와 "파이썬 버전"은 벡터 거리가 가깝지만, "날씨"와는 거리가 멉니다.
FAISS는 Facebook이 만든 고속 벡터 검색 라이브러리로, 수백만 개의 벡터에서도 밀리초 단위로 유사한 벡터를 찾을 수 있습니다. 그 다음으로, VectorStoreRetrieverMemory를 생성할 때 retriever를 설정합니다.
search_kwargs={"k": 3}은 현재 질문과 가장 유사한 상위 3개 대화만 검색하도록 지정합니다. 대화가 추가될 때마다 자동으로 임베딩으로 변환되어 FAISS에 저장됩니다.
이 과정에서 추가 API 호출이 발생하지만, 임베딩은 LLM보다 훨씬 저렴(약 1/100)하므로 비용 효율적입니다. 마지막으로, 새 질문이 들어오면 먼저 질문 자체를 임베딩으로 변환합니다.
그리고 FAISS에서 코사인 유사도(cosine similarity)가 가장 높은 3개 대화를 검색합니다. "내 파이썬 버전이 뭐였지?"는 "파이썬 3.9" 대화와 유사도가 높고, "pandas 라이브러리" 대화와도 약간 관련이 있지만, "날씨"나 "저녁" 대화와는 유사도가 낮습니다.
결과적으로 관련성 높은 3개만 프롬프트에 포함되어 LLM에 전달되며, 불필요한 잡담은 자동으로 필터링됩니다. 여러분이 이 코드를 사용하면 수십~수백 회의 긴 대화에서도 항상 관련된 정보만 전달하여 LLM의 응답 품질을 높일 수 있습니다.
또한 토큰 사용량이 대화 길이에 비례하여 증가하지 않고 일정하게 유지되어(항상 3개만 전달) 비용을 예측 가능하게 만듭니다. 고객 지원에서 여러 주제를 오가는 대화, 교육 튜터에서 다양한 개념을 다루는 상황 등에서 특히 효과적입니다.
다만 임베딩 생성과 검색에 약간의 지연이 추가되므로 실시간 성능이 중요한 경우 캐싱 전략을 함께 사용하세요.
실전 팁
💡 k 값을 실험하세요. 간단한 대화는 k=2, 복잡한 대화는 k=5처럼 도메인에 맞게 조정하세요.
💡 하이브리드 접근을 고려하세요. 최근 2개는 무조건 포함하고, 나머지는 벡터 검색으로 선택하면 최신성과 관련성을 모두 확보할 수 있습니다.
💡 임베딩을 캐싱하세요. 동일한 대화를 여러 번 임베딩하지 않도록 Redis에 캐시하면 성능과 비용을 개선할 수 있습니다.
💡 메타데이터를 활용하세요. 대화에 타임스탬프, 주제 태그를 추가하여 "최근 1시간 내 파이썬 관련" 같은 복합 검색이 가능합니다.
💡 재순위화(re-ranking)를 적용하세요. 벡터 검색 후 LLM으로 한번 더 필터링하면 정확도가 크게 향상되지만 비용이 증가합니다.