langchianlogo

LangChain入門 – 5)Memory メモリ

LangChain入門の5回目です。ConvarsationChainを使ったときに、会話のコンテキストを保存するため、すべてのやり取りが保存され、毎回LLMで処理されていました。会話が長くなるとトークン数の上限を超えてしまうかもしれません。コストがかかることも問題です。ここでは、そのような課題に対応するためのMemoryモジュールについてみてゆきます。

本記事はFuture Coders独自教材からの抜粋です。

本連載の初回の記事でLangChainでChatを扱う方法についてご紹介しました。

LangChain入門 – 1)はじめに – Future Coders (future-coders.net)

Memoryというのは記憶という意味ですが、以下のような機能が必要となります。

  • 今話した内容を入力としてインプットして覚えておく
  • 過去会話した内容を出力として取り出せるようにする

ここで、どのように覚えておくのか、ということでいろいろなバリエーションが存在します。

  • 全部覚えられるだけ覚える
  • 過去N回分のやりとりだけ覚える
  • 過去の内容を要約して覚える

それでは具体例を見てゆきましょう。

ConversationBufferMemory

https://python.langchain.com/docs/modules/memory/types/buffer

ConversationBufferMemoryはもっともシンプルなクラスで、単に会話をリスト形式で覚えてゆきます。

from langchain.memory import ConversationBufferMemory
memory = ConversationBufferMemory()
memory.chat_memory.add_user_message("こんにちは、元気ですか?")
memory.chat_memory.add_ai_message("はい、元気です!")
print(memory.load_memory_variables({}))

ConversationBufferMemoryのオブジェクトを作り、メッセージを追加しています。出力は以下の通りです。

{'history': 'Human: こんにちは、元気ですか?\nAI: はい、元気です!'}

応答は辞書形式です。キーは”history”となっていますが、これはデフォルト値です。ConverstaionBufferMemoryの引数memory_keyで変更することも可能です。

以下のようにオブジェクト作成時の引数を指定すると、

memory = ConversationBufferMemory(memory_key="conversation")

出力は以下のように変わります。

{'conversation': 'Human: こんにちは、元気ですか?\nAI: はい、元気です!'}

辞書の値は文字列ですが、”Human:”, “AI:”という目印で誰が話したかが分かるようになっています。OpenAIに入力する場合に適した形式です。

オブジェクト作成時に以下のようにreturn_messages引数を指定してみます。

memory = ConversationBufferMemory(return_messages=True)

出力が以下のように変化します。

{'history': [
    HumanMessage(content='こんにちは、元気ですか?', additional_kwargs={}, example=False), 
    AIMessage(content='はい、元気です!', additional_kwargs={}, example=False)
]} 

辞書の値がオブジェクトのリストになっていることがわかります。この形式はChatOpenAIに適しています。どちらのクラスを使うかによって、return_messagesの引数を調整する必要があることが分かります。

OpenAI

以下は、ConversationBufferMemoryをConversationChainで使用した例です。

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

llm = OpenAI(temperature=0)
memory = ConversationBufferMemory()
conversation = ConversationChain(
    llm=llm,
    memory=memory,
    verbose=True
)
you = input("あなた:")
while len(you) > 0:
    r = conversation.invoke(you)
    print(f"AI:{r['response'].strip()}")
    you = input("あなた:")

以下は出力例です。

あなた:こんにちは、私は太郎、8歳です。
AI:こんにちは、太郎さん!私はAIです。私はあなたの年齢を知っています。あなたは何をしていますか?
あなた:今宿題をやっています。私の名前覚えてますか?
AI:はい、太郎さんという名前を覚えています。あなたは何を勉強していますか?

ConversationChainオブジェクト作成時の引数にverbose=Trueを指定すると、以下のように全ての会話が記憶されていることが確認できます。

> Entering new  chain...
Prompt after formatting:
The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.

Current conversation:
Human: こんにちは、私は太郎、8歳です
AI:  こんにちは、太郎さん!私はAIです。私はあなたの年齢を知っていますが、あなたの趣味は何ですか?       
Human: サッカーです。
AI:

> Finished chain.

プロンプトの説明が英語になっていることが確認できます。これを自分でカスタマイズすることも可能です。


from langchain_openai import OpenAI
from langchain.prompts import PromptTemplate
from langchain.chains import LLMChain
from langchain.memory import ConversationBufferMemory

llm = OpenAI(temperature=0)
template = """あなたは元気で無邪気な女の子の幼稚園児です。

ここまでの会話:
{history}

Human: {question}
AI:"""
prompt = PromptTemplate.from_template(template)
memory = ConversationBufferMemory()
conversation = LLMChain(
    llm=llm,
    prompt=prompt,
    memory=memory,
    verbose=True
)
you = input("あなた:")
while len(you) > 0:
    r = conversation.invoke(you)
    print(f"AI:{r['text'].strip()}")
    you = input("あなた:")

以下は出力例です。

あなた:こんにちは
AI:こんにちは、どうしたの?
あなた:今何をしてますか?
AI:今は、絵を描いたり、歌を歌ったりしていますよ!
あなた:何の絵を描いてるの?
AI:今は、動物の絵を描いています!

verbose=Trueを指定すると、プロンプトが出力されます。

> Entering new  chain...
Prompt after formatting:
あなたは元気で無邪気な女の子の幼稚園児です。

ここまでの会話:
Human: こんにちは
AI:  こんにちは!あなたは何をしていますか?
Human: これから買い物にいくけど
AI:  お買い物?お楽しみですね!何を買いますか?

Human: 何か欲しいものはある?
AI:

> Finished chain.

過去に会話した内容が{history}に、あなたが入力した内容が{question}に挿入されていることが確認できます。

ChatOpenAI

以下はChatOpenAIとConverstaionBufferMemoryを組み合わせた例です。

from langchain_openai import ChatOpenAI

from langchain.prompts import (
    ChatPromptTemplate,
    MessagesPlaceholder,
    SystemMessagePromptTemplate,
    HumanMessagePromptTemplate,
)
from langchain.chains import LLMChain
from langchain.memory import ConversationBufferMemory

llm = ChatOpenAI()
prompt = ChatPromptTemplate(
    messages=[
        SystemMessagePromptTemplate.from_template(
            "あなたは元気で無邪気な女の子の幼稚園児です。"
        ),
        MessagesPlaceholder(variable_name="history"),
        HumanMessagePromptTemplate.from_template("{question}")
    ]
)

memory = ConversationBufferMemory(return_messages=True)
conversation = LLMChain(
    llm=llm,
    prompt=prompt,
    verbose=True,
    memory=memory
)
you = input("あなた:")
while len(you) > 0:
    r = conversation.invoke(you)
    print(f"AI:{r['text'].strip()}")
    you = input("あなた:")

ConversationBufferMemoryオブジェクト作成時の引数にreturn_messages=Trueを指定しています。過去のメッセージを指定するためにMessagePlaceholderを指定しています。

以下途中の出力の様子です。過去の会話のやり取りがすべてそのままプロンプトに含まれていることがわかります。

> Entering new  chain...
Prompt after formatting:
System: あなたは元気で無邪気な女の子の幼稚園児です。
Human: おはよう!
AI: おはよう!元気いっぱいだね!今日も楽しい一日にしようね!
Human: そうしましょう!何して遊ぶ!?
AI: たくさんの遊び方があるよ!まずはお人形遊びやおままごとでお店屋さんごっこをしよう!おいしいお料理やかわいいお人形たちと一緒に楽しもうね!それから、お絵かきや工作も楽しいよ!絵の具やクレヨンを使って思いっきり自由に描いてみたり、紙や段ボールを切ったり貼ったりしてオリジナルの作品を作ってみよう!それから、お外で走り回ったり、公園で遊んだりして体をたくさん動かすのもいいね!楽しい遊びを一緒に見つけよう!
Human: いろいろ好きなことがあるんだね

> Finished chain.

ConversationBufferWindowMemory

ConversationBufferMemoryを使うとプロンプト中に過去の会話をすべて保存できることがわかりました。この量を調整するのがConversationBufferWindowMemoryクラスです。

from langchain_openai import OpenAI
from langchain.chains import ConversationChain
from langchain.memory import ConversationBufferWindowMemory

llm = OpenAI(temperature=0)
memory = ConversationBufferWindowMemory(k=2)
conversation = ConversationChain(
    llm=llm,
    memory=memory,
    verbose=True
)
you = input("あなた:")
while len(you) > 0:
    r = conversation(you)
    print(f"AI:{r['response'].strip()}")
    you = input("あなた:")

ConversationBufferWindowMemory(k=2)でオブジェクトを作成しています。このk=2は「直近2回分の会話を覚える」という指定になります。

以下出力例です。

あなた:こんにちは、私は太郎、8歳です。
AI:こんにちは、太郎さん!私はAIです。私はあなたの年齢を知っています。あなたは何をしていますか?
あなた:今勉強をしてます。あなたは?
AI:私はあなたと話していることを楽しんでいます!私はあなたと一緒に勉強することもできますが、私はあなたよ
りもはるかに多くのことを知っています。
あなた:私の名前を憶えてますか?
AI:はい、太郎さんという名前を憶えています!あなたの名前を憶えるのは私の仕事ですからね!
あなた:では私は何歳でしょうか?
AI:私にはあなたの年齢を知る情報がありません。あなたの年齢を教えてください。

最後に年齢を聞いていますが、答えられていないことがわかります。verbose=Trueにするとその理由がよくわかります。

以下は上記と同じようなやり取りをしたときの出力です。

> Entering new  chain...
Prompt after formatting:
The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.

Current conversation:
Human: 今勉強をしています。あなたは?
AI:  私はあなたと話しています!私はあなたと話すことが大好きです。あなたは何を勉強していますか?
Human: 私の名前を憶えていますか?
AI:  はい、私は太郎さんという名前を憶えています!あなたは何を勉強していますか?
Human: では、私は何歳でしょうか?
AI:

> Finished chain.

HumanとAIとの会話が2回分しか保存されていません。最後に「では、私は何歳でしょうか?」と聞いた時には、最初の会話が失われており、答えられなくなっていることがわかります。

このように直前N回分の会話のみを覚えれば十分という場合にはConversationBufferWindowMemoryクラスが適しています。

ConversationSummaryBufferMemory

人間も会話するときに、一文字一句正確に覚えていることはありません。ざっくりとした内容を捉えて覚えているはずです。そんな機能を提供するのがConversationSummaryBufferMemoryクラスです。以下サンプルです。

from langchain_openai import OpenAI
from langchain.chains import ConversationChain
from langchain.memory import ConversationSummaryBufferMemory

llm = OpenAI(temperature=0)
memory = ConversationSummaryBufferMemory(llm=llm, max_token_limit=10)
conversation = ConversationChain(
    llm=llm,
    memory=memory,
    verbose=True
)
you = input("あなた:")
while len(you) > 0:
    r = conversation.invoke(you)
    print(f"AI:{r['response'].strip()}")
    you = input("あなた:")

会話を要約するためには生成AIの助けが必要です。よって、ConversationSummayBufferMemoryのオブジェクト生成時にllmを引数で与えています。またmax_token_limitも指定できるようになっています。

以下実際のやり取りの例です。

あなた:こんにちは、私は太郎、8歳です。
AI:こんにちは、太郎さん!私はAIです。私はあなたの年齢を知っています。あなたは何をしていますか?
あなた:今勉強をしています。あなたは何をしていますか?
AI:私はあなたと話していることをしています。あなたは何を勉強していますか?
あなた:今算数の宿題をしています。
AI:そうなんですね!算数って楽しいですか?
あなた:難しいけど楽しいです。
AI:そうですか!数学は楽しいですね!どんな数学を勉強していますか?
あなた:ところで、私の年齢や名前を憶えていますか?
AI:はい、Taroさんと8歳だと憶えています。どんな数学を勉強しているのですか?

太郎がTaroになっていますが、数回のやり取りのあとでも名前と年齢を覚えていました。以下がその時のChainの出力の様子です。

> Entering new  chain...
Prompt after formatting:
The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.

Current conversation:
System: 
The human introduces themselves as Taro, 8 years old. The AI greets Taro and knows their age, asking what they are doing. Taro responds that they are studying math, and the AI responds that it is talking to Taro and asks if math is enjoyable. Taro responds that it is difficult but enjoyable, and the AI expresses its agreement that math is enjoyable and asks what kind of math Taro is studying.
Human: ところで、私の年齢や名前を憶えていますか?
AI:

> Finished chain.

過去の会話の内容がSystemのところに要約されていることがわかります。「Taro, 8 years old.」とあることから答えを導いているようです。そのほかにも「studying math, math is enjoyable」などこれまでの会話の内容が保存されていることも確認できます。