Difyには2024年8月にリリースされたv0.7.0から会話変数(Conversation Variables)機能が追加されています。
今回は会話を補助するためのDifyチャットフローを組んでみたのでそれの紹介と、そこで使用した会話変数機能を中心としたDifyの機能の紹介をします。
Difyが何か知らない人は公式のヘルプページなどを参照してください。
海外製ツールなのに日本語でのヘルプが充実しています。
会話補助ボット
DSLファイルを以下に共有します。
.yml形式のファイルをダウンロードしてDifyにインポートできます。
Difyに会員登録したりLLM用のAPIキーを設定すれば使用できるようになります。
ちなみにこのチャットフローは私が特定の用途で普段使いするために作成していたものを、今回の記事で公開する用に、幅広い場面で使えるようプロンプトなどを調整したものです。
用途
自分と相手の1対1での、チャットでの会話を想定しています。
多人数でのやり取りや口頭での会話は対象外です。
会話の内容をある程度の区切りごとでボットに送信することで助言を受けます。
機能
以下のような形式で会話内容を送信します。
相手「久しぶり!覚えてるかな?」
自分「久しぶり。覚えてるよ。急にどうしたの?」
相手「ちょっと懐かしくなっちゃってさ。元気?」
それに対してボットは以下の内容を返します。
- 良かった点や改善点など、自分の発言を100点満点で評価
→コミュ障は自身の会話を省みて次に活かせる - 会話内容から想定される相手の意図
→発言内容から相手の意図が読み取りにくいコミュ障を助ける - 返信内容の提案
→具体的に次にどのような返信をするのが良いかを考えられる
また、ボットは自分や相手に関する情報、自分と相手のそれまでの会話内容を記憶しています。
記憶の内容はボット自身が随時書き換えて最適な状態を保ちます。
情報を保存していることで、状況を正確に把握した上で返答を作成できるという利点と、会話ログを全て保存するのに比べてトークンを節約できるという利点があります。
実行サンプル
具体的に、今回のチャットフローを実行すると以下のような結果になります。
サンプルの会話の内容はChatGPTと一緒に考えました。
最初にオプションとして、自分と相手の簡単な情報、会話内容(現在の状況)の要約を与えることができます。
この内容はボットによって随時上書きされるので、空欄でも問題ありません。
その後はやり取りの内容を随時入力していくと返信が得られます。
利用者の書き込み1
相手「久しぶり!覚えてるかな?」
自分「久しぶり。覚えてるよ。急にどうしたの?」
相手「ちょっと懐かしくなっちゃってさ。元気?」
自分「元気だよ。そっちは?」
相手「俺も元気!仕事とか忙しい?」
自分「まあまあかな。普通くらい」
相手「そっかそっか。なんか最近、昔の友達に連絡するのが楽しくてさ」
ボットの返信1
評価: 75点
久しぶりの相手との会話で、丁寧な言葉遣いを意識し、相手の質問にも適切に答えています。会話のキャッチボールはできており、相手への配慮も感じられます。しかし、相手が連絡してきた具体的な理由を探るには、もう少し踏み込んだ質問をしても良かったかもしれません。
良かった点
相手の問いかけにしっかりと応答している点。「元気だよ。そっちは?」のように、相手にも質問を投げかけ、会話を続けようとする姿勢が見られます。
「まあまあかな。普通くらい」と、相手に安心感を与えるような、程よい返答をしています。
改善点
相手が「なんか最近、昔の友達に連絡するのが楽しくてさ」と言っているのに対し、共感や深掘りが少し足りないかもしれません。例えば、「そうなんだ!何かきっかけがあったの?」のように質問することで、会話がより広がりやすくなります。
相手の連絡の意図をもう少し探る質問をしても良いでしょう。
相手の考え:
「久しぶりに連絡取れて嬉しいな。特に深い理由はないんだけど、本当にただ懐かしくなったんだよね。最近、昔のこととか色々思い出してさ。みんな元気にしてるのかなって。〇〇(自分)の声聞けて、なんかホッとしたよ。共通の話題とか見つけて、また色々話したいな。」
返信の提案:
相手は特に深い理由はないものの、純粋に懐かしさから連絡してきたようです。この流れを受け止めつつ、会話をさらに発展させるための提案をします。
提案1:共感を示しつつ、相手の気持ちを深掘りする
相手が「昔の友達に連絡するのが楽しい」と感じている点に共感し、その理由を尋ねることで、より親密な会話に繋げることができます。
例:「そうなんだね!確かに、たまに昔のこと思い出すと、連絡してみたくなる気持ち、すごく分かるよ。何かきっかけがあったりするの?」
提案2:共通の話題を探る
学生時代の思い出話など、共通の話題に触れることで、会話が盛り上がりやすくなります。
例:「懐かしいね!そういえば、最近〇〇(共通の友人や出来事)のこと思い出したんだけど、覚えてる?」
提案3:相手の近況について、もう少し具体的に質問する
相手が「仕事とか忙しい?」と聞いてきた流れを受け、相手の近況についてもう少し具体的に質問することで、相手への関心を示すことができます。
例:「そっかそっか。最近何か面白いこととかあった?仕事以外でも何かしてるのかなと思って。」
これらの提案は、相手が純粋に会話を楽しみたいと思っている可能性が高いことを踏まえています。焦らず、自然な流れで会話を深めていくことを意識しましょう。
利用者の書き込み2
自分「そっかそっか。最近何か面白いこととかあった?仕事以外でも何かしてるのかなと思って。」
相手「でさ、話してて思ったんだけど、ちょっと紹介したい人がいるんだよね」
自分「紹介?誰?」
相手「うん。俺の知り合いなんだけど、面白い人でさ。一回話してみてほしいなって」
自分「え、何で急に?何か用事でもあるの?」
相手「いやいや、特に深い意味はないんだけど、なんか気が合いそうな気がして」
自分「そうなんだ…でも、なんで俺?」
相手「いや、昔から○○って面白い奴だったしさ。その人とも話が合うと思うんだよね」
自分「うーん、ちょっとよくわからないけど」
相手「まあ、あんまり考えすぎなくていいよ!軽い感じでさ」
ボットの返信2
評価: 70点
相手の紹介の申し出に対して、慎重に対応している点は評価できます。自分の疑問を率直に伝えようとしている姿勢も良いでしょう。しかし、「何か用事でもあるの?」という質問は、相手に警戒心を与え、会話の流れを少し硬直させてしまう可能性があります。もう少し柔らかい表現で、相手の意図を探る質問をしても良かったかもしれません。
良かった点
相手の紹介に対して、すぐに承諾せず、疑問点を質問している点。「紹介?誰?」「え、何で急に?」など、相手の意図を確認しようとする姿勢が見られます。
「なんで俺?」と、素直な疑問を投げかけている点は、相手に自分の気持ちを伝える上で重要です。
改善点
「何か用事でもあるの?」という質問は、相手に不信感を与えかねません。「何か特別な理由があるのかなと思って」のように、もう少し柔らかい表現にすると、相手も答えやすかったかもしれません。
相手が「特に深い意味はない」と言っているのに対し、まだ疑念を持っている様子が伝わります。もう少し相手の言葉を信じて、話を聞いてみる姿勢を見せても良いかもしれません。
相手の考え:
「えー、そんなに警戒しなくても(笑)。本当にただ、〇〇(自分)と気が合いそうだって思っただけだよ!その人も結構面白い人で、共通の話題とかありそうだし。それに、単純に知り合いが増えるって良いことじゃない?別に何か売りつけようとか、そういうんじゃないから安心してよ!」
返信の提案:
相手はあなたの警戒心を察知し、改めて深い意味はないことを強調しています。ここは、相手の言葉をある程度受け入れつつ、もう少し具体的な情報を引き出す方向に進むのが良いでしょう。
提案1:紹介したい人物について、もう少し具体的な情報を尋ねる
相手が「面白い人」と言っているだけでは、まだ具体的なイメージが湧きません。どのような点が面白いのか、共通の話題がありそうというのは具体的にどのようなことなのかを尋ねることで、紹介への抵抗感を和らげることができます。
例:「そうなんだね、ありがとう。どんなところが面白い人なの?共通の話題って、例えばどんなことかな?」
提案2:軽い感じで、会うことへのハードルを下げる提案をする
相手が「軽い感じで」と言っていることを受け止め、実際に会うとしても、まずは気軽に話せるような状況を提案することで、プレッシャーを軽減できます。
例:「なるほどね。まあ、本当に軽い感じで会うくらいなら良いかな。例えば、みんなでご飯食べるとか?」
提案3:改めて、なぜ自分を紹介したいと思ったのか、相手の気持ちを尋ねる
相手が「昔から○○って面白い奴だったしさ」と言っている点を深掘りし、改めて自分のどのようなところに魅力を感じて紹介したいと思ったのかを尋ねることで、相手の真意をより深く理解できるかもしれません。ただし、しつこくならないように注意が必要です。
例:「そうか、昔からそう思ってくれてたんだ、ありがとう。改めて聞きたいんだけど、どうして僕とその人を紹介したいと思ったの?」
これらの提案は、相手の意図を理解しようとする姿勢を示しつつ、自分のペースで話を進めていくことを意識しています。焦らず、相手とのコミュニケーションを楽しみながら、次のステップに進んでいきましょう。
利用者の書き込み3
会話内容以外に文章を書いて助言を求めることもできます。
自分「なるほどね。まあ、みんなでご飯食べるとかぐらいなら」
相手「いいね!都合の良い曜日とかある?」
自分「夜?」
相手「多分夜になる!平日の仕事の後でも、土日でも」
予定は以下なので、返信内容に盛り込みたいです。
ちょっと怪しい感じがしているので、雲行きが怪しくなった時に適当な理由を付けて抜けれるように予防線を張っておきたいです。
・月曜は残業入りがちなので難しい
・土日は、18(土)と26(日)は予定がある
ボットの返信3
評価: 85点
相手の提案に前向きな姿勢を示しつつ、自分の都合を伝えようとしている点は良いです。「みんなでご飯食べるとかぐらいなら」という表現は、まだ少し様子を見たいというニュアンスを含んでおり、警戒心も程よく示せています。具体的な日程調整に入ったことで、会話が次の段階に進んでいます。
良かった点
相手の提案に対して、具体的な行動(みんなでご飯)で応じる意思を示した点。
「ぐらいなら」という言葉で、完全に乗り気ではないニュアンスを伝えつつ、会話を繋げている点。
具体的な時間帯について質問し、情報収集を行っている点。
改善点
現状では特にありません。この後の返信で、具体的な曜日を伝える際に、予防線を張るための表現を工夫する必要があります。
相手の考え:
「お、いいね!じゃあ、早めに日程決めたいね!夜なら、仕事終わりでも大丈夫だし、ゆっくり話せるかなと思って。平日は〇〇(自分)は何曜日が都合良い感じ?土日も予定あるんだね、了解!」
返信の提案:
相手は具体的な日程調整に入りたいと考えています。ここで、あなたの都合を伝えつつ、予防線を張るための表現を盛り込む必要があります。
提案1:都合の良い曜日を伝えつつ、時間帯を限定し、他の参加者の都合も考慮する余地を残す
具体的な曜日を伝えることで、相手に検討してもらいやすくなります。しかし、時間帯を限定することで、もし雲行きが怪しくなった場合に抜け出す理由を作りやすくなります。また、「他の人の都合も」という点を加えることで、すぐに日程が確定しないようにすることができます。
例:「そうですね、夜なら火曜日か水曜日あたりなら比較的都合が良いことが多いです。ただ、その日の仕事の状況にもよるので、確定ではないのと、もし他に誰か来るなら、その人の都合も聞かないとですね。」
提案2:土日を避ける理由を明確にしつつ、平日の候補日を提示する
土日に予定があることを明確に伝え、平日に絞って提案することで、日程調整をスムーズに進めつつ、土日の怪しい集まりへの参加を避けることができます。
例:「土日はちょっと予定があって難しい日が多いんですよね。平日の夜なら、火曜日か水曜日あたりなら比較的調整しやすいと思います。」
提案3:具体的な日程を提示せず、少し先の日程で検討する意思を示す
すぐに具体的な日程を提示せず、少し先の日程で検討する意思を示すことで、時間的な余裕を作り、冷静に判断する機会を得ることができます。
例:「そうですね、夜なら参加できそうですが、ちょっと直近は立て込んでいるので、少し先の日程で調整できたら嬉しいです。例えば、再来週以降とかで都合の良い日があれば教えてもらえますか?」
これらの提案は、あなたの警戒心と相手との関係性を考慮し、バランスの取れたコミュニケーションを目指しています。状況に応じて、最適な提案を選択してください。
実行後の記憶の内容
最初に画面で入力した自分や相手の情報などは、上記のやり取りを実施後には以下のように更新されています。
会話内容を踏まえて記憶を更新していることが分かります。
ユーザーの情報
20代男性。友人との再会を楽しんでいるが、相手の意図や紹介に対して懐疑的な面も見せている。相手が紹介したい人についての理由を探ろうとしている。特に月曜は残業が入ることが多く、土日の予定もあるため、柔軟に対応したいと考えている。
会話相手の情報
20代男性。昔の友人との懐かしさから、共通の知り合いを紹介したいと思っている。特に深い理由はなく、気軽に紹介を提案している。相手は紹介したい人物と自分を合わせたいと考えている。
会話内容の要約
友人が知り合いを紹介したいと言い出し、ユーザーは懐疑的な姿勢を見せている。相手は特に深い意図はなく、軽い感じでの紹介を希望しているが、ユーザーは納得できていない。予定を考慮しつつ、会う計画を進める流れにある。ユーザーは月曜は残業が多く、土日は18日と26日が予定であることを考慮し、適当な理由を用意しておきたいと思っている。
解説
記事の本題です。会話変数周りを中心に、その他の工夫についても紹介します。
最上段が初回に実行されるもので、最初の画面で入力された内容を会話変数に初期値として代入しています。
中段が返答を作成している部分です。
最後の段では会話変数の内容を更新しています。
会話変数機能
この機能は、LLM(大規模言語モデル)に「メモ」を提供するようなもので、メモリエラーによる情報のバイアスを回避できます。
会話変数はLLMで読み書きできる変数です。変数の内容は、一連のチャットフローの中では保持されます。別のチャットフローや、別のユーザーのチャットではそれぞれ別の変数が参照されます。
類似の機能に環境変数がありますが、こちらはワークフロー全体で共有されるので、APIのキーなどを保存するのに向いています。
初回フラグで分岐
会話変数は「ユーザーの情報」「会話相手の情報」「会話内容の要約」の3つ以外に、「初回のチャットかどうか」のフラグも用意しています。
フラグは初期値として「0」を持たせています。
最初に条件分岐ノードで初回かどうかが判定され、初回(=フラグが0)のみ変数代入ノードに分岐します。
変数代入ノードでは、画面で入力された内容を会話変数に保存し、フラグは1に書き換えます。これにより初回以外は変数代入ノードを通らなくなります。
フラグでなく「会話変数の内容が空かどうか」で分岐してもフラグで管理する場合と基本的には同様の挙動をしますが、もし何かのタイミングでLLMが会話変数の内容を空欄に書き換えることがあったら困るのでフラグ管理にしています。
会話変数の参照
回答を作成するLLMのプロンプトは以下のようにしています。
回答作成LLMのプロンプト
# 指示
あなたは会話のやりとりを助けるエージェントです。与えられた会話内容などの情報を分析し、ユーザー(自分)と会話相手が円滑にコミュニケーションを取るのに有益な情報を提供します。
会話は`自分「」相手「」`の形式で与えられます。
# 出力すべき内容
- 評価:
- ユーザー(自分)の発言を評価し、100点満点で採点し講評を加えます。採点基準は場面に合わせて設定してください。
- 良かった点と改善点を具体的に挙げてください。特に改善点は積極的に指摘します。
- 相手の考え:
- 会話相手の発言意図が明確になるよう感情や考えを推測し、会話相手になりきってロールプレイングしてください。
- エージェントの過去の推測内容には依存せず、会話内容を元に推測します。
- 返信の提案:
- ユーザーが次に返信すべき内容を提案してください。
- 直近の会話への返信内容だけでなく、会話全体の流れを提案しても良いです。
- 必要があれば複数案の提案をしても良いです。
- 提案は具体的な返信文のみの提案は避け、提案内容をユーザーが応用できるように意識します。
# LLMが分析したユーザーの情報
{{#conversation.userInformation#}}
# LLMが分析した会話相手の情報
{{#conversation.partnerInformation#}}
# LLMが分析した現在までの会話の内容の要約
{{#conversation.topicsSummary#}}
最後の部分の# LLMが分析したユーザーの情報
などに会話変数の内容を代入することでLLMに情報を伝えています。
会話変数の更新
会話変数の更新もLLMによって行います。
LLMに更新内容をjson形式で出力させ、それを会話変数に代入するという流れです。
会話変数上書きLLMのプロンプトは以下のようになっています。
会話変数上書きLLMのプロンプト
# 指示
あなたは会話内容を理解し整理するエージェントです。入力された会話の内容から以下の内容を分析し、必要に応じて情報の追加や削除を行った上で上書きします。分析にはエージェントのアドバイス内容を使用することもできます。
会話は`自分「」相手「」`の形式で与えられます。自分と相手の発言を混同しないよう注意してください。
# 出力内容
## ユーザーの情報
- 後続のエージェントがアドバイスを作成する際にユーザーの属性を把握することを助けます
- 基本的なプロフィール、価値観、話題の傾向などを保存します
## 会話相手の情報
- 後続のエージェントがロールプレイングを行う際に相手の属性を把握することを助けます
- 基本的なプロフィール、価値観、話題の傾向などを保存します
## 現在までの会話内容の要約
- 後続のエージェントが文脈を理解することを助けます
- トークンを節約できるよう、会話の全てを保存するのではなくやり取り全体の流れを要約します
# 注意点
- 出力は現在の設定値を踏襲したjson形式で行ってください。json以外の文字列を出力した場合はエラーとなります
- 上書きの必要がない場合は元の内容から変更する必要はありません
- 各項目について、推測は許されますが、その場合は疑問形で記述し事実と推測を区別します
- 後続のLLMには3項目すべての内容が与えられるため、3項目の情報の重複が少なくなるよう意識します
- 情報は蓄積して保存しますが、不要になった情報や矛盾した情報は削除します
- それぞれ500文字以内に収まるよう調整してください
# 現在の設定値
```json
{
"userInformation": "{{#conversation.userInformation#}}"
"partnerInformation": "{{#conversation.partnerInformation#}}",
"topicsSummary": "{{#conversation.topicsSummary#}}"
}
```
プロンプトの# 現在の設定値
部分に会話変数の内容を代入しています。
またこのLLMの出力は後続で利用するので厳密にjson形式で出力してくれないと困ります。そのためJSON Schemaを使って出力形式を強制します。
JSON Schema
{
"name": "output_schema",
"strict": true,
"schema": {
"type": "object",
"properties": {
"userInformation": {"type": "string", "description": "ユーザーの情報"},
"partnerInformation": {"type": "string", "description": "会話相手の情報"},
"topicsSummary": {"type": "string", "description": "現在までの会話内容の要約"}
},
"required": ["userInformation", "partnerInformation", "topicsSummary"],
"additionalProperties": false
}
}
余談なんですが、JSON Schemaの書式はLLMによって若干異なるようです。
上記はOpenAI用のもので、同じままではGoogleのGeminiではエラーになりました。Geminiの場合は以下のようにします。
Geminiの場合のJSON Schema
{
"type": "object",
"properties" : {
"userInformation" : {
"type": "string"
},
"partnerInformation" : {
"type": "string"
},
"topicsSummary" : {
"type": "string"
}
}
}
LLMによって出力されたjsonは「ツール」内のJSON Parseノードで受け取ります。
このノードはjsonを入力として、指定したパラメータの内容を出力できます。
あとは初回の時と同様、変数代入ノードでそれぞれの変数を代入すれば完了です。
その他の工夫
会話変数を活用することが今回の主題ですが、その他の細かい工夫についても触れておきます。
モデル選び
回答の作成と会話変数の更新で2つのLLMノードを使用していますが、今回は回答の作成にはGemini 2.0 Flash Thinking Exp 1219
を、会話変数の更新にはgpt-4o-mini
を使用しています。
Geminiは執筆時現在、無料でAPIを利用できるのがすごいです。
OpenAIのo1のように、「考えるAI」であるGemini 2.0 Flash Thinking Exp 1219
も(レートの制限はありますが)無料で利用できます。
レートの制限は、アプリとして多数に公開するには足りませんが、個人用アプリを自分で使う分には問題になりません。
回答部分には高度なモデルを使用しましたが、会話変数の更新部分は安価なモデルでも十分なパフォーマンスを発揮すると思います。
文章を要約するのなんてLLMの得意分野ですしね。
最初は会話変数の更新もGoogleのGemini 1.5 Pro
を使用していたのですが、ある時から出力がバグり始めた(同じ内容を延々と数千文字出力する)のでgpt-4o-mini
に切り替えました。
GPT系では繰り返しを抑止するパラメータであるPresence Penalty
やFrequency Penalty
を設定できるのですが、Google系にはそのパラメータがないっぽいんですよね。
プロンプト
プロンプト本体は会話変数の項で貼ったので、いくつかの工夫についての補足です。
まず出力形式について、会話変数上書きLLMはJSON Schemaの指定などで出力形式を厳密に指定しています。
一方で回答作成LLMは特に指定していません。
指定すれば当然その通りにしてくれるので、項目などを指定すれば出力が安定するのですが、指定しなくても内容自体は良い感じにしてくれたので敢えて自由にさせています。
採点基準は場面に合わせて設定してください。
回答作成LLMに、会話内容の採点をお願いする部分です。
考える系AIは、採点基準をこちらで指定したりするよりも「ここはそっちで考えておいてね」とした方が質が良くなる感じがします。
具体的な返信文のみの提案は避け、提案内容をユーザーが応用できるように意識します。
LLMの考えてくれる文章は、自分の口調とかにはなっていないのでそのままでは使いにくいと思っています。何も言わないと「これ返したら良いよ」みたいに文章だけ投げてくることがあるので、そういうのやめてねという内容です。
それぞれ500文字以内に収まるよう調整してください
会話変数上書きLLMに注意点として与えた内容です。
500文字は厳密に守ってもらう必要はないですが、長文を書かれるとトークンの節約という目的に反するので目安を伝えています。
エラー制御
回答作成LLMの後に、エラーブランチとして別のLLMノードを追加しています。
エラーブランチ機能は2024年12月のv0.14.0で追加された新しめの機能です。
なんかGeminiのAPIは時々サーバーエラーが返ってくることがあるんですよね。時間を置いて試すと成功しますが。
そういう時に回答が返ってこないことを避けるために、エラー時には代わりにOpenAIのLLMで回答を作成するようにしています。
2つの回答は変数集約器ノードで1つにまとめることで、その後のノードでは1つのものとして扱えるようにしています。
Json Parseノード部分は、エラーが起こった場合は後続の変数代入ノードは実行されずに処理が止まります。
なのでLLMから変なjsonが返されたりしても不正な値が会話変数に保存されることはないはずです。
パラレルラン
回答の作成→会話変数上書きの順で処理していますが、回答内容は会話変数の上書きの前に表示されるようにしています。
これは回答内容が表示されるまでの速度を上げるためです。
画像だと変数集約器ノードの後に回答ノードと会話変数上書きLLMに分岐しています。
これにより会話変数の上書きが完了するのを待つ必要がなくなります。
発展させるとしたら
私はこれをDifyのアプリケーション公開機能(URLにアクセスするだけでアプリを利用できる)でスマホから開いて利用しています。
この方法はとても手軽ですが、例えばスマホのキーボードの改行を押すと改行でなく送信してしまう(そのため複数行の内容は別アプリで書いてからコピペしてる)問題があります。
Difyで作成したアプリはAPI経由でアクセスすることができるので、やろうと思えば自分で好きなようにフロントエンドを実装することができます。
なので自分と相手の会話内容を個別の入力欄に入力するなど、アプリの用途に沿ったUIを作成するのも良いかも知れません。
現状の入力欄が1つのチャット画面も、会話の履歴と相談を一度にできるのでそれはそれで便利なのですが……。
あと会話変数の内容をチャット画面上からでも随時確認できると楽しくて良いですね。
現状は、気になった時にDifyのログ画面から確認しています。
会話変数を参照するAPIは存在しないようなので、これは自分でフロントエンドを実装しても表示はできません。
もちろん回答に会話変数の内容を出力することはできますが、それだとチャットログの入力トークン数が嵩んでしまうので……。
チャットのやり取りとは別枠で情報を出力できるような機能があったら良いですね。
関連記事