世界一わかりみの深いAIエージェント

皆さん、こんにちは。サイオステクノロジー武井です。今回は、生成AIの発展形であり、与えられた課題に対して自分で思考し、行動を決定し、実行する「AIエージェント」について、一筆したためたいと思います。

目次

AIエージェントとは?

ChatGPTを代表とした生成AIはとても便利ですよね。質問を行うと、まるで人間が答えているかのような自然な対話が可能であり、世界中の話題をさらいました。もう普段の生活やビジネスにもなくてはならない存在かと思います。

しかし、そんな生成AIも万能ではありません。LLM(大規模言語モデル: 生成AIの頭脳のようなもの)が知っていることであれば、スラスラっと答えてくれます(たまにハルシネーションと言って間違いも起こしますが)。しかし、LLMをが知らないことについては、お手上げになります。

これは、人間でも同じですよね。人間も、自分が知っていることであれば、スラスラと答えることができますが、知らないことに関しては、すぐに答えることができません。

こんなとき、人間はどうするでしょう?

人間は、いろんなツールを使って、必要な情報を集め、それらが正しいかどうかを確認・検証し、答えを作ります。

例えば
トム・クルーズの誕生日にケーキをプレゼントしたいです。彼の年齢分のろうそくを購入するための金額を計算してください。ろうそくは一本100円です。
という質問が与えられたときのことを考えてみます。

そんなとき人間は、まず「トム・クルーズの年齢」を調べます。人間は、最大の叡智「インターネット」というツールを持っているので、インターネットでトムの年齢を調べます。2024年8月27日時点、彼は62歳です。

年齢がわかったので、次はろうそくの値段を調べます。ここでは、ろうそくは1本100円という情報が与えられているので、これを使います。トムの年齢は62歳なので、62本のろうそくが必要です。62本 * 100円 = 6200円です。

こんな感じの思考プロセスが人間にはできます。

これと同じことをAIにもさせることができれば、AIは生成AI以上の能力を発揮することができるのではないかと考えられます。つまりこのようなAIを「AIエージェント」と呼びます。

AIエージェントは、LLMが知らないことについても、与えられたツールを使って、情報を収集し、それを使って問題を解決することができるのです。
もうこれって人間とほぼ同じですよね。ドラえもんやコロ助みたいなのが登場する現実がすぐそこまで差し迫っているのかもしれません!!

AIエージェントを使わない場合と使った場合との比較

ここでは、AIエージェントをより深く理解するために、AIエージェントを使わない場合と使った場合との比較を行います。

まずは、AIエージェントを用いない場合、LLMに直接問いかけるとどうなるかを見てみましょう。
この場合、LLMは「トム・クルーズの年齢」や「ろうそくの金額」についての知識を持っていないため、正しい回答を導き出すことができません。2021年時点での年齢を算出していますね。これはモデルの中の知識だけを使っているためです。

このように、LLMは知識がない場合、正しい回答を導き出すことが難しいのです。

では、AIエージェントを用いた場合はどうでしょうか?
AIエージェントを用いることで、LLMは、問題を解決するための思考プロセスを段階的に分解し、それぞれのステップに対して質問を行うことで、正確な回答を導き出すことができます。

トム・クルーズの年齢分のろうそくの合計金額を計算するために、LLMは自身で判断し、段取りを行います。まずは、トムの年齢を調べるためにインターネットを検索し、次にその結果をもとにろうそくの本数を計算し、最終的に金額を算出します。

AIエージェントを実現するプロンプトエンジニアリング「ReAct」

ReActというのは、フロントエンドのフレームワークではありません。プロンプトエンジニアリングの一種で、先程の人間のような思考を行わせるためのテクニックです。
ReActはAIエージェントを理解するための基本概念です。AIエージェントを実現するいろんなツールやフレームワークがありますが、ReActの考え方さえしっかり理解すれば、どんなに新しいツールやフレームワークがでてきても、すんなりものにすることができるでしょう。
ということで、ReActについて、深堀りしたいと思います。
そこで、そもそもプロンプトエンジニアリングとは何かということに立ち返りたいと思います。

プロンプトエンジニアリングというのは、AIに対してどのように質問や指示を与えれば、より良い結果が得られるかを工夫するプロセスです。

ReAct以外にも、いくつかのプロンプトエンジニアリングの手法があります。例えば、Few-shot LearningやChain-of-Thoughtなどがあります。
■ Few-shot Learning
Few-shot Learningは、少数の具体例をAIに提示して、それに基づいて新しい入力に対する応答をさせる手法です。この方法では、いくつかのサンプルを提示するだけで、AIが新しいパターンに適応しやすくなります。

以下に例を示します。
<プロンプト>
英語を日本語に翻訳してください。
英語: apple 日本語: りんご
英語: orange 日本語: オレンジ
英語: grape 日本語:
<AIの応答>
ぶどう
■ Chain-of-Thought
Chain-of-Thoughtは、AIに複雑な質問を分解して考えさせる方法です。通常、問題解決型の質問に対して、1つ1つのステップを順番に処理させることで、正確な回答を得ることが目的です。この手法では、AIが自分で考える過程を示しながら応答します。

以下に例を示します。
<プロンプト>
問題:5人が10個のりんごを分けます。それぞれ何個持ちますか?
回答をステップごとに示してください。
<AIの応答>
  1. まず、10個のりんごを5人で割り算します。
  2. 10 ÷ 5 = 2 です。
  3. それぞれ2個ずつ持つことになります。
このように、Chain-of-Thoughtではステップごとに回答を導き出し、複雑な問題も解決できるようにします。

ReActもこれらのプロンプトエンジニアリングと同じように、LLMに精度の高い回答を導き出すための手法のひとつなのですが、いささかReActは他のプロンプトエンジニアリングに比べると、少々複雑なので、順を追って説明していきます。

ReActの手法

ReActは、以下の図のように思考を分解し、それぞれのステップに対してAIに質問を行い、AIがそのステップを実行することで、最終的な回答を導き出す手法です。
それでは、ReActのプロセスの図に従い、ReActを使った場合、AIエージェントがどのように問題を解決するかを見ていきましょう。先ほど例に上げた「トム・クルーズの誕生日にケーキをプレゼントしたいです。彼の年齢分のろうそくを購入するための金額を計算してください。ろうそくは一本100円です。」という問題を考えてみます。

質問

最初に、AIが質問を受け取ります。この場合の質問は、
トム・クルーズの誕生日にケーキをプレゼントしたいです。彼の年齢分のろうそくを購入するための金額を計算してください。ろうそくは一本100円です。
となります。

これが、プロンプトに対してAIが最初に解釈する「問題の提示」の部分です。ReActでは、この段階でAIがどんな情報を探すべきかを認識します。

思考 (1回目)

次に、AIはこの問題の答えを出すために何をすべきかを考えます。まず、AIは、トム・クルーズの年齢を知る必要があることに気づきます。そのため、AIは「トム・クルーズの年齢」を調べるためにインターネットを検索します。このとき「行動の入力」として、インターネットを検索する際のキーワードを生成します。この場合でしたら「トム・クルーズ 年齢」などが考えられます。

行動 (1回目)

AIは、インターネットでトム・クルーズの年齢を調べます。先ほど「行動の入力」で与えられた検索キーワード「トム・クルーズ 年齢」をもとに、インターネットを検索して、トム・クルーズの様々な情報を取得します。

そして、このときの検索結果を「観察(行動の結果)」として記憶し、次のステップに進みます。

思考 (2回目)

1回目の「行動」「行動の入力」「観察(行動の結果)」より、さらに思考します。現時点では、まだトム・クルーズの年齢しかわかっておらず、最終目的である「ろうそくの金額」はわかっていません。よってさらに思考を続けます。

トム・クルーズの年齢は先の「観察(行動の結果)」でわかっていますし、ろうそくの値段はプロンプトに明示(1本100円)されているので、次にやるべきことは「ろうそくの本数」を計算することです。

このとき、行動の入力として、「62 * 100」といったような計算式を生成します(トムの年齢は62歳とします)。

行動 (2回目)

AIは、先ほど生成した計算式「62 * 100」を計算し、ろうそくの本数を計算します。計算結果を「観察(行動の結果)」として記憶し、次のステップに進みます。

思考 (3回目)

AIは、「観察(行動の結果)」より、すでにろうそくの金額が計算されていることを認識します。最終的な回答が得られたので、これ以上の思考や行動は行わず、最終回答が生成されたと判断します。

最終回答

AIは、最終的な回答として「6200円」と出力します。

このように、ReActはAIに対して、問題を解決するための思考プロセスを段階的に分解し、それぞれのステップに対して質問を行うことで、AIがより正確な回答を導き出すことができるのです。

先ほど紹介したReActの構成要素である「質問」「思考」「行動」「観察(行動の結果)」をマッピングしたのが以下の図になります。
このようにして、ReActを用いることで、モデル自身がしらない知識を補完し、より正確な回答を導き出すことができるのです。

Reactを実現するためのプロンプト

LLMへの指示はプロンプトを通じて行います。では、ReActのように、LLM自身が自ら思考し、行動を起こすためのプロンプトはどのように書けばよいのでしょうか?ここではそれを学びます。

ReActの場合、LLMへの指示は複数回に分けて行われます。「ReActの手法」でも紹介したように、思考と行動のプロセスは最終回答が出るまで複数回行われることを説明しました。まさにこれと同じことをプロンプトを通じて行うのです。

ここでもまた、先ほどの「トム・クルーズの誕生日にケーキをプレゼントしたいです。彼の年齢分のろうそくを購入するための金額を計算してください。ろうそくは一本100円です。」という問題を例にとって、ReActを実現するためのプロンプトを書いてみましょう。

まず、最初の質問を与えるプロンプトは以下のようになります。
次の質問にできる限り答えてください。次のツールにアクセスできます。
– 検索ツール:検索するときに使います
– 計算ツール:計算するときに使います。
次のフォーマットを使用します。
質問:回答する必要がある入力質問
思考:次に何をすべきかを常に考える
行動:実行するアクションは、「計算ツール」「検索ツール」のいずれかである必要があります。
行動の入力:アクションへの入力
観察:行動の結果
…(この思考/行動/行動の入力/観察はN回繰り返すことができます)
最終的な答えがわかったら以下を出力します。
思考:今、最終的な答えが分かりました
最終回答:元の入力質問に対する最終回答
計算ツールを使うときの行動の入力は、計算式を入力してください。
例:52 * 100
例:100 – 1
質問:{question}
さぁ始めましょう。
思考:
では、このプロンプトの詳細を見ていきましょう。
次の質問にできる限り答えてください。次のツールにアクセスできます。
– 検索ツール:検索するときに使います
– 計算ツール:計算するときに使います。
この部分では、AIに与えられたツールを説明しています。人間は頭の中に問題を解決するためのいろんなツールがありますよね。例えば、わからないことはインターネットで調べたり、計算が必要な場合は電卓を使ったりします。AIは人間ほど柔軟ではないので、このツールを使ってと明示的に指示する必要があります。

ここでは、検索ツールと計算ツールの2つのツールを使うことができるように設定しています。検索ツールは、インターネットで情報を検索するために使います。後でこのプロンプトとPythonを利用したReActの実装を紹介しますが、その実装の中ではDuckDuckGoというPythonのライブラリを使って検索を行います。

計算ツールについては、計算式をPythonのeval関数を使って計算を行います。
次のフォーマットを使用します。
質問:回答する必要がある入力質問
思考:次に何をすべきかを常に考える
行動:実行するアクションは、「計算ツール」「検索ツール」のいずれかである必要があります。
行動の入力:アクションへの入力
観察:行動の結果
…(この思考/行動/行動の入力/観察はN回繰り返すことができます)
最終的な答えがわかったら以下を出力します。
思考:今、最終的な答えが分かりました
最終回答:元の入力質問に対する最終回答
この部分では、このフォーマット使用して、ReActを実現してくださいという指示を与えています。ReActのプロセスは、「ReActの手法」で説明した通り、「質問」「思考」「行動」「行動の入力」「観察」の5つのステップで構成されています。このステップを繰り返すことで、AIが問題を解決するための思考プロセスを段階的に分解し、それぞれのステップに対して質問を行うことで、AIがより正確な回答を導き出すことができるのです。

「質問」は、AIに行う質問を与える部分です。今回の例で言えば、「トム・クルーズの誕生日にケーキをプレゼントしたいです。彼の年齢分のろうそくを購入するための金額を計算してください。ろうそくは一本100円です。」という質問を与えます。

「思考」は、AIが次に何をすべきかを考える部分です。AIは、この部分で自分が次に何をすべきかを考えます。今回の例で言えば、「まずは、トム・クルーズの年齢をインターネットで調べ、その結果をもとにろうそくの本数を計算する」という段取りを考える部分です。

「行動」は、思考の結果、AIが次に何をするべきかを示す部分です。今回の例で言えば、ここに入るのは「検索ツール」「計算ツール」のどちらかになります。

「行動の入力」は、行動を実行する際に必要な入力を示す部分です。今回の例で言えば、計算ツールを使う場合は「62 * 100」といった計算式を入力します。

「観察」は、行動の結果を示す部分です。今回の例で言えば、トム・クルーズの年齢をインターネットで調べた結果や、ろうそくの合計金額を計算した結果を示します。

このプロセスを繰り返し、最終的な答えがでたら、「思考」の結果として、「今、最終的な答えが分かりました」と出力し、「最終回答」を示します。
計算ツールを使うときの行動の入力は、計算式を入力してください。
例:52 * 100
例:100 – 1
この部分では、計算ツールを使う際の行動の入力のサンプルを示しています。Pythonのeval関数を使って計算を行うため、規定のフォーマットに従って計算式を入力する必要があるからです。
質問:トム・クルーズの誕生日にケーキをプレゼントしたいです。彼の年齢分のろうそくを購入するための金額を計算してください。ろうそくは一本100円です。
さぁ始めましょう。
思考:
最後に、最初の質問を示す部分です。ここでは、先ほどの「質問」を表示します。また、「さぁ始めましょう。」というメッセージを表示し、AIに問題を解決するための思考プロセスを開始するよう促します。

思考(1回目)の結果

では、実際にReActを使って、AIエージェントが問題を解決するプロセスを見ていきましょう。

先ほど示したプロンプトをAzure OpenAI ServiceのLLMに与えた結果を見ていきます。
黄色い部分が、Azure OpenAI Serviceのレスポンスです。「思考」の部分には、AIが考えた段取りが表示されていますね。

そして、「行動」では、次に行うべきツールの名称である「検索ツール」が示されています。
「行動の入力」には、検索ツールを使う際の入力が示されています。この場合は、「トム・クルーズ 年齢」という検索キーワードが入力されています。つまり、DuckDuckGoを使って「トム・クルーズ 年齢」というキーワードで検索を行うことになります。

行動(1回目)の結果

次に、AIが検索を行った結果を見ていきます。
「思考(1回目)の結果」では、LLMの出力結果として、「行動」に「検索ツール」、「行動の入力」に「トム・クルーズ 年齢」が示されていました。よって、これに従い、DuckDuckGoを使って「トム・クルーズ 年齢」というキーワードで検索を行い、その結果を「観察」に記録したのが、水色の部分になります。

細かい話をしますと、この「観察」に示した結果は、実際にはDuckDuckGoの検索結果上位5件のページの内容を入れています。

そして、このプロンプトを次の「思考(2回目)」で、再びLLMモデルに入力します。

思考(2回目)の結果

「行動(1回目)」で組み立てたプロンプトをAzure OpenAI ServiceのLLMに再度与えた結果を見ていきます。
緑色の部分がLLMが出力した結果になります。

LLMは、「観察」の結果(DuckDuckGoが「トム・クルーズ 年齢」でインターネットを検索して取得したページの内容)をもとに思考し、その結果、「トム・クルーズは現在62歳です。」という考察を得ました。

さらにLLMは思考を重ね、次に何をすべきかを考えた結果、次に行うべき行動として「計算ツール」を使うことを判断しました。トムの年齢がわかったので、あとは計算だけですからね。「思考(1回目)」の段取りで、計算ツールを使うことを決めていたので、この結果は正しいです。

そして、「行動の入力」には、「62 * 100」という計算式が入力されています。これは、トム・クルーズの年齢(62歳)に対して、ろうそくの値段(1本100円)をかけることで、ろうそくの合計金額を計算するための計算式です。

行動(2回目)の結果

最後に、AIが計算を行った結果を見ていきます。
青色の部分が、AIが計算を行った結果です。

思考(2回目)でLLMが出力した行動の入力「62 * 100」をPythonのeval関数に渡し、その結果を記録したのが、「観察」の部分です。計算結果は「6200」となりました。

さらにこのプロンプトを次の「思考(3回目)」で、再びLLMモデルに入力します。

思考(3回目)の結果

「行動(2回目)の結果」で組み立てたプロンプトをAzure OpenAI ServiceのLLMに再度与えた結果を見ていきます。
赤色の部分がLLMが出力した結果になります。

今までのプロンプトの内容からLLMはすでにろうそくの合計金額が計算されていることを認識し、最終的な回答が得られたと判断しました。

そして、最終回答として、「6200」と出力しました。

いかがでしょうか?AIが自律的に考え、段取りを組んで問題を解決していく様子を見ていただけたでしょうか?

なんか未来感じちゃいますよね。

ReActの実装

Reactを実現するためのプロンプト」でご紹介したプロンプトのやり取りを実現したPythonのコードを以下に示します。
from openai import AzureOpenAI
from duckduckgo_search import DDGS

# OpenAI APIの初期化
aoai_endpoint = "https://hogehoge.openai.azure.com/"
aoai_api_version = "2024-06-01"
aoai_api_key = "XXXXXX"
aoai_embedding_model_name = "text-embedding-ada-002-deploy"
aoai_chat_model_name = "gpt-4-deploy"

template = """
次の質問にできる限り答えてください。次のツールにアクセスできます。
- 検索ツール:検索するときに使います
- 計算ツール:計算するときに使います。
次のフォーマットを使用します。
質問:回答する必要がある入力質問
思考:次に何をすべきかを常に考える
行動:実行するアクションは、「計算ツール」「検索ツール」のいずれかである必要があります。
行動の入力:アクションへの入力
観察:行動の結果
...(この思考/行動/行動の入力/観察はN回繰り返すことができます)
最終的な答えがわかったら以下を出力します。
思考:今、最終的な答えが分かりました
最終回答:元の入力質問に対する最終回答

計算ツールを使うときの行動の入力は、計算式を入力してください。

例:52 * 100
例:100 - 1

質問:{question}

さぁ始めましょう。

思考:
"""
question = "トム・クルーズの誕生日にケーキをプレゼントしたいです。彼の年齢分のろうそくを購入するための金額を計算してください。ろうそくは一本本100円です。"

message = template.format(question=question)

final_observation = ""

while True:
    # 推論と行動のプロセス
    openai_client = AzureOpenAI(azure_endpoint=aoai_endpoint, api_key=aoai_api_key, api_version = aoai_api_version)
    response = openai_client.chat.completions.create(
        model=aoai_chat_model_name,
        messages=[
            {"role": "assistant", "content": message},
        ],
        stop=["観察:"]
    )
    result = response.choices[0].message.content.strip()
    # resultの変数の最後の行に、つまりresultの変数を改行で分割した配列の最後の要素に「最終回答:」がある場合、終了する。
    if "最終回答:" in result.split("\n")[-1]:
        final_observation = result.split("最終回答:")[1].strip()
        break

    # resultの"行動"と"行動の入力"を取得し、それにあったツールを実行する。
    if "行動" in result:
        action = result.split("行動の入力:")[0].split("行動:")[1].strip()
        action_input = result.split("行動の入力:")[1].strip()
        
        observation = ""
        if action in "検索ツール":
            # 検索ツール実行
            search_results = DDGS().text(action_input, max_results=5)
            for search_result in search_results:
                observation = observation + search_result["body"] + "\n"
        elif action in "計算ツール":
            # 計算ツール実行
            observation = str(eval(action_input))
    message = message + "\n行動:" + action + "\n行動の入力:" + action_input + "\n観察:" + observation + "\n思考:"
    print(message)

print("\n最終的な観察:")
print(final_observation)
では、このソースコードの詳細を見ていきましょう。

事前準備として、必要なライブラリをインストールします。
pip install openai duckduckgo-search
これで必要なライブラリがインストールされました。

Azure OpenAPI Serviceの初期化

# OpenAI APIの初期化
aoai_endpoint = "https://hogehoge.openai.azure.com/"
aoai_api_version = "2024-06-01"
aoai_api_key = "XXXXXX"
aoai_embedding_model_name = "text-embedding-ada-002-deploy"
aoai_chat_model_name = "gpt-4-deploy"
Azure OpenAI Serviceのエンドポイント、APIバージョン、APIキー、モデル名を設定します。

プロンプトのテンプレートの設定

template = """
次の質問にできる限り答えてください。次のツールにアクセスできます。
- 検索ツール:検索するときに使います
- 計算ツール:計算するときに使います。
次のフォーマットを使用します。
質問:回答する必要がある入力質問


...(省略)...


質問:{question}


さぁ始めましょう。


思考:
"""
question = "トム・クルーズの誕生日にケーキをプレゼントしたいです。彼の年齢分のろうそくを購入するための金額を計算してください。ろうそくは一本本100円です。"


message = template.format(question=question)
プロンプトのテンプレートを設定します。このプロンプトは、「ReActを実現するためのプロンプト」で説明したプロンプトと同じ内容です。`{question}`の部分には、変数`question`の値が、`template.format(question=question)`で代入されます。

回答の取得

final_observation = ""

while True:
    # 推論と行動のプロセス
    openai_client = AzureOpenAI(azure_endpoint=aoai_endpoint, api_key=aoai_api_key, api_version = aoai_api_version)
    response = openai_client.chat.completions.create(
        model=aoai_chat_model_name,
        messages=[
            {"role": "assistant", "content": message},
        ],
        stop=["観察:"]
    )
    result = response.choices[0].message.content.strip()
Azure OpenAI ServiceのAPIを使って、LLMにプロンプトを与え、回答を取得します。`stop=[“観察:”]`は、プロンプトの中で「観察:」という文字列が出力されたら、その時点で推論を終了するように指定しています。これは、LLMが自身の持っている知識で回答してしまうのを防ぐためです。ReActでは「観察:」は、あくまで行動の結果のものであり、LLMの思考の結果、出力されるものではありません。ただし、LLMは自身の持っている知識で回答し、「観察:」を出力することがあるため、それを防ぐためにこのような設定を行っています。

最終回答のチェック

    if "最終回答:" in result.split("\n")[-1]:
        final_observation = result.split("最終回答:")[1].strip()
        break
LLMの出力結果に「最終回答:」が含まれている場合、それを`final_observation`に代入し、ループを抜けます。

ツールの実行

    if "行動" in result:
        action = result.split("行動の入力:")[0].split("行動:")[1].strip()
        action_input = result.split("行動の入力:")[1].strip()
        
        observation = ""
        if action in "検索ツール":
            # 検索ツール実行
            search_results = DDGS().text(action_input, max_results=5)
            for search_result in search_results:
                observation = observation + search_result["body"] + "\n"
        elif action in "計算ツール":
            # 計算ツール実行
            observation = str(eval(action_input))
    message = message + "\n行動:" + action + "\n行動の入力:" + action_input + "\n観察:" + observation + "\n思考:"
LLMの出力結果に「行動」が含まれている場合、その内容に応じて行動を実行します。行動の内容によって、検索ツールを使うか、計算ツールを使うかを判断し、それに応じた処理を行います。検索ツールを使う場合は、DuckDuckGoを使って検索を行い、その結果を`observation`に代入します。計算ツールを使う場合は、Pythonのeval関数を使って計算を行い、その結果を`observation`に代入します。

最終的な観察結果の表示

print("\n最終的な観察:")
print(final_observation)
最終的な観察結果を表示します。

このようにして、ReActを実現するためのプロンプトを使って、AIエージェントが問題を解決するプロセスを実装することができます。

Function calling

前の章では、ReActというプロンプトエンジニアリングを使って、AIエージェントを実装しました。しかし、やはり、プロンプトを書くのは面倒ですよね。ReActのプロンプトにしたがって、思考と行動を繰り返し、最終回答が得られるまで繰り返す処理を実装するのは、かなり手間がかかります。

そこで、Function callingです。Function callingを使うと、もっとシンプルなコードでAIエージェントを実装することができます。

Function callingの概要

Function callingは、あらかじめ必要な関数を登録しておき、プロンプトの内容に応じて、適切な関数を呼び出すことができる機能です。

といっても、これだけの説明ではわかりにくいかもしれません。図を交えて説明します。
まず、アプリケーションは、Azure OpenAI ServiceなどのLLMに渡すプロンプトに、関数の内容も一緒に渡します。例えば、上図では、「トム・クルーズの現在の年齢を調べて。」というユーザーメッセージを渡すと同時に、関数の定義も一緒に渡しています。

関数の定義に必要なのは、関数名、関数の内容、引数です。上図では、2つの関数を定義しています。一つは、指定したクエリでWebを検索する関数と、もう一つは、与えられた計算式に基づいて計算をする関数です。

このプロンプトをLLMに渡すと、ユーザーメッセージ(質問)の内容と、登録されている関数の内容を照らし合わせて適切な関数の情報を返してくれます。例えば、この場合は、現在のパリの天気を調べるにはWebを検索することが必要であると判断され、関数名serach_webの情報を返してくれます。そして関数に渡す引数も返してくれます。この場合「トム 」と返してくれました。

この説明から分かる通り、関数の説明は非常に重要です。これが適切でないとLLMは正しい関数を選択できません。

LLMが返してくれるのは、関数名や引数の情報だけなので、その実装はアプリケーション側で用意して上げる必要があります。上図では`search_web`(Webを検索する関数)と`calculate`(計算をする関数)の実装を示しています。LLMから返ってきた関数名と引数を使って、適切な関数を呼び出す処理を実装します。

つまり、Function callingを使うと、LLMが自分で考えて、適切な関数を選択してくれるので、より複雑な処理を簡単に実装することができます。つまりAIエージェントのような動きをしてくれるのです。

Function callingのプロンプト

Function callingのプロンプトを見てみます。

{
  "messages": [
    {
      "role": "system",
      "content": "あなたは役立つアシスタントです。"
    },
    {
      "role": "user",
      "content": "トム・クルーズの誕生日にケーキをプレゼントしたいです。彼の年齢分のろうそくを購入するための金額を計算してください。ろうそくは一本100円です。"
    }
  ],
  "functions": [ ⋯ ①
    {
      "name": "search_web", ⋯ ②
      "description": "指定されたクエリでWebを検索します。", ⋯ ③
      "parameters": { ⋯ ④
        "type": "object",
        "properties": {
          "query": {
            "type": "string",
            "description": "検索するクエリ"
          }
        },
        "required": [
          "query"
        ]
      }
    },
    {
      "name": "calculate",
      "description": "与えられた計算式に基づき計算を行います。",
      "parameters": {
        "type": "object",
        "properties": {
          "formula": {
            "type": "string",
            "description": "計算式(例: 52 * 100、100 - 1)"
          }
        },
        "required": [
          "formula"
        ]
      }
    }
  ],
  "function_call": "auto"
}

①のfuntionsは、関数を定義するフィールドです。ここに関数を定義してきます。

②のnameは、関数名を指定します。

③のdescriptionは、関数の説明を指定します。この内容は超重要で、ユーザーの質問とこの関数の説明を比較して、LLMは関数の実施を決定します。

④のparametersは、関数に与える引数になります。関数の引数はJSONの形で与えられ、propertiesフィールドで定義した内容がそのまま引数になります。例えば、上記の内容であれば、{ “query”: “検索するクエリ”}という形式で引数が与えられます。

そして、以下がプロンプトに対するレスポンスになります。

{
  "choices": [
    {
      "content_filter_results": {},
      "finish_reason": "function_call",
      "index": 0,
      "logprobs": null,
      "message": {
        "content": null, ⋯ ①
        "function_call": { ⋯ ②
          "arguments": "{\n  \"query\": \"トム・クルーズの年齢\"\n}",
          "name": "search_web"
        },
        "role": "assistant"
      }
    }
  ],
…(省略)…
}

①の「content」フィールドには、本来質問に対する回答が返ってくるはずですが、Function callingで実施すべき関数がある場合は、nullとなります。

②では、そのかわりにfunction_callというフィールドが登場し、そこのargumentsに関数の引数、nameに実施すべき関数名が入っています。

これらの情報を見て、実施すべき関数を決定します。

シンプルなFunction callingの実装

では、簡単なFunction callingの実装を見ていきましょう。

またもやトム・クルーズの年齢分のろうそくの合計金額を計算する問題を例に取ってみます。

「Function callingの概要」で説明した2つの関数を定義します。一つは、指定したクエリでWebを検索する関数`search_web`、もう一つは、与えられた計算式に基づいて計算をする関数`calculate`です。

そして、プロンプトには「トム・クルーズの現在の年齢を調べて。」というユーザーメッセージを渡します。

想定される結果は、関数名`search_web`の関数が実行されて、その検索結果が返ってくることです。

ではソースコードの全文になります。
import requests
import json
from duckduckgo_search import DDGS

# Azure OpenAI Serviceの設定
AOAI_ENDPOINT = "https://hogehoge.openai.azure.com/" # Azure OpenAI Serviceのエンドポイント
AOAI_API_VERSION = "2024-06-01" # Azure OpenAI ServiceのAPIバージョン
AOAI_API_KEY = "XXXXXXX" # Azure OpenAI ServiceのAPIキー
AOAI_CHAT_MODEL_NAME = "gpt-4-deploy" # Azure OpenAI Serviceのデプロイモデル名

# リクエストURL
url = f"{AOAI_ENDPOINT}openai/deployments/{AOAI_CHAT_MODEL_NAME}/chat/completions?api-version={AOAI_API_VERSION}"

# リクエストヘッダー
headers = {
    "Content-Type": "application/json",
    "api-key": AOAI_API_KEY
}

# 最初のリクエストデータ: Web検索関数を自動呼び出し
data = {
    "messages": [
        {"role": "system", "content": "あなたは役立つアシスタントです。"},
        {"role": "user", "content": "トム・クルーズの現在の年齢を調べて。"}
    ],
    "functions": [
        {
            "name": "search_web",
            "description": "指定されたクエリでWebを検索します。",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "検索するクエリ"
                    }
                },
                "required": ["query"]
            }
        },
        {
            "name": "calculate",
            "description": "与えられた計算式に基づき計算を行います。",
            "parameters": {
                "type": "object",
                "properties": {
                    "formula": {
                        "type": "string",
                        "description": "計算式(例: 52 * 100、100 - 1)"
                    }
                },
                "required": ["formula"]
            }
        }
    ]
}

# 指定されたクエリでWebを検索する関数
def search_web(query: str):
    final_result = ""
    search_results = DDGS().text(query, max_results=10)
    for search_result in search_results:
        final_result = final_result + search_result["body"] + "\n"
    return final_result

# 与えられた計算式に基づき計算を行う関数
def calculate(formula: str): 
    return eval(formula)

# リクエストを送信
response = requests.post(url, headers=headers, data=json.dumps(data))

# 結果を出力
result = response.json()

# 関数を実行する。
message = result["choices"][0]["message"]["function_call"]
function_name = message["name"]
arguments = json.loads(message["arguments"])  # 文字列を辞書に変換

print(f"実行された関数: {function_name}")
print(f"関数に渡された引数: {arguments}")

func = globals()[function_name]
result = func(**arguments)
print(f"関数の実行結果: {result}")
では、このソースコードの詳細を見ていきましょう。

Azure OpenAI Serviceの設定

# Azure OpenAI Serviceの設定
AOAI_ENDPOINT = "https://hogehoge.openai.azure.com/" # Azure OpenAI Serviceのエンドポイント
AOAI_API_VERSION = "2024-06-01" # Azure OpenAI ServiceのAPIバージョン
AOAI_API_KEY = "XXXXXXX" # Azure OpenAI ServiceのAPIキー
AOAI_CHAT_MODEL_NAME = "gpt-4-deploy" # Azure OpenAI Serviceのデプロイモデル名
Azure OpenAI Serviceにアクセスするための設定を行っています。`AOAI_ENDPOINT`はAzureのエンドポイントURLで、これはAzureのAIサービスにアクセスするための入り口のURLです。`AOAI_API_VERSION`は使うAPIのバージョンを指定しています。`AOAI_API_KEY`はサービスにアクセスするための鍵のようなものです。これがないと認証できないので、セキュリティ的に重要な情報です。そして、`AOAI_CHAT_MODEL_NAME`はどのAIモデルを使うかを指定しています。ここではGPT-4を使っていますが、他のモデルを使うこともできます。

リクエストURL

url = f"{AOAI_ENDPOINT}openai/deployments/{AOAI_CHAT_MODEL_NAME}/chat/completions?api-version={AOAI_API_VERSION}"
次に、リクエストを送るためのURLを構築しています。このURLは、Azure OpenAI Serviceにアクセスし、指定したモデルを使ってチャットリクエストを処理してもらうためのものです。URLにモデル名やAPIバージョンを動的に追加しています。

リクエストヘッダー

# リクエストヘッダー
headers = {
    "Content-Type": "application/json",
    "api-key": AOAI_API_KEY
}
続いて、リクエストを送るときに必要な情報を定義します。`headers`では、リクエストの形式やAPIキーを設定しています。ここで、`Content-Type``application/json`であることから、リクエストボディがJSON形式であることが分かります。

メッセージの定義

# 最初のリクエストデータ: Web検索関数を自動呼び出し
data = {
    "messages": [
        {"role": "system", "content": "あなたは役立つアシスタントです。"},
        {"role": "user", "content": "トム・クルーズの現在の年齢を調べて。"}
    ],
次に、送信するデータ(`data`)を作成しています。この部分は、実際にGPTに対して「トム・クルーズの現在の年齢を調べて。」というリクエストを送る内容を定義しています。`messages`の中に、「あなたは役立つアシスタントです」とAIに役割を伝え、ユーザーからの質問として「トム・クルーズの現在の年齢を調べて。」というメッセージを渡しています。

関数の定義

    "functions": [
        {
            "name": "search_web",
            "description": "指定されたクエリでWebを検索します。",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "検索するクエリ"
                    }
                },
                "required": ["query"]
            }
        },
        {
            "name": "calculate",
            "description": "与えられた計算式に基づき計算を行います。",
            "parameters": {
                "type": "object",
                "properties": {
                    "formula": {
                        "type": "string",
                        "description": "計算式(例: 52 * 100、100 - 1)"
                    }
                },
                "required": ["formula"]
            }
        }
    ]
`functions`の中には、関数の情報を定義しています。ここでは、`search_web``calculate`の2つの関数を定義しています。それぞれの関数には、関数名、関数の説明、引数の情報が含まれています。`search_web`関数は、指定されたクエリでWebを検索する関数で、`query`という引数を必要とします。`calculate`関数は、与えられた計算式に基づいて計算を行う関数で、`formula`という引数を必要とします。

特に大事なのは`description`です。これは、LLMが、ユーザーの質問に基づき関数を選択する際に参照する情報です。この情報が適切でないと、LLMが正しい関数を選択できません。

`parameters`の中には、引数の情報が含まれています。`type`は引数の型を指定しています。`properties`の中には、引数の名前と説明が含まれています。`required`は、必須の引数を指定しています。

`calculate`関数の引数の`description`には、計算式の例が記載されています。これは、LLMが計算式を入力する際のフォーマットを理解しやすくするためのものです。ここできちっと例を示しておかないと、LLMが正しい計算式を入力することができません。

指定されたクエリでWebを検索する関数

# 指定されたクエリでWebを検索する関数
def search_web(query: str):
    final_result = ""
    search_results = DDGS().text(query, max_results=10)
    for search_result in search_results:
        final_result = final_result + search_result["body"] + "\n"
    return final_result
`search_web`関数は、指定されたクエリでWebを検索する関数です。この関数は、`query`という引数を受け取り、そのクエリでWebを検索して、検索結果を返します。

上位10件の検索結果を取得しており、それぞれの検索結果の`body`というフィールドに格納されている本文の要約部分を取り出し、`final_result`に追加して連結しています。

与えられた計算式に基づき計算を行う関数

# 与えられた計算式に基づき計算を行う関数
def calculate(formula: str): 
    return eval(formula)
`calculate`関数は、与えられた計算式に基づいて計算を行う関数です。この関数は、`formula`という引数を受け取り、その計算式を評価して結果を返します。

リクエストの送信

# リクエストを送信
response = requests.post(url, headers=headers, data=json.dumps(data))


# 結果を出力
result = response.json()
リクエストを送信し、結果を取得します。

関数の実行

# 関数を実行する。
message = result["choices"][0]["message"]["function_call"]
function_name = message["name"]
arguments = json.loads(message["arguments"]) # 文字列を辞書に変換


print(f"実行された関数: {function_name}")
print(f"関数に渡された引数: {arguments}")


func = globals()[function_name]
result = func(**arguments)
print(f"関数の実行結果: {result}")
最後に、LLMから返ってきた関数名と引数を使って、適切な関数を呼び出しています。

`message`から関数の情報を取得しています。

`function_name`には、関数名が入っています。`arguments`には、関数に渡す引数が入っています。`json.loads`を使って、文字列を辞書に変換しています。

`globals()[function_name]`によって、関数名を使って関数を取得し、その関数を呼び出しています。`**arguments`を使って、辞書を展開して、関数に引数を渡しています。結果を`result`に格納して、出力しています。

では、動かしてみましょう。実行結果は以下の通りになります。
実行された関数: search_web
関数に渡された引数: {'query': 'トム・クルーズ現在の年齢'}
関数の実行結果: トム・クルーズのサイエントロジーに対する開けた態度は、...(以下略)...
バッチリですね。

では、次は、計算式を入力して、計算を行ってみましょう。「トム・クルーズの現在の年齢を調べて。」を「52 ✕ 100を計算して。」という計算式に変更して、実行してみます。あえてLLMが掛け算の記号を「*」ではなく「✕」として、LLMが正しく認識して計算できるかを確認してみます。
実行された関数: calculate
関数に渡された引数: {'formula': '52 * 100'}
関数の実行結果: 5200
バッチリですね。`calculate`関数が正しく実行され、計算結果が返ってきました。

Function callingでAIエージェント

実は、Function callingを使うと、ReActのようなAIエージェントを実装することもできます。

Funtion callingのすごいところは、質問(ユーザーメッセージ)から、適切な関数を選択して実行することができる点だけではなく、ReActのように、思考と行動を繰り返し、最終回答が得られるまで繰り返す処理を実装することもできる点です。つまり段取りが行えます。

与えられた質問(ユーザーメッセージ)と複数の関数から、まず最初にどの関数を実行して、次に何の関数を実行すべきなのか、そんなこともしてくれるのです。

処理の流れ

Function callingによるAIエージェントの処理の流れは、以下のようになります。

Function callingによるAIエージェントは、何回もLLMにリクエストを送ります。最終結果がでていないとLLMが判断した場合は、実行すべき関数をレスポンスとして返し、アプリケーションはその関数に対応したアプリケーションの実装を実行します。LLMが最終結果がでたと判断したら処理は終了です。「思考」と「行動」を繰り返すReActと同じ流れです。

1回目の処理 (概念図)

では、複数回行われる処理を一つずつバラして、説明してきます。まずは1回目からです。処理の概念図から見ていきます。
また例によって、トム・クルーズの年齢分のろうそくの合計金額を計算する問題を例に取ってみます。同じように2つの関数を定義します。一つは、指定したクエリでWebを検索する関数`search_web`、もう一つは、与えられた計算式に基づいて計算をする関数`calculate`です。

まず、1回目の実行を見てみてください。
指定したクエリでWebを検索する関数`search_web`、もう一つは、与えられた計算式に基づいて計算をする関数`calculate`の2つの関数が定義されている状態で、以下の質問がなされました。
トム・クルーズの誕生日にケーキをプレゼントしたいです。彼の年齢分のろうそくを購入するための金額を計算してください。ろうそくは一本本100円です。
このとき、Function callingは段取りを行います。つまり、トム・クルーズの年齢分のろうそくの合計金額を計算するためには、まずWebを検索して、トム・クルーズの年齢を調べて、その後に計算を行う必要があると判断するのです。

その結果、複数ある関数から、最初に`search_web`関数を実行することを選択します。そして、`search_web`関数を実行して、その結果を返します。

1回目の処理 (プロンプト)

処理の概念がわかったことろで、実際にやり取りされるプロンプトの中身を見てみましょう。
{
  "messages": [
    {
      "role": "system",
      "content": "あなたは役立つアシスタントです。"
    },
    {
      "role": "user",
      "content": "トム・クルーズの誕生日にケーキをプレゼントしたいです。彼の年齢分のろうそくを購入するための金額を計算してください。ろうそくは一本100円です。"
    }
  ],
  "functions": [ ⋯ ①
    {
      "name": "search_web", ⋯ ②
      "description": "指定されたクエリでWebを検索します。", ⋯ ③
      "parameters": { ⋯ ④
        "type": "object",
        "properties": {
          "query": {
            "type": "string",
            "description": "検索するクエリ"
          }
        },
        "required": [
          "query"
        ]
      }
    },
    {
      "name": "calculate",
      "description": "与えられた計算式に基づき計算を行います。",
      "parameters": {
        "type": "object",
        "properties": {
          "formula": {
            "type": "string",
            "description": "計算式(例: 52 * 100、100 - 1)"
          }
        },
        "required": [
          "formula"
        ]
      }
    }
  ],
  "function_call": "auto"
}

①のfuntionsは、関数を定義するフィールドです。ここに関数を定義してきます。

②のnameは、関数名を指定します。

③のdescriptionは、関数の説明を指定します。この内容は超重要で、ユーザーの質問とこの関数の説明を比較して、LLMは関数の実施を決定します。

④のparametersは、関数に与える引数になります。関数の引数はJSONの形で与えられ、propertiesフィールドで定義した内容がそのまま引数になります。例えば、上記の内容であれば、{ “query”: “検索するクエリ”}という形式で引数が与えられます。

1回目の処理 (レスポンス)

次に、1回目のプロンプトに対するレスポンスを見てみます。
{
  "choices": [
    {
      "content_filter_results": {},
      "finish_reason": "function_call",
      "index": 0,
      "logprobs": null,
      "message": {
        "content": null, ⋯ ①
        "function_call": { ⋯ ②
          "arguments": "{\n  \"query\": \"トム・クルーズの年齢\"\n}",
          "name": "search_web"
        },
        "role": "assistant"
      }
    }
  ],
…(省略)…
}

①の「content」フィールドには、本来質問に対する回答が返ってくるはずですが、Function callingで実施すべき関数がある場合は、nullとなります。

②では、そのかわりにfunction_callというフィールドが登場し、そこのargumentsに関数の引数、nameに実施すべき関数名が入っています。

ここでは、LLMはプロンプトにて与えられた質問と関数の内容から判断して、まずはトム・クルーズの年齢をsearch_web(インターネットを検索する関数)を使って調べて、その後に計算をする関数(calculate)を使って、トムの年齢とろうそく1本あたりの値段をかけるという推論をしました。その結果、まず一発目に実施する関数はsearch_webであり、その引数は「トム・クルーズの年齢」というレスポンスを返したという結果になります。

2回目の処理 (概念図)

次に、2回目の処理の概念図を見ていきます。
キモは、プロンプトに1回目の実行結果を含めて、再度質問をすることです。これによって、LLMは以下のように解釈します。
最初に段取りした「トム・クルーズの年齢分のろうそくの合計金額を計算するためには、まずWebを検索して、トム・クルーズの年齢を調べて、その後に計算を行う必要がある」という流れのうち、最初の段取りである「トム・クルーズの年齢を調べる」はすでに完了しているんだな。では、その実行結果を見て、トム・クルーズの現在の年齢を割り出して、次にろうそくの合計金額の計算を行おう。
つまり、LLMに1回目の実行は終わり、その結果を渡すことで、続きの段取りを行うことができるのです。

2回目の処理 (プロンプト)

2回目の処理でLLMに送るプロンプトは以下の通りとなります。
{
  "messages": [
    {
      "role": "system",
      "content": "あなたは役立つアシスタントです。"
    },
    {
      "role": "user",
      "content": "トム・クルーズの誕生日にケーキをプレゼントしたいです。彼の年齢分のろうそくを購入するための金額を計算してください。ろうそくは一本100円です。"
    },
    { ⋯ ①
      "content": null,
      "function_call": {
        "arguments": "{\n  \"query\": \"トム・クルーズの年齢\"\n}",
        "name": "search_web"
      },
      "role": "assistant"
    },
    { ⋯ ②
      "role": "function",
      "name": "search_web",
      “content”: “トム・クルーズ. トム・クルーズ (英: Tom Cruise, 1962年 7月3日 - )、…(以下省略)…  ],
    }
  ],
  "functions": [
    {
      "name": "search_web",
      "description": "指定されたクエリでWebを検索します。",
      "parameters": {
        "type": "object",
        "properties": {
          "query": {
            "type": "string",
            "description": "検索するクエリ"
          }
        },
        "required": [
          "query"
        ]
      }
    },
    …(以下省略)…  
  ],
  "function_call": "auto"
}
①は、1回目の処理の実行結果です。search_web関数を実行して、「トム・クルーズの年齢」というクエリでインターネット検索せよという内容になります。
②は、1回目の処理の実行結果を受けて、PythonのDuckDuckGoライブラリで「トム・クルーズの年齢」というクエリでインターネット検索した内容を追加しています。これはReActで言うところの「観察」の部分と同じです。

2回目の処理 (レスポンス)

{
  "choices": [
    {
      "content_filter_results": {},
      "finish_reason": "function_call",
      "index": 0,
      "logprobs": null,
      "message": {
        "content": null, ⋯ ①
        "function_call": { ⋯ ②
          "arguments": "{\n  \"formula\": \"62 * 100\"\n}",
          "name": "calculate"
        },
        "role": "assistant"
      }
    }
  ],
  …(省略)…
}

①の「content」フィールドには、本来質問に対する回答が返ってくるはずですが、Function callingで実施すべき関数がある場合は、nullとなります。

②では、そのかわりにfunction_callというフィールドが登場し、そこのargumentsに関数の引数、nameに実施すべき関数名が入っています。

ここでは、先程のプロンプト内にあったsearch_webの実行結果(トム・クルーズ. トム・クルーズ (英: Tom Cruise, 1962年 7月3日 – )、…(以下省略)… )を受けて、LLMはトム・クルーズの年齢は62歳だと判別し、次に実行するべき処理は、年齢 ✕ ろうそく一本あたりの値段だと思考し、その内容のfunction_callを返しています。

3回目の処理 (概念図)

最後の実行である3回目の処理の概念図を見てみてください。
今度は、プロンプトに1回目の実行結果に加えて、2回目の実行結果を加えています。これでLLMは、最初に段取りしたすべての工程が完了したと判断し、処理を終了します。

3回目の処理 (プロンプト)

3回目の処理の際にLLMに送るプロンプトです。
{
  "messages": [
    {
      "role": "system",
      "content": "あなたは役立つアシスタントです。"
    },
    {
      "role": "user",
      "content": "トム・クルーズの誕生日にケーキをプレゼントしたいです。彼の年齢分のろうそくを購入するための金額を計算してください。ろうそくは一本100円です。"
    },
    {
      "content": null,
      "function_call": {
        "arguments": "{\n  \"query\": \"トム・クルーズの年齢\"\n}",
        "name": "search_web"
      },
      "role": "assistant"
    },
    {
      "role": "function",
      "name": "search_web",
      “content”: “トム・クルーズ. トム・クルーズ (英: Tom Cruise, 1962年 7月3日 - )、…(以下省略)…  ],
    },
    { ⋯ ①
      "content": null,
      "function_call": {
        "arguments": "{\n  \"formula\": \"62 * 100\"\n}",
        "name": "calculate"
      },
      "role": "assistant"
    },
    { ⋯ ②
      "role": "function",
      "name": "calculate",
      "content": "6000"
    }
  ],
  "functions": [
    …(以下省略)…  
  ],
  "function_call": "auto"
}
2回目の処理のプロンプトにさらに、①と②が追加となっています。
①は、2回目のプロンプトの実行結果です。calcurate関数を実行して、「62 * 100」という計算式で計算せよという内容になります。
②は、2回目の処理の実行結果を受けて、Pythonのeval関数で「62 * 100」という計算式で計算した内容を追加したものになります。これはReActで言うところの「観察」の部分と同じです。

3回目の処理 (レスポンス)

3回目の処理のプロンプトに対するレスポンスになります。
{
  "choices": [
    {
      …(省略)…
      "message": {
        "content": "トム・クルーズは62歳なので、彼の年齢分のろうそくを購入するためには6200円が必要です。", ⋯ ①
        "role": "assistant"
      }
    }
  ],
  …(省略)…
}
①で、「トム・クルーズは62歳なので、彼の年齢分のろうそくを購入するためには6200円が必要です。」という形で最終回答が出たので、LLMは処理終了と判断します。

すごいですね、Function calling!!

重ねていいますが、Function callingは段取りまでやってくるところにその真価があります。ReActのようなAIエージェントを実装するのに非常に便利です。

Function callingによるAIエージェントの実装

それでは、Function callingを使って、AIエージェントを実装するサンプルコードを見ていきましょう。
import requests
import json
from duckduckgo_search import DDGS

# Azure OpenAI Serviceの設定
AOAI_ENDPOINT = "https://hogehoge.openai.azure.com/" # Azure OpenAI Serviceのエンドポイント
AOAI_API_VERSION = "2024-06-01" # Azure OpenAI ServiceのAPIバージョン
AOAI_API_KEY = "XXXXXXXX" # Azure OpenAI ServiceのAPIキー
AOAI_CHAT_MODEL_NAME = "gpt-4-deploy" # Azure OpenAI Serviceのデプロイモデル名

# リクエストURL
url = f"{AOAI_ENDPOINT}openai/deployments/{AOAI_CHAT_MODEL_NAME}/chat/completions?api-version={AOAI_API_VERSION}"

# リクエストヘッダー
headers = {
    "Content-Type": "application/json",
    "api-key": AOAI_API_KEY
}

# 初期のメッセージ
messages = [
    {"role": "system", "content": "あなたは役立つアシスタントです。"},
    {"role": "user", "content": "トム・クルーズの誕生日にケーキをプレゼントしたいです。彼の年齢分のろうそくを購入するための金額を計算してください。ろうそくは一本本100円です。"}
]

# 関数の定義
functions = [
    {
        "name": "search_web",
        "description": "指定されたクエリでWebを検索します。",
        "parameters": {
            "type": "object",
            "properties": {
                "query": {
                    "type": "string",
                    "description": "検索するクエリ"
                }
            },
            "required": ["query"]
        }
    },
    {
        "name": "calculate",
        "description": "与えられた計算式に基づき計算を行います。",
        "parameters": {
            "type": "object",
            "properties": {
                "formula": {
                    "type": "string",
                    "description": "計算式(例: 52 * 100、100 - 1)"
                }
            },
            "required": ["formula"]
        }
    }
]

# 指定されたクエリでWebを検索する関数
def search_web(query: str):
    final_result = ""
    search_results = DDGS().text(query, max_results=10)
    for search_result in search_results:
        final_result = final_result + search_result["body"] + "\n"
    return final_result

# 与えられた計算式に基づき計算を行う関数
def calculate(formula: str):
    return str(eval(formula))

# メインループ
while True:
    # リクエストデータの準備
    data = {
        "messages": messages,
        "functions": functions,
        "function_call": "auto"
    }
    
    # リクエストを送信
    response = requests.post(url, headers=headers, data=json.dumps(data))
    result = response.json()
    
    # アシスタントの返信を取得
    assistant_message = result["choices"][0]["message"]
    messages.append(assistant_message)  # アシスタントのメッセージを履歴に追加
    
    # アシスタントが関数を呼び出したか確認
    if "function_call" in assistant_message:
        function_call = assistant_message["function_call"]
        function_name = function_call["name"]
        arguments = json.loads(function_call["arguments"])  # 文字列を辞書に変換
        
        # 関数を実行
        func = globals()[function_name]
        function_response = func(**arguments)

        print(f"実行された関数: {function_name}")
        print(f"関数に渡された引数: {arguments}")
        print(f"関数の実行結果: {function_response}")        

        # 関数の応答をメッセージに追加
        messages.append({
            "role": "function",
            "name": function_name,
            "content": function_response
        })
    else:
        # 最終的な回答が得られた場合
        final_observation = assistant_message["content"]
        print(f"最終回答: {final_observation}")
        break  # ループを終了
では、このソースコードの詳細を見ていきましょう。「シンプルなFunction callingの実装」のところで説明した部分は割愛します。

メッセージの定義

messages = [
    {"role": "system", "content": "あなたは役立つアシスタントです。"},
    {"role": "user", "content": "トム・クルーズの誕生日にケーキをプレゼントしたいです。彼の年齢分のろうそくを購入するための金額を計算してください。ろうそくは一本本100円です。"}
]
初期のメッセージを定義しています。ここでは、システムからのメッセージとユーザーからのメッセージを定義しています。システムからのメッセージは、アシスタントが役立つことを伝えるメッセージです。ユーザーからのメッセージは、トム・クルーズの年齢分のろうそくの合計金額を計算するための質問です。

関数の定義

functions = [
    {
        "name": "search_web",
        "description": "指定されたクエリでWebを検索します。",
        "parameters": {
            "type": "object",
            "properties": {
                "query": {
                    "type": "string",
                    "description": "検索するクエリ"
                }
            },
            "required": ["query"]
        }
    },
    {
        "name": "calculate",
        "description": "与えられた計算式に基づき計算を行います。",
        "parameters": {
            "type": "object",
            "properties": {
                "formula": {
                    "type": "string",
                    "description": "計算式(例: 52 * 100、100 - 1)"
                }
            },
            "required": ["formula"]
        }
    }
]
関数の定義を行っています。これは「シンプルなFunction callingの実装」で説明した部分と同じです。

関数の実装

# 指定されたクエリでWebを検索する関数
def search_web(query: str):
    final_result = ""
    search_results = DDGS().text(query, max_results=10)
    for search_result in search_results:
        final_result = final_result + search_result["body"] + "\n"
    return final_result

# 与えられた計算式に基づき計算を行う関数
def calculate(formula: str):
    return str(eval(formula))
関数の実装を行っています。これは「シンプルなFunction callingの実装」で説明した部分と同じです。

メインループ

# メインループ
while True:
    # リクエストデータの準備
    data = {
        "messages": messages,
        "functions": functions,
        "function_call": "auto"
    }
ここからが、Function CallingによるAIエージェントのキモの部分です。

メインループを定義しています。このメインループで延々と処理を繰り返すことで、AIエージェントのような動きを実現しています。最終回答が出るまで、繰り返し処理を行います。

ここでは、リクエストデータを準備しています。`messages`には、初期のメッセージが入っています。`functions`には、関数の定義が入っています。

`”function_call”: “auto”`は、Function Callingにおける設定の一つで、LLMに対して、自動的に関数を呼び出すかどうかを判断させるためのオプションです。この設定を使用することで、LLMは受け取ったユーザーのリクエストに基づいて、「どの関数を呼び出すべきか」を自分で判断し、適切な関数を自動で呼び出します。

リクエストの送信

    # リクエストを送信
    response = requests.post(url, headers=headers, data=json.dumps(data))
    result = response.json()
リクエストを送信して、結果を取得します。

アシスタントの返信を取得

    # アシスタントの返信を取得
    assistant_message = result["choices"][0]["message"]
    messages.append(assistant_message) # アシスタントのメッセージを履歴に追加
LLMからの回答を取得して、`messages`に追加します。これによって、アシスタントのメッセージの履歴を保持することができます。

関数の実行

    # アシスタントが関数を呼び出したか確認
    if "function_call" in assistant_message:
        function_call = assistant_message["function_call"]
        function_name = function_call["name"]
        arguments = json.loads(function_call["arguments"])  # 文字列を辞書に変換
        
        # 関数を実行
        func = globals()[function_name]
        function_response = func(**arguments)

        print(f"実行された関数: {function_name}")
        print(f"関数に渡された引数: {arguments}")
        print(f"関数の実行結果: {function_response}")

        # 関数の応答をメッセージに追加
        messages.append({
            "role": "function",
            "name": function_name,
            "content": function_response
        })
    else:
        # 最終的な回答が得られた場合
        final_observation = assistant_message["content"]
        print(f"最終回答: {final_observation}")
        break  # ループを終了
最初のif文`if “function_call” in assistant_message:`は、LLMが、次の関数を呼び出すかどうかを判断するためのものです。もし、呼び出す必要があるのであれば、LLMからの返答(変数`assistant_message`)は以下のようになっています。
{
    "content": None,
    "function_call": {
        "name": "search_web",
        "arguments": "{\"query\": \"トム・クルーズの年齢\"}"
    },
    "role": "assistant"
}
通常であれば、`content`にはLLMからのテキストメッセージが入っています。しかし、Function callingを使う場合は、`content`はNoneになり、そのかわり`function_call`に関数の情報が入っています。`name`には、呼び出す関数の名前が入っています。`arguments`には、関数に渡す引数が入っています。この引数は、文字列として格納されているので、`json.loads`を使って辞書に変換しています。

もう呼び出す関数がない、つまり最終的な回答が得られた場合は、`content`にテキストメッセージが入っています。この場合は、そのメッセージを出力して、ループを終了します。
{
    "content": "トム・クルーズは現在60歳なので、彼の年齢分のろうそくを購入するには6000円必要です。",
    "role": "assistant"
}
`function_response = func(**arguments)`は、関数を呼び出しています。そして、その結果を`messages`に追加しています。

以上で、Function callingによるAIエージェントの実装が完了です。

では、実行結果を見てみましょう。
実行された関数: search_web
関数に渡された引数: {'query': 'トム・クルーズの年齢'}
関数の実行結果: トム・クルーズのサイエントロジーに対する開けた態度は、...(以下略)...


実行された関数: calculate
関数に渡された引数: {'formula': '60 * 100'}
関数の実行結果: 6000


最終回答: トム・クルーズの年齢は60歳なので、彼の年齢分のろうそくを購入するためには6000円が必要です。
バッチリですね。Function callingによって、AIエージェントを実装することができました。

LangChainでAIエージェント

実は、LangChainもFunction callingを使って、AIエージェントを実装することができます。

LangChainは、LLMを使ったアプリケーションを簡単に構築するためのフレームワークです。特に、生成AIを用いたアプリケーションで、外部データへのアクセスや複雑なタスクの実行を必要とする場合に有用です。

AgentExecutorを使うことで、Function callingを使ったAIエージェントを簡単に実装することができます。AgentExecutorは、LangChainの中で、Function callingを使って、複数の関数を呼び出すことができる機能です。

ただ、いきなりLangChainのような便利なフレームワークを使ってしまうと、AIエージェントの仕組みがわかりにくくなってしまうかもしれません。なので、まずは、Function callingを使って、AIエージェントを実装する方法を理解してから、LangChainを使ってみるのが良いでしょう。

それでは、LangChainを使って、Function callingを使ったAIエージェントを実装してみましょう。
from langchain_openai import AzureChatOpenAI
from langchain.agents import tool
from langchain_community.tools import DuckDuckGoSearchRun
from langchain.callbacks import StdOutCallbackHandler
from langchain_core.prompts import MessagesPlaceholder, ChatPromptTemplate
from langchain.agents import create_tool_calling_agent, AgentExecutor
from langchain.memory import ConversationBufferWindowMemory

# Azure OpenAI Serviceの設定
AOAI_ENDPOINT = "https://hogehoge.openai.azure.com/" # Azure OpenAI Serviceのエンドポイント
AOAI_API_VERSION = "2024-06-01" # Azure OpenAI ServiceのAPIバージョン
AOAI_API_KEY = "XXXXXXX" # Azure OpenAI ServiceのAPIキー
AOAI_CHAT_MODEL_NAME = "gpt-4-deploy" # Azure OpenAI Serviceのデプロイモデル名

# LLMの初期化
llm = AzureChatOpenAI(
    azure_endpoint=AOAI_ENDPOINT,
    api_key=AOAI_API_KEY,
    api_version=AOAI_API_VERSION,
    openai_api_type="azure",
    azure_deployment=AOAI_CHAT_MODEL_NAME)

# 指定されたクエリでWebを検索する関数
@tool
def search_web(query: str):
    """
    指定されたクエリでWebを検索します。
    """
    search = DuckDuckGoSearchRun()
    return search.run(query)

# 与えられた計算式に基づき計算を行う関数
@tool
def calculate(formula: str):
    """
    与えられた計算式に基づき計算を行います。
    """
    return str(eval(formula))

tools = [search_web, calculate]

prompt = ChatPromptTemplate.from_messages([
    ("system", "あなたは役立つアシスタントです。"),
    MessagesPlaceholder(variable_name="chat_history"),
    ("user", "{input}"),
    MessagesPlaceholder(variable_name="agent_scratchpad")
])

agent = create_tool_calling_agent(llm, tools, prompt)

exucutor = AgentExecutor(
    agent=agent,
    tools=tools,
    verbose=True,
    memory=ConversationBufferWindowMemory(
            return_messages=True,
            memory_key="chat_history",
            k=10
        )
)

exucutor.invoke(
    {"input": "トム・クルーズの誕生日にケーキをプレゼントしたいです。彼の年齢分のろうそくを購入するための金額を計算してください。ろうそくは一本本100円です。"},
    callback_handler=StdOutCallbackHandler()
)
では、このソースコードの詳細を見ていきましょう。

Azure OpenAI Serviceの設定

# Azure OpenAI Serviceの設定
AOAI_ENDPOINT = "https://hogehoge.openai.azure.com/" # Azure OpenAI Serviceのエンドポイント
AOAI_API_VERSION = "2024-06-01" # Azure OpenAI ServiceのAPIバージョン
AOAI_API_KEY = "XXXXXXX" # Azure OpenAI ServiceのAPIキー
AOAI_CHAT_MODEL_NAME = "gpt-4-deploy" # Azure OpenAI Serviceのデプロイモデル名
Azure OpenAI Serviceの設定を行っています。ここでは、Azure OpenAI Serviceのエンドポイント、APIバージョン、APIキー、デプロイモデル名を定義しています。

LLMの初期化

# LLMの初期化
llm = AzureChatOpenAI(
    azure_endpoint=AOAI_ENDPOINT,
    api_key=AOAI_API_KEY,
    api_version=AOAI_API_VERSION,
    openai_api_type="azure",
    azure_deployment=AOAI_CHAT_MODEL_NAME)
Azure OpenAI Serviceを初期化しています。ここでは、Azure OpenAI Serviceのエンドポイント、APIキー、APIバージョン、APIタイプ、デプロイモデル名を指定しています。

指定されたクエリでWebを検索する関数

# 指定されたクエリでWebを検索する関数
@tool
def search_web(query: str):
    """
    指定されたクエリでWebを検索します。
    """
    search = DuckDuckGoSearchRun()
    return search.run(query)
関数`search_web`を定義しています。ここでは、`DuckDuckGoSearchRun`を使って、指定されたクエリでWebを検索しています。トム・クルーズの年齢を調べるために使います。

`@tool`デコレータを使って、関数をツールとして定義しています。これによって、LangChainが関数をFunction callingで呼び出す関数として認識することができます。

`指定されたクエリでWebを検索します。`は、関数の説明です。これは、LLMが関数を選択する際に参照する情報です。Function callingのところで説明した`description`と同じです。

与えられた計算式に基づき計算を行う関数

# 与えられた計算式に基づき計算を行う関数
@tool
def calculate(formula: str):
    """
    与えられた計算式に基づき計算を行います。
    """
    return str(eval(formula))
関数`calculate`を定義しています。ここでは、与えられた計算式に基づいて計算を行っています。ろうそくの合計金額を計算するために使います。

`@tool`デコレータを使って、関数をツールとして定義しています。これによって、LangChainが関数をFunction callingで呼び出す関数として認識することができます。

`与えられた計算式に基づき計算を行います。`は、関数の説明です。これは、LLMが関数を選択する際に参照する情報です。Function callingのところで説明した`description`と同じです。

関数のリスト化

tools = [search_web, calculate]
関数`search_web``calculate`をリストにまとめています。後ほど、AgentExecutorに渡します。

プロンプトの定義

prompt = ChatPromptTemplate.from_messages([
    ("system", "あなたは役立つアシスタントです。"),
    MessagesPlaceholder(variable_name="chat_history"),
    ("user", "{input}"),
    MessagesPlaceholder(variable_name="agent_scratchpad")
])
プロンプトを定義しています。ここでは、システムからのメッセージ、ユーザーからのメッセージ、エージェントのスクラッチパッドを定義しています。

`(“system”, “あなたは役立つアシスタントです。”)`は、システムからのメッセージを定義しています。

`MessagesPlaceholder(variable_name=”chat_history”)`は、メッセージの履歴を保持するためのものです。これによって、アシスタントが過去のメッセージを参照することができます。

`(“user”, “{input}”)`は、ユーザーからのメッセージを定義しています。`{input}`は、ユーザーからの入力を受け取るための変数です。

`MessagesPlaceholder(variable_name=”agent_scratchpad”)`は、エージェントのスクラッチパッドを定義しています。これによって、エージェントが情報を保持することができます。

エージェントの作成

agent = create_tool_calling_agent(llm, tools, prompt)
`create_tool_calling_agent`関数を使って、エージェントを作成しています。ここでは、LLM、ツール、プロンプトを渡しています。

AgentExcecutorの定義

exucutor = AgentExecutor(
    agent=agent,
    tools=tools,
    verbose=True,
    memory=ConversationBufferWindowMemory(
            return_messages=True,
            memory_key="chat_history",
            k=10
        )
)
`AgentExecutor`を使って、エージェントを実行しています。ここでは、エージェント、ツール、メモリを渡しています。

それぞれの引数について説明します。

`agent`には、先ほど`create_tool_calling_agent`関数で作成したエージェントを渡しています。

`tools`には、関数`search_web``calculate`をリストにまとめて渡しています。

`verbose=True`は、エージェントの実行ログを表示するためのオプションです。

`memory`には、メモリを渡しています。ここでは、`ConversationBufferWindowMemory`を使って、メッセージの履歴を保持するためのメモリを定義しています。`return_messages=True`は、メッセージを返すためのオプションです。`memory_key=”chat_history”`は、メモリのキーを指定しています。`k=10`は、メッセージの履歴を保持するためのバッファサイズを指定しています。

`chat_history`は、メッセージの履歴を保持するためのキーであり、先程のプロンプトで定義した`MessagesPlaceholder(variable_name=”chat_history”)`と対応しています。

エージェントの実行

exucutor.invoke(
    {"input": "トム・クルーズの誕生日にケーキをプレゼントしたいです。彼の年齢分のろうそくを購入するための金額を計算してください。ろうそくは一本本100円です。"},
    callback_handler=StdOutCallbackHandler()
)
`invoke`メソッドを使って、エージェントを実行しています。ここでは、ユーザーからの入力を渡しています。また、`callback_handler=StdOutCallbackHandler()`を使って、コールバックハンドラを指定しています。ここでは、`StdOutCallbackHandler`を使って、標準出力にメッセージを表示しています。

`input`には、ユーザーからの入力を渡しています。ここでは、トム・クルーズの年齢分のろうそくの合計金額を計算するための質問を渡しています。

以上で、LangChainを使って、Function callingを使ったAIエージェントの実装が完了です。

では、実行結果を見てみましょう。
> Entering new AgentExecutor chain...
まず、トム・クルーズの現在の年齢を知る必要があります。それを知るためには、インターネットで彼の誕生日を調べる必要があります。


Action: duckduckgo-search
Action Input: 'Tom Cruise birth date'Tom Cruise. Thomas Cruise Mapother IV (born July 3, 1962) is an ...(以下略)...


Action: duckduckgo-search
Action Input: 'today's date'Details about today's date with count of days, weeks, and months, Sun and Moon cycles, Zodiac signs and holidays. ...(以下略)...


Action: duckduckgo-search
Action Input: 'current date'Details about today's date with count of days, weeks, and months, Sun and Moon cycles, Zodiac signs and holidays. Tuesday September 24, 2024 . ...(以下略)...


Action: Calculator
Action Input: {'operation': 'subtract', 'operands': [2024, 1962]}Answer: 62トム・クルーズは今年で62歳になります。次に、ろうそくの価格とトム・クルーズの年齢を掛けることで、必要なろうそくの総額を計算します。


Action: Calculator
Action Input: {'operation': 'multiply', 'operands': [62, 100]}Answer: 6200トム・クルーズの年齢分のろうそくを購入するためには、6200円が必要です。


Final Answer: 6200円が必要です。
ちゃんと動きましたね!!

まとめ

なかなかにわかりにくいAIエージェントの仕組みについて、世界一わかりみの深い説明を行いました。AIエージェントは未来を感じますね。AIエージェントの登場により、これまで夢に見た未来が現実のものとなりつつあります。生成AIの技術を活用して、人間の介入なしに自ら思考し、自ら行動する「AIエージェント」が今、大きな注目を集めています。

AIエージェントは、複雑なタスクを人間のように処理します。与えられた課題に対して自ら考え、次に何をすべきかを判断し、行動に移します。まさに、かつて夢に見たドラえもんやターミネーターのような未来のロボットたちが、今、目の前に現れようとしているのです。

さぁ、みなさんも一緒に盛り上がりましょう!!へ(゚ω゚*へ)ワショーィ(ノ*゚ω゚)ノワショーィ
ご覧いただきありがとうございます! この投稿はお役に立ちましたか?

役に立った 役に立たなかった

0人がこの投稿は役に立ったと言っています。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です