ChatGPTと会話していて、過去に会話した内容について、質問したり話したりできたら嬉しくないですか?
以前の記事で、LangChainを用いて実装したのですが、今回、ChatGPTのfunction callingでも試してみました。
実際の動作例としては、以下の通りです。
【一週間前の会話 (2023-06-28 19:03:42) 】
Human: 最近暑くなってきたからさっぱりしたモノが食べたくて、冷麺を食べたよ。
AI: いいね!ちなみに、盛岡冷麺、韓国冷麺、どっち…?
【一昨日の会話 (2023-07-03 21:53:02)】
Human: 今日は友達とイタリアンに行ったよ。イカスミパスタと真鯛のカルパッチョを食べたんだけど、どっちも美味しかったぁ。
AI: いいなぁ。僕もイタリアン食べたい!!
【昨日の会話 (2023-07-04 20:11:56)】
Human: 今日の昼は中華レストランでよだれ鶏を食べんたんだ。最近暑いからか、あの酸味と辛味を欲してしまうんだよね
AI: 暑いと酸味と辛味を欲するの?なんで?
Human: 昨日食べた料理が美味しかったから、自分でも作ってみたよ。何を作ったでしょうか?
AI: よだれ鶏を作ったのですね。お疲れ様でした。美味しくできましたか?
Human: 7/3に僕が食べた料理を答えてみて。
AI: 7月3日には、イカスミパスタと真鯛のカルパッチョを食べたとのことですね。どちらも美味しかったとのこと、良かったですね。
概要とまとめ
概要
過去の会話内容について質問したり話したりするために、ChatGPT自身に、過去の会話内容へのアクセスの要否を判断させる というものです。
以前の記事では、LangChainのAgent
の機能を使用して実装したのですが、今回は、OpenAI APIの【function calling】を使用して実装します。
function callingを用いることで、会話の最中に、特定の関数(ここでは「過去の会話内容へのアクセス」)を必要に応じて実行することができます。
注意点としては、以下3点です。
- ChatGPTが関数要否を判断するため、うまく呼び出せない場合がある
- 関数の実行自体はAPIを呼び出す側で実施する (ChatGPTから「関数実行して」と返ってくるので、指定通りに実行する必要があります)
- API呼び出し回数が増えるので、応答時間が長くなる
まとめ(頭出し)
先にまとめを書いちゃいます。
Function Callingを用いることで、所望の通り、過去の会話内容に関連する質問に回答することができました。
GPT-3.5、GPT-4で試してみましたが、やはり GPT-4の方が性能が高く、今回試した例では全て想定通り関数を呼び出すことができました。
一方で、GPT-3.5では、過去に関する直接的な質問 (例:「昨日は何を食べたっけ?」)では正しく動作しましたが、間接的な質問 (例:「昨日食べた料理を自分でも作ってみたよ。何を作ったでしょう?」)ではうまく「過去の会話へのアクセス」を呼び出せませんでした。
「結果」に、実際のGPT-3.5 / 4の出力を載せていますので、ご参照ください。
ソースコード
GitHubにnotebookをあげていますので、よろしければご参照ください。
Google Colaboratoryで開く場合はこちらから。
実装
環境構築
Google Colabを使用していきます。
まずは、必要なPythonパッケージをインストールします。
! pip install openai
今回はOpenAI APIを使用するので、アクセストークンを指定します。
import os
os.environ['OPENAI_API_KEY'] = 'ご自身のアクセストークンに書き換えてください'
1. 会話をJSON形式で読み書きする
import json
from datetime import datetime
def save_conversation_from_messages(outdir, datetime_str, messages, **kwargs):
chat_datetime = datetime.strptime(datetime_str, '%Y-%m-%d %H:%M:%S')
json_filepath = os.path.join(outdir, 'chat_history_{}.json'.format(chat_datetime.strftime('%Y%m%d')))
dat = dict({'datetime': datetime_str, 'messages': messages}, **kwargs) # 任意の情報を保存可能
with open(json_filepath, 'w') as fout:
json.dump(dat, fout)
print('Saved ...', json_filepath)
def load_conversation_from_json(json_filepath):
with open(json_filepath, 'r') as fin:
dict_all = json.load(fin)
return dict_all
# 日別で会話内容を保存
for datetime_str, messages in past_conversations:
save_conversation_from_messages('./', datetime_str, messages)
(参考) past_conversations の内容
2. function callingを用いて、過去の会話内容を呼び出す
OpenAIの公式サイト OpenAI Guides - Function Calling を参照しました。
日本語の文献も多数ありますので、以下では使用方法の概略のみ記載します。
function callingの使用方法
関数定義
function callingの使用方法は簡単で、openai.ChatCompletion.create()
を実行する際に、functions=
に関数定義を指定するだけです。
(参考) 関数定義例(OpenAI公式サイトより引用)
functions = [
{
"name": "get_current_weather",
"description": "Get the current weather in a given location",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and state, e.g. San Francisco, CA",
},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
},
"required": ["location"],
},
}
]
関数実行の判定
関数が不要と判断された場合、通常通り"content"
にモデル出力のテキストが格納されます。一方で、関数実行が必要と判断された場合には、"content"
は null (None)となり、代わりに "function_call"
に実行対象の関数名とその引数 が格納されます。
具体的には、以下のような感じです。
# print(response['choices'][0]['message'])
{
"role": "assistant",
"content": null,
"function_call": {
"name": "recall_past_chats",
"arguments": "{\n \"the_date\": \"2023-07-03\"\n}"
}
}
関数実行結果の渡し方
指定された関数を実行した後、結果をモデルに返す必要があります。
以下のように、"role"を "function" としてチャット履歴に追加して、ChatCompletion.create()
のmessages=
に指定します。
function_message = {
"role": "function",
"name": function_name, # 関数名
"content": function_response, # 実行結果
}
message_history.append(function_message) # これをChatCompletionに指定
実行用のクラス定義
まとめると、手順は以下の通りです。
-
ChatCompletion.create()
の返値を確認し、 -
function_call
が定義されていたら指定の関数を実行し、 - 実行結果をチャット履歴に追加して、再度
ChatCompletion.create()
を実行
以下、function callingを実行可能なチャット機能を、クラスとして定義します。
関数chat()
の中で、上記の3ステップを実施しています。
class MyChatBot():
SYSTEM_PROMPT = """You are a helpfule assistant.
You need to follow the following rules:
- lang:ja
- please be polite (e.g. use です, ます)
- short reply (less than 100 japanese charactors)
"""
def __init__(self, model='gpt-4', temparature=0.0):
self._model = model
self._temparature = temparature
self.reset_messages()
# For function calling
self._available_functions = {}
self._function_properties = {}
def reset_messages(self):
self._messages = [{"role": "system", "content": MyChatBot.SYSTEM_PROMPT}]
def get_messages(self):
return self._messages.copy()
def add_function(self, function, func_props):
if not callable(function):
raise TypeError('function must be callable.')
func_name = func_props['name']
self._available_functions[func_name] = function
self._function_properties[func_name] = func_props
@property
def function_properties(self):
if len(self._function_properties) == 0:
return None
return list(self._function_properties.values())
# 会話用インタフェース (function calling は最大一回のみ)
def chat(self, input, verbose=False):
user_message = {'role': 'user', 'content': input}
response = self._chat_completion(user_message)
# add assistant message
response_message = response['choices'][0]['message']
self._messages.append(response_message)
# Function Calling
if response_message.get('function_call'):
function_name = response_message['function_call']['name']
fuction_to_call = self._available_functions[function_name]
function_args = json.loads(response_message['function_call']['arguments'])
function_response = fuction_to_call(**function_args)
if verbose:
print(f'request function: {function_name}')
print(' args - ', function_args)
function_message = {
'role': 'function',
"name": function_name,
"content": function_response,
}
response = self._chat_completion(function_message)
return response
# API call (function calling 有無で共通化)
def _chat_completion(self, message, enable_functions=True):
# update messages
self._messages.append(message)
# for function calling
additional_args = {}
if enable_functions and (len(self._function_properties) > 0):
additional_args['functions'] = list(self._function_properties.values())
# run chat-completion
response = openai.ChatCompletion.create(
model=self._model,
temperature=self._temparature,
messages=self._messages,
**additional_args
)
return response
呼び出す関数の実装
続いて、呼び出すための関数と、function calling用の関数定義です。
関数自体は通常通り定義します。ここでは、引数として「日付(YYYY-MM-DD)」を指定すると、その日付の会話内容を返す関数を定義しています。
function calling用の関数定義は、関数名(name)や用途(description)、引数情報(parameters)などを指定します。おそらく日本語で問題ないと思いますが、今回は英語で記載しています。
import re
def recall_past_chats(the_date):
if re.match(r'\d{4}-\d{2}-\d{2}', the_date) is None: # is not date format
return None
json_file = 'chat_history_{}.json'.format(the_date.replace('-', ''))
chats_history = load_conversation_from_json(json_file)
messages = chats_history['messages']
# make output
output = f"The following is chats on {the_date} between the user and YOU (i.e. assistant);"
for msg in messages:
role = msg['role']
content = msg['content']
output += f"\n{role}: {content}"
return output
func_props = {
'name': 'recall_past_chats',
'description': 'Access the past chats between you and the user. Today is 2023-07-05.',
"parameters": {
"type": "object",
"properties": {
"the_date": {
"type": "string",
"description": "Specify the date of the chats you need to access in the format 'YYYY-MM-DD'",
},
},
"required": ["the_date"],
}
}
# function callingを有効にして会話する
my_chatbot = MyChatBot()
my_chatbot.add_function(recall_past_chats, func_props)
response = my_chatbot.chat('一昨日僕が食べた料理を答えてみて。')
print(response['choices'][0]['message']['content'])
# 一昨日、あなたは友達とイタリアンに行き、イカスミパスタと真鯛のカルパッチョを食べましたね。どちらも美味しかったとのことです。
結果
それでは、実際の回答結果を見てみましょう。
ますは、【GPT-3.5、GPT-4の両方で】 正しく回答できた例です。
input : 一昨日僕が食べた料理を答えてみて。
GPT-3.5 : 一昨日、あなたはイカスミパスタと真鯛のカルパッチョを食べました。
GPT-4 : 一昨日、あなたは友達とイタリアンに行き、イカスミパスタと真鯛のカルパッチョを食べましたね。どちらも美味しかったとのことです。
input : 7/4に食べた料理は何でしょうか?
GPT-3.5 : 7月4日に食べた料理は「よだれ鶏」でした。暑い日には、酸味と辛味を欲することがあるんですよね。
GPT-4 : 7月4日には中華レストランでよだれ鶏を召し上がったようですね。
続いて、【GPT-4のみで】 正しく回答できた例です。
(GPT-3.5では、関数呼び出しを要求できませんでした…)
input : 昨日食べた料理が美味しかったから、自分でも作ってみたよ。何を作ったでしょうか?
GPT-3.5 : それは素晴らしいですね!昨日食べた料理を自分でも作ってみたのですね。では、昨日食べた料理は何でしたか?
GPT-4 : よだれ鶏を作ったのですね。お疲れ様でした。美味しくできましたか?
input : 昨日食べた料理が美味しかったから、自分でも作ってみたよ。何を作ったか当ててみて!
GPT-3.5 : それは素晴らしいですね!では、昨日作った料理は何でしょうか?楽しみにしています!
GPT-4 : よだれ鶏を作ったのではないでしょうか。
少し表現を変えて試してみましたが、GPT-3.5では出来ませんでした。
成功例との違いは、間接的に過去のことについて聞いている点、でしょうか?
GPT-3.5が成功した例では、直接的に過去のことについて質問しています。一方で、失敗ケースでは、「質問自体は過去に関するものではなく、ただ、回答するには過去の情報が必要」となっています。
Web版ChatGPTを使用していて、個人的には、GPT-3.5でも十分な性能を感じていますが、やはりGPT-4の方が文脈を正しく捉える能力が高いということがわかりますね。
おわりに
本記事では、過去の会話内容に基づいて会話をするチャットボットを作るべく、OpenAI APIの Function Calling を試してみました。
GPT-3.5とGPT-4で実行し、両モデルとも、過去に関する質問にちゃんと答えられることを確認しました。ただし、GPT-3.5では、関数(過去の会話内容の参照)の実行要否の判断を誤る場合がありました。
function calling自体は 2023年6月に発表された機能ですので、すでに多くの日本語記事がありますが、本記事の内容が何かの参考になりましたら幸いです。
今回の記事は以上となります。
最後まで読んでいただき誠にありがとうございます。
少しでも楽しんでいただけたり、参考になる部分があったりしましたら幸いです。
それでは!(*ˊᗜˋ)ノシ