728x90
반응형

오늘은 24일차 학습 기록으로, 테디노트의 RAG 비법노트 강의 중 Memory 파트를 다뤄보려 합니다.

 

대화형 AI를 만들 때 빠질 수 없는 요소가 바로 Memory인데요,

사용자와의 이전 대화를 기억하고, 이를 바탕으로 맥락 있는 응답을 이어가는 핵심 역할을 합니다.

 

이번 포스팅에서는 LangChain을 활용한 GPT 및 로컬 모델 기반 RAG 강의에서 다룬 Memory 개념과 활용법을 정리하고, 실제로 느낀 점과 배운 인사이트를 공유해보겠습니다.


1. ConversationBufferMemory

대화형 AI에서 가장 기본적이고 직관적인 메모리 방식이 바로 ConversationBufferMemory입니다.

이 방식은 말 그대로 사용자와 모델 간의 모든 대화를 순차적으로 버퍼에 저장하고, 새로운 입력이 들어올 때마다

이전 기록을 그대로 붙여서 모델에게 전달합니다.

특징

  • 간단함: 구현이 단순하고 바로 적용 가능.
  • 맥락 유지: 이전 대화를 그대로 이어주기 때문에 모델이 맥락을 잘 파악할 수 있음.
  • 토큰 낭비: 하지만 대화가 길어질수록 토큰이 계속 늘어나 비효율적.

코드 예시(LangChain)

우선, 기본 객체 생성과 상태를 보겠습니다.

from langchain.memory import ConversationBufferMemory

memory = ConversationBufferMemory()
print(f"memory: {memory}")

 

출력 예시:

memory: chat_memory=InMemoryChatMessageHistory(messages=[])
[/var/folders/d9/hbg9z20n23n364852jwpcdlr0000gn/T/ipykernel_13474/2541306174.py:3](<https://file+.vscode-resource.vscode-cdn.net/var/folders/d9/hbg9z20n23n364852jwpcdlr0000gn/T/ipykernel_13474/2541306174.py:3>): LangChainDeprecationWarning: Please see the migration guide at: <https://python.langchain.com/docs/versions/migrating_memory/>
  memory = ConversationBufferMemory()
  • 기본값으로 생성하면 history를 하나의 문자열로 관리합니다.
  • 이 문자열은 “Human: …\nAI: …” 형식으로 누적됩니다.

대화를 저장해보겠습니다.

 

save_context(inputs, outputs) 메서드를 사용하여 대화 기록을 저장할 수 있습니다.

  • 이 메서드는 inputs와 outputs 두 개의 인자를 받습니다.
  • inputs는 사용자의 입력을, outputs는 AI의 출력을 저장합니다.
  • 이 메서드를 사용하면 대화 기록이 history 키에 저장됩니다.
  • 이후 load_memory_variables메서드를 사용하여 저장된 대화 기록을 확인할 수 있습니다.
# inputs: dictionary(key: "human" or "ai", value: 질문)
# outputs: dictionary(key: "ai" or "human", value: 답변)

memory.save_context(
    inputs={"human": "네, 신분증을 준비했습니다. 이제 무엇을 해야 하나요?"},
    outputs={
        "ai": "감사합니다. 신분증 앞 뒤를 명확하게 촬영하여 업로드해 주세요. 이후 본인 인증 절차를 진행하겠습니다."
    }
)

print(memory.load_memory_variables({})['history'])

 

출력 결과:

Human: 안녕하세요, 비대면으로 은행 계좌를 개설하고 싶습니다. 어떻게 시작해야 하나요?
AI: 안녕하세요! 계좌 개설을 원하신다니 기쁩니다. 먼저, 본인 인증을 위해 신분증을 준비해 주시겠어요?
Human: 네, 신분증을 준비했습니다. 이제 무엇을 해야 하나요?
AI: 감사합니다. 신분증 앞 뒤를 명확하게 촬영하여 업로드해 주세요. 이후 본인 인증 절차를 진행하겠습니다.

 

한 번 더 새로운 문장들로 저장해보겠습니다.

memory.save_context(
    inputs={"human": "사진을 업로드 했습니다. 본인 인증은 어떻게 진행되나요?"},
    outputs={
        "ai": "업로드해 주신 사진을 확인했습니다. 이제 휴대폰을 통한 본인 인증을 진행해 주세요. 문자로 발송된 인증번호를 입력해 주시면 됩니다."
    }
)

memory.save_context(
    inputs={"human": "인증번호를 입력했습니다. 계좌 개설은 이제 어떻게 하나요?"},
    outputs={
        "ai": "본인 인증이 완료되었습니다. 이제 원하는 계좌 종류를 선택하고 필요한 정보를 입력해 주세요. 예금 종류, 통화 종류 등을 선택할 수 있습니다."
    }
)

print(memory.load_memory_variables({})['history'])

 

출력 결과:

Human: 안녕하세요, 비대면으로 은행 계좌를 개설하고 싶습니다. 어떻게 시작해야 하나요?
AI: 안녕하세요! 계좌 개설을 원하신다니 기쁩니다. 먼저, 본인 인증을 위해 신분증을 준비해 주시겠어요?
Human: 네, 신분증을 준비했습니다. 이제 무엇을 해야 하나요?
AI: 감사합니다. 신분증 앞 뒤를 명확하게 촬영하여 업로드해 주세요. 이후 본인 인증 절차를 진행하겠습니다.
Human: 사진을 업로드 했습니다. 본인 인증은 어떻게 진행되나요?
AI: 업로드해 주신 사진을 확인했습니다. 이제 휴대폰을 통한 본인 인증을 진행해 주세요. 문자로 발송된 인증번호를 입력해 주시면 됩니다.
Human: 인증번호를 입력했습니다. 계좌 개설은 이제 어떻게 하나요?
AI: 본인 인증이 완료되었습니다. 이제 원하는 계좌 종류를 선택하고 필요한 정보를 입력해 주세요. 예금 종류, 통화 종류 등을 선택할 수 있습니다.

 

return_messages=True로 설정하면 HumanMessage와 AIMessage 객체를 반환합니다.

memory = ConversationBufferMemory(return_messages=True)

memory.save_context(
    inputs={
        "human": "안녕하세요, 비대면으로 은행 계좌를 개설하고 싶습니다. 어떻게 시작해야 하나요?"
    },
    outputs={
        "ai": "안녕하세요! 계좌 개설을 원하신다니 기쁩니다. 먼저, 본인 인증을 위해 신분증을 준비해 주시겠어요?"
    },
)

memory.save_context(
    inputs={"human": "네, 신분증을 준비했습니다. 이제 무엇을 해야 하나요?"},
    outputs={
        "ai": "감사합니다. 신분증 앞뒤를 명확하게 촬영하여 업로드해 주세요. 이후 본인 인증 절차를 진행하겠습니다."
    },
)

memory.save_context(
    inputs={"human": "사진을 업로드했습니다. 본인 인증은 어떻게 진행되나요?"},
    outputs={
        "ai": "업로드해 주신 사진을 확인했습니다. 이제 휴대폰을 통한 본인 인증을 진행해 주세요. 문자로 발송된 인증번호를 입력해 주시면 됩니다."
    },
)

print(memory.load_memory_variables({})['history'])
  • 이제 history는 문자열이 아니라 메시지 객체 리스트로 반환됩니다.

 

출력 결과:

[HumanMessage(content='안녕하세요, 비대면으로 은행 계좌를 개설하고 싶습니다. 어떻게 시작해야 하나요?', additional_kwargs={}, response_metadata={}), AIMessage(content='안녕하세요! 계좌 개설을 원하신다니 기쁩니다. 먼저, 본인 인증을 위해 신분증을 준비해 주시겠어요?', additional_kwargs={}, response_metadata={}), HumanMessage(content='네, 신분증을 준비했습니다. 이제 무엇을 해야 하나요?', additional_kwargs={}, response_metadata={}), AIMessage(content='감사합니다. 신분증 앞뒤를 명확하게 촬영하여 업로드해 주세요. 이후 본인 인증 절차를 진행하겠습니다.', additional_kwargs={}, response_metadata={}), HumanMessage(content='사진을 업로드했습니다. 본인 인증은 어떻게 진행되나요?', additional_kwargs={}, response_metadata={}), AIMessage(content='업로드해 주신 사진을 확인했습니다. 이제 휴대폰을 통한 본인 인증을 진행해 주세요. 문자로 발송된 인증번호를 입력해 주시면 됩니다.', additional_kwargs={}, response_metadata={})]
  • 모델/체인과 연결할 때 역할(role) 정보가 명확해져 후처리나 체인 결합이 편합니다.

 

이제는 Chain과 함께 사용해보도록 하겠습니다.

from langchain_openai import ChatOpenAI
from langchain.memory import ConversationBufferMemory
from langchain.chains import ConversationChain

llm = ChatOpenAI(temperature=0, model='gpt-4o')

conversation = ConversationChain(
    llm=llm,
    memory=ConversationBufferMemory()
)

response = conversation.predict(
    input="안녕하세요, 비대면으로 은행 계좌를 개설하고 싶습니다. 어떻게 시작해야 하나요?"
)
print(response)

response = conversation.predict(
    input="이전 답변을 불렛포인트 형식으로 정리하여 알려주세요."
)
print(response)

  • ConversationChain은 내부적으로 매 턴마다:
    1. memory.load_memory_variables()로 과거 history를 가져오고,
    2. 이번 턴의 입력과 함께 LLM에 전달한 뒤,
    3. 생성된 답변을 memory.save_context()로 다시 저장합니다.
  • 그래서 두 번째 요청에서 “이전 답변을 불렛포인트로”라고 해도, 모델은 메모리에 남아있는 이전 턴 맥락을 보고 응답할 수 있습니다.

ConversationBufferMemory는 “지금까지의 모든 대화”를 버퍼에 그대로 쌓아 매 턴 프롬프트에 붙여 넣는 가장 단순한 메모리입니다.

기본 설정에서는 history가 문자열로, return_messages=True에서는 메시지 리스트로 제공됩니다.

 

장점은 구현 간단/맥락 충실, 단점은 대화가 길어질수록 토큰 낭비가 커진다는 점입니다.


2. ConversationBufferWindowMemory

ConversationBufferMemory는 모든 대화 기록을 계속 붙여두기 때문에 대화가 길어질수록 토큰 낭비가 심해집니다. 이를 해결하기 위해 나온 것이 ConversationBufferWindowMemory입니다.

 

핵심 아이디어는 “최근 N턴만 기억하자” 입니다. → 마치 채팅창에서 직전 대화 몇 개만 보여주는 느낌

 

코드로 바로 살펴보겠습니다.

코드 예시(LangChain)

from langchain.memory import ConversationBufferWindowMemory

memory = ConversationBufferWindowMemory(k = 2, return_messages=True)

memory.save_context(
    inputs={
        "human": "안녕하세요. 비대면으로 은행 계좌를 개설하고 싶습니다. 어떻게 시작해야 하나요?"
    },
    outputs={
        "ai": "안녕하세요! 계좌 개설을 원하신다니 기쁩니다. 먼저, 본인 인증을 위해 신분증을 준비해 주시겠어요?"
    }
)
memory.save_context(
    inputs={"human": "네, 신분증을 준비했습니다. 이제 무엇을 해야 하나요?"},
    outputs={
        "ai": "감사합니다. 신분증 앞뒤를 명확하게 촬영하여 업로드해 주세요. 이후 본인 인증 절차를 진행하겠습니다."
    },
)
memory.save_context(
    inputs={"human": "사진을 업로드했습니다. 본인 인증은 어떻게 진행되나요?"},
    outputs={
        "ai": "업로드해 주신 사진을 확인했습니다. 이제 휴대폰을 통한 본인 인증을 진행해 주세요. 문자로 발송된 인증번호를 입력해 주시면 됩니다."
    },
)
memory.save_context(
    inputs={"human": "인증번호를 입력했습니다. 계좌 개설은 이제 어떻게 하나요?"},
    outputs={
        "ai": "본인 인증이 완료되었습니다. 이제 원하시는 계좌 종류를 선택하고 필요한 정보를 입력해 주세요. 예금 종류, 통화 종류 등을 선택할 수 있습니다."
    },
)
memory.save_context(
    inputs={"human": "정보를 모두 입력했습니다. 다음 단계는 무엇인가요?"},
    outputs={
        "ai": "입력해 주신 정보를 확인했습니다. 계좌 개설 절차가 거의 끝났습니다. 마지막으로 이용 약관에 동의해 주시고, 계좌 개설을 최종 확인해 주세요."
    },
)
memory.save_context(
    inputs={"human": "모든 절차를 완료했습니다. 계좌가 개설된 건가요?"},
    outputs={
        "ai": "네, 계좌 개설이 완료되었습니다. 고객님의 계좌 번호와 관련 정보는 등록하신 이메일로 발송되었습니다. 추가적인 도움이 필요하시면 언제든지 문의해 주세요. 감사합니다!"
    },
)

 

대화 기록을 확인해 보면 최근 2개의 메시지만 반환하는 것을 확인할 수 있습니다.

memory.load_memory_variables({})['history']

 

출력 결과:

[HumanMessage(content='정보를 모두 입력했습니다. 다음 단계는 무엇인가요?', additional_kwargs={}, response_metadata={}),
 AIMessage(content='입력해 주신 정보를 확인했습니다. 계좌 개설 절차가 거의 끝났습니다. 마지막으로 이용 약관에 동의해 주시고, 계좌 개설을 최종 확인해 주세요.', additional_kwargs={}, response_metadata={}),
 HumanMessage(content='모든 절차를 완료했습니다. 계좌가 개설된 건가요?', additional_kwargs={}, response_metadata={}),
 AIMessage(content='네, 계좌 개설이 완료되었습니다. 고객님의 계좌 번호와 관련 정보는 등록하신 이메일로 발송되었습니다. 추가적인 도움이 필요하시면 언제든지 문의해 주세요. 감사합니다!', additional_kwargs={}, response_metadata={})]

ConversationBufferWindowMemory는 “최근 N턴만 기억하는 메모리”입니다.

모든 대화를 저장하는 BufferMemory의 단점을 보완하여 토큰 사용량을 줄이고, 대화가 길어져도 모델이 최신 맥락에 집중할 수 있도록 합니다.

다만, 초반부 맥락이 중요하다면 적절한 k 값을 설정하거나 요약형 메모리와 함께 써야 합니다.


3. ConversationTokenBufferMemory

  • ConversationBufferMemory는 모든 대화를 저장 → 토큰 낭비 문제.
  • ConversationBufferWindowMemory는 최근 N턴만 저장 → 오래된 대화 맥락 손실 문제.
  • ConversationTokenBufferMemory는 이 둘을 절충해서,
  • 👉 “최근 대화 기록을 토큰 단위로 제한”하는 메모리입니다.

즉, 전체 기록 중에서 가장 최근 대화부터 추가하면서, 정해진 토큰 한도를 넘지 않도록 앞부분을 잘라냅니다.

코드 예시(LangChain)

from langchain.memory import ConversationTokenBufferMemory
from langchain_openai import ChatOpenAI

# LLM 필요 (토큰 수 계산용)
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

# 최근 100 토큰까지만 기억
memory = ConversationTokenBufferMemory(llm=llm, max_token_limit=50)

memory.save_context(
    inputs={"human": "안녕하세요, 비대면 계좌 개설이 가능한가요?"},
    outputs={"ai": "네, 가능합니다. 우선 신분증을 준비해주세요."}
)

memory.save_context(
    inputs={"human": "신분증 준비했습니다. 이제 어떻게 하나요?"},
    outputs={"ai": "앞뒤를 촬영해 업로드해주세요."}
)

memory.save_context(
    inputs={"human": "사진을 업로드했습니다. 다음 절차는 무엇인가요?"},
    outputs={"ai": "휴대폰 본인 인증을 진행해주세요."}
)

print(memory.load_memory_variables({})["history"])

 

출력 결과:

AI: 앞뒤를 촬영해 업로드해주세요.
Human: 사진을 업로드했습니다. 다음 절차는 무엇인가요?
AI: 휴대폰 본인 인증을 진행해주세요.
  • LangChain은 내부적으로 llm.get_num_tokens() 같은 메서드를 호출해서 현재 대화 기록의 토큰 수를 계산합니다.
  • max_token_limit을 초과하면 → 가장 오래된 대화부터 잘라냄.
  • 따라서 토큰 단위로 “슬라이딩 윈도우”처럼 동작한다고 보면 됩니다.

장점

  • 대화 길이에 따라 토큰 수를 정확히 관리 가능 → LLM의 context window를 넘지 않음.
  • WindowMemory보다 더 세밀한 제어 가능 (턴 수가 아니라 토큰 수 기준).
  • 긴 대화에서도 안정적으로 동작.

⚠️ 단점

  • 토큰 계산 때문에 약간의 연산 비용이 추가됨.
  • 지나치게 작은 max_token_limit을 주면 초반 맥락이 너무 빨리 사라짐.

4. ConversationEntityMemory

ConversationEntityMemory는 대화 중 등장하는개체(entities)— 예: 사람, 조직, 제품, 장소 등 — 와 그 개체에 대한 속성 정보를 자동으로 추출하고 저장합니다.

즉, 단순히 "대화 내용을 그대로 붙여넣는 것"이 아니라,

👉“누가 누구인지, 무슨 특징이 있는지”를 요약해 카드 형태로 관리

하는 메모리 방식입니다.

 

코드로 살펴보겠습니다

코드 예시(LangChain)

from langchain_openai import ChatOpenAI
from langchain.chains import ConversationChain
from langchain.memory import ConversationEntityMemory
from langchain.memory.prompt import ENTITY_MEMORY_CONVERSATION_TEMPLATE

 

Entity 메모리를 효과적으로 사용하기 위하여 제공되는 프롬프트를 사용합니다.

# Entity Memory를 사용하는 프롬프트 내용을 출력합니다.
print(ENTITY_MEMORY_CONVERSATION_TEMPLATE.template)

 

출력 결과:

You are an assistant to a human, powered by a large language model trained by OpenAI.

You are designed to be able to assist with a wide range of tasks, from answering simple questions to providing in-depth explanations and discussions on a wide range of topics. As a language model, you are able to generate human-like text based on the input you receive, allowing you to engage in natural-sounding conversations and provide responses that are coherent and relevant to the topic at hand.

You are constantly learning and improving, and your capabilities are constantly evolving. You are able to process and understand large amounts of text, and can use this knowledge to provide accurate and informative responses to a wide range of questions. You have access to some personalized information provided by the human in the Context section below. Additionally, you are able to generate your own text based on the input you receive, allowing you to engage in discussions and provide explanations and descriptions on a wide range of topics.

Overall, you are a powerful tool that can help with a wide range of tasks and provide valuable insights and information on a wide range of topics. Whether the human needs help with a specific question or just wants to have a conversation about a particular topic, you are here to assist.

Context:
{entities}

Current conversation:
{history}
Last line:
Human: {input}
You:
llm = ChatOpenAI(model='gpt-4o', temperature=0)

conversation = ConversationChain(
    llm=llm,
    prompt=ENTITY_MEMORY_CONVERSATION_TEMPLATE,
    memory=ConversationEntityMemory(llm=llm)
)

print(conversation)
  • ConversationChain에 ConversationEntityMemory를 연결.
  • llm은 ChatOpenAI 모델(gpt-4o).
  • 이 구조 덕분에 대화를 할 때마다 개체 추출 → 요약 → 저장 과정이 자동으로 실행됩니다.

대화를 시작합니다.

 

입력한 대화를 바탕으로 ConversationEntityMemory는 주요 Entity 정보를 별도로 저장합니다.

conversation.predict(
    input="테디와 셜리는 한 회사에서 일하는 동료입니다."
    "테디는 개발자이고 셜리는 디자이너입니다. "
    "그들은 최근 회사에서 일하는 것을 그만두고 자신들의 회사를 차릴 계획을 세우고 있습니다."
)

 

출력 결과:

'그렇군요! 테디와 셜리가 함께 회사를 차리기로 한 결정은 정말 흥미로운데요. 개발자와 디자이너의 조합은 새로운 제품이나 서비스를 창출하는 데 있어 강력한 시너지를 발휘할 수 있습니다. 그들이 어떤 분야에 집중할 계획인지, 그리고 어떤 목표를 가지고 있는지 궁금하네요. 혹시 그들의 계획에 대해 더 알고 계신 부분이 있나요?’

 

엔티티 메모리를 출력해보겠습니다.

print(conversation.memory.entity_store.store)

 

출력 결과:

{'테디': '테디는 개발자이며, 셜리와 함께 자신들의 회사를 차릴 계획을 세우고 있습니다.',
'셜리': '셜리는 디자이너로, 테디와 함께 자신들의 회사를 차릴 계획을 세우고 있습니다.'}

✅ 장점

  • 대화가 길어져도 토큰을 아끼면서 핵심 개체 정보만 유지 가능
  • “테디는 뭐 하는 사람이었지?” 같은 질문에도 정확하게 맥락을 이어갈 수 있음
  • 고객 상담, 멀티 세션(장기 대화) 시 사용자 프로필 관리에 유용

⚠️단점/주의할 점

  • 개체명이 흔하면(예: “영희”, “지훈”) 다른 사람과 혼동될 수 있음 → 식별자 추가 권장
  • 모델이 개체 추출을 잘못하면 틀린 사실이 저장될 수 있음 → 프롬프트 튜닝 필요

지금까지 본 4가지 메모리 타입에 대해 표로 요약해보았습니다:

메모리 타입 핵심 개념 무엇을 유지하나 장점 단점 이런 때 좋아요 주요 파라미터/옵션  load_memory_variables 반환
ConversationBufferMemory 대화 전체를 그대로 누적 모든 턴(문자열) 구현/이해가 가장 쉬움, 맥락 충실 길어질수록 토큰 폭증 짧은 데모, 프로토타입 return_messages {"history": str} 또는 메시지 리스트
ConversationBufferWindowMemory 최근 N턴만 유지 최근 k개의 턴 토큰 절약, 최신 맥락 유지 초반 정보 유실 긴 대화에서 최신 문맥만 필요 k, return_messages 동일
ConversationTokenBufferMemory 토큰 수 기준으로 최근만 유지 최근 기록(토큰 한도 내) 컨텍스트 윈도 한도 준수, 세밀 제어 토큰 계산 오버헤드, 한도 작으면 과도 손실 대화 길이 가변적일 때 안정적 운용 llm, max_token_limit, return_messages 동일
ConversationEntityMemory 대화에서 개체/속성을 추출해 카드처럼 요약 저장 인물·조직 등 엔티티별 요약 장기 정보 가볍게 유지, 검색/참조 용이 개체 추출 오류/동명이인 혼동 고객 프로필, 인물/제품 정보 장기 관리 llm, entity_extraction_prompt, entity_summarization_prompt {"history": str} + memory.entity_store.store로 엔티티 확인

빠른 팁

  • 짧고 빠른 데모 → BufferMemory
  • 긴 대화지만 최신 맥락만 → BufferWindowMemory(k=3~5)
  • 컨텍스트 초과가 잦음 → TokenBufferMemory(max_token_limit=…)
  • 사람/제품 같은 사실을 오래 기억 → EntityMemory (식별자·정규화 권장)

마무리

이번 글에서는 LangChain에서 제공하는 4가지 Memory 타입—ConversationBufferMemory, ConversationBufferWindowMemory, ConversationTokenBufferMemory, ConversationEntityMemory—를 살펴보았습니다.

  • BufferMemory는 가장 단순하지만 토큰이 기하급수적으로 늘어난다는 한계가 있었고,
  • BufferWindowMemory는 최근 N턴만 유지하여 토큰을 절약할 수 있었으며,
  • TokenBufferMemory는 토큰 단위로 세밀하게 제어할 수 있어 긴 대화에서도 안정적으로 동작했습니다.
  • 마지막으로 EntityMemory는 단순 대화 기록이 아닌 개체 중심의 지식 카드를 축적하여 장기적인 맥락을 관리하는 데 강력한 도구임을 확인했습니다.

➡️ 정리하자면, Memory는 단순한 “기록 장치”를 넘어, 대화형 AI가 사용자와 맥락 있는 상호작용을 이어가는 핵심 기술입니다.

 

프로젝트의 성격(짧은 데모, 긴 대화, 장기 사용자 관리 등)에 따라 적절한 메모리 방식을 선택하는 것이 중요합니다.

 

읽어주셔서 감사합니다.

 

728x90
반응형

+ Recent posts