🤖

Function callingは自然言語からいい感じのjsonを作れることこそが真髄ではないか

2023/09/12に公開

OpenAI の API として6 月にリリースされた Function callingを最近使っていて、その使い方の真髄はpromptの文脈から関数を呼び出してAIの返答に反映できること…ではなく、ファジーな自然言語から柔軟にjsonを生成できることの部分にあるのではないかと考えています。
すでにOpenAIのAPIを関数のように使っている方には常識になっていそうですが、自分の備忘と思考整理を兼ねて考えを書いてみました。

Function calling の概要について

まずそもそも Function calling がどのようなものかを公式の例を元に説明します。

例として提示されているのは図のようなユーザと AI、つまり任意のLLMとのやり取りです。
しかしLLMは超膨大なデータの集積ではありますが、過去のデータを元にして「今現在の任意の場所の状況を正確に判断すること」はできません。
そこで「LLMがあるシーンにおいてに使うべき処理」を LLMへのリクエスト時にFunctionsとして指定することで、LLMがその処理を使用して取得するように振る舞わせることができます。
今回の場合は天気を調べたいときに使うべき処理、についてが会話内容にともに指定されています。

つまり、ユーザーが AIに投げかけている会話の内容はこのような内容です

この会話のリクエストを以て、以下のようなフローでデータのやり取りが行われることにより、AIに指定の場所の現在の情報を発話させることができているというわけです。

しかし、この公式サンプルで示されている利用フローを踏襲する場合いくつかの注意すべき事項があります。

注意点①:トークンの使用量が多くなりすぎるケースがある

前項目のフロー図の⑤の部分を見てみると、AIのリクエスト内容がやけにかさんでいることがわかると思います。
実際にリクエストしている messsages パラメータの内容は以下のようになっています。

  "messages": [
    {"role": "user", "content": "What is the weather like in Boston?"},
    {"role": "assistant", "content": null, "function_call": {"name": "get_current_weather", "arguments": "{ \"location\": \"Boston, MA\"}"}},
    {"role": "function", "name": "get_current_weather", "content": "{\"temperature\": "22", \"unit\": \"celsius\", \"description\": \"Sunny\"}"}
  ],

この例では指定位置の天気情報だけなので大した文字数にはなっていませんが、例えば 東京のホテルを教えて というような内容である場合、複数件のホテルを、施設名や金額、プラン名やその他のオプション情報をつけて返す…というような形にする場合、あっという間にトークン数が膨れ上がってしまうことが想像できるかと思います。

ただし、昨今の OpenAIのAPIのModelはGPT3.5−Turboなどを筆頭にかなり大きいトークンリクエストに耐えられるものもあります・・・が、API リクエストなどを無加工で無邪気に突っ込んでしまうとトークン数オーバーによる 400 エラーの発生が起こることをはじめ、利用料金が嵩んでしまうことも予想されます。

このため、functionのcontentに入れる要素についてはある程度プロパティや件数をサマリーしたものを入れるようにすることが望ましいと考えられますが、ユーザの問いかけへの返答に対してたくさんの情報量が求められる場合にはマッチしづらいユースケースになります。

注意点②:一度のリクエストで複数のFunctionのargumentsの情報を得ることができない

Function callingでは複数個のfunctionsを設定することができますが、assistantのResponseの中には1度に1つのfunctionの情報しか含まれません。
そのため、例えば「ボストンの今の天気とおすすめのお店を教えて」というようなpromptに対して、指定位置の天気を取得する処理指定位置のお店情報を取得する処理について個別に都度応答と処理を行う必要があります。

Function calling は自然言語の文脈からjsonを生成する使い方が便利なのではないか

「AIがAPIの結果を加味した返答をしてくれる」というのはAIとの対話を楽しむことに意義のあるインタフェースを提供するようなサービスならば多いに価値があると思いますが、世に存在する多くのシステムは「ユーザーの入力に対して、任意の処理・APIを呼び出してその結果を返す」というようなものになっているかと思います。

そして、それらのユースケースの場合は以下の部分のみで十分であり、このフローの②の部分こそが Function callingの真髄の部分ではないかと考えています。

ユースケース例

より具体的なユースケースを例にして考えてみます。

ホットペッパーのグルメ検索 APIを使用する例を考えてみましょう。
あるユーザーが デートに行くので雰囲気が良くて夜遅くまでやっているところがいい。 というような言葉でお店を探しているとします。

この自然言語に対応するAPIのリクエスト条件を Function callingで作成しようとすると以下のような内容になります

sample.ts
  const promptText = `緯度:35.6619707
経度:139.703795
要望:デートに行くので雰囲気が良くて夜遅くまでやっているところがいい。
`;

  const prompt: ChatCompletionRequestMessage = {
    role: "user",
    content: promptText,
  };

  const res = await openai.createChatCompletion({
    model,
    messages: [prompt],
    temperature: 0,
    function_call: "auto",
    functions: [
      {
        name: "SearchGourmetByHotPepper",
        description: "宿泊施設を検索して取得",
        parameters: {
          type: "object",
          properties: {
            latitude: {
              type: "number",
              description: "緯度",
            },
            longitude: {
              type: "number",
              description: "経度",
            },
            name_any: {
              type: "string",
              description:
                "お店の名前または読みかな両方をOR検索(部分一致)します。",
            },
            wifi: {
              type: "number",
              description: "wifiの有無。0:なし 1:あり",
            },
            course: {
              type: "number",
              description: "コースの有無。0:なし 1:あり",
            },
            free_drink: {
              type: "number",
              description: "飲み放題プランの有無。0:なし 1:あり",
            },
            /****** 他のパラメータについては記載を省略 ******/
            night_view: {
              type: "number",
              description: "夜景きれいかの条件絞り込み。0:なし 1:あり",
            },
            lunch: {
              type: "number",
              description: "ランチの条件絞り込み。0:なし 1:あり",
            },
            midnight: {
              type: "number",
              description: "「23時以降も営業」の条件絞り込み。0:なし 1:あり",
            },
            midnight_meal: {
              type: "number",
              description: "「23時以降食事OK」の条件絞り込み。0:なし 1:あり",
            },
            pet: {
              type: "number",
              description: "ペット可の条件絞り込み。0:なし 1:あり",
            },
            child: {
              type: "number",
              description: "お子様連れの条件絞り込み。0:なし 1:あり",
            },
          },
          required: ["latitude", "longitude"],
        },
      },
    ],
  });

Function calling を用いると、以下のレスポンスが assistant から得られます。

message {
  role: 'assistant',
  content: null,
  function_call: {
    name: 'SearchGourmetByHotPepper',
    arguments: '{\n' +
      '  "latitude": 35.6619707,\n' +
      '  "longitude": 139.703795,\n' +
      '  "night_view": 1,\n' +
      '  "midnight": 1\n' +
      '}'
  }
}

このargumentsの json文字列から自然言語からAPIのための絞り込み条件を作成できます。
作成された検索条件の内容は以下のとおりです。

  • 緯度(latitude):35.6619707
  • 経度(longitude):139.703795
  • 夜景がキレイ(night_view):1(絞り込み条件に入れる)
  • 23 時以降も営業(midnight):1(絞り込み条件に入れる)

夜景が綺麗で深夜帯も営業しているお店、という「デートで行く雰囲気が良いお店」という条件にだいぶふさわしい検索条件が作成できていると思います。
ただ、使用するAPIのパラメータによっては同時に指定することで相反する指定条件になってしまい期待した結果が取れなくなったり、ほぼ該当しないような条件みたいなものが指定されてしまうこともあるので、その辺りは注意と調整が必要です。

あとは HOT PEPPER のAPIにリクエストを行い、ユーザには任意の形のレスポンスにして必要情報を返却するだけです。

まとめ

会話のような文章表現から、処理に与えるためにふさわしいパラメータを選出するのは、自前で作成する場合は自然言語処理として結構大変な実装が必要になると思いますが、Function callingを用いることで簡単に実現できるようになります。
余談ですが、temperatureを0にすると同じ問い合わせに対しては同じ結果がほぼ返りますが、値を大きくすると同じようにデートプランの問い合わせに対する返答でも、ワインを薦めたりコース料理を薦めたりと都度変わった結果になるので、場合によっては面白い使い方ができるかもしれません。

あとは、この方法を用いることで求める内容が確実にjsonで得られるというのが大きなポイントです。
Function calling登場以前はAIから「ちゃんとしたjsonを返させる」だけでも割りと苦心がありました…。

AIを使った開発は今までは大変だったところを数段飛ばしにして実現できるようなものや、体験として大変面白い物が作れるので非常に楽しいです。
みなさんも是非試してみてください。

参考

https://openai.com/blog/function-calling-and-other-api-updates

Discussion