🌊

OpenAIのFunctionCallingを理解する

2023/06/15に公開

2023/06/13 OpenAIの大きなアップデートが発表されました。
その中でも新たに加わった目玉機能がFunction callingです。

このFunction calling、一見すると「APIのレスポンスをいい感じのJSONにしてくれるのかな?」と思ってしまうのですが、それは使い方の一部で本質ではありません*。本記事では、この少し概念がややこしいFunction callingを早く、正確に理解できるように具体的な実装を交えてご紹介します。

*記事の最後にレスポンスをJSONにする方法もご紹介はします。


Function callingとは

Function callingとは、OpenAI API(以降OpenAI)のレスポンスが外部関数の呼び出しを検知し、教えてくれる仕組みです。これにより、OpenAIと外部のシステム連携をミスなく正確に行うことができるようになります。

具体例として、「お天気お姉さんチャットボット」をOpenAIを使って作ることを考えてみましょう。GPTは直近の天気予報の情報は持っていないので、正しい天気を得るには外部の関数(天気のAPI)を呼び出す必要があります。流れとしては以下のようになるでしょう。

しかし、ここで1つ問題が起きます。いつ天気のAPIを呼び出せばいいのでしょうか?
ユーザーは天気以外のことも自由に入力できます。お天気お姉さんと雑談したい時もあるかもしれません笑 Function CallingのないOpenAIでは、この天気 APIを呼び出すタイミングを検知することが困難でした。

そこで登場したのが、Function callingです。Function callingを利用することで、会話の中で天気取得関数の呼び出しが必要になったら、OpenAIのレスポンスが教えてくれます。

サーバーはレスポンスから関数の実行指示を取り出して、適宜必要な関数呼び出しを行えば良いわけですね 👌🏼


試してみる

概念が理解できたところで、実際にお天気お姉さんチャットボットを実装しながら、Function Callingを試してみましょう。コードはこちらに公開しています。手元で動かしたい方はどうぞ。

https://github.com/kazuooooo/functions_calling_example

functionsの定義

最初に外部関数に当たるfunctionsプロパティを定義します。
今回実行したい天気取得APIの関数が以下のようなものだとします。

type WeatherAPIResponse = {
  date: string;
  location: string;
  weather: string;
};

export const getWeather = (
  location: string,
  date: string
): Promise<WeatherAPIResponse> => {
  // NOTE: モックの実装実際には非同期のAPIリクエストが入る。
  return new Promise((resolve) => resolve({ date, location, weather: "晴れ" }));
};

このgetWeathre関数の呼び出しのインターフェースに合わせて、functionsプロパティを定義します。各プロパティの意味はコメントで追記しておきました。より詳しい定義はドキュメントをご覧ください。

const functions: FunctionObject[] = [
  {
    # 関数の名前
    name: "get_weather",
    # 関数の説明
    description: "指定された場所と日付の天気を取得する",
    # パラメータ
    parameters: {
      type: "object",
      # 引数
      properties: {
        location: {
          type: "string",
          description: "都道府県や市、町の名前, e.g. 東京都文京区",
        },
        date: {
          type: "string",
          description: "Date formatted in YYYY/mm/dd, e.g. 2023/06/13",
        },
      },
      # 引数の中で必須のもの
      required: ["location", "date"],
    },
  },
];

Completion APIの呼び出し

では、このfunctionsをプロパティに含めて、Create Completion APIを呼び出します。

const conversation = new Conversation();
conversation.addMessage("user", "明日の東京の天気わかりますか?");

const response: CompletionAPIResponse = await chatCompletionRequest(
  conversation.messages,
  functions
);

cnosole.log(response)

レスポンスは以下のようになります。
通常のメッセージのレスポンスとは異なり、

  • finish_reasonfunction_call
  • contentnull
  • functions_call に関数名(name)とstringifyしたjson形式の引数(arguments)
    が含まれていることがわかります。
{
  // ...
  choices: [
    {
      index: 0,
      message: {
        role: "assistant",
        content: null,
        // 関数呼び出し
        function_call: {
          // 関数名
          name: "get_tommorow_weather",
          // 引数
          arguments: '{\n  "location": "東京",\n  "date": "2023/06/14"\n}'
        }
      },
    // 関数の呼び出しを意味する
      finish_reason: "function_call"
    }
  ]
}

レスポンスを受け取ったら、finish_reasonfunction_call であれば、arguments内のjsonをパースして、関数(天気のAPI)を実行すればいいわけですね👍🏼

// 関数の呼び出しが必要か
if (response.choices[0].finish_reason === "function_call") {
  const functionCall = firstResponse.choices[0].message.function_call;

  // 引数をparse
  const { location, date } = JSON.parse(functionCall.arguments);
  // 引数を使って天気APIを実行
  const weather = await getWeather(location, date);
}

role: function messageを使って回答の生成もお願いする

続いて、天気APIから取得したレスポンスをユーザーに返却する回答にフォーマットします。
まず思いつくのは以下のようにフォーマットしてしまう方法です。

`${date}${location}天気は${weathre}です` // ex 2023/06/14の東京の天気は晴れです

これでもいいのですが、実は新たに追加されたmessage role:functionを使って、API側に回答を生成してもうらうことができます。

具体的にはリクエストを以下のようにします。ポイントは、contentに関数(天気API)の実行結果を渡している点です。

{
  model: "gpt-3.5-turbo-0613",
  messages: [
    { role: "user", content: "明日の東京の天気わかりますか?", name: undefined },
   // 関数を表すメッセージ
    {
      role: "function",
    // 関数の実行結果をjson stringifyしたもの
      content: '{"date":"2023/06/13","location":"東京","weather":"晴れ"}',
      name: "get_weather"
    }
  ],
  functions: [
    {
      name: "get_weather",
      description: "指定された場所と日付の天気を取得する",
      parameters: {
        type: "object",
        properties: { location: [Object], date: [Object] },
        required: [ "location", "date" ]
      }
    }
  ]
}

レスポンスは以下です。
messagesの内容を見て、いい感じに回答を生成してくれます。

{
  //...
  choices: [
    {
      index: 0,
      // いい感じにフォーマットした回答を生成してくれる
      message: { role: "assistant", content: "明日の東京の天気は晴れです。" },
      finish_reason: "stop"
    }
  ]
}

コードは以下のようになります。リクエストは1回増えますが、messageに要素を追加するだけでシンプルな形で書くことができます。

const weather = await getWeather(location, date);

// role: messageを追加
conversation.addMessage(
  "function",
  JSON.stringify(weather),
  functionCall.name
);

// リクエスト
const secondResponse: CompletionAPIResponse = await chatCompletionRequest(
  conversation.messages,
  functions
);
// => message: { role: "assistant", content: "明日の東京の天気は晴れです。" },

【番外】Funciton callingを使って、APIのレスポンスをJSONにパースする

冒頭にFunction callingはOpen AI APIのレスポンスをJSONにフォーマットするものではないとお伝えしました。

しかし、実は少し工夫してこれを実現することができます。回答が関数呼び出しになるように定義してあげるのです。

例を見てみましょう。

以前自分が開発した、AI本屋さんでは回答をJSON形式でもらうために以下のようなプロンプトを書いていました。

おすすめの本を教えてください。
出力は以下のフォーマットに従ってください。

\`\`\`json
{
 title: string, // 本のタイトル
 description: string // 本の説明
}
\`\`\`

さらに結果をparseしてJSONにしています。

export const parseGPTResponse = (gptResponse: string): GPTBook[] => {
  const regex = /```json([\s\S]*?)```/gm
  const match = regex.exec(gptResponse)

  if (match === null || match?.[1] === null) {
    throw new Error("JSON content not found in the string")
  }
  const jsonData: object = JSON.parse(match[1])

  return jsonData as GPTBook[]
}

これでも、JSON以外の文字列が返ってきたり、うまくparseできなかったりとそこそこの確率でエラーになります。

初めから以下のJSONだけ返してくれれば理想です。

{
 title: string, // 本のタイトル
 description: string // 本の説明
}

これをFunctionCallingを使って実現します。前述の通り、回答結果が関数の呼び出しになるようにすれば良いのです。
具体的には以下のようなrecommend_book 関数を定義します。

const functions: FunctionObject[] = [
  {
    name: "recommend_book",
    description: "おすすめの本を1冊紹介する",
    parameters: {
      type: "object",
      properties: {
        title: {
          type: "string",
          description: "本のタイトル",
        },
        description: {
          type: "string",
          description: "本の内容",
        },
      },
      required: ["title", "description"],
    },
  },
]

これで、本のおすすめを聞けば、function_call.args に理想のJSONを含めて返してくれます。

const conversation = new Conversation()
conversation.addMessage(
  "user",
  "悩めるウォンバットにおすすめの本を教えてください。"
)

const response: CompletionAPIResponse = await chatCompletionRequest(
  conversation.messages,
  functions,
  { name: "recommend_book" }
)

const functionCall = response.choices[0].message.function_call
const bookJSON = JSON.parse(functionCall.arguments)
console.log(bookJSON)
// {
//   title: "幸せになるためのウォンバットの道",
//   description: "ウォンバットのための幸せの秘訣を紹介した本です。幸せなウォンバットになるための心の持ち方や行動のコツが詳しく解説されています。是非読んでみてください!"
// }

参考
Function calling and other API updates
openai-cookbook/examples/How_to_call_functions_with_chat_models.ipynb at main · openai/openai-cookbook
OpenAI API


今後もAIを使ったサービス開発について発信していきますので、ぜひTwitterなどフォローお願いします〜
コーディングをAIで自動化するサービスも考えますので、興味ある方ぜひ事前登録お願いします🙏
https://supernova.studio.site/

Discussion