jiku log

データサイエンスの核心を掴む : 学びと発見の記録

「RAG・AIエージェント[実践]入門」を読む ~第2章 OpenAIのチャットAPIの基礎②Function calling~

はじめに

業務でLLMを活用していくためには,検索拡張生成(RAG)やAIエージェントといった技術が必要になってくると考えられる。これらの技術の基礎知識と実践経験を積むために,西見公宏・吉田真吾・大嶋勇樹 著「LangChainとLangGraphによるRAG・AIエージェント[実践]入門」を読むことにした。

本記事は,第2章 「OpenAIのチャットAPIの基礎」のFunction callingに関する読書メモである。

  • 本書の紹介ページ

gihyo.jp

2.6 Function calling

本節では,Function callingについて説明している。

Function callingは,利用可能な関数をLLMに伝えておいて,LLMに「関数を使いたい」という判断をさせる機能である。
LLMにJSONなどの形式で出力させて,その内容をもとにプログラム中の関数を実行するような処理に用いる。

なお,LangChainにおいて同様の機能はTool Callingと呼ばれ,AnthropicのAPIではTool useと呼ばれる。

Function callingの処理の流れ

Function callingの処理の流れは以下のようになる。

  1. Pythonプログラム→Chat Completions API】質問を渡す
  2. Pythonプログラム←Chat Completions API】質問を理解し,利用する関数名と引数を渡す
  3. Pythonプログラム→Chat Completions API】関数を実行し,関数の実行結果を渡す
  4. Pythonプログラム←Chat Completions API】関数の実行結果を踏まえて最終的な応答を返す

LLM自体は使いたい関数名を返すだけで,関数の実行はPythonプログラム側で行なうという流れになる。

Function callingのサンプルコード

サンプルコードを動かしながら,挙動を確認した。
github.com

Step 0. 関数の定義

Function callingを利用するにあたり,

  • Pythonプログラム中における関数の定義
  • LLMが使用できる関数の一覧の定義

を行なう必要がある。


本書中では,関数の定義を以下のようにしている。

  • サンプルコード(修正前)
import json

def get_current_weather(location, unit="fahrenheit"):
    if "tokyo" in location.lower():
        return json.dumps({"location": "Tokyo", "temperature": "10", "unit": unit})
    elif "san francisco" in location.lower():
        return json.dumps(
            {"location": "San Francisco", "temperature": "72", "unit": unit}
        )
    elif "paris" in location.lower():
        return json.dumps({"location": "Paris", "temperature": "22", "unit": unit})
    else:
        return json.dumps({"location": location, "temperature": "unknown"})


これは,locationを小文字に変換したのちに,locationに文字列"tokyo", "san francisco", または"paris"が含まれているかどうかを判定し,それぞれの変数に応じた"temperature"の値を返す。一方で,変数locationに上記のいずれも含まれない場合は,"temperature"を"unknown"として返す。

ただこの場合,locationstr型である保証がないので,if "tokyo" in location.lower():のように文字列を含むかどうかの判定が上手くいかないことがある。
したがって,location = str(location)のように,明示的に文字列型に変換してから処理を行なうようにする。

  • サンプルコード(修正後)
import json

def get_current_weather(location, unit="fahrenheit"):
    location = str(location)
    if "tokyo" in location.lower():
        return json.dumps({"location": "Tokyo", "temperature": "10", "unit": unit})
    elif "san francisco" in location.lower():
        return json.dumps(
            {"location": "San Francisco", "temperature": "72", "unit": unit}
        )
    elif "paris" in location.lower():
        return json.dumps({"location": "Paris", "temperature": "22", "unit": unit})
    else:
        return json.dumps({"location": location, "temperature": "unknown"})


toolsの中身を確認する。

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_current_weather",
            "description": "Get the current weather in a given location",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "The city and state, e.g. San Francisco, CA",
                    },
                    "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
                },
                "required": ["location"],
            },
        },
    }
]

toolsの内容を細かく確認すると,以下のような内容になっている。

  • tools自体はdictのlistになっている。
  • dictであるtools[0]には,'type'および'function'という2つのkeyがある。
  • functionには,namedesctriptionparametersなどの要素が存在する。
Step 1. Chat Completions APIの呼び出し

Chat Completions APIを呼び出し,質問文「東京の天気はどうですか」を渡すとともに,使える関数の一覧をtoolsという引数で渡す。
そうすると,応答におけるtool_calls要素に,関数名と引数が格納されて返ってくる。

  • サンプルコード
from openai import OpenAI

client = OpenAI()

messages = [
    {"role": "user", "content": "東京の天気はどうですか?"},
]

response = client.chat.completions.create(
    model="gpt-4o",
    messages=messages,
    tools=tools,
)
print(response.to_json(indent=2))
  • 出力
(中略)
        "tool_calls": [
          {
            "id": "call_flNaIflWbgCivRiEaB19ibpK",
            "function": {
              "arguments": "{\"location\":\"Tokyo, Japan\",\"unit\":\"celsius\"}",
              "name": "get_current_weather"
            },
            "type": "function"
          }
        ]
(以下略)

上記のように,Chat Completion APIの応答における"tool_calls"という要素の中に,LLMからの解答である"Tokyo, Japan"といった内容が含まれていることが分かる。また,使用したい関数名"get_currnet_weather"も含まれている。

Step 2. 関数の実行

Chat Completions APIの出力である,使用したい関数名とその引数を用いて,関数を実行する。
得られた結果は以下のようになる。

  • 出力
{"location": "Tokyo", "temperature": "10", "unit": "celsius"}

このように,get_current_weather関数の定義にしたがって,locationの文字列の"tokyo"が含まれると判定し,結果を出力している。
なお,先述のようにlocation = str(location)のように変数を文字列型にしておかないと,"temperature"の部分が"unknown"となってしまった。

Step 3. 最終的な解答を得る

上記のやり取りの結果を受けて,最終的な解答をLLMから出力されることで,Chat Completions APIの処理における一連の流れが完了する。

まとめと感想

Chat Completion APIを利用しながら,Function callingの挙動を理解した。

途中,get_current_weather関数において,変数の明示的な変換を行なわないと本書のような挙動にならないところがあったため,修正しながら動作確認をした。
今回は処理がかなりシンプルだったので気付けたが,処理の途中でLLMの出力を用いており,不確実性があるので,デバッグが大変なのでは,という懸念が生じた。
Chat Completion APIを用いたプログラム開発のベストプラクティスについては,別途調べてみたい。


本記事を最後まで読んでくださり,どうもありがとうございました。