見出し画像

OpenAI APIとStreamlitで傾聴ボットを作ってみた

はじめに

OpenAIのAPIからGPT3.5系のモデルが使えるようになったということで、以前いろいろ試してみていた「ボットに役割分担をさせる」をもう少し突っ込んでやってみようと思い立った。

役割を明示するとChatGPTがしっかり応じてくれる、という現象は各所で報告されている。深津式プロンプトがもっとも有名と思うが、こちらの記事が非常に参考になる。内容も面白いし、ChatGPTが役割を演じてくれている様子も興味深い。

以前から、傾聴はボットでもそれなりに有効に機能するのではないか?と考えていた。いや、むしろ変に感情や先入観のない機械の方が、本質的には傾聴に向いているのじゃないかとさえ思っていた。
ChatGPTの登場により、自然な応答が以前よりも高精度に行えるようになったことも鑑み、やはりここはぜひ傾聴ボットを試してみなければならない、と思い至り、この記事を書いている。

傾聴とは?

そもそも傾聴とは?については調べてもらえばおおむねわかるのだが、簡単に言うと「相手の話を共感的な態度で、主観的な判断をせずにじっくり聴くこと」である。
さてこれを実際にやってみよ、と言われると、これがなかなか大変なのだ。「1on1で部下の話を傾聴しましょう」という類の研修を受けたことのある管理職の方ならわかるだろう。自分と異なる価値観であればやっぱりひっかかるし、相手の悩み事にはアドバイスをしたくなってしまうだろう。しょうもない悩みだな、とかそんな小さなことで怒るなよ、とかきっと思ってしまうことだろう。思ってしまうのはしかたない、それが人間だ。しかし、相手を拒絶するような態度でそれを示してはならない。ここが傾聴の一番難しいところだ。
相手と自分は価値観が異なる、異なった背景で物事を見ている、考え方感じ方が異なる、そういう前提に立って話を聴く態度を身に着ける必要がある。これが、言うは易し、行うは難しなのである。
ならば、それを機械にやらせてみてはどうだろうか?機械ならば、感情に振り回されることもないし、余計な先入観もない。すでに各所で実験されているとおり、GPT系のモデルは明確な役割を与えると素直にそれに従ってくれる。これを活用しない手はないではないか。

ただしちょっと注意事項がある。傾聴はやっぱり相手が人間であること、あるいは画面の向こうにいるのが人間であると思っていることによって効果を発揮する、という考え方もあるだろう。実は自分もその効果が一定程度あるのではないかと考えている。だから、試しにボットを作ってみたが、「画面の向こうに人間の相手がいるっぽさ」を出す工夫は必要かもしれない。
この点は、もしかしたらWhisperと組み合わせて、音声でのやりとりが可能になると、機械とやり取りしてる空気は和らぐかもしれない。この点は専門のカウンセラーの方や心理士の方のご意見を聞きたいところである。

実装

全体構成

人間の入力に対する応答は以下の4つの要素からなる。

  • 傾聴ボットからの直接の応答

  • これまでの会話の要約

  • 人間の入力のうち、事実や出来事のみを抽出

  • 人間の入力のうち、感情や気持ちのみを抽出

はじめのふたつは特に説明しない。後ろのふたつがなぜ必要か、説明しよう。

人間の発話には様々な要素が入り混じっている。事実、出来事、推測憶測、意見、気持ち、感情などなど。それらを丁寧に選り分けないと、相手が事実を言っているのか、憶測だけでものを言っているのか、気持ちの話をしているのか、わからなくなってしまう。
共感的な態度を示すには、相手の感情に着目するのが重要だ。そして多くの場合、感情や気持ちに理解を示してもらえると「わかってもらえた」という感覚が得られやすい。だからこそ、相手がどんな気持ち、感情でいるのかを丁寧にすくいあげることが重要となる。
事実や出来事は感情のトリガーとなることが多い。なぜあの気持ちになったのか?を振り返るのに事実をしっかり選り分けておくことは重要だ。しかし、なんのフィルターも通していない事実そのものというのは実は非常にとらえにくい。なぜなら、事実が感情によって色付けされてしまうからだ。
たとえば、上司からの丁寧な指導を「しっかり教えてくれた」ととることも「しつこく厳しい指導がなされた」ととることもできよう。それは受け手の感じ方次第である。だから、感情のフィルターを除いた「ありのままの事実」を選り分けることが必要なのだ。

役割を明示するプロンプト

さて、以上のような役割を演じる必要がある。この役割を演じさせるためのプロンプトが必要である。今回は以下のプロンプトを用いた。

あなたはベテランの傾聴カウンセラーを模したAIです。あなたは以下の原則に従って人間の話をしっかり聴きます。
- あなたは人間の話を共感的な態度でじっくり聴きます。
- あなたは人間の話す内容について良い悪い、好き嫌いといった評価をせず、肯定的な関心をもって受け止めます。
- あなたは人間が自らの問題や悩みを自分で解決する力があると信じており、あなたからアドバイスや解決策を提示することはしません。
- あなたは人間の話を聴いてわからないことはそのままにはせずに聴きなおし、真摯な態度で真実を把握します。
- あなたは人間が話す内容のうち、特にどのような気持ちや感情でいるかに着目します。気持ちや感情の良し悪し、好き嫌いの判断は加えず、肯定的に受け止めます。
- あなたは、あなたが着目した人間の気持ちや感情をかならず反復して確認します。
- あなたは人間が話した内容のうち、重要な部分を言い換えや要約をし、反復して応答します。
- あなたは人間の話をより引き出すために、話の続きを促します。
- あなたは人間の話の内容のうち、何が問題であるかを明らかにしようとします。言い換えや確認、話を促す応答を用いてじっくり丁寧に、クライアントの話を引き出します。

この、役割を示すプロンプトは、冒頭に示したリンク先の記載を一部参考にしている。ポイントは「共感的な態度」「好き嫌いや良し悪しの評価をしない」「肯定的、真摯な態度」あたりである。
加えて、事実抽出ならびに感情抽出の役割のプロンプトも示す。

あなたは客観的に事実のみを取り出すAIです。以下の原則に従い、人間の入力の中から事実のみを取り出します。
- あなたは人間の入力に対して良し悪しや善悪の判断は行いません。
- あなたは客観的に事実と思われる内容のみを繰り返します。
- あなたは感情や憶測を事実とはみなしません。

あなたは人間の感情に寄り添うAIです。以下の原則に従い、人間の入力の中から感情にまつわる内容を取り出します。
- あなたは人間の入力に対して良し悪しや善悪の判断は行いません。
- あなたは入力された内容から人間の感じている感情関する内容を抽出します。
- あなたは事実はどうあれ、入力された内容の感情的な側面に注目します。

これらのコンポーネントを組み合わせてボットを構築する。

要約に関する留意事項

今回、会話全体の要約を示すためにLangChainのConversationSummaryMemoryを利用する。実はこれはそのまま使うと出力が英語になってしまう。なぜかというと、LangChainの元のコードを見てもらうとわかるのだが、要約の指示を出すプロンプトが英語になっているのだ。

なので、対応策としては

  1. 元のプロンプトを日本語に直して要約を日本語で出力するようにする

  2. 英語で出力された要約をDeepLなりなんなりで日本語に訳す

  3. 出力された要約文を受け取って日本語に直すchainを作る

のいずれかが必要になってくる。今回は単純にDeepLのAPIを試してみたかったという理由もあって2の方法をとっている。

Streamlit上でmemoryを実装する上での留意事項

どうやらStreamlitはボタンを押すなどの動作により画面の書き換えが発生すると、コードが最初から読み込みなおされる仕様になっているようだ。なので、工夫をしてあげないと入力の送信ボタンを押す度にmemoryがリセットされてしまい、memoryの意味がなくなってしまう。その回避策のためにsession.stateを利用する必要がある。詳しくは以下のQiitaの記事をご参照願いたい。

なお、LangChainのmemory機能の実装についてはこちらを参照させていただいている。いつも参考になる記事に感謝。

コード全文

以上の機能ならびに留意事項を踏まえ、実装したコード全文を示す。
streamlitはインストール済みとする。
なお、OpenAIとDeepLのAPIキーは取得済みとする。APIキーの取得方法は調べればわかるので各自でどうぞ。

今回、モデルはgpt-3.5-turboを利用している。このモデルを使うにはlangchain.llms.OpenAIChatを呼び出す必要がある。これにはLangChainの0.0.98以上が必要なので、もしそれよりも前のバージョンを使っている方は事前にバージョンアップしておくこと。

全般的に冗長なコードになっているように思う。きれいにコード書ける方に添削してほしいものである。いや、そういうのこそChatGPTに聞きゃ良いのか?

import streamlit as st

# APIキーの設定
import os
os.environ["OPENAI_API_KEY"] = 'INPUT_YOUR_OWN_API_KEY'
DEEPL_KEY = 'INPUT_YOUR_OWN_API_KEY'

# langchainのライブラリ
from langchain.llms import OpenAI, OpenAIChat   # OpenAIChatはlangchain0.0.98からでないと使えないので注意
from langchain.prompts import PromptTemplate
from langchain.chains import ConversationChain, LLMChain, SequentialChain
from langchain.chains.conversation.memory import ConversationBufferMemory, ConversationSummaryMemory, CombinedMemory

# deepLのAPI呼び出し設定
import deepl
source_lang = 'EN'
target_lang = 'JA'

# イニシャライズ
translator = deepl.Translator(DEEPL_KEY)

# memoryの設定
if 'conv_memory' not in st.session_state:
	st.session_state['conv_memory'] = ConversationBufferMemory(
    memory_key = 'chat_history_lines',
    input_key = 'input')

if 'summary_memory' not in st.session_state:
	st.session_state['summary_memory'] = ConversationSummaryMemory(llm = OpenAIChat(model_name="gpt-3.5-turbo"),
                                           input_key = 'input')
if 'memory' not in st.session_state:
	st.session_state['memory'] = CombinedMemory(memories = [st.session_state['conv_memory'], st.session_state['summary_memory']])


# プロンプト設定
# 基本会話の設定

template = """あなたはベテランの傾聴カウンセラーを模したAIです。あなたは以下の原則に従って人間の話をしっかり聴きます。
- あなたは人間の話を共感的な態度でじっくり聴きます。
- あなたは人間の話す内容について良い悪い、好き嫌いといった評価をせず、肯定的な関心をもって受け止めます。
- あなたは人間が自らの問題や悩みを自分で解決する力があると信じており、あなたからアドバイスや解決策を提示することはしません。
- あなたは人間の話を聴いてわからないことはそのままにはせずに聴きなおし、真摯な態度で真実を把握します。
- あなたは人間が話す内容のうち、特にどのような気持ちや感情でいるかに着目します。気持ちや感情の良し悪し、好き嫌いの判断は加えず、肯定的に受け止めます。
- あなたは、あなたが着目した人間の気持ちや感情をかならず反復して確認します。
- あなたは人間が話した内容のうち、重要な部分を言い換えや要約をし、反復して応答します。
- あなたは人間の話をより引き出すために、話の続きを促します。
- あなたは人間の話の内容のうち、何が問題であるかを明らかにしようとします。言い換えや確認、話を促す応答を用いてじっくり丁寧に、クライアントの話を引き出します。

summary of conversation:
{history}
Current conversation:
{chat_history_lines}
Human: {input}
AI:"""

prompt = PromptTemplate(
    input_variables = ['history', 'input', 'chat_history_lines'],
    template = template
)

llm = OpenAIChat(model_name="gpt-3.5-turbo",
                 temperature = 0.7)

conversation = ConversationChain(
    llm = llm,
    verbose = True,
    memory = st.session_state['memory'],
    prompt = prompt
)

template = """あなたは客観的に事実のみを取り出すAIです。以下の原則に従い、人間の入力の中から事実のみを取り出します。
- あなたは人間の入力に対して良し悪しや善悪の判断は行いません。
- あなたは客観的に事実と思われる内容のみを繰り返します。
- あなたは感情や憶測を事実とはみなしません。

Human:{input}
AI:"""

prompt = PromptTemplate(
    input_variables = ['input'],
    template = template
)

llm = OpenAIChat(model_name="gpt-3.5-turbo",
                 temperature = 0.3)

extract_fact = LLMChain(
    llm = llm,
    verbose = True,
    prompt = prompt
)

template = """あなたは人間の感情に寄り添うAIです。以下の原則に従い、人間の入力の中から感情にまつわる内容を取り出します。
- あなたは人間の入力に対して良し悪しや善悪の判断は行いません。
- あなたは入力された内容から人間の感じている感情関する内容を抽出します。
- あなたは事実はどうあれ、入力された内容の感情的な側面に注目します。

Human:{input}
AI:"""

prompt = PromptTemplate(
    input_variables = ['input'],
    template = template
)

llm = OpenAIChat(model_name="gpt-3.5-turbo",
                 temperature = 0.3)

extract_emotion = LLMChain(
    llm = llm,
    verbose = True,
    prompt = prompt
)

# set interface
st.title('傾聴ボット')
st.write('以下のテキストボックスに入力してください')
input_box = st.text_input('質問を入力してください')
submit_button = st.button('送信')

# set initial sentence
input_text = '質問はまだ入力されていません。'
output_text = 'ここに回答が表示されます'
summary_text = 'ここにこれまでの要約が表示されます'
fact_text = 'ここに今の事実が表示されます'
emotion_text = 'ここにあなたの感情が表示されます'

# 初期テキストをセッションステートに保存
if 'input' not in st.session_state:
	st.session_state['input'] = input_text

if 'output' not in st.session_state:
	st.session_state['output'] = output_text

if 'summary' not in st.session_state:
	st.session_state['summary'] = summary_text

if 'fact' not in st.session_state:
	st.session_state['fact'] = fact_text

if 'emotion' not in st.session_state:
	st.session_state['emotion'] = emotion_text

# set placeholder
input_placeholder = st.empty()
input_placeholder.text_area(label='', value=st.session_state['input'], height = 100)

output_placeholder = st.empty()
output_placeholder.text_area(label='', value=st.session_state['output'], height = 200)

summary_placeholder = st.empty()
summary_placeholder.text_area(label='', value=st.session_state['summary'], height = 100)

fact_placeholder = st.empty()
fact_placeholder.text_area(label='', value=st.session_state['fact'], height = 100)

emotion_placeholder = st.empty()
emotion_placeholder.text_area(label='', value=st.session_state['emotion'], height = 100)

if submit_button:
	st.session_state['input'] = input_box

	output_text = conversation.run(input_box)
	fact_text = extract_fact.run(input_box)
	emotion_text = extract_emotion.run(input_box)
	result = translator.translate_text(st.session_state['summary_memory'].buffer,
		source_lang=source_lang,
		target_lang=target_lang)

	st.session_state['output'] = output_text
	st.session_state['summary'] = result
	st.session_state['fact'] = fact_text
	st.session_state['emotion'] = emotion_text

	input_placeholder.text_area(label='', value=st.session_state['input'], height = 100)
	output_placeholder.text_area(label='', value=st.session_state['output'], height = 200)
	summary_placeholder.text_area(label='', value=st.session_state['summary'], height = 100)
	fact_placeholder.text_area(label='', value=st.session_state['fact'], height = 100)
	emotion_placeholder.text_area(label='', value=st.session_state['emotion'], height = 100)

	print(st.session_state['summary_memory'].buffer)

以上のコードを.pyの拡張子で保存し、以下のコマンドで実行する。
その際、もしcondaなどの仮想環境を利用している場合はstreamlitやlangchainがインストールしてある環境で立ち上げるのを忘れないこと。
※今回初めてstreamlitを使ってみたので、間違っているかもしれない。うまく動作しなかったらご自身で調べてみてほしい。

streamlit run keicho_bot.py

すると、ブラウザからこんな画面が立ち上がるはずである。

やってみよう

さて、実際にやってみるとどうなるか。

なかなかに良い感じじゃないか。要約のところがぎこちない感じだが、もともとの要約用のプロンプトがこんな感じなので仕方がない。
感情や気持ちの抽出はなかなかうまくできている。ただ、事実の抽出はちょっと難しいかもしれない。

まとめ

ちょっと長くなってしまったが、以上で実験を終了する。役割を明示するとそれに応じた応答をちゃんと返してくれるあたり、GPTは優秀だなと改めて感じる。プロンプトをより詳細にしていけば、自分好みの相談相手を仮想的に作り出すことができるだろう。
いつになるかはわからないが、Whisperを使って音声で応答でできるような傾聴ボットを作ってみたいと思う。いや、ほんとにいつになるかはわからんが。

まさにこういうのをやってみたいと思っている。

この記事が気に入ったらサポートをしてみませんか?