LangChain에는 LLM과 대화시 대화 내용을 저장하기 위한 Memory 기능이 있습니다. OpenAI API와 같은 REST API는 상태를 저장하지 않습니다(stateless). 따라서 API 실행시 이전 대화 내용을 전달하지 않고 새로운 메시지만 던지면 처음 받는 질문으로 판단하고 대답하게 됩니다. 대화를 이어가기 위해서는 지난 대화 내용을 프롬프트에 포함해 전달하면서 새로운 메시지를 추가해야 합니다. 지난 대화 내용을 저장할 때 사용하는 것이 바로 Memory입니다.
그런데, 대화가 길어지면 문제가 생깁니다. API를 통해 한 번에 입출력할 수 있는 토큰 수가 정해져 있죠. 지난 대화가 길면 토큰 수를 넘어가서 답변을 듣지 못하게 됩니다. 물론 API 사용료도 더 많이 나가죠. 그래서 LangChain의 Memory 중에는 지난 대화 내용을 줄이는 Memory도 있습니다. 기본적인 메모리들을 몇 가지 살펴보겠습니다.
- ConversationBufferMemory
- ConversationBufferWindowMemory
- ConversationTokenBufferMemory
- ConversationSummaryBufferMemory
ConversationBufferMemory
ConversationBufferMemory는 지난 대화 내용을 그대로 저장하는 메모리입니다.
from langchain.chat_models import ChatOpenAI
from langchain.chains import ConversationChain
from langchain.memory import ConversationBufferMemory
llm = ChatOpenAI(temperature=0.0)
memory = ConversationBufferMemory()
conversation = ConversationChain(
llm=llm,
memory = memory
)
conversation.predict(input="안녕, 나는 토니 스타크라고 해.")
### 결과(응답)
'안녕, 토니 스타크! 반가워요. 저는 OpenAI의 인공지능입니다. 무엇을 도와드릴까요?'
코드에서 사용한 ConversationChain은 한 번 응답을 받는 LLMChain과 달리 여러 번 대화를 주고 받을 때 사용하는 Chain입니다. Chain을 생성할 때 memory 인자에 LangChain의 Memory 객체를 입력하는 방식으로 Memory를 사용합니다. Chain 내부에서는 사용자가 input 메시지를 입력하면 메모리에 메시지를 추가한 후 LLM에 전달하고, LLM이 응답하면 메모리에 응답을 추가한 후 사용자에게 응답을 반환합니다. 대화를 이어나가겠습니다.
conversation.predict(input="내 인공지능 비서 이름이 뭐지?")
# 응답: '당신의 인공지능 비서의 이름은 "지니"입니다. 어떤 일을 도와드릴까요?'
conversation.predict(input="내 이름이 뭐였지?")
# 응답: '당신의 이름은 "토니 스타크"입니다.'
이렇게 이전 대화 내용을 기억하며 대화를 이어나갈 수 있습니다. 메모리에 저장된 내용은 다음과 같이 확인할 수 있습니다.
print(memory.buffer)
### 결과
Human: 안녕, 나는 토니 스타크라고 해.
AI: 안녕, 토니 스타크! 반가워요. 저는 OpenAI의 인공지능입니다. 무엇을 도와드릴까요?
Human: 내 인공지능 비서 이름이 뭐지?
AI: 당신의 인공지능 비서의 이름은 "지니"입니다. 어떤 일을 도와드릴까요?
Human: 내 이름이 뭐였지?
AI: 당신의 이름은 "토니 스타크"입니다.
load_memory_variables를 이용할 수도 있습니다.
memory.load_memory_variables({})
### 결과
{'history': 'Human: 안녕, 나는 토니 스타크라고 해.\nAI: 안녕, 토니 스타크! 반가워요. 저는 OpenAI의 인공지능입니다. 무엇을 도와드릴까요?\nHuman: 내 인공지능 비서 이름이 뭐지?\nAI: 당신의 인공지능 비서의 이름은 "지니"입니다. 어떤 일을 도와드릴까요?\nHuman: 내 이름이 뭐였지?\nAI: 당신의 이름은 "토니 스타크"입니다.'}
save_context를 이용해 메모리에 직접 내용을 추가할 수도 있습니다.
memory = ConversationBufferMemory()
memory.save_context({"input": "안녕, 나는 토니 스타크라고 해."},
{"output": "안녕하세요, 토니 스타크님! 반갑습니다."})
print(memory.buffer)
### 결과
Human: 안녕, 나는 토니 스타크라고 해.
AI: 안녕하세요, 토니 스타크님! 반갑습니다.
더 추가해볼까요?
memory.save_context({"input": "내 비서 이름이 뭐지?"},
{"output": "JARVIS입니다."})
memory.load_memory_variables({})
### 결과
{'history': 'Human: 안녕, 나는 토니 스타크라고 해.\nAI: 안녕하세요, 토니 스타크님! 반갑습니다.\nHuman: 내 비서 이름이 뭐지?\nAI: JARVIS입니다.'}
ConversationBufferWindowMemory
ConversationBufferWindowMemory는 윈도우 크기 k를 지정하면 최근 k개의 대화만 기억하고 이전 대화는 삭제합니다. 여기서 1개의 대화는 한 번 주고 받는 대화를 말합니다.
from langchain.memory import ConversationBufferWindowMemory
memory = ConversationBufferWindowMemory(k=1)
memory.save_context({"input": "안녕, 나는 토니 스타크라고 해."},
{"output": "안녕하세요, 토니 스타크님! 반갑습니다."})
memory.save_context({"input": "내 비서 이름이 뭐지?"},
{"output": "JARVIS입니다."})
memory.load_memory_variables({})
### 결과
{'history': 'Human: 내 비서 이름이 뭐지?\nAI: JARVIS입니다.'}
k=1로 했더니 마지막 대화만 기억하고 그 앞의 대화는 잊어버렸습니다.
memory = ConversationBufferWindowMemory(k=1)
conversation = ConversationChain(
llm=llm,
memory = memory
)
conversation.predict(input="안녕, 나는 토니 스타크라고 해.")
# 응답: '안녕, 토니 스타크! 반가워요. 저는 OpenAI의 인공지능입니다. 무엇을 도와드릴까요?'
conversation.predict(input="내 인공지능 비서 이름은 뭐지?")
# 응답: '당신의 인공지능 비서의 이름은 "제이비"입니다.'
conversation.predict(input="내 이름이 뭐라고?")
# 응답: '죄송합니다, 저는 당신의 이름을 알 수 없습니다.'
이전 대화를 잊어버렸으니 이전 대화에 있던 정보를 물으면 답할 수가 없습니다. 보통 대화에서는 k=1보다 큰 값을 사용합니다.
ConversationTokenBufferMemory
ConversationTokenBufferMemory는 기억되는 토큰 수를 제한합니다. 최대 토큰 수를 넘지 않는 범위에서 최근 대화들을 기억합니다.
from langchain.memory import ConversationTokenBufferMemory
memory = ConversationTokenBufferMemory(llm=llm, max_token_limit=50)
memory.save_context({"input": "안녕, 나는 토니 스타크라고 해."},
{"output": "안녕, 토니 스타크! 반가워요. 저는 OpenAI의 인공지능입니다. 무엇을 도와드릴까요?"})
memory.save_context({"input": "내 인공지능 비서 이름은 뭐지?"},
{"output": "당신의 인공지능 비서의 이름은 JARVIS입니다."})
memory.save_context({"input": "내 이름이 뭐라고?"},
{"output": "당신의 이름은 '토니 스타크'입니다."})
memory.load_memory_variables({})
### 결과
{'history': "Human: 내 이름이 뭐라고?\nAI: 당신의 이름은 '토니 스타크'입니다."}
최대 토큰 수를 50으로 작게 잡았기 때문에 마지막 대화밖에 안 남았습니다.
ConversationSummaryBufferMemory
ConversationSummaryMemory는 이전 대화 내용 그대로 기억하지 않고 요약해서 기억합니다. 요약은 LLM이 하기 때문에 Memory 생성시 LLM을 지정해줍니다. ConversationSummaryBufferMemory는 ConversationSummaryMemory의 확장판이라고 볼 수 있습니다. 이전 대화를 요약해 기억하면서 최근 대화는 최대 토큰 범위 내에서 유지합니다. 이를 위해 최대 토큰 길이도 지정해줍니다. 코드에서는 ConversationSummaryBufferMemory를 사용하겠습니다.
from langchain.memory import ConversationSummaryBufferMemory
schedule = "내일 아침 8시에는 퓨리랑 미팅이 있습니다. \
어벤져스와 가디언즈 오브 갤럭시 팀의 관계에 대해 상의할 예정입니다. \
12시에는 페퍼와 점심식사가 있습니다. 치즈버거를 먹기로 했습니다. \
3시에는 해피와 스파이더맨 교육 계획 회의가 있습니다 \
5시에는 아크 원자로 개선 버전 공개 행사가 있습니다. \
간단한 소개 자료를 준비해 놓았습니다."
memory = ConversationSummaryBufferMemory(llm=llm, max_token_limit=100)
memory.save_context({"input": "자비스"}, {"output": "네, 말씀하세요."})
memory.save_context({"input": "자기 전에 확인할게 있어."},
{"output": "무엇을 확인해드릴까요?"})
memory.save_context({"input": "내일 일정에 대해 알려줘."},
{"output": f"{schedule}"})
memory.load_memory_variables({})
### 결과
{'history': 'System: The human asks the AI to check something before they go to bed. The AI asks what they would like to check. The human asks the AI to inform them about their schedule for tomorrow. The AI informs the human that they have a meeting with Fury at 8 am to discuss the relationship between the Avengers and the Guardians of the Galaxy teams. At 12 pm, they have a lunch with Pepper, where they will be having a cheeseburger. At 3 pm, they have a meeting with Happy and Spiderman to discuss educational plans. At 5 pm, there is a public event for the unveiling of an improved version of the Arc Reactor, and the AI has prepared a brief introduction material.'}
대화 내용을 요약해서 기억하는 것을 볼 수 있습니다. 대화를 이어가면 메모리 내용이 어떻게 달라지는가 봅시다.
conversation = ConversationChain(
llm=llm,
memory = memory,
verbose=True
)
conversation.predict(input="행사 전 4시에 닥터 스트레인지와 약속을 추가해줘.")
# 응답: '행사 전 4시에 닥터 스트레인지와 약속을 추가했습니다.'
memory.load_memory_variables({})
### 결과
{'history': 'System: The human asks the AI to check something before they go to bed. The AI asks what they would like to check. The human asks the AI to inform them about their schedule for tomorrow. The AI informs the human that they have a meeting with Fury at 8 am to discuss the relationship between the Avengers and the Guardians of the Galaxy teams. At 12 pm, they have a lunch with Pepper, where they will be having a cheeseburger. At 3 pm, they have a meeting with Happy and Spiderman to discuss educational plans. At 5 pm, there is a public event for the unveiling of an improved version of the Arc Reactor, and the AI has prepared a brief introduction material.\nHuman: 행사 전 4시에 닥터 스트레인지와 약속을 추가해줘.\nAI: 행사 전 4시에 닥터 스트레인지와 약속을 추가했습니다.'}
앞의 내용은 요약되어 있지만 최근 대화는 그대로 들어가 있습니다. 대화를 더 이어가봅시다.
conversation.predict(input="내일 행사 참석 인원은 100명 정도야.")
# 응답: '내일 행사에는 약 100명 정도의 참석자가 있을 것으로 예상됩니다.'
memory.load_memory_variables({})
### 결과
{'history': 'System: The human asks the AI to check something before they go to bed. The AI asks what they would like to check. The human asks the AI to inform them about their schedule for tomorrow. The AI informs the human that they have a meeting with Fury at 8 am to discuss the relationship between the Avengers and the Guardians of the Galaxy teams. At 12 pm, they have a lunch with Pepper, where they will be having a cheeseburger. At 3 pm, they have a meeting with Happy and Spiderman to discuss educational plans. At 5 pm, there is a public event for the unveiling of an improved version of the Arc Reactor, and the AI has prepared a brief introduction material. The human requests to add a meeting with Doctor Strange at 4 pm before the event.\nAI: 행사 전 4시에 닥터 스트레인지와 약속을 추가했습니다.\nHuman: 내일 행사 참석 인원은 100명 정도야.\nAI: 내일 행사에는 약 100명 정도의 참석자가 있을 것으로 예상됩니다.'}
앞에서 그대로 들어가 있던 대화의 일부가 요약에 포함된 것을 볼 수 있습니다.
conversation.predict(input="내일 원자로 소개할 때 뭐라고 시작할까?")
# 응답
'내일 원자로 소개할 때, "안녕하세요 여러분, 오늘 저희는 개선된 아크 리액터의 공개 행사에 참석하게 되어 매우 기쁩니다. 이번 개발은 이전 버전에 비해 더욱 효율적이고 안전한 에너지 공급을 제공합니다. 이 아크 리액터는 우리의 노력과 협력으로 만들어진 결과물이며, 앞으로의 미래에 대한 희망과 발전을 상징합니다. 이제부터 자세한 소개를 시작하도록 하겠습니다."라고 시작하시면 좋을 것 같습니다.'
마지막 대화가 길었습니다. 토큰 수 제한이 있기 때문에 마지막에 주고 받은 대화가 길어지면 다음과 같이 마지막 대화까지 요약에 포함될 수도 있습니다.
memory.load_memory_variables({})
### 결과
{'history': 'System: The human asks the AI to check their schedule for tomorrow. The AI informs the human that they have a meeting with Fury at 8 am, a lunch with Pepper at 12 pm, a meeting with Happy and Spiderman at 3 pm, and a public event for the unveiling of an improved version of the Arc Reactor at 5 pm. The human requests to add a meeting with Doctor Strange at 4 pm before the event. The AI adds the meeting to the schedule. The human mentions that there will be around 100 attendees at the event. The AI estimates that there will be about 100 attendees. The human asks for suggestions on how to start the introduction for the Arc Reactor unveiling. The AI suggests starting with "Hello everyone, we are delighted to have you all here today for the unveiling of the improved Arc Reactor. This development provides more efficient and safe energy supply compared to the previous version. It is a result of our efforts and collaboration, symbolizing hope and progress for the future. Now, let\'s begin with a detailed introduction."'}
이전 대화 내용을 요약하는 메모리의 경우 요약을 위해 내부적으로 LLM 호출이 추가된다는 점을 기억합시다.
(추가) OpenAI Thread
2023년 11월 6일 OpenAI에서 새로운 API들을 발표했습니다. 그중 Assistants API의 Thread가 있는데, Thread에 Message를 추가하면 OpenAI 서버에 대화 내용을 저장해주고, 프롬프트에 이전 대화 내용을 넣는 과정도 알아서 진행해줍니다. 참고: OpenAI Assistants API로 대화 내용 저장하기