🔎

Google検索APIをFunction Calling経由で実行する(ChatGPT API)

2023/12/12に公開

はじめに

Google検索をFunction Calling経由で実行するメモです。

環境

  • node 18.14.2
  • typescript 5.0.2
  • openai 4.16.1
  • googleapis 126.0.1
  • esbuild 0.17.12
  • esbuild-register 3.4.2

OpenAIクライアントの初期化

openai.ts
import OpenAI from "openai";

export const openAi = new OpenAI({
  apiKey: process.env["OPEN_AI_API_KEY"] ?? "",
});

OpenAIクライアントを初期化して export しておきます。

Google検索APIを叩くための準備

まずGoogleカスタム検索APIを叩くための準備をします。

下記のブログなどを参考に、検索エンジンIDとAPIキーを取得して控えておきます。

LangChain の Googleカスタム検索 連携を試す|npaka

  1. 検索エンジンIDとAPIキーの取得
    (1) Googleカスタム検索のサイトを開き、「使ってみる」を押す。
    (2) 新しい検索エンジンを作成。
    (3) 「基本」の「検索エンジンID」をコピー。
    (4) 「プログラマティックなアクセス」「Custom Search JSON API」の「開始する」を押す。
    (5) 「プログラム可能検索エンジン (無料版) ユーザー」の「キーの取得」を押し、「APIキー」をコピー。

環境変数に控えた上記を設定しておきます。

# APIキー
export GOOGLE_API_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
# 検索エンジンID
export GOOGLE_CUSTOM_SEARCH_ID=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

あと、OpenAIのAPIキーも環境変数にセットしておきましょう。

# OpenAI APIキー
export OPEN_AI_API_KEY=sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Google Custom Searchを実装

まず searchByGoogle 関数を実装します。

google.ts
import { google } from "googleapis";

export const searchByGoogle = async (
  keyword: string
): Promise<{ title: string; url: string; overview: string }[]> => {
  if (keyword === "") {
    throw new Error("キーワードは必須です。");
  }

  const customSearch = google.customsearch("v1");
  const result = await customSearch.cse.list({
    auth: process.env["GOOGLE_API_KEY"] ?? "",
    cx: process.env["GOOGLE_CUSTOM_SEARCH_ID"] ?? "",
    q: keyword,
  });

  if (result.data.items == null) {
    throw new Error("何も見つかりませんでした。");
  }

  // 上位3件のタイトル、URL、概要を取得する
  const res = result.data.items.slice(0, 3).map((item) => ({
    title: item.title ?? "",
    url: item.link ?? "",
    overview: item.snippet ?? "",
  }));
  return res;
};

検索キーワードを引数にとる関数にしています。

Google検索の結果を受け取った後、上位3つのタイトル、URL、概要を取得して返しています。対象にする数をもっと増やしてもいいですが、その分OpenAI APIの消費トークンが増えることになります。

Function Callingに組み込む

では、上記の関数をFunction Callingに組み込んでいきます。

chat.ts
const SEARCH_GOOGLE = "searchGoogle";

// Function Callingの定義
const functions = [
  {
    name: SEARCH_GOOGLE,
    description:
      "あなたがわからない情報を求められた場合や最新情報が必要な場合に、インターネット(Google)で情報を検索する。",
    parameters: {
      type: "object",
      properties: {
        keyword: {
          type: "string",
          description: "Google検索キーワード",
        },
      },
      required: ["keyword"],
    },
  },
];

export const chat = async (userMessage: string) => {
  const completion = await openAi.chat.completions.create({
    model: "gpt-4",
    messages: [
      {
        role: "user",
        content: userMessage,
      },
    ],
    functions,
    function_call: "auto",
  });

  if (completion.choices[0]!.finish_reason !== "function_call") {
    // Function Callingでない場合(通常の会話)
    // 早期リターンして終了
    return completion.choices[0]?.message.content ?? "";
  }

  // Function Callingの場合
  const message = completion.choices[0]!.message;
  if (message.function_call?.name === SEARCH_GOOGLE) {
    // Google検索の場合
    const args = JSON.parse(message.function_call!.arguments);
    const searchRes = await searchByGoogle(args.keyword);
    if (searchRes.length === 0) {
      throw new Error("何も見つかりませんでした。");
    }

    const completion = await openAi.chat.completions.create({
      model: "gpt-4",
      messages: [
        {
          role: "function",
          content: JSON.stringify(searchRes),
          name: SEARCH_GOOGLE,
        },
        {
          role: "system",
          content:
            "Google検索で取得した情報と参考URLをユーザに回答してください。",
        },
      ],
      functions,
      function_call: "none",
    });

    return completion.choices[0]?.message.content ?? "";
  } else {
    // ここには来ない想定
    throw new Error("不正なタイプのFunctionCallingです。");
  }
};

あなた(ChatGPT)がわからない情報を求められた場合や最新情報が必要な場合 に検索をしてほしいという内容で説明文を書きました。会話の中でChatGPTがこの説明を根拠としてハンドリングしてくれることを意図しています。検索キーワードはChatGPTに考えてもらうようにします(keyword プロパティ)。

そして実際に function_callSEARCH_GOOGLE になったらGoogle検索処理を呼びます。そのあと、検索結果をさらにChatGPT APIに渡して返答を作ってもらいます。

この時、2度目のChatGPT呼び出しにも functions を設定しているのですが、 function_call: "none" でFunctionが呼ばれることはないようにしています。Function定義を渡しておくことで、ChatGPTがより正確に情報を解釈する助けとなるためです。ただ、実際には functionsfunction_call の両方を空にしておいてもあまり問題なく動作します。おそらく name: SEARCH_GOOGLE で指定した関数名から内容を推測してくれるんだと思います。この場合にはFunctionへの適切な命名を心がけましょう。

実行

では実行していきます。

「葬送のフリーレンの作者は?」と聞いてみます。

main.ts
const main = async () => {
  const chatGptMessage = await chat("葬送のフリーレンの作者は?");
  console.log(chatGptMessage);
};
実行
node -r esbuild-register main.ts
出力
『葬送のフリーレン』は、原作が山田鐘人氏、作画がアベツカサ氏の日本の漫画作品です。2020年から「週刊少年サンデー」で連載が開始されました。アベツカサ氏は、Twitter上で『葬送のフリーレン』に関する情報を発信しています[^1^][^2^][^3^]。

[^1^]: [『葬送のフリーレン』作者について|過去作やSNSなどの情報 ...](https://times.abema.tv/articles/-/10093989)
[^2^]: [アベツカサ (@abetsukasa) / X](https://twitter.com/abetsukasa)
[^3^]: [葬送のフリーレン - Wikipedia](https://ja.wikipedia.org/wiki/%E8%91%AC%E9%80%81%E3%81%AE%E3%83%95%E3%83%AA%E3%83%BC%E3%83%AC%E3%83%B3)

Googleで取得したリンクを使って使って回答してくれました。

おわりに

今回はいい感じの回答が得られましたが、実際のところGoogle検索結果の概要(snippet)から得られる情報はそれほど多くありません。そのため、例えば技術的なドキュメントの中身を説明させるだとか、より詳細な情報を正確にChatGPTに返してほしい場合、検索結果のWebページにもアクセスしてスクレイピングする必要がありますが、それは今後の展望としたいと思います。

以上、参考になれば幸いです。

参考

Discussion