【０から目指すAIエンジニア】Day3：Function callingの基礎

# はじめに
こんにちは、慶應義塾大学理工学部機械工学科3年の金賢佑です！
私は東大発AIベンチャーの株式会社2WINSの元でインターンをしています！

これからの記事では自分がインターンを通して最強のAIエンジニアを目指していきます！
第三回はFanction callingの基礎を記録しました！

# Function callingとは
Function callingとはLLMに「この機能が使える！」と判断させる機能のこと！
なのでLLM側がその機能を実行するわけではありません。


例えばあなたが「東京の天気を摂氏で教えて」といった時、
1.LLMがツールから使える関数、例えば`get_current_weather`を選んでくれる。
2.`{"location": "東京", "unit": "celsius"}`という引数を組み立ててあなたのプログラムにこの関数を実行して！と伝えてくれる。
3.あなたのプログラム側でその関数を実行する。
4.結果をLLMに返し、それを元に自然な文章で返事をしてくれる。

#### LLMって？
LLMとはLarge Language Model（大規模言語モデル）の略で言語モデルの一種！
前回扱ったChat GPTもLLmの一つですね。
言語モデルについては「3Blue1BrownJapan」さんが出している動画がとてもわかりやすいです！
2025/4/1現在、Chapter7くらいまであります。
https://youtu.be/tc8RTtwvd5U?si=ynBpv-bFHHjfwOCz



# Function callingを使ってみよう
## 関数の定義
以下のコードを実行してください。
```python
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'})
```
これはもらった場所の名前が「Tokyo」「San Francisco」「Paris」だったらそれぞれの温度を返す、それ以外であれば`unknown`と返す関数！
ちなみに返す内容はJSON形式の文字列。
今回も細かく解説していきます。


```python
import json
```
`json`ライブラリを読み込みます。
この後`json.dumps()`を使うためにインポートしておきます。

```python
def get_current_weather(location, unit='fahrenheit'):
```
`get_current_weather`という関数を定義しています。
`location`と`unit`という二つの引数が必要とされていますが、`unit`にはデフォルトで`'fahrenheit'`という引数が与えられていますね。

２つ目の引数について実例を記載しておきます。

```python
get_current_weather("Tokyo")  # → unit は省略されて 'fahrenheit' になる

get_current_weather("Tokyo", "celsius")  # → unit = 'celsius' に上書き
```


```python
  if 'tokyo' in location.lower():
    return json.dumps({'location':'Tokyo', 'temperature':'10', 'unit': unit})
```
`lower()`は小文字に直す関数です。つまり`location`に渡された値(ユーザーが入力した値)が`TOKYO`でも`Tokyo`でも`ToKyo`でも全て`tokyo`に直してくれるんですね。
そうすることで一文目のif分で`location`に`tokyo`が入力されているのかどうか正確に判断することができますね。

その後、`Tokyo`のデータをJSON形式で返しています。
#### どうしてJSON形式に直すの？
最初に説明したようにFunction callingではプログラムはPython側で実行し、その結果はLLM（今回だとChatGPT）に返しますよね。（その後LLMが自然な文章に直して返事をしてくれる！）
だからLLMが読みやすい形式に直すのがいいんです！

```python
  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'})
```
その後も同じロジックで処理が書かれていますね。
これで`get_current_weather`関数の定義が完成しましたね。
## toolの作成
先程関数の定義を行いましたが、これだけではFunction callingは機能しません。
LLMが🤖「この関数が使える！」と処理できるようにLLM用の関数の説明書、メニュー表のようなものが必要です。
それを`tool`と言ったりします。

以下のコマンドを打ってみてください。

``` python
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 Fracisco, CA",
          },
          unit: { type: "string", enum: ["celsius", "fahrenheit"] },
        },
        required: ["location"],
      },
    },
  },
];
```


今回も細かく解説していきます。

```python
tools = [
    {
        'type': 'function',
```
「このツールは関数だよ！」と書いてあります。


```python
        'function': {
            'name': 'get_current_weather',
            'description': 'Get the current weather in a given location',
```
関数の名前と説明文が記載されていますね。


``` python
            'parameters': {
                'type':'object',
```
関数が受け取る引数の情報です。
`'type': 'object'`は引数がオブジェクトですということを意味しています。
オブジェクトとは複数の値をセットで受け取るためのまとまりだと思ってください。（dict型みたいなもの！）
今回だと`location`というキーに`Tokyo`という値が入っていたりしますよね。


``` python
                'properties': {
                    'location': {
                      'type': 'string',
                      'description': 'The city and state, e.g. San Fracisco, CA',
                },
                'unit':{'type': 'string', 'enum': ['celsius', 'fahrenheit']},
            },
```
`properties`は使えるパラメータの一覧です。
`location`は`"Tokyo"`などが入ります。
`unit`は`"celsius"`か`"fahrenheit"`のどちらかに限定します。
`enum`で列挙型にすることでこれを可能にしています。

```python
            'required': ['location'],

            },
        },
    }
]
```
`required`は必須パラメータです。
ここに`unit`が入っていないのは、`unit`にはデフォルトで値が設定されているからでしたね。
## Chat Completion APIを呼び出してみよう

以下のコマンドを打ってみてください。
```python
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))
```
今回の会話の履歴はユーザーからの「東京の天気を知りたい！」というだけですね。
`model`は`'gpt-4o'`を使っています。
`messages`は先程の会話の履歴の`messages`を（名前が一緒だからわかりにくい）
`tools`にも先程作成した`tools`を代入しています。（これも名前が一緒だからわかりにくい）

返ってっきた`response`をJSON形式に直してprintしていますね。
ちなみに`indent=2`は見やすく整形しているだけです。
#### 実行結果
![スクリーンショット 2025-04-06 20.16.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4047475/b8790672-e0d2-47a5-af45-7999b64d4760.png)

長々と書かれていますが、注目して欲しいのは赤枠の部分。
LLMが先程の会話履歴から、「`get_current_weather`関数を`東京`という引数で実行するべきだ」と判断したことが書かれています。
（if 'tokyo' in location.lower():`は`  if 'tokyo' in location.lower() or '東京' in location:`に直しておいた方が良さそうですね。）
この判断をもとにPythonで関数を実行してあげれば良さそうですね。
ではこのメッセージを会話履歴として残しておきましょう。
以下のコマンドを実行してください。
```python
response_message = response.choices[0].message
messages.append(response_message.to_dict())
```
`response`とはさっき長々とprintされた文のことです。
LLMは出力を何種類も出すので`response`の最初の選択肢にするよう`choices[0]`としています。
ちなみに今回`[0]`としたのは先程の出力で`"index":0`となっていたのでそれに対応するようにですね。
まとめるとさっきの出力の`message`の部分を丸々`response_message`に記録（代入）しています。

2行目では、そうして記録された会話を`messages`に`append()`、つまり追加しています。
こうして会話がどんどん残るようにしているのですね。
## LLMが実行したい関数をこちらで実行してあげよう
以下のコマンドを打ってください。
``` python
available_functions = {
    'get_current_weather': get_current_weather,
}

for tool_call in response_message.tool_calls:

  function_name = tool_call.function.name
  function_to_call = available_functions[function_name]
  function_args = json.loads(tool_call.function.arguments)
  function_response = function_to_call(
      location=function_args.get('location'),
      unit=function_args.get('unit'),

  )
  messages.append(
          {
              'tool_call_id': tool_call.id,
              'role': 'tool',
              'name': function_name,
              'content': function_response,

          }
      )
      
print(function_response)
```
ここあたりから情報量が多くなってくるので、躓いたら前に戻りながら確認してください。

``` python
available_functions = {
    'get_current_weather': get_current_weather,
}
```
先程GPTからの返事の中に`"tool_calls"`がありました。
その中に使いたい関数が`"name": "get_current_weather"`として書かれています。
しかし`"get_current_weather"`は文字列のため、対応する関数と紐付ける必要があります。
上のコードはそれを行なっています。


``` python
for tool_call in response_message.tool_calls:
```
`response_massage`とは会話が記録される箱でしたね。さっき作りました。
その中で必要とされている関数、`tool_calls`を一つずつ呼び出しているんですね。

``` python
  function_name = tool_call.function.name
```
呼び出したい関数

``` python
  function_to_call = available_functions[function_name]
```
呼び出したい関数の名前を、最初に作った`available_functions`の辞書から取得する。
`get_current_weather()`が入ります。


``` python
  function_args = json.loads(tool_call.function.arguments)
```
`function_args`にはGPTが提案してきた関数の引数を渡します。
今回だと`"arguments": "{\"location\":\"東京\"}"`でしたね。



``` python
  function_response = function_to_call(
      location=function_args.get('location'),
      unit=function_args.get('unit'),

  )
```
実際に関数を呼び出します。
今回、`function_to_call`には`get_current_weather()`が入っていましたね。


``` python
  messages.append(
          {
              'tool_call_id': tool_call.id,
              'role': 'tool',
              'name': function_name,
              'content': function_response,

          }
      )
```
`messages`にこの`tool_call`にはこういう返信をしたよ〜って記録する！

``` python
print(function_response)
```
結果は
``` python
{"location": "Tokyo", "temperature": "10", "unit": "celsius"}
```
となって、ちゃんと返ってきていますね。

## もう一度APIリクエストを送ってみよう
先程の7.で追加したメッセージを使ってAPIにメッセージを送ってみます。

``` python
second_response = client.chat.completions.create(
    model='gpt-4o',
    messages=messages,

)
print(second_response.to_json(indent=2))
```
出力は
``` python
{
  "id": "chatcmpl-BJXBntAtL9pgUlUEeKUkRWGBZDi3M",
  "choices": [
    {
      "finish_reason": "stop",
      "index": 0,
      "logprobs": null,
      "message": {
        "content": "東京の現在の気温は約10℃です。天気の詳細について知りたい場合は、具体的な情報をご確認ください。",
        "refusal": null,
        "role": "assistant",
        "annotations": []
      }
    }
  ],
  "created": 1743995511,
  "model": "gpt-4o-2024-08-06",
  "object": "chat.completion",
  "service_tier": "default",
  "system_fingerprint": "fp_898ac29719",
  "usage": {
    "completion_tokens": 32,
    "prompt_tokens": 64,
    "total_tokens": 96,
    "prompt_tokens_details": {
      "cached_tokens": 0,
      "audio_tokens": 0
    },
    "completion_tokens_details": {
      "reasoning_tokens": 0,
      "audio_tokens": 0,
      "accepted_prediction_tokens": 0,
      "rejected_prediction_tokens": 0
    }
  }
}
```
先程の実行結果も踏まえた回答が送られてきていますね。

# 実行コードまとめ
``` python
#JSONのインポート
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'})

#ツールの作成
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 Fracisco, CA",
          },
          unit: { type: "string", enum: ["celsius", "fahrenheit"] },
        },
        required: ["location"],
      },
    },
  },
];

# OpenAIにAPIを送る
from openai import OpenAI

client = OpenAI()

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

response = client.chat.completions.create(
    model='gpt-4o',
    messages=messages,
    tools=tools,

#こちら側で関数を実行してあげよう
available_functions = {
    'get_current_weather': get_current_weather,
}

for tool_call in response_message.tool_calls:

  function_name = tool_call.function.name
  function_to_call = available_functions[function_name]
  function_args = json.loads(tool_call.function.arguments)
  function_response = function_to_call(
      location=function_args.get('location'),
      unit=function_args.get('unit'),

  )
  messages.append(
          {
              'tool_call_id': tool_call.id,
              'role': 'tool',
              'name': function_name,
              'content': function_response,

          }
      )
      
print(function_response)

#もう一度リクエストを送ってみよう
second_response = client.chat.completions.create(
    model='gpt-4o',
    messages=messages,

)
print(second_response.to_json(indent=2))
```

# まとめ
今回はFunction callingの基礎を解説していきました。
次回はcurlでopenaiのAPIを動かしてみます！

お疲れ様でした！