ConversationSummaryBufferMemory で SlackBOT を ChatGPT っぽく

FacebooktwitterredditpinterestlinkedinmailFacebooktwitterredditpinterestlinkedinmail

Python では Slack BOT の作成も簡単で、 LangChain を使用すれば OpenAI の API と接続して ChatBOT に LLM で応答させることも容易でした。また、Slack は、 Socket Mode で起動させればローカル PC でも特別な手間なく動作確認が行えるのも便利でした(どちらかというと、 Web 上での OAuth の権限設定の方が手間でした)。

ただし、前回までの方法だと一問一答形式であり、 ChatGPT のようにそれまでの会話の文脈を踏まえて、深耕していくような会話には向きません。そこで今回は、Slack Bot に ConversationBufferMemory より賢い、ConversationSummaryBufferMemory を搭載します。ついでに、記憶が混乱しないように、ユーザーごとに分けてメモリを作成します。

LangChain の ConversationSummaryBufferMemory

参考:https://langchain.readthedocs.io/en/latest/modules/memory/types/summary_buffer.html

ConversationSummeryMemory と ConversationBufferMemory のアイディアを足したようなメモリです(誤解のないように書きますが、ユーザーごとに分ける機能はこのメモリ自体にはありません)。

これは、一定あるいは指定した量(token)のメッセージを保存し、新しいログはメッセージをそのまま格納し、古いログに関しては Summarize して格納する、普通に考えると非常に効率的なメモリとなります。その代わり、 Summarize のために LLM を起動する必要があるので、

  1. 実行速度が遅い
  2. token を消費する(コストが高い)

といったデメリットがあります。長期間のログ保存の必要がなければ、 ConversationBufferMemory の方が向いているかもしれません。

使い方

from langchain.memory import ConversationSummaryBufferMemory
from langchain.chat_models import ChatOpenAI
llm = ChatOpenAI()
memory = ConversationSummaryBufferMemory(llm=llm, max_token_limit=10)

max_token_limit は、省略することもできます。過去の会話をSummarize する token 数の閾値として使用されます(上記の例では動作確認するために小さくなっています)。

llm は、Summarize に使用する LLM のインスタンスを指定します。今回は ChatOpenAI (GPT-3.5-turbo)を指定しています。筆者がまだGPT-4のwaitlist内だからということもありますが、コスト面、速度面を考えると GPT-4が広く使用されるようになっても、ここはGPT-3.5-turbo を指定した方がいいのではないでしょうか。

LLMChain での ConversationSummaryBufferMemory の利用

LangChain の Memory オブジェクトの使い方としては共通です。

# 日本語で ChatGPT っぽく丁寧に説明させる
# chat_history の位置に Memory の出力を入れる
system_message_prompt = SystemMessagePromptTemplate.from_template("""You are an assistant who thinks step by step and includes a thought path in your response.
Here's a summary of the conversation:
{chat_history}

A new message came from a human. Answer in Japanese.""")
# ユーザーからの入力
human_template="{text}"
# User role のテンプレートに
human_message_prompt = HumanMessagePromptTemplate.from_template(human_template)

# ひとつのChatTemplateに
chat_prompt = ChatPromptTemplate.from_messages([system_message_prompt, human_message_prompt])
# input_variables に過去の対話を掲載するため、chat_historyを指定
chat_prompt.input_variables = ['text', 'chat_history']
# memory_key に input_varibales の 対話履歴に対応するように文字列を代入する
memory = ConversationSummaryBufferMemory(llm=ChatOpenAI(), memory_key='chat_history')
# LLM, Prompt, Memory を入れて chain を実体化
chain = LLMChain(llm=llm, prompt=chat_prompt, memory=memory, verbose=True)
# LLMを動作
chain.run(text='こんにちは')

Memory をLLMChainのカスタマイズされた Prompt で動作させるためには、過去の対話を挿入する位置を示すディレクティブを Prompt 内に記載する必要があります。上記の例では、 {chat_history} がそれに該当します。

ChatPromptTemplate では、 from_messages メソッドでインスタンスを作成する際に、 input_variables を同時に指定できないようなので、実体化後にディレクティブ名をリストを代入します。

更に、 ConversationSummaryBufferMemory を作成する際、 LLM と同時に memory_key 引数に、 Prompt のディレクティブ名と同じ文字列を指定します(一致させます)。

上記手順を実行後に、 LLMChain のコンストラクタに、LLM, カスタム Prompt, Memory を与えて作成します。GPT-3.5-turbo 以前の、シンプルな LLM インスタンスだと prompt の作成がもう少しシンプルなので手順が分かりやすいのですが……。

Slack Bot でのユーザーの特定

前回作成したSlackBotはメンションに対して反応する形式でした(全てに反応すると token を無限に食べるのでオススメしません……bot同士を対話させるには面白い会場でしょうが)。メンションにしろダイレクトメッセージにしろ、1つのインスタンスで複数のユーザーを扱う場合、メモリを区別しないと面倒なことになります(ダイレクトメッセージで奥さんとの喧嘩の相談をしている同僚の会話内容を踏まえたアドバイスをされても困ります)。

とはいえ、 Memory 自体は単純な Python のオブジェクトなので、それをユーザーIDと紐付けて保存すれば問題ないでしょう。

from collections import defaultdict
# ユーザーごとのメモリを格納する辞書
album = defaultdict(lambda: ConversationSummaryBufferMemory(llm=ChatOpenAI(), memory_key='chat_history'))
def get_user_memory(user):
    return album[user]

# メンションされた時
@app.event("app_mention")
def command_handler(body, say):
    # User ID の取得
    user = body['event']['user']
    say('ただいま思考中です......')
    memory = get_user_memory(user)

Slack の UserID は eventオブジェクトのuserに格納されていますので、それを key としてメモリを格納します。必要があれば、この辞書型をシリアライズして保存すれば、再起動時にも再利用ができます。

動作イメージ

ユーザーが同じか違うかが少し分かりにくいですが……、中小企業診断士という文脈を持っているユーザーと持っていないユーザーで返答が異なっていることが見て取れます。また、メモリが空のときは、会話のスタートだと認識しているのか、必ず挨拶を入れるようになっているのが、GPT-3.5-turbo がチャット用としてチューンされていることが分かって面白いですね。

ただ、(ChatGPTのように)チャネルごとに異なる文脈を持たせたい場合は、ユーザーとチャネル両方の ID で多次元の辞書型にするなどの工夫が必要かもしれません。

逆に、チャネルの中で複数のユーザーと一緒に LLM に発言してもらいたい場合などは、チャネル全体のメッセージをメモリに格納し、かつ、各ユーザーを区別できるように Promptを工夫する必要があるでしょう。

業務サポートチャットボットに Memory は必要か?

ChatGPT のように、汎用的なチャットボットであれば Memory は必要だと思います。また(現時点で、サービスするのはなかなか勇気が必要ですが)、顧客向けのサービスでも、文脈を保持するために Memory の実装は必要でしょう。

一方で、社内ドキュメントを検索したり、SQLを操作したりする BOT であれば必ずしも Memory は必要ないかもしれません。というのも、変に文脈を読んで意図と異なる検索が行われてしまい生産性が低下する恐れがあるからです。加えて、 Memory といっても実際に OpenAI の提供する GPT内に記憶を生成するわけではなく、 Prompt に会話の記録に相当するものが埋め込まれるだけという点にも注意が必要です。

これは、必要のない会話の記録が埋め込まれることによって、 Token 量が増加してしまい、運用コストにそのまま影響します。

メモリをフラッシュするコマンドを実装するといった対応も考えられますが、エンドユーザーへの教育コストもかかりますのでそれであればいっそのこと Memory を排除するといった対応もやむなしかと思われます。

※ GPT-3.5-turbo であれば 1000token で 0.26円($1 = 130円換算)ですから、分コスト換算 40円~50円程度の現場の知恵袋的立ち位置のリーダーの手を煩わせないで済むのであれば、50,000 token くらい使ってもいいのでは……と思ってしまいますが(問題はその浮いた時間で知恵袋的リーダーがコストに見合う生産性を発揮できるようになるか? という点ですが)。

まとめ

  • ConversationSummaryBufferMemory は高機能だけど高コスト。実は用途が限定されるかもしれない。
  • Slack のユーザー名を取得して、dict にすればMemory の切り替えは難しくない
  • LangChain の Memory 機能といっても、本当に LLM の オンメモリに展開されるわけではなく、Prompt に埋め込まれるだけ
  • Memory の種類と前後の Promptとの兼ね合い(と、許容出来るToken)が重要
FacebooktwitterredditpinterestlinkedinmailFacebooktwitterredditpinterestlinkedinmail

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください

最新の記事