はじめに
ChatGPTのシナリオの1つとして、「社内データを検索して回答を作る」というは人気のシナリオかと思います。ドキュメントなどを検索して~というのももちろん大事ですが、メールのやりとりをもとにしたChatGPTを実際に作ってみたので、そのポイントについて記載したいと思います。(メールデータはさすがに1ミリも公開できないので)
やったことの概要は以下の図です。事前にメールのやりとりを要約してベクトル化して、質問が来たらベクトル検索して参考になるやりとりを見つけて、それを使って回答を作る。これだけです。
なお、メールやりとり(メールスレッド)はどうしても長くなりますし、メールのやりとりなので、挨拶や催促、署名などの参考にするには不要なデータがたくさん含まれてしまいます。大体text-embedding-ada-002の上限8kトークンを超えてしまうため、そのため今回は事前準備として、既存のメールデータを分析&ChatGPTをつかって要約してその結果を使う形にしています。
なお、利用したソフトウェアやモデル名は以下の感じです。
- Outlook
- gpt-35-turbo(0613)
- JupyterNotebook
1. 対象のメールデータをフォルダにコピーする
わかりやすくやるために、参考にしたいメールのやりとりを適当なoutlookのフォルダをつくってコピーします。サポートメールのやりとりでも、技術対応のやりとりでも何でもいいです。
2. コピーしたフォルダを対象にメールデータをCSVで引っこ抜く
本当はpstでエクスポートして分析に回したいところでしたが、pstの分析自体に色々とありそうだったので、わかりやすくcsvで引っこ抜きます。
Outlookの「ファイル」→「開く/エクスポート」→「インポート/エクスポート」→「ファイルにエクスポート」→「テキストファイル(コンマ区切り)→対象のフォルダを選択。という流れです。
バージョンによって多少文言など違うかもしれませんが、要はフォルダをCSVにエクスポートできればいいです。
3. エクスポートしたCSVを分析、事前準備する
分析にはとりあえずPandasを使います。CSVを見てもらえればと思いますが、日本語の場合件名は、「件名」になっていますね。
import pandas as pd
#CSVファイルを読み込む
df = pd.read_csv('エクスポートしたCSVファイル名.csv', encoding='utf-8')
subject = df['件名']
3.1 メールスレッドの最新のメールを判別する
メールのやりとりは通常過去のメール文章を含んだ形で返信されているかと思いますので、最新だけをつかって分析したいため、まずは最新を判別します。(この辺はもしルールが違うようであればそれに合わせた対応が必要ですけど。)
ReとかRe:とかRe.とか[外部]とか返信にあたって分析に余計な件名になっている可能性があるのですが、複雑なことはせず件名の後方一致度で判別します。もしかしたらもっといい方法あるかもしれませんがとりあえず。
以下のような関数を用意して、ある行の件名と次の行件名のマッチ率を出していきます。
こちらはChatGPTに作らせた関数です。もうちょっと変えるとかはお好みで。
# 末尾の一致度を計算する関数を定義する
def match_rate(str1, str2):
# 一致した文字数をカウントする変数を初期化する
count = 0
# 文字列1と文字列2の短い方の長さを取得する
min_len = min(len(str1), len(str2))
# 短い方の長さ分だけループする
for i in range(min_len):
# 文字列1と文字列2の末尾からi番目の文字が一致したら、カウントを増やす
if str1[-(i+1)] == str2[-(i+1)]:
count += 1
# 一致しなかったら、ループを抜ける
else:
break
# 一致度をパーセントで計算する
rate = count / min_len * 100
# 一致度を返す
return rate
これによってRe:とかre.とかがついている場合は90%~くらいのマッチ度になって件名が変わったら0とか低い数字がとれるようになります。すっごい似た件名が出たり、同じ件名の頭に違う質問を入れていくスタイルだとうまくかないかもしれませんが、手元の80メールを使った限りでは問題なさげでした。このあたりは微調整いるかもです。
3.2 メールスレッドの最新のメールについてChatGPTで要約をつける
スレッドごとの最新メールがとれたら、それらに対して要約を実施します。とても長いメールスレッドなどがある場合は、10kトークンくらいで切り落とすとか、あるいは分割して要約+さらにマージの要約なども必要になるかもです。今回は10kトークンくらいで切り落としました。大体欲しい情報は救えたのですが、これはメール内容次第なので個別調整が必要になります。
ここが一つポイントになりますが、メールのやりとりから、最終的にChatGPTに参考にして欲しい情報をここでうまいこと抽出・要約させます。 例えば以下みたいなプロンプトです。
system_prompt = """\
入力された製品担当チームとお客様とのやり取りのメールに対して、以下の観点で詳細を残しつつまとめてください。
なお、個人名や個人情報などは含めません。
- タイトル
- 質問内容
- 質問の背景や前提条件、制限事項など
- 最終的な状態
- 最終的な回答
- 引用したURLとその概要
**URLはhttpからはじまるリンクを正確に残してください。**
# 回答例
タイトル: xxxx
質問内容: xxxx
質問の背景や前提条件、制限事項など: xxxx
最終的な状態: 解決済み、調査中など
最終的な回答: xxxx
引用したURLとその概要:
- https://xxxxxxxxx
このサイトではAについて言及しています。
- https://yyyyyyyyy
このサイトではBについて説明しています。
"""
また大量のデータ分析にChatGPTを使う場合はリトライとタイムアウトがかなり重要になってきます。今回は20秒タイムアウト、リトライはとりあえず適当に3回、リトライ回数に併せてちょっとだけ長く待つスタイルにしました(以下)。このあたりはお好みですね。
# エラーが発生したら同じ処理を3回くらい返す
for i in range(3):
try:
# ChatGPTを呼びだす
response = get_completion_from_messages(messages)
return response
except Exception as e:
print("error occured "+str(i),e)
# 待機(0秒->3秒->6秒待機)
time.sleep(i*3)
pass
3.3 分析した結果をCSVにエクスポートする
最新以外のメール行だとか、不要な列だとかをドロップした上で、さっくりと分析結果をCSVにエクスポートします。
df.to_csv('分析後のデータ.csv', encoding='utf-8', index=False)
3.4 分析後のデータをもとに、ベクトル化する
from openai.embeddings_utils import get_embedding
を使って、ベクトル化してストアに保存していきます。
4. 質問を受け付け、ベクトル検索してから回答を作る
保存したベクトルデータがベクトルDBならその機能で、そうでなければ、''cosine_similarity'なんかをつかって検索していきます。
今回はメールのやりとから情報を探すため、ベクトル検索でとれた最近似の1件のみを対象にしてみました。このあたりはどんなメールのやりとりでどんな回答が欲しいか次第で複数件いれればいいですね。
以下のようなプロンプトです。チケット情報に上位3件抜いたうちの1件目だけをいれてる感じです。
system_prompt = f"""\
あなたはAIアシスタントです。ユーザからの質問に以下のチケット情報のみを使って回答してください。
なお、チケット情報のなかで引用されているURLは回答に含めてください。
# チケット情報
{top3[0]}
"""
このChatGPT回答に併せて、検索した元データも提示すると変な要約があっても救えるかなと思います。
まとめ
実際のメールデータが1ミリも公開できませんため、とりあえず流れだけの記事になりましたが、大体の流れとして参考になれば幸いです。
メールのやりとり色々使えると思うんですよね。
- サポートチームのメールやり取り
- 問い合わせ窓口のメールやり取り
- 技術QA
などなど
メールデータを直接検索するとやっぱり色々な情報がはいっていて求める情報に辿り着けないなんてことが多くあろうかと思いますので、一回人間系でまとめていた、なんてケースは今回紹介した流れでChatGPTにまとめさせるといいかもしれません。
あとはメールのやり取りの中で複数の問題に対応しているケースなんかは、事前分析において最新だけでなく個別のメールを分析させていかないとダメかもしれませんので、このあたりはどんなメールやりとりでどんな情報を抜きたいか次第ですね。どこまで情報をきちんと抜き出したいか、最終的にどんなことができればよいかのうまい落としどころを見つけうまいこと知財活用できたらと思います。というわけで、ポイント紹介でした。