見出し画像

【ChatGPT】Function callingで一度の問い合わせで複数関数の呼び出しに対応できる実装方法を紹介(公式サンプルそのままだとできません)

Penmarkの河村です。
6/13にChatGPTの大型アップデートがあり、Function calling機能が提供されました。これはとてもおもしろい機能で、Penmarkでもこれを活かすことで学生によりよいサービスを提供できないか模索中です。
Function calling機能は、ChatGPTがこちらが作成した任意の関数を呼び出して回答を作成してくれる機能ですが、単純に関数を一回呼び出せるだけの機能ではありません。「ある関数を実行するために必要な情報を、別の関数で取得してから呼び出す。」というような、「1回のリクエストで複数の関数を順序立てて呼び出すこと」も可能です。

例えば、下記のような2つの関数を用意しておきます。
・geocoding(location_name)→lat,lng (地名から緯度経度を取得)
・get_map(lat,lng)→地図URL (緯度経度から地図のURLを取得)
このときに、「東京駅の地図を見せて」というと、geocoding関数で東京駅の緯度経度を取得してから、get_map関数で地図URLを取得する。という様に、処理を組み立て実行してくれます。

デフォルトで対応されている機能なのですが、OpenAIの公式サンプルがこれに対応できる書き方ではなかったので、できるということに気づいてない人も多いのではと思い、この記事を書くことにしました。
この記事を見るとChatGPTのFunction calling機能で、複数関数呼び出しに対応するための実装方法がわかります。

Function callingの処理フロー概要

まずはおさらいです。
Function callingは先週リリースされたばかりの機能ですし、知らない方も多いと思いますので、Function callingの処理フローを簡単に説明します。
勘違いしやすいのは、こちらで定義した関数をChatGPTから直に呼び出すわけではないです。そうではなくて、どの関数をどういう引数で実行して結果を教えてね。というレスポンスが返ってくるのでその通りに実行して教えてあげる。という処理になります。これを理解してないと公式のサンプルを読んでも理解しにくいです。

  1. [こちらの処理]chatGptにチャット内容を送る際に、利用できる関数の仕様を合わせて伝えます。

  2. [ChatGPT側の処理]チャット内容を見て、もし関数を使わずに回答が作成できるのであれば、ユーザへの回答を作成して返却します。関数を使わないと回答できないと判断したときは、「実行してほしい関数名」と「実行する際の引数」を返却します。

  3. [こちらの処理]レスポンス内容を見て、回答が返ってきたのか、関数呼び出し依頼が来たのかを判断します。回答が来たのなら、それをユーザに提示して終了です。 関数呼び出し依頼が来た場合は下記流れになります。

    1. [こちらの処理]レスポンスにある関数名と引数を用いて関数を実行し、これまでの流れに関数の結果を追加してChatGPTに送ります。

    2. [ChatGPTの処理]ChatGPTは、これまでの流れと、関数の結果を総合してユーザへの返答を作成します。

    3. [こちらの処理]ユーザへの返答を作成します。

公式ドキュメントに記載されているサンプルコードは下記です。

import openai
import json


# Example dummy function hard coded to return the same weather
# In production, this could be your backend API or an external API
def get_current_weather(location, unit="fahrenheit"):
    """Get the current weather in a given location"""
    weather_info = {
        "location": location,
        "temperature": "72",
        "unit": unit,
        "forecast": ["sunny", "windy"],
    }
    return json.dumps(weather_info)


def run_conversation():
    # Step 1: send the conversation and available functions to GPT
    messages = [{"role": "user", "content": "What's the weather like in Boston?"}]
    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"],
            },
        }
    ]
    response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo-0613",
        messages=messages,
        functions=functions,
        function_call="auto",  # auto is default, but we'll be explicit
    )
    response_message = response["choices"][0]["message"]

    # Step 2: check if GPT wanted to call a function
    if response_message.get("function_call"):
        # Step 3: call the function
        # Note: the JSON response may not always be valid; be sure to handle errors
        available_functions = {
            "get_current_weather": get_current_weather,
        }  # only one function in this example, but you can have multiple
        function_name = response_message["function_call"]["name"]
        fuction_to_call = available_functions[function_name]
        function_args = json.loads(response_message["function_call"]["arguments"])
        function_response = fuction_to_call(
            location=function_args.get("location"),
            unit=function_args.get("unit"),
        )

        # Step 4: send the info on the function call and function response to GPT
        messages.append(response_message)  # extend conversation with assistant's reply
        messages.append(
            {
                "role": "function",
                "name": function_name,
                "content": function_response,
            }
        )  # extend conversation with function response
        second_response = openai.ChatCompletion.create(
            model="gpt-3.5-turbo-0613",
            messages=messages,
        )  # get a new response from GPT where it can see the function response
        return second_response


print(run_conversation())

私が書いた処理フローの1がStep1、3がStep2、3-aがStep3、3-cがStep4に該当します。というのを踏まえて公式のサンプルコードを見てもらうとわかりやすいかなと思います。

複数関数呼び出しへの対応方法

このドキュメントのメインです。
実は、こちらでトリッキーな何かをするとかそういうことでは一切ないです。複数関数の処理が必要とChatGPTが判断した場合は、一つづつ順番に呼び出し依頼が来ます。1回目の関数について結果を送った際のレスポンス。つまり、上記手順の3.3の処理の際に、「この関数『も』実行してほしい」と2回目の関数処理依頼が返ってきます。なので、初回と同様に関数を実行して再度送ってあげれば良いというそれだけです。最終的に何回呼び出されるかはわからないので、再帰呼び出しで実装したほうがよいですが、それだけで対応できます。

サンプルコードは
・geocoding(location_name)→lat,lng (地名から緯度経度を取得)
・get_map(lat,lng)→地図URL (緯度経度から地図のURLを取得)
という2つのfunctionを定義し、「東京駅の地図を見せて」という問い合わせに答えてくれる。という題材で書いています。

まずfunctionの定義部分と、ローカルの関数実行部分はこのような感じになります。

const functionDefinition = [
  {
    name: 'geocoding',
    description: "指定された場所の緯度経度情報(lat,lng)を取得します。",
    parameters:{
      type: 'object',
      properties: {
        location :{
          type: 'string',
          desciption: '緯度経度が知りたい地名を記入してください。'
        }
      },
      required:['location']
    },
  },
  {
    name: 'get_map',
    description: "緯度経度を引数に地図URLを返却します。",
    parameters:{
      type: 'object',
      properties:{
        lat:{
          type: 'number',
          desciption: '地図を知りたい地点のLATを指定してください。'
        },
        lng:{
          type: 'number',
          description: '地図を知りたい地点のLNGを指定してください。',
        }
      },
      required:['lat','lng']
    }
  }  
]

const executeFunction = async (functionCall:any):Promise<ChatCompletionRequestMessage> =>{
  const functionName = functionCall.name;
  if(functionName === 'geocoding'){
    return {
      role: 'function',
      name: functionName,
      content: '{"lat":35.681236,"lng":139.767125}'
    }    
  }else if(functionName === 'get_map'){
    const args = JSON.parse(functionCall.arguments!);
    const mapUrl = `https://www.google.com/maps/search/?api=1&query=${args.lat},${args.lng}`;
    return {
      role: 'function',
      name: functionName,
      content: mapUrl,
    }    
  }
  throw new Error('not implemented');
};

次がメイン処理です。
最初に呼び出すのはchatSessionで、ここからchatRecursiveを呼び出します。 ChatGPTからのレスポンスが、ユーザへの返答だった場合は、それを表示して終了しますが、関数を呼び出してほしいという依頼だった場合は、関数を実行した後、再帰呼び出しとなります。

const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
const OPENAI_MODEL = "gpt-3.5-turbo-0613";

const configuration = new Configuration({
  apiKey: OPENAI_API_KEY,
});

const chatRecursive = async (sessionMessages:[ChatCompletionRequestMessage]) => {
  const openai = new OpenAIApi(configuration);  
  const response = await openai.createChatCompletion({
    model: OPENAI_MODEL,
    messages : sessionMessages,
    functions: functionDefinition,
    function_call: 'auto'
  })
  const message = response.data.choices[0].message!;
  if(message.content){
    console.log(`user-response:${message.content}`);
    return;
  }else if(message.function_call){
    console.log(`function-start:${message.function_call.functionName},args:${message.function_call.arguments}`);
    const functionMessage = await executeFunction(message.function_call);
    console.log(`function-message ${functionMessage}`);
    sessionMessages.push(message);
    sessionMessages.push(functionMessage);
    await chatRecursive(sessionMessages);
    return ;
  }
}

const chatSession = async(content:string) => {
  console.log(`user-reqest:${content}`);
  const message:ChatCompletionRequestMessage = {
    role: 'user',
    content: content,
  }  
  await chatRecursive([message]);
}

const main = async() => {
  await chatSession('東京駅の地図見せて');
}

これで「東京駅の地図を見せて」とリクエストすると、「東京駅の緯度経度を得るためにgeocoding関数の呼び出し」と「緯度・経度を用いて、地図URLの取得関数」の2つが順に呼び出され、その結果をもとにユーザに返答されます。
下がその際の実行ログです。ユーザのリクエストレスポンスの他に関数実行時とその結果をログ出力してるので下記のようになります。

user-request:東京駅の地図見せて

function-start:geocoding,args:{"location": "東京駅"}
function-message:{role: 'function',name: 'geocoding',content: '{"lat":35.681236,"lng":139.767125}'}
function-start:get_map,args:{"lat": 35.681236,"lng": 139.767125}
function-message:{role: 'function',name: 'get_map',content: 'https://www.google.com/maps/search/?api=1&query=35.681236,139.767125'}

user-response:
東京駅の地図はこちらです。
[地図を見る](https://www.google.com/maps/search/?api=1&query=35.681236,139.767125)

まとめ

以上で一回のメッセージを複数関数を用いて処理できるようになります。公式のサンプルは一回呼び出す時の処理がシーケンシャルに書かれているのですが、そのまま拡張しても複数呼び出しに対応できません。なので、複数呼び出しに対応したい方はこの記事を参考にしてもらえればと思います。(推測ですが、サンプルを再帰処理にしてしまうと説明がしにくいので、説明がしやすいシーケンシャルな書き方にしたのかな…と思います。)

ちなみにこの機能、かなりすごいです。実際にいろいろ試していますが、素材となるfunctionを幾つか渡しておけば、ChatGPTが必要なものを勝手に組み立て実行してくれます。システムのUIとして十分に機能する領域に到達しているので、今後、様々なシステムのUIが言語ベースのUIに置き換わっていくと思います。

Penmarkでは、この機能を活用した新しいサービスを検討しています。それについてもいつかお話できると想いますので少しお待ちください。

PenmarkではChatGPTを活用を積極的に推進しています。
ChatGPTの文章解釈力をデータ作成に活かしてサービスに利用するデータを作成するなど、実用化例も出始めています。それについてもノートを書いてるのでぜひ読んでみてください。

書籍抽出作業をChatGPTにお願いしたら完全に人間で驚いた。

エンジニア積極採用中です!

急成長するプロダクトを一緒に開発して頂けるエンジニアを積極採用中です。ペンマークのエンジニアは様々なバックグラウンドを持った魅力的な仲間が多いのが特徴です。ペンマークに興味を持ってくださる方は、是非一度お話しましょう🔥

アプリのダウンロードはこちらから

この記事が参加している募集

AIとやってみた