🤖

Function calling に Pydantic を使ってみよう

2023/10/13に公開

OpenAI の文章生成 API(Chat Completions API)には、2023 年 6 月に「Function calling」という機能が追加されました。

この Function calling ですが、そのまま使おうとすると少し記述の手間が大きいと感じないでしょうか?

この記事では、Function calling をちょっと楽に使うため、Pydantic を使う方法を紹介します。

Function calling とは

まず、Function calling について簡単に説明します。
Function calling は例えば次のように実装します。

OUTPUT_RECIPE_FUNCTION = {
    "name": "output_recipe",
    "description": "レシピを出力する",
    "parameters": {
        "type": "object",
        "properties": {
            "ingredients": {
                "type": "array",
                "description": "材料",
                "items": {"type": "string"},
                "examples": [["鶏もも肉 300g", "玉ねぎ 1個"]],
            },
            "steps": {
                "type": "array",
                "description": "手順",
                "items": {"type": "string"},
                "examples": [["材料を切ります。", "材料を炒めます。"]],
            },
        },
        "required": ["ingredients", "steps"],
    },
}

response = openai.ChatCompletion.create(
    model="gpt-3.5-turbo",
    messages=[{"role": "user", "content": "カレーのレシピを教えてください。"}],
    functions=[OUTPUT_RECIPE_FUNCTION],
    function_call={"name": OUTPUT_RECIPE_FUNCTION["name"]},
)

response_message = response["choices"][0]["message"]
function_call_name = response_message["function_call"]["name"]
function_call_args = response_message["function_call"]["arguments"]

print(function_call_name)
print(function_call_args)

これは「レシピを出力する」という意味の「output_recipe」という関数があることを教えつつ、「カレーのレシピを教えてください。」というプロンプトで LLM を呼び出しているということです。

このコードの実行結果は次のようになります。

output_recipe
{
  "ingredients": [
    "玉ねぎ",
    "にんじん",
    "じゃがいも",
    "豚肉",
    "カレールー",
    "水",
    "油",
    "塩"
  ],
  "steps": [
    "玉ねぎ、にんじん、じゃがいもを切ります。",
    "豚肉を炒めます。",
    "玉ねぎ、にんじん、じゃがいもを炒めます。",
    "水を加えて煮込みます。",
    "カレールーを加えて溶かします。",
    "塩で味を調えます。"
  ]
}

これはざっくり言えば、「output_recipe という関数をこのパラメータで呼び出したい」という内容です。

Function calling はこのように、LLM に利用可能な関数を教えておいて、どの関数をどんなパラメータで使いたいか生成させる機能になります。

関数のパラメータは「JSON Schema」という形式で指定することになっています。

参考: https://platform.openai.com/docs/api-reference/chat/create#chat/create-functions

JSON Schema の記述の手間

さて、このように実装すれば Function calling が使えるわけですが...
この JSON Schema を書くのは、少し手間ではないでしょうか?

上記の例ぐらいなら構わないかもしれませんが、実際にはさらに複雑なスキーマを書いたりすることになります。
例えば以下のような JSON を生成したい場合を考えてみます。

{
  "ingredients": [
    {
      "ingredient": "玉ねぎ",
      "quantity": "1個"
    },
    :
  ],
  "instructions": [
    "玉ねぎ、にんにく、しょうがをみじん切りにする。",
    :
  ]
}

この場合、JSON Schema は例えば次のようになります。

RECIPE_JSON_SCHEMA = {
    "type": "object",
    "properties": {
        "ingredients": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "ingredient": {
                        "description": "材料",
                        "type": "string",
                        "examples": ["鶏もも肉"],
                    },
                    "quantity": {
                        "description": "分量",
                        "type": "string",
                        "examples": ["300g"],
                    },
                },
                "required": ["ingredient", "quantity"],
            },
        },
        "steps": {
            "type": "array",
            "description": "手順",
            "items": {"type": "string"},
            "examples": [["材料を切ります。", "材料を炒めます。"]],
        },
    },
    "required": ["ingredients", "steps"],
}

この形式は、当然 JSON Schema の仕様と違えばエラーになったりします。

しかも、LLM が返してきたパラメータを Python の自作クラスに変換したくなることも多いです。
すると、次のように JSON Schema の記述と Python のクラス定義が二重管理になってしまったりするわけです。

RECIPE_JSON_SCHEMA = {
    ...
}

class Recipe:
    ...

Pydantic を使う

このような問題を解決するため、この記事では Pydantic を使う方法を紹介します。

LangChain でも似たことができますが、これだけの目的で LangChain まで使うのは避けたい、というケースもあると思います。
そんなとき、Pydantic を使うだけでもだいぶ楽になります。

Pydantic とは

Pydantic は、Python でデータの入れ物のクラスを簡単に実装できるパッケージです。
Python 公式の dataclass と異なり、バリデーションなどの機能も提供しています。

https://docs.pydantic.dev/latest/

Pydantic は、FastAPI からも使われているとても有名なパッケージです。
ちなみに、openai パッケージも v1.0.0 へのアップデートで Pydantic に依存する予定です。

参考: https://github.com/openai/openai-python/discussions/631

Pydantic のモデルの作成

ここから、Pydantic を使って Function calling を実装する例を書いていきます。

まず、Function calling のパラメータを Pydantic のモデル(Pydantic の BaseModel を継承したクラス)として定義します。

from pydantic import BaseModel, Field

class Ingredient(BaseModel):
    ingredient: str = Field(description="材料", examples=["鶏もも肉"])
    quantity: str = Field(description="分量", examples=["300g"])

class Recipe(BaseModel):
    ingredients: list[Ingredient]
    steps: list[str] = Field(description="手順", examples=[["材料を切ります。", "材料を炒めます。"]])

Pydantic のモデルから JSON Schema を生成

Pydantic のモデルから、JSON Schema を生成することができます。

schema = Recipe.model_json_schema()

この schema という変数の内容を JSON 形式で表示すると、次のようになります。

{
  "$defs": {
    "Ingredient": {
      "properties": {
        "ingredient": {
          "description": "材料",
          "examples": ["鶏もも肉"],
          "title": "Ingredient",
          "type": "string"
        },
        "quantity": {
          "description": "分量",
          "examples": ["300g"],
          "title": "Quantity",
          "type": "string"
        }
      },
      "required": ["ingredient", "quantity"],
      "title": "Ingredient",
      "type": "object"
    }
  },
  "properties": {
    "ingredients": {
      "items": {
        "$ref": "#/$defs/Ingredient"
      },
      "title": "Ingredients",
      "type": "array"
    },
    "steps": {
      "description": "手順",
      "examples": [["材料を切ります。", "材料を炒めます。"]],
      "items": {
        "type": "string"
      },
      "title": "Steps",
      "type": "array"
    }
  },
  "required": ["ingredients", "steps"],
  "title": "Recipe",
  "type": "object"
}

これだけ複雑な JSON Schema を、Python のクラスを書くだけで生成できてしまうわけです。

Function calling の呼び出し

これを使えば Function calling の呼び出しは簡単です。

OUTPUT_RECIPE_FUNCTION = {
    "name": "output_recipe",
    "description": "レシピを出力する",
    "parameters": Recipe.model_json_schema(),
}

response = openai.ChatCompletion.create(
    model="gpt-3.5-turbo",
    messages=[{"role": "user", "content": "カレーのレシピを教えてください。"}],
    functions=[OUTPUT_RECIPE_FUNCTION],
    function_call={"name": OUTPUT_RECIPE_FUNCTION["name"]},
)

response_message = response["choices"][0]["message"]
function_call_args = response_message["function_call"]["arguments"]

LLM の応答を Pydantic のモデルに変換

さらに、Function calling の実行結果を Pydantic のモデルに変換するのも簡単です。

recipe = Recipe.model_validate_json(function_call_args)
print(type(recipe))
print(recipe)

このコードの実行結果は次のようになります。

<class '__main__.Recipe'>
ingredients=[Ingredient(ingredient='玉ねぎ', quantity='1個'), Ingredient(ingredient='にんじん', quantity='1本'), Ingredient(ingredient='じゃがいも', quantity='2個'), Ingredient(ingredient='牛肉', quantity='300g'), Ingredient(ingredient='カレールー', quantity='100g'), Ingredient(ingredient='水', quantity='600ml'), Ingredient(ingredient='油', quantity='大さじ2'), Ingredient(ingredient='塩', quantity='小さじ1'), Ingredient(ingredient='こしょう', quantity='少々')] steps=['玉ねぎ、にんじん、じゃがいもをそれぞれ一口大に切る。', '牛肉を一口大に切る。', '鍋に油を熱し、牛肉を炒める。', '牛肉に火が通ったら、玉ねぎ、にんじん、じゃがいもを加えて炒める。', '野菜に火が通ったら、水を加えて煮込む。', '煮込んだらカレールーを加え、溶かして混ぜる。', 'カレーがとろっとしたら塩とこしょうで味を調える。', 'ご飯と一緒に食べる。']

このように Pydantic を使うことで、Function calling を楽に使うことができます。

まとめ

LLM を使ったアプリケーション開発が流行しているため、急に慣れない Python を使うことになったという方も多いと思います。

Pydantic は Python でおさえておきたいパッケージの 1 つです。
とても便利なので、是非さわってみてください。

ソースコード

この記事に登場するソースコードの実際に動作する全体は GitHub で公開しています。

https://github.com/os1ma/function-calling-pydantic

Discussion