카테고리 없음

#20. ChatGPT에서 대화 히스토리 유지하기

Terry Cho 2024. 2. 21. 15:52

조대협 (http://bcho.tistory.com)

노트 : 이글은 LLM 개발 프레임웍 Langchain의 일부 글입니다. 연재 글은 https://bcho.tistory.com/category/%EB%B9%85%EB%8D%B0%EC%9D%B4%ED%83%80%20%26%20%EB%A8%B8%EC%8B%A0%EB%9F%AC%EB%8B%9D/%EC%83%9D%EC%84%B1%ED%98%95%20AI%20%28ChatGPT%20etc%29 를 참고하세요.

 

LLM 기반의 챗봇 에서는 질문에 대한 답변을 기존의 대화의 내용이나 컨텍스트(문맥)을 참고하는 경우가 많다. 예를 들어, “서울에서 유명한 여행지는 어디야?” 라는 질문 후에, “그 근처에 맛있는 식당이 어디있어?” 라고 질문을 하면 챗봇은 서울의 유명한 여행지를 추천한 내용을 기반으로 해서, 그 근처의 맛있는 식당을 추천한다. 이렇게 기존 대화 내용을 참고하려면 챗봇이 기존 대화 내용을 알고 있어야 하는데, LLM 모델은 미리 학습이 된 모델로, 대화 내용을 기억할 수 있는 기능이 없고, Stateless 형태로 질문에 대한 답변만을 제공하는데, 최적화가 되어 있다. 

그렇다면 LLM 기반의 애플리케이션들은 어떻게 기존의 컨택스트를 기억할 수 있을까? 이렇게 기존의 컨택스트를 기억하는 기능이 langchain에서 Memory라는 컴포넌트이다. 

 

기본적인 개념은 다음과 같다. 

채팅 애플리케이션에 질문(Question 1)을 하면 애플리케이션에서 미리 정의되어 있는 프롬프트 템플릿에 질문을 추가하여 LLM에 질문한다. 답변이 나오면 질문 (Question 1)과 답변 (Answer 1)을 메모리에 저장한다.

<그림. 챗봇에서 첫번째 질문을 했을때 >

 

다음 대화에서 질문이 추가로 들어오면, 메모리에 저장된 기존의 대화 내용 (Question 1, Answer 1)을 불러서, 프롬프트 템플릿에 컨텍스트 정보로 추가하고, 여기에 더해서 새로운 질문 (Question 2)를 추가하여 LLM에 질의 한다. 

 

<그림. 챗봇에서 두번째 질문을 했을때 >

 

마찬가지로 Question 2, Answer 2를 다시 메모리에 저장한다. 

이렇게 대화 히스토리를 메모리에 저장한후, 새로운 대화가 시작될때 기존 대화 내용을 프롬프트안에 삽입하는 방식을 사용한다. 즉, 대화 히스토리를 기억할 수 있는 양은 LLM이 한번에 받아들일 수 있는 입력 텍스트 토큰의 길이와 동일하다. ChatGPT 3.5 Turbo의 경우 16K 토큰, ChatGPT 4.0 Turbo의 경우 128K를 사용할 수 있다. 

Conversational Buffer Memory

메모리를 지원하는 컴포넌트는 여러가지가 있는데, 그중에서 가장 기본적인 ConversationalBufferMemory를 먼저 살펴보자. 아래 예제는 ConversationalBufferMemory를 테스트하는 코드이다. ConversationalBufferMemory는 대화 내용을 그대로 저장하는 메모리 형태이다. 

이 메모리에 대화내용 (컨택스트)를 저장하기 위해서는 save_context를 이용하여, 사람의 질문은 input이라는 키로 전달하고,  챗봇의 답변은 output 키에 서술한다. 

from langchain.memory import ConversationBufferMemory

 

memory = ConversationBufferMemory(memory_key='chat_history',return_messages=True)

memory.clear()

memory.save_context({"input":"Hello chatbot!"},{"output":"Hello. How can I help you?"})

memory.save_context({"input":"My name is Terry"},{"output":"Nice to meet you Terry"})

memory.save_context({"input":"Where is Seoul?"},{"output":"Seoul is in Korea"})

 

memory.load_memory_variables({})

# returning list of chat message

 

ConversationBufferMemory를 생성한다. 이때 두 가지 옵션을 줄 수 있는 데, 첫번째는 memory_key, 두번째는 return_message 이다.

ConversationBufferMemory에 저장된 기존의 대화 내용은 결과적으로 프롬프트에 삽입되게 되는데, 프롬프트에 삽입되는 위치를 템플릿 변수로 지정한다. 이때 이 템플릿 변수의 이름이 “memory_key”의 값이 된다. 

return_messages는 메모리에서 대화 내용을 꺼낼 때 어떤 형식으로 꺼낼것인가인데, return_messages=True로 되어 있으면 아래 출력 결과를 HumanMessage, AIMessage  식의 리스트형태로 리턴을 해준다. 이 메세지 포맷은 Langchain에서 ChatModel을 사용할때 사용해야 하는 포맷이다. 

 

{'chat_history': [HumanMessage(content='Hello chatbot!'),

  AIMessage(content='Hello. How can I help you?'),

  HumanMessage(content='My name is Terry'),

  AIMessage(content='Nice to meet you Terry'),

  HumanMessage(content='Where is Seoul?'),

  AIMessage(content='Seoul is in Korea')]}



반대로 return_messages=False로 할 경우, 채팅 히스토리를 리스트 형식이 아니라, 아래와 같이 문자열로 리턴한다. 이 문자열 형태는 LLM 모델이 ChatModel 을 지원하지 않는 경우에 프롬프트 템플릿에 채팅히스토리 문자열을 삽입하는 방식으로 기존의 컨택스트를 유지하는 방식으로 유용하게 사용할 수 있다. 

 

{'chat_history': 'Human: Hello chatbot!\nAI: Hello. How can I help you?\nHuman: My name is Terry\nAI: Nice to meet you Terry\nHuman: Where is Seoul?\nAI: Seoul is in Korea'}



챗봇에서 Conversational Buffer Memory 

그러면, CoversationalBufferMemory를 이용해서, 챗봇 서비스를 제공하는 코드를 살펴보자. 

 

from langchain_openai import ChatOpenAI

from langchain.prompts import (

    ChatPromptTemplate,

    MessagesPlaceholder,

    SystemMessagePromptTemplate,

    HumanMessagePromptTemplate,

)

from langchain.chains import LLMChain

from langchain.memory import ConversationBufferMemory

import os

 

llm = ChatOpenAI(openai_api_key="{YOUR_OPENAI_KEY}")

prompt = ChatPromptTemplate(

    messages=[

        SystemMessagePromptTemplate.from_template(

            "You are a chatbot having a conversation with a human."

        ),

        MessagesPlaceholder(variable_name="chat_history"),

        HumanMessagePromptTemplate.from_template("{question}")

    ]

)

memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True)

conversation = LLMChain(

    llm=llm,

    prompt=prompt,

    verbose=True,

    memory=memory

)

 

conversation.invoke({"question": "hi my name is Terry"})

conversation.invoke({"question": "Can you recommend fun activities for me?"})

conversation.invoke({"question": "What is my name?"})

memory.load_memory_variables({})

채팅 애플리케이션이기 때문에, ChatOpenAI로 채팅 모델을 만들고, 채팅에서 사용할 프롬프트 템플릿을 정의한다. 아래와 같이 템플릿에는 SystemMessage에 LLM 모델의 역할을 챗봇이라고 정의했는데, 필요하다면 추가적인 프롬프트를 넣을 수 있다. MessagePlaceholder에는 외부로 부터 받은 컨택스트를 프롬프트에 포함하기 위한 위치인데, “chat_history”를 키로 해서, 이 부분에는 메모리에 저장된 기존의 채팅 히스토리 내용을 삽입한다.(이 키 값은 이후에 선언하는 ConversationalBufferMemory의 memory_key의 값과 일치해야 한다. ) 마지막으로 HumanMessagePrompt에는 {question}으로 들어온 내용을 삽입하는데, 이는 사용자가 챗봇에게 질의한 내용이 된다. 

 

prompt = ChatPromptTemplate(

    messages=[

        SystemMessagePromptTemplate.from_template(

            "You are a chatbot having a conversation with a human."

        ),

        MessagesPlaceholder(variable_name="chat_history"),

        HumanMessagePromptTemplate.from_template("{question}")

    ]

)

프롬프트가 준비되었으면 ConversationalBufferMemory를 생성하고, return_messages=True 로 하여, 메모리의 히스토리를 리턴할때 챗봇 형태의 리스트 데이터형으로 리턴하도록 한다. ConverationalBufferMemory에서 memory_key를 “chat_history”로 하여, 프롬프트에 “chat_history” 프롬프트 변수가 있는 곳에 채팅 히스토리를 삽입하도록 연결한다. 

 

프롬프트와 메모리가 준비되었으면, LLMChain을 생성하여, LLM 모델,프롬픝, 메모리를 연결한다. 

Chain 이 완성되었으면 Chain의 invoke 메서드를 이용하여 채팅 체인을 호출 한다. 이때 인자로는 “question”을 키로 하여 대화 내용을 전달한다. 

 

LLMChain 생성시 verbose를 True로 해놨기 때문에 중간 과정을 다음과 같이 모니터링 할 수 있다. 실행 결과는 다음과 같다. 3번의 대화에서 2번째,3번째 대화 내용은 각각 앞의 대화 내용을 모두 포함하는 것을 확인할 수 있다. 

> Entering new LLMChain chain...

Prompt after formatting:

System: You are a chatbot having a conversation with a human.

Human: hi my name is Terry

 

> Finished chain.



> Entering new LLMChain chain...

Prompt after formatting:

System: You are a chatbot having a conversation with a human.

Human: hi my name is Terry

AI: Hello Terry! Nice to meet you. How can I assist you today?

Human: Can you recommend fun activities for me?

 

> Finished chain.



> Entering new LLMChain chain...

Prompt after formatting:

System: You are a chatbot having a conversation with a human.

Human: hi my name is Terry

AI: Hello Terry! Nice to meet you. How can I assist you today?

Human: Can you recommend fun activities for me?

AI: Of course! I'd be happy to help. Could you let me know a bit more about your interests and preferences? That way, I can provide you with more tailored recommendations. Are you looking for indoor or outdoor activities? Do you have any particular hobbies or things you enjoy doing?

Human: What is my name?

 

> Finished chain.

{'chat_history': [HumanMessage(content='hi my name is Terry'),

  AIMessage(content='Hello Terry! Nice to meet you. How can I assist you today?'),

  HumanMessage(content='Can you recommend fun activities for me?'),

  AIMessage(content="Of course! I'd be happy to help. Could you let me know a bit more about your interests and preferences? That way, I can provide you with more tailored recommendations. Are you looking for indoor or outdoor activities? Do you have any particular hobbies or things you enjoy doing?"),

  HumanMessage(content='What is my name?'),

  AIMessage(content='Your name is Terry. You mentioned it at the beginning of our conversation. Is there anything specific you would like assistance with, Terry?')]}



LLM 모델에서 Conversational Memory 사용

ChatModel이 아닌 일반 LLM모델에서도 메모리를 사용할 수 있는데, 차이점은 별도로 LLM 용 프롬프트 템플릿을 정의해주고, CoversationBufferMemory에서 return_messages=False로 하여, 메모리내의 히스토리가 문자열로 리턴되게 하고,  LLMChain이 이 메모리 내용을 프롬프트에 삽입하도록 해주면 된다. 

 

from langchain_openai import OpenAI

from langchain.prompts import PromptTemplate

from langchain.chains import LLMChain

from langchain.memory import ConversationBufferMemory

import os

 

model = OpenAI(openai_api_key="{YOUR_OPENAI_KEY}"

               ,temperature=0)

 

template = """You are a chatbot having a conversation with a human.

 

Previous conversation history:

{chat_history}

 

New human question: {question}

Response:"""

prompt = PromptTemplate.from_template(template)

 

memory = ConversationBufferMemory(memory_key="chat_history")

conversation = LLMChain(

    llm=model,

    prompt=prompt,

    verbose=True,

    memory=memory

)

conversation.invoke({"question": "hi my name is Terry"})

conversation.invoke({"question": "Can you recommend fun activities for me?"})

conversation.invoke({"question": "What is my name?"})

memory.load_memory_variables({})



프롬프트의 형태는 다음과 같다. 처음에 챗봇에 역할을 정해주고, 다음 기존 채팅 히스토리가 들어간다는 것을 정의한후에, 채팅 히스토리가 들어갈 곳을 템플릿 변수 {chat_history}로 정의해준다. 이 키 값은 뒤에 ConversationalBufferMemory에서 지정하는 memory_key의 값과 일치해야 연결이 된다.  다음 마지막으로 사용자 질문이 들어갈 공간을 정의하고 질문을 프롬프트 변수 {question}으로 지정한다. 

 

template = """You are a chatbot having a conversation with a human. <-- 쳇봇 역할 지정

 

Previous conversation history: <-- 기존 대화 히스토리임을 설명

{chat_history}

 

New human question: {question} <-- 새로운 사용자 질문임을 설명

Response:"""

프롬프트와 메모리가 준비되었으면 마찬가지로 LLMChain을 이용하여 메모리, 모델, 프롬프트를 연결한 후 체인의 Invoke 명령을 이용하여 모델을 호출한다. 

 

그리드형