Memory

Joo Hee Paige Kim
17 min read2 hours ago

--

2024.10.08

대화 버퍼 메모리(ConversationBufferMemory)

  • 메시지를 저장한 다음 변수에 메시지를 추출

대화 기록 저장

save_context(inputs, outputs) 메서드를 사용

  • inputs = 사용자 입력
  • outputs = AI 출력 저장
  • 대화기록이 history 키에 저장

대화 기록 조회

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

Chain 적용

  • RunnableWithMessageHistory 이용

대화 버퍼 윈도우 메모리(ConversationBufferWindowMemory)

  • 시간이 지남에 따라 대화의 상호작용 목록을 유지
  • 모든 대화내용을 활용하는 것이 아닌 최근 K개 의 상호작용만 사용
from langchain.memory import ConversationBufferWindowMemory

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

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

...

memory.load_memory_variables({})["history"]
  • 버퍼가 너무 커지지 않도록 가장 최근 상호작용의 슬라이딩 창을 유지하는 데 유용

대화 토큰 버퍼 메모리(ConversationTokenBufferMemory)

  • 최근 대화의 히스토리를 버퍼를 메모리에 보관
  • 대화의 개수가 아닌 토큰 길이 를 사용하여 대화내용을 플러시(flush)할 시기를 결정

max_token_limit

  • 대화 내용을 저장할 최대 토큰의 길이 설정
from langchain.memory import ConversationTokenBufferMemory
from langchain_openai import ChatOpenAI


# LLM 모델 생성
llm = ChatOpenAI(model_name="gpt-4o")

# 메모리 설정
memory = ConversationTokenBufferMemory(
llm=llm, max_token_limit=150, return_messages=True # 최대 토큰 길이를 50개로 제한
)

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

# 대화내용을 확인
memory.load_memory_variables({})["history"]

대화 엔티티 메모리(ConversationEntityMemory)

  • 특정 엔티티에 대한 주어진 사실을 기억
  • 엔티티에 대한 정보를 추출하고(LLM 사용) 시간이 지남에 따라 해당 엔티티에 대한 지식을 축적

prompt 사용

  • Entity 메모리를 효과적으로 사용하기 위하여

대화 지식그래프 메모리(ConversationKGMemory)

  • 지식 그래프의 힘을 활용하여 정보를 저장하고 불러옴
  • 모델이 서로 다른 개체 간의 관계를 이해하는 데 도움을 주고, 복잡한 연결망과 역사적 맥락을 기반으로 대응하는 능력을 향상
from langchain_openai import ChatOpenAI
from langchain.memory import ConversationKGMemory

llm = ChatOpenAI(temperature=0)

memory = ConversationKGMemory(llm=llm, return_messages=True)
memory.save_context(
{"input": "이쪽은 Pangyo 에 거주중인 김셜리씨 입니다."},
{"output": "김셜리씨는 누구시죠?"},
)
memory.save_context(
{"input": "김셜리씨는 우리 회사의 신입 디자이너입니다."},
{"output": "만나서 반갑습니다."},
)

memory.load_memory_variables({"input": "김셜리씨는 누구입니까?"})

대화 요약 메모리(ConversationSummaryMemory)

  • 시간 경과에 따른 대화의 요약 을 생성
  • 시간 경과에 따른 대화의 정보를 압축하는 데 유용할
  • 대화가 진행되는 동안 대화를 요약하고 현재 요약을 메모리에 저장
  • 지금까지의 대화 요약을 프롬프트/체인에 삽입 가능
  • 과거 메시지 기록을 프롬프트에 그대로 보관하면 토큰을 너무 많이 차지할 수 있는 긴 대화에 가장 유용

ConversationSummaryMemory

from langchain.memory import ConversationSummaryMemory
from langchain_openai import ChatOpenAI

memory = ConversationSummaryMemory(
llm=ChatOpenAI(model_name="gpt-4o", temperature=0), return_messages=True)

ConversationSummaryBufferMemory

  • 최근 대화내용의 버퍼를 메모리에 유지하되, 이전 대화내용을 완전히 플러시(flush)하지 않고 요약으로 컴파일하여 두 가지를 모두 사용
  • 대화내용을 플러시할 시기를 결정하기 위해 상호작용의 개수가 아닌 토큰 길이 를 사용
from langchain_openai import ChatOpenAI
from langchain.memory import ConversationSummaryBufferMemory

llm = ChatOpenAI()

memory = ConversationSummaryBufferMemory(
llm=llm,
max_token_limit=200, # 요약의 기준이 되는 토큰 길이를 설정
return_messages=True,
)

벡터저장소 검색 메모리(VectorStoreRetrieverMemory)

  • 벡터 스토어에 메모리를 저장하고 호출될 때마다 가장 ‘눈에 띄는’ 상위 K개의 문서를 쿼리
  • 대화내용의 순서를 명시적으로 추적하지 않는다는 점 에서 다른 대부분의 메모리 클래스와 다름
import faiss
from langchain_openai import OpenAIEmbeddings
from langchain.docstore import InMemoryDocstore
from langchain.vectorstores import FAISS
from langchain.memory import VectorStoreRetrieverMemory

# 임베딩 모델을 정의
embeddings_model = OpenAIEmbeddings()

# Vector Store 를 초기화
embedding_size = 1536
index = faiss.IndexFlatL2(embedding_size)
vectorstore = FAISS(embeddings_model, index, InMemoryDocstore({}), {})

retriever = vectorstore.as_retriever(search_kwargs={"k": 1})
memory = VectorStoreRetrieverMemory(retriever=retriever)

search_kwargs = { “k” : 1 }

  • Vector Store 로 부터 1개(k=1 이기 때문)의 가장 관련성 높은 대화를 반환

LCEL Chain 에 메모리 추가

  • 임의의 체인에 메모리를 추가하는 방법
  • 현재 메모리 클래스를 사용할 수 있지만 수동으로 연결
from operator import itemgetter
from langchain.memory import ConversationBufferMemory
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables import RunnableLambda, RunnablePassthrough
from langchain_openai import ChatOpenAI


# ChatOpenAI 모델을 초기화
model = ChatOpenAI()

# 대화형 프롬프트를 생성
# 이 프롬프트는 시스템 메시지, 이전 대화 내역, 그리고 사용자 입력을 포함
prompt = ChatPromptTemplate.from_messages(
[
("system", "You are a helpful chatbot"),
MessagesPlaceholder(variable_name="chat_history"),
("human", "{input}"),
]
)

대화내용을 저장할 메모리인 ConversationBufferMemory 생성하고 return_messages 매개변수를 True로 설정하여, 생성된 인스턴스가 메시지를 반환하

memory_key 설정:

  • 추후 Chain 의 prompt 안에 대입될 key 입니다. 변경하여 사용할 수 있습니다.
# 대화 버퍼 메모리를 생성하고, 메시지 반환 기능을 활성화
memory = ConversationBufferMemory(return_messages=True, memory_key="chat_history")

# 메모리 변수를 빈 딕셔너리로 초기화
memory.load_memory_variables({})

RunnablePassthrough.assign을 사용하여 chat_history 변수에 memory.load_memory_variables 함수의 결과를 할당하고, 이 결과에서 chat_history 키에 해당하는 값을 추출

runnable = RunnablePassthrough.assign(
chat_history=RunnableLambda(memory.load_memory_variables)
| itemgetter("chat_history") # memory_key 와 동일하게 입력
)

prompt = ChatPromptTemplate.from_messages(
[
("system", "You are a helpful chatbot"),
MessagesPlaceholder(variable_name="chat_history"),
("human", "{input}"),
]
)

chain = runnable | prompt | model

# chain 객체의 invoke 메서드를 사용하여 입력에 대한 응답을 생성
response = chain.invoke({"input": "만나서 반갑습니다. 제 이름은 테디입니다."})
# invoke만 하였기 때문에 빈 배열 반환 {'chat_history': []}
memory.load_memory_variables({})


# 입력된 데이터와 응답 내용을 메모리에 저장
memory.save_context(
{"human": "만나서 반갑습니다. 제 이름은 테디입니다."}, {"ai": response.content}
)

# 저장된 대화기록을 출력합니다.
memory.load_memory_variables({})
# {'chat_history': [HumanMessage(content='만나서 반갑습니다. 제 이름은 테디입니다.'),
# AIMessage(content='만나서 반가워요, 테디님! 무엇을 도와드릴까요?')]}

커스텀 ConversationChain 구현

class MyConversationChain(Runnable):

def __init__(self, llm, prompt, memory, input_key="input"):

self.prompt = prompt
self.memory = memory
self.input_key = input_key

self.chain = (
RunnablePassthrough.assign(
chat_history=RunnableLambda(self.memory.load_memory_variables)
| itemgetter(memory.memory_key) # memory_key 와 동일하게 입력합니다.
)
| prompt
| llm
| StrOutputParser()
)

def invoke(self, query, configs=None, **kwargs):
answer = self.chain.invoke({self.input_key: query})
self.memory.save_context(inputs={"human": query}, outputs={"ai": answer})
return answer


# ChatOpenAI 모델을 초기화
llm = ChatOpenAI(model_name="gpt-4o", temperature=0)

# 대화형 프롬프트를 생성
prompt = ChatPromptTemplate.from_messages(
[
("system", "You are a helpful chatbot"),
MessagesPlaceholder(variable_name="chat_history"),
("human", "{input}"),
]
)

# 대화 버퍼 메모리를 생성하고, 메시지 반환 기능을 활성화
memory = ConversationBufferMemory(return_messages=True, memory_key="chat_history")

# 요약 메모리로 교체할 경우
# memory = ConversationSummaryMemory(
# llm=llm, return_messages=True, memory_key="chat_history"
# )

conversation_chain = MyConversationChain(llm, prompt, memory)

SQLite 에 대화내용 저장

  • Structured Query Language (SQL)은 프로그래밍에 사용되는 도메인 특화 언어로, 관계형 데이터베이스 관리 시스템(RDBMS)에서 데이터를 관리하거나 관계형 데이터 스트림 관리 시스템(RDSMS)에서 스트림 처리를 위해 설계
  • 특히 엔티티와 변수 간의 관계를 포함하는 구조화된 데이터를 다루는 데 유용
  • SQLAlchemy는 MIT 라이선스에 따라 배포되는 Python 프로그래밍 언어용 오픈 소스 SQL 툴킷이자 객체 관계 매퍼(ORM)
  1. session_id - 사용자 이름, 이메일, 채팅 ID 등과 같은 세션의 고유 식별자
  2. connection - 데이터베이스 연결을 지정하는 문자열이고 SQLAlchemy의 create_engine 함수에 전달
from langchain_community.chat_message_histories import SQLChatMessageHistory

# SQLChatMessageHistory 객체를 생성하고 세션 ID와 데이터베이스 연결 파일을 설정
chat_message_history = SQLChatMessageHistory(
session_id="sql_history", connection="sqlite:///sqlite.db"
)

# 사용자 메시지를 추가
chat_message_history.add_user_message(
"안녕? 만나서 반가워. 내 이름은 테디야. 나는 랭체인 개발자야. 앞으로 잘 부탁해!"
)
# AI 메시지를 추가
chat_message_history.add_ai_message("안녕 테디, 만나서 반가워. 나도 잘 부탁해!")
# 메시지 조회
chat_message_history.messages

Chain 에 적용

메시지 기록 클래스를 LCEL Runnables 와 쉽게 결합 가능

from langchain_core.prompts import (
ChatPromptTemplate,
MessagesPlaceholder,
)
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables.utils import ConfigurableFieldSpec

prompt = ChatPromptTemplate.from_messages(
[
# 시스템 메시지
("system", "You are a helpful assistant."),
# 대화 기록을 위한 Placeholder
MessagesPlaceholder(variable_name="chat_history"),
("human", "{question}"), # 질문
]
)

# chain 을 생성
chain = prompt | ChatOpenAI(model_name="gpt-4o") | StrOutputParser()

# sqlite.db에서 대화 내용 추출
def get_chat_history(user_id, conversation_id):
return SQLChatMessageHistory(
table_name=user_id,
session_id=conversation_id,
connection="sqlite:///sqlite.db",
)

# config_fioelds 설정하여 대화정보 조회할 때 참고 정보 활용
config_fields = [
ConfigurableFieldSpec(
id="user_id",
annotation=str,
name="User ID",
description="Unique identifier for a user.",
default="",
is_shared=True,
),
ConfigurableFieldSpec(
id="conversation_id",
annotation=str,
name="Conversation ID",
description="Unique identifier for a conversation.",
default="",
is_shared=True,
),
]

chain_with_history = RunnableWithMessageHistory(
chain,
get_chat_history, # 대화 기록을 가져오는 함수를 설정
input_messages_key="question", # 입력 메시지의 키를 "question"으로 설정
history_messages_key="chat_history", # 대화 기록 메시지의 키를 "history"로 설정
history_factory_config=config_fields, # 대화 기록 조회시 참고할 파라미터를 설정
)

# config 설정
config = {"configurable": {"user_id": "user1", "conversation_id": "conversation1"}}

# 질문과 config 를 전달하여 실행
chain_with_history.invoke({"question": "안녕 반가워, 내 이름은 테디야"}, config)
  • config를 바꾸면 실행 제대로 안됨

--

--