Node.jsで、Function callingを試してみた

2023.06.16

こんにちは。データアナリティクス事業本部 サービスソリューション部の北川です。

OpenAIのアップデータが発表され、Function callingなるものが利用できるようになりました。

Function callingでは、プロンプトに応じて、必要とあれば関数を呼び出し、パラメータを生成してくれます。概要について、こちらのエントリで解説されています。

また、OpenAIのアップデート全体については、以下のエントリで解説されています。

今回は、Function callingをNode.jsで利用していきたいと思います。

試してみた

ますば、任意のフォルダでプロジェクトを作成しします。

$ npm init -y
$ npm install --save-dev typescript @types/node ts-node

Function Calling

OpenAIを利用できるようインストールします。

npm install openai

今回は、プロンプトを以下のようにしてみました。

const prompt: ChatCompletionRequestMessage = {
  role: "user",
  content:
    "くらにゃんの住んでいる国の有名なイベントについて日本語で教えてください。",
};

Function callingにより、この時"くらにゃんの住んでいる国"について取得する関数を、必要とあれば実行してくれるようにできます。

実行する関数は、このようにしました。ちなみに、本当はどこに住んでいるのか知りません。勉強不足です。

const getLivingCountry = (userName: string) => {
  return userName === "くらにゃん" ? "日本" : "アメリカ";
};

では、index.tsファイルを作成し、以下のように記述します。

index.ts

import { ChatCompletionRequestMessage, Configuration, OpenAIApi } from "openai";

const configuration = new Configuration({
  apiKey: "OpenAI APIキーを指定",
});
const openai = new OpenAIApi(configuration);

const prompt: ChatCompletionRequestMessage = {
  role: "user",
  content:
    "くらにゃんの住んでいる国の有名なイベントについて日本語で教えてください。",
};

const getLivingCountry = (userName: string) => {
  return userName === "くらにゃん" ? "日本" : "アメリカ";
};

const functions = {
  getLivingCountry,
} as const;

const func = async () => {
  const res = await openai.createChatCompletion({
    model: "gpt-4-0613",
    messages: [prompt],
    function_call: "auto",
    functions: [
      {
        name: "getLivingCountry",
        description: "住んでいる国を取得",
        parameters: {
          type: "object",
          properties: {
            name: {
              type: "string",
              description: "ユーザー名",
            },
          },
          required: ["name"],
        },
      },
    ],
  });

  const message = res.data.choices[0].message;
  console.log("message", message);
  const functionCall = message?.function_call;

  if (functionCall) {
    const args = JSON.parse(functionCall.arguments || "{}");

    // @ts-ignore
    const functionRes = functions[functionCall.name!](args.name);

    // 関数の結果をもとに再度質問
    const res2 = await openai.createChatCompletion({
      model: "gpt-4-0613",
      messages: [
        prompt,
        message,
        {
          role: "function",
          content: functionRes,
          name: functionCall.name,
        },
      ],
    });
    console.log("answer", res2.data.choices[0].message);
  }
};

func();

最初に実行されるcreateChatCompletionで、関数を呼び出すかどうか判定し、呼び出す際はfunctionCallに引数を生成します。

実行してみます。

tsc index.ts

node index.js

実行結果は以下のようになりました。

>> node index.js
message {
  role: 'assistant',
  content: null,
  function_call: { name: 'getLivingCountry', arguments: '{\n"name": "くらにゃん"\n}' }
}
answer {
  role: 'assistant',
  content: 'くらにゃんが住んでいる日本には、たくさんの有名なイベントがあります。その中でも特に有名なものをいくつか紹介します。\n' +
    '\n' +
    '1. お正月(新年): 日本では1月1日を元日とし、その日から数日間をお正月休み(新年休み)とします。この期間には、初詣(神社や寺への初めての参拝)やお年玉(親や親戚などから子供へのお金の贈り物)、飾り付けなどの伝統的な儀式が行われます。\n' +
    '\n' +
    '2. 桜の花見: 春になると日本全国で桜の花が咲き、多くの人々が花見(桜の花を見て楽しむこと)に出かけます。\n' +
    '\n' +
    '3. 夏祭りと花火大会: 夏には、日本全国の多くの地域で夏祭りが行われます。お祭りでは郷土の踊りや屋台、焼き物、ゲームなどが楽しめます。また、それと同時期には花火大会も開催され、美しい花火を観賞することができます。\n' +
    '\n' +
    '4. お盆: 8月中旬にはお盆があり、故人を迎えるための仏教の行事が行われます。家族が一堂に会して墓参りを行ったり、特別な食事をしたりします。\n' +
    '\n' +
    '5. クリスマス: 西洋の伝統的な行事であるクリスマスも、日本では広く祝われています。ケーキを食べたり、プレゼントを交換したりします。\n' +
    '\n' +
    'なお、これらのイベントは地域によって違いや特色があるため、一概に全ての地域で同じように行われているわけではありません。また、新型コロナウイルスの影響で、イベントが中止になったり、規模が縮小されたりする場合もあります。'
}

くらにゃんの住んでいる国が日本として、responseが返ってきていますね。

message {
  role: 'assistant',
  content: null,
  function_call: { name: 'getLivingCountry', arguments: '{\n  "name": "くらにゃん"\n}' }
}

この時ログを確認すると、function_callがプロパティに存在し、関数名とそれに渡す引数が指定されていることが確認できます。つまり今回のプロンプトでは、getLivingCountryが呼ばれたことを意味します。関数を呼ぶ場合には、contentがnullになります。

試しに、プロンプトを以下のように変更して再度実行し直してみます。

const prompt: ChatCompletionRequestMessage = {
  role: "user",
  content: "日本の有名なイベントについて日本語で教えてください。",
};

実行結果

node index.js
message {
  role: 'assistant',
  content: '日本にはたくさんの有名なイベントがありますが、その中でも特に注目すべきものをいくつかご紹介します。\n' +
    '\n' +
    '1. **桜祭り(さくら祭り):** 春の訪れを告げる桜の季節には、全国各地でお花見が行われます。公園や川沿いなど桜の名所では、地元住民や観光客が桜の下で飲食を楽しみます。\n' +
    ...
}

ログを見てわかる通り、contentがnullではなくそのまま答えを返しています。つまり今回のプロンプトでは、関数は呼ばれませんでした。この関数を呼ぶ、呼ばないの判断は、function_call: "auto"によるものです。function_call: "none"にすると、関数は呼ばれなくなります。

選択できる関数を増やす

Function callでは関数を複数指定することもできます。その場合、どちらの関数を実行するのか、またはどの関数も実行しないのかは、API側で判断してくれます。

試しに、index.tsを以下のように変更し、getStrongestSportsCountryという関数を新たに追加します。

import { ChatCompletionRequestMessage, Configuration, OpenAIApi } from "openai";

const configuration = new Configuration({
  apiKey: "OpenAI APIキーを指定",
});
const openai = new OpenAIApi(configuration);

const prompt: ChatCompletionRequestMessage = {
  role: "user",
  content:
    "くらにゃんの住んでいる国の有名なイベントについて日本語で教えてください。",
};

const getLivingCountry = (userName: string) => {
  return userName === "くらにゃん" ? "日本" : "アメリカ";
};

const getStrongestSportsCountry = (sport: string) => {
  switch (sport) {
    case "サッカー":
      return "アルゼンチン";
    case "レスリング":
      return "日本";
    case "卓球":
      return "中国";
    default:
      return "アメリカ";
  }
};

const functions = {
  getLivingCountry,
  getStrongestSportsCountry,
} as const;

const func = async () => {
  const res = await openai.createChatCompletion({
    model: "gpt-4-0613",
    messages: [prompt],
    function_call: "auto",
    functions: [
      {
        name: "getLivingCountry",
        description: "住んでいる国を取得",
        parameters: {
          type: "object",
          properties: {
            name: {
              type: "string",
              description: "user name",
            },
          },
          required: ["name"],
        },
      },
      {
        name: "getStrongestSportsCountry",
        description: "スポーツの強い国を取得",
        parameters: {
          type: "object",
          properties: {
            name: {
              type: "string",
              description: "sport",
            },
          },
          required: ["name"],
        },
      },
    ],
  });

  const message = res.data.choices[0].message;
  console.log("message", message);

  const functionCall = message?.function_call;

  if (functionCall) {
    const args = JSON.parse(functionCall.arguments || "{}");

    // @ts-ignore
    const funcRes = functions[functionCall.name!](args.name);

    // 関数の結果をもとに再度質問
    const res2 = await openai.createChatCompletion({
      model: "gpt-4-0613",
      messages: [
        prompt,
        message,
        {
          role: "function",
          content: funcRes,
          name: functionCall.name,
        },
      ],
    });
    console.log("answer", res2.data.choices[0].message);
  }
};

func();

このままでは、実行結果は変わらないので、プロンプトを変更してみます。

const prompt: ChatCompletionRequestMessage = {
  role: "user",
  content: "サッカーの強い国の首都はどこですか?",
};

では、再度実行してみます。

❯❯ node index.js
message {
  role: 'assistant',
  content: null,
  function_call: {
    name: 'getStrongestSportsCountry',
    arguments: '{\n  "name": "サッカー"\n}'
  }
}
answer { role: 'assistant', content: 'アルゼンチンの首都はブエノスアイレスです。' }

プロンプトの内容をもとに、今回はgetStrongestSportsCountryの方が呼ばれ、想定通りの結果が返ってきています。

まとめ

今回は、OpenAIのアップデータにより発表されたFuncton callingを利用してみました。実装自体は苦戦せずに動きを確認できましたが、どのような機能なのか理解するのには時間がかかりました。今後もうまく活用できるよう、知見を深めていきます。今回の記事が誰かの参考になれば幸いです。

ではまた。