🧰

LangChainとGPT-4oで作る商品選定AIエージェント(前編) ~Function Calling~

2025/02/19に公開

はじめに

大規模言語モデル (LLM) がデータや計算ツールを活用して答えるしくみを、Function calling という機能を使って作りました。
この記事では、まず背景をご紹介し、GPT-4o の API と langChain を使った実装方法tips を解説します。

記事が長くなったので、本記事で Function Calling について扱い、後編でエージェント化の部分を解説したいと思います。

目指す世界観

例えば、EC サイトで部品を探すときに、

長さ700mmで100kgの装置を載せられるアルミフレームはどれ?

と聞いたら

以下のアルミフレームは、長さ700mmで100kgの装置を安全に支えることができます(安全率10):

1. **HFS6-50100** - 最大荷重: 1070.80 kgf
2. **HFS6-30120** - 最大荷重: 1071.50 kgf
3. **EFS6-30120** - 最大荷重: 1101.69 kgf
4. **GFS8-4590** - 最大荷重: 1142.09 kgf
5. **HFSH8-8080** - 最大荷重: 1281.18 kgf
6. **HFS8-9090** - 最大荷重: 1471.30 kgf

というように、条件に合った商品を提案してくれたら便利そうではありませんか?
(製造業、工場関係者の外の方は、自分の欲しいものに置き換えてみてください)

このような世界観を目指して、AI Agent を作ってみました。

背景と課題、問題設定

LLM のドメイン知識の限界

GPT や Gemini といった LLM は、当社の商品について具体的な知識を持っておらず、答えることができません。

商品のスペックなどの情報を基に応答させるには、必要な情報を外部知識として LLM に与えることになります。
このような手法では RAG がよく知られています。RAG ではテキストなどの外部データの中から必要な箇所を検索して LLM のプロンプトに与えます。

また、2024年には LLM のロングコンテキスト対応が進んだので、外部データの文書やデータテーブル全体を LLM に入力して読解してもらうこともできるようになりました。
ただ、構造化されたデータベースが整備されている場合、SQL で必要な情報だけ取得して LLM に渡す方が高速・確実・安価です。
データベースを柔軟に使いこなすなら SQL 文を LLM に生成させるのもよさそうです。一方、定型化できる操作なら、SQL のフォーマット文を用意しておいて、入力文に応じて値を埋めて使うこともできます。

LLM での数値計算の限界

機械部品を選定するとき、耐荷重などの数値計算が必要なことがあります。

GPT-4o は、"たわみ計算"をプロンプトの工夫なしでも解答できました。[1] しかし、小数点以下の細かい計算で何度かに1度は間違えるようなケースもありました。
また、質問の文脈が複雑だと簡単な計算も間違えやすいという報告もあります。
現状では数値計算を全て LLM に任せるのは不安です。

今回の方針

上の2点を背景として、LLM と外部関数とで役割分担させることにしました:

  • 数値計算の実行は Python の外部関数が担当
  • 計算に必要なパラメータをテーブルから取得する外部関数を用意
  • LLM は適切な外部関数を選び、その結果を使って応答を生成する

このような分担は、Function calling というLLMの機能を使って実現できます。
次の章で実装方法を含めて紹介します。

Function Calling を使った LLM Agent

Function Calling とは

ここでは Function Calling について具体的に説明をします。

Function Calling は、プロンプトで関数リストを与えると、回答に必要な関数を LLM が選んでくれる機能です。
関数の引数や使い方の情報を以下のように構造化して定義し、プロンプトでLLMに渡します。

{
"name": "calc_load_no1",
"description": "Calculate the max load on a cantilever beam when load is applied to the tip.",
"parameters": {"type": "object",
    "properties": {
        "L": {"type": "number", "description": "Beam length (mm)"},
        "I": {"type": "number", "description": "Cross Sectional Moment of inertia (mm^4)"}},
    "required": ["L", "I"]}
}

この状態で指示を与えると、LLM が必要に応じて呼び出す関数引数を選択してくれます。

"calc_load_no1(L=1000, I=28300)"

2023年6月に OpenAI からリリースされて以降、各社追随してきました。

Function calling、Tool calling、Tool use といった呼び方がありますが、本記事の中では Function calling と呼びます(特定のサービス名として使う場合を除く)。

Function Calling の実装方法

Function Calling の機能は OpenAI や Anthropic などそれぞれの公式ツールで実装できますが、インターフェイスが違います。どのモデルでもLangChain を使えば同じインターフェイスで実装できて、モデルの比較や切り替えに対応しやすいメリットがあります。

Function calling を使うにはまず、数値計算、DB の参照といった関数を定義します。
次に、個々の関数について、関数名や説明、引数といった情報を以下のような決まったフォーマットで用意します。

LangChain の Tool Calling では、3 通りの実装方法があります。

  1. 辞書 / JSON 型 前節のように辞書型または JSON 文字列で関数宣言を書く方法です。Python 関数ではなく、API を呼び出す場合は前節のように関数宣言の辞書を作ることになります。
  2. tool Python では、@toolデコレータをつけて関数を定義することもできます。1. の場合は、辞書型の関数定義とは別に関数の実装もしないといけないですが、toolを使うと一度に済むので便利です。
  3. Pydantic Pydantic classで書く方法もありますが、ここでは紹介しません。

関数定義をプロンプトとして事細かに決めたい場合は 1 の辞書 / JSON 型がよいです。Python 関数の実装が必要な場合は 2 か 3 が便利です。

  1. 辞書 / JSON 型
tools = [
        {
        "name": "calc_load_no1",
        "description": "Calculate the max load on a cantilever beam when load is applied to the tip.",
        "parameters": {"type": "object",
            "properties": {
                "L": {"type": "number", "description": "Beam length (mm)"},
                "I": {"type": "number", "description": "Cross Sectional Moment of inertia (mm^4)"}},
                "safety_factor": {"type": "number", "description": "Safety factor for the load (default 10)."}
            "required": ["L", "I"]}
        },
        ]

前節で紹介した形式です。

上の例ではLIの二つの引数を必ず指定するために、"required"にリストアップしています。
一方で、引数safety_factorには外部関数にデフォルト値が設定されています(2. tool で紹介する関数実装を参照ください)。ユーザーから言及がなければ LLM はこの値を指定せず、関数のデフォルト値が使われます。
システムプロンプトに、"安全率は通常10に設定します"のように書くこともできます。

Huggingface が提供する関数を使うと、既にある関数から関数宣言を自動で作ることができます。

JSON 形式の関数宣言を自動生成する方法

transformersget_json_schemaを使うと、関数の名前や引数、docstring から JSON 形式の関数宣言を自動で作れます。

以下のように関数[2]を定義します。

  • 引数には型のヒントを必ず付けます。
  • Docstring には全ての引数の説明を、以下の例のような書式で含めます。Args:以降に、引数名と説明を:を挟んで書くのがポイントです。
  • 出力型 (e.g. -> float) はなくてもいいです。付けると出力のスキーマに'return': {'type': 'number'}が追加されます。
def calc_load_no1(L: float, I: float, safety_factor: float = 10.) -> float:
    """
    Calculate the max load on a cantilever beam when load is applied to the tip.

    Args:
        L: Beam length (mm) 
        I: Cross Sectional Moment of inertia (mm^4)
        safety_factor: Safety factor for the load. (default 10).
    """
    capable_deflection = L * capable_defl_rate / safety_factor
    return capable_deflection * 3 * E * I / L**3

定義した関数をget_json_schemaに渡すと、

from transformers.utils import get_json_schema
schema = get_json_schema(calc_load_no1)

以下のような関数宣言が出力されます。

{'type': 'function',
 'function': {'name': 'calc_load_no1',
  'description': 'Calculate the max load on a cantilever beam when load is applied to the tip.',
  'parameters': {'type': 'object',
   'properties': {'L': {'type': 'number', 'description': 'Beam length (mm) '},
    'I': {'type': 'number', 'description': 'Cross Sectional Moment of inertia (mm^4)'},
    'safety_factor': {'type': 'number', 'description': 'Safety factor for the load (default 10).'}},
   'required': ['L', 'I']},
  'return': {'type': 'number'}}}
  1. tool
    @toolデコレータをつけて関数を定義するだけで、自動で定型フォーマットの関数宣言テキストが作られます。関数や変数の説明は関数の docstring に書きます。
    以下のたわみ計算式やヤング率、許容荷重の上限はミスミが公開しているものを使いました。
from langchain_core.tools import tool

# アルミフレームに関する数値設定
# (https://jp.misumi-ec.com/special/alumiframe/tech/capacity/)
## アルミのヤング率
E = 69972 # (N/mm^2)
## ミスミでは、フレーム長の1/1000のたわみを実用上の最大許容荷重としている
capable_defl_rate = 0.001

# 関数定義
@tool
def calc_load_no1(L: float, I: float, safety_factor: float = 10.) -> float:
    """
    Calculate the max load on a cantilever beam when load is applied to the tip.

    Args:
        L: Beam length (mm) 
        I: Cross Sectional Moment of inertia (mm^4)
        safety_factor: Safety factor for the load. (default 10).
    """
    capable_deflection = L * capable_defl_rate / safety_factor
    return capable_deflection * 3 * E * I / L**3

...

tools = [calc_load_no1, ...]

後で使うため、定義した関数をtoolsリストに羅列しています。

LangChain の Tool Calling
関数の用意ができたら、LLM に渡して使えるようにします。
ここでは LangChain の Tool Calling 機能を使う実装方法を紹介します。

まず LLM の API を呼び出します。ここでは GPT-4o を指定して、Azure OpenAI を使います。OpenAI の他のモデルを使うにはmodel_name"gpt-4o"を書き換えます。Azure 以外のモデルを使うには API を変えます(e.g. Claude ならlangchain_anthropic)。API キー等は適宜設定します。

import os
from langchain_openai import AzureChatOpenAI

AZURE_OPENAI_ENDPOINT = os.environ.get("AZURE_OPENAI_ENDPOINT")
AZURE_OPENAI_API_KEY = os.environ.get("AZURE_OPENAI_API_KEY")

llm = AzureChatOpenAI(
        azure_endpoint = AZURE_OPENAI_ENDPOINT, 
        api_key=AZURE_OPENAI_API_KEY,  
        openai_api_version="2023-05-15",
        model_name="gpt-4o"
        )

関数のリストをbind_toolsに渡すとシステムプロンプトに取り込まれます。

llm_with_tools = llm.bind_tools(tools)

これで準備完了です。

クエリを渡して Function Calling を実行します。

from langchain_core.messages import HumanMessage

query = "長さ 1000 mm、断面2次モーメント 28300 mm^4 の梁がたわみに耐えられる最大の荷重は?"

messages = [HumanMessage(query)]    
ai_msg = llm_with_tools.invoke(messages)

Function calling の応答は、引数を埋めた関数のリストです。LangChain を使ったときは、返り値(上のai_msg)の中のtool_callsに、呼び出す関数と引数の値が入っています。

[{'name': 'calc_load_no1', 'args': {'L': 1000, 'I': 28300}, 'id': '...', 'type': 'tool_call'}]

Function calling は呼び出すべき関数を選ぶところまでやってくれます。

実用的には Function calling の後、関数を実行し、その結果を基に応答を生成します。
例えば、以下のようにできます。

Function calling 後に関数を実行する例

Function calling で呼び出す関数名と、それに対応する実行可能な関数とを紐づけた辞書を定義しておきます。
GPT の出力の中から呼び出された関数の情報を取得して、関数を実行していけばよいです。

# 関数辞書を定義
tools_dict = {"calc_load_no1": calc_load_no1,
              "calc_load_no2": calc_load_no2,
              ...
              }

from langchain_core.messages import ToolMessage

for tool_call in ai_msg.tool_calls:
    # Function Calling で呼び出された関数名と引数を取得
    selected_tool_name = tool_call["name"].lower()
    tool_args = tool_call["args"]

    # 対応する関数を取得
    selected_tool = tools_dict[selected_tool_name]
    
    # 関数を実行
    tool_output = selected_tool(**tool_args)
    # 実行結果を格納
    messages.append(ToolMessage(tool_answer, tool_call_id=tool_call["id"]))

@tool を付けて定義した関数など Pydantic オブジェクトの場合は、関数の実行部分を selected_tool.invoke(tool_args) に変更が必要です。

LangChain には Tool calling agent が用意されており、Function Calling を使って応答するエージェントを簡単に作ることができます。
この応答生成までを自動でやってくれるエージェントを作る方法については、後編で解説します。

Function calling を使うときの Tips

各社から Function calling のベストプラクティスが紹介されています。
共通しているのは、関数の説明をしっかりと書く、ということです。

書かれていることは各社微妙に違うのでモデルごとに効果の差があるかもしれませんが、どのモデルにも共通して使えそうな知恵も多いと思います。

具体的に工夫すべき点を、以下に抜粋してまとめました。

関数宣言の Tips

どのモデルでも、関数宣言に最も力を入れるべきとしています。
各関数の宣言では、LLM にわかりやすいようにかつ正確に関数を定義するとよいです。
意図通り関数が呼ばれないなら、以下のように関数名や説明文を見直します。

  • 関数名
    • 直感的にわかるように命名し、略称やイニシャルは使わない
  • 引数
    • 直感的にわかるように命名し、略称やイニシャルは使わない
    • 型指定する。パラメータ値が有限集合なら enum を使う
    • 引数の説明文にはパラメータの意味や目的、制約と、関数の動作にどう影響するか書く。場合によっては具体例も書く
  • 関数の説明文
    • 関数の目的や、いつ使うべきか/使わないか、どうふるまうかを詳細に書く

説明文に具体例を書くかどうかについては、温度差を感じました。
OpenAI や Google は必要に応じて関数や引数の説明文に例を入れるとしています。
一方で Anthropic は例を書くのは2の次で説明を優先すべきと言っています(例を書くこと自体は否定していません)。
GPT-4o で試した限りでは、引数の説明に例を書くだけでうまく機能することもありました。例えば、型番の読み取りは言葉で説明する方がむしろ難しいです。

また、OpenAI は関数について、できるだけコードで処理をさせて、LLM が考えることを減らす方がいいとしています。

  • 既知の値はコード内で関数に渡して、LLM に埋めさせない
  • いつも続けて使われる複数の関数は1個にまとめる

システムプロンプトの Tips

システムプロンプトにも関数の使い方や呼び出すタイミングを書くとよいとされています。
OpenAI や Google の API には関数定義を受け取るパラメータ (e.g. tools) があり、Function Calling を機能させるシステムプロンプトが自動で用意されるので、直接システムプロンプトを書くことは少ないかもしれません。
Google は、ユーザープロンプトの先頭に関数の使い方やタイミングを書くとよいとしています。

OpenAI や Anthropic は tool_choice パラメータで、Google は mode で、Function Calling を必ず実行させる制御もできます。

関数の数に関する Tips

OpenAI は一度に扱う関数の数を抑えた方がうまくいきやすいとしています。

  • 関数の数を抑えた方がいい。
    • 1 回の API call で関数は 20 個までにした方がいい。典型的に 10-20 個で正しい関数を選ぶ性能が落ちることがある。
    • 多くの関数が必要な場合は、Finetuning か、関数をグループに分けてマルチエージェント化するといい。
  • ユーザーの様々な入力に対して、正しく関数が呼び出されるか、正しい引数が生成されるかを測定する評価システムの設定を推奨。
  • Finetuning で Function calling の性能向上できる。特に関数が多い場合や、関数が複雑であったり、違いが微妙な似た関数を使う場合に有効。

おわりに

Function calling を使うことで、人が用意した関数の中から、LLM が必要な物を適切に選択できるようになりました。この機能を使えば、商品データベースや計算ツールを使って商品選定をできそうです。

ただし、Function calling の機能は、関数の選択と引数の生成までです。選ばれた関数を実行し、その結果を基に応答を生成する流れを用意する必要があります。例えばある関数で求めた値を次の関数の引数として使うケースや、複数の関数の結果をとりまとめて回答するケースなど、処理が複雑になると流れを一々実装するのは大変です。

このようなタスクでは LLM エージェントとして実装するのが良いです。
LLM をエージェント化すると、与えられた指示に対して、どの関数をどの順番に実行するか、関数の出力をどう使うかなどを自律的に計画して一連の処理を行い、最終的な応答を生成してくれます。例えば LangChain で用意されている Function calling のエージェントを作る機能 を活用するのが便利です。後編ではこの機能を使ってエージェントとして実装した部分を紹介する予定です。

Version 情報

  • GPT-4o (version:2024-08-06)
  • LangChain 0.3.7
  • transformers 4.47.1
脚注
  1. GPT-4o に例えば「断面2次モーメント 104000 mm^4、長さ 500 mm の梁の中心に 800 N の荷重を掛けたときのたわみを求めてください。」と聞くと「0.29 mm」と計算してくれるはずです。参考までに、たわみ計算式はこちらの解説で確認いただけます。 ↩︎

  2. 関数の中身はミスミが公開しているたわみ算出計算式を利用しています。 ↩︎

ミスミ DataTech ブログ

Discussion