2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ChatGPTのfunction callingを用いて過去の会話内容について質問する

Last updated at Posted at 2023-09-18

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: 暑いと酸味と辛味を欲するの?なんで?

今日の会話 (2023-07-05)
Human: 昨日食べた料理が美味しかったから、自分でも作ってみたよ。何を作ったでしょうか?
AI: よだれ鶏を作ったのですね。お疲れ様でした。美味しくできましたか?

Human: 7/3に僕が食べた料理を答えてみて。
AI: 7月3日には、イカスミパスタと真鯛のカルパッチョを食べたとのことですね。どちらも美味しかったとのこと、良かったですね。

概要とまとめ

概要

過去の会話内容について質問したり話したりするために、ChatGPT自身に、過去の会話内容へのアクセスの要否を判断させる というものです。

以前の記事では、LangChainのAgentの機能を使用して実装したのですが、今回は、OpenAI APIの【function calling】を使用して実装します。

function callingを用いることで、会話の最中に、特定の関数(ここでは「過去の会話内容へのアクセス」)を必要に応じて実行することができます。

注意点としては、以下3点です。

  1. ChatGPTが関数要否を判断するため、うまく呼び出せない場合がある
  2. 関数の実行自体はAPIを呼び出す側で実施する (ChatGPTから「関数実行して」と返ってくるので、指定通りに実行する必要があります)
  3. 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 の内容
```python: past_conversations # json_file, datetime, chat_history past_conversations = [ [ '2023-06-28 19:03:42', [ { "role": "user", "content": "最近暑くなってきたからさっぱりしたモノが食べたくて、冷麺を食べたよ。", }, { "role": "assistant", "content": "いいね!ちなみに、盛岡冷麺、韓国冷麺、どっち…?", }, ] ], [ '2023-07-03 21:53:02', [ { "role": "user", "content": "今日は友達とイタリアンに行ったよ。イカスミパスタと真鯛のカルパッチョを食べたんだけど、どっちも美味しかったぁ。", }, { "role": "assistant", "content": "いいなぁ。僕もイタリアン食べたい!!", }, ] ], [ '2023-07-04 20:11:56', [ { "role": "user", "content": "今日の昼は中華レストランでよだれ鶏を食べんたんだ。最近暑いからか、あの酸味と辛味を欲してしまうんだよね", }, { "role": "assistant", "content": "暑いと酸味と辛味を欲するの?なんで?", }, ] ], ] ```

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"に実行対象の関数名とその引数 が格納されます。

具体的には、以下のような感じです。

function calling の返値
# 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に指定

実行用のクラス定義

まとめると、手順は以下の通りです。

  1. ChatCompletion.create()の返値を確認し、
  2. function_callが定義されていたら指定の関数を実行し、
  3. 実行結果をチャット履歴に追加して、再度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)などを指定します。おそらく日本語で問題ないと思いますが、今回は英語で記載しています。

function calling で呼び出す関数の定義
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月に発表された機能ですので、すでに多くの日本語記事がありますが、本記事の内容が何かの参考になりましたら幸いです。

今回の記事は以上となります。
最後まで読んでいただき誠にありがとうございます。
少しでも楽しんでいただけたり、参考になる部分があったりしましたら幸いです。

それでは!(*ˊᗜˋ)ノシ

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?