Zenn
🪔

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

2025/03/28に公開

はじめに

大規模言語モデル (LLM) がデータや計算ツールを自律的に使って質問に答えるしくみを、Function calling 機能を使った AI エージェントとして作りました。Function calling については前編の記事で紹介しました。
この記事は、簡単なデモ用に昨年行った実験の紹介で、GPT-4o の API と LangChain の Tool calling agent を使った実装方法試行錯誤を解説します。[1]

既に、インターネット検索と LLM を組み合わせる便利なツールやサービスは様々あります (e.g. browser-use, phind)。一方で、社内のデータベースや API といったプライベートのリソースを LLM に活用させたいときは、自分たちで作る必要があります (e.g. 商品の在庫数や納期、価格などをリアルタイムで反映させたい)。

目指す世界観

前の記事のおさらいです。
例えば、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 での数値計算の限界 機械部品を選定する際には耐荷重など数値計算が必要ですが、LLM の計算能力は完全には信頼できません。

そこで、データの取得や数値計算の実行を Python の関数に任せ、LLM は適切な関数を使って応答を生成するという役割分担にしました。
今回は、GPT の Function calling 機能と LangChain のエージェント機能を利用して実現します。

LLM で外部ツールやデータベースを使うしくみには、このサーベイで挙げられているように Modular RAG もあります。
複数ツールを適切に順序立てて使うには、"プランニング"もするエージェント、またはエージェンティック RAG の構成にする必要があります。
本記事では LangChain の"エージェント"機能を使っていること、今後より充実したエージェントシステムを作る準備活動ということで、"エージェント"と呼称しています。

Function Calling を使った LLM Agent

エージェントは、ユーザーの指示に対して自律的に必要な手順を計画・実行する AI システムです。
LLM エージェントについては例えばこの記事などをご参照ください。

Function Calling は、プロンプトで関数リストを与えると、LLM がユーザーの質問に答えるのに必要な関数を選んでくれる機能です。
この機能を活用して、LLM にツールを操作させます。

Tool Calling Agent を使った実装

LangChain には Tool calling agent が用意されており、Function Calling を使った応答をするエージェントを簡単に作ることができます。
ここから実装方法を紹介します。

まずは LLM が呼び出す関数を定義します。
LangChain の Tool Calling では、辞書 / JSON 型toolPydantic class の 3 通りの関数宣言の方法があります。詳しくは前の記事をご覧ください。

ここでは、toolを使った記法で関数を用意しました。
以下に、たわみを計算する関数calc_load_no1と、条件に合う型番候補を取得する関数get_aef_candidates、断面 2 次モーメントの値を csv ファイルから取得する関数select_moment_of_inertiaを例示します。
たわみ計算式やヤング率、許容荷重の上限はミスミが公開しているものを使いました。
get_aef_candidatesはいくつか条件を入れるとマッチする商品の候補をデータから取ってくる関数ですが、煩雑な割に一般性がないので中身は割愛しました。

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) -> 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)
    """
    capable_deflection = L * capable_defl_rate
    return capable_deflection * 3 * E * I / L**3
...

@tool
def get_aef_candidates(aeftype=None, series=None, extrusion=None, H=None, W=None) -> pd.DataFrame:
    """
    Get the list of candidate aluminum extrusions from data base.

    Args:
        aeftype: e.g. HFS, EFS, GFS, NFS, HFSG, HFSL, HFSH, KHFS, CAF, HFSP, NFSL, NEFS
        series: 5,6,8. e.g. 6 of HFS6-3030-1000
        extrusion: e.g. 3030 of HFS6-3030-1000
        H: (mm) Long side width of the extrusion cross section
        W: (mm) Short side width of the extrusion cross section
    """
    df = func_aef_candidates(aeftype, series, extrusion, H, W)
    return df

@tool
def select_moment_of_inertia(product_type: str) -> float:
    """
    Get the cross sectional moment of inertia of the extrusion from data base.

    Args:
        product_type: e.g. EFS5-2525, HFS6-3030, GFS8-4040
    """
    df = pd.read_csv('I_data.csv', index_col=None)

    df_matched = df[df['prd_type'] == prd_type]

    I = df_matched['moment_of_inertia']
    return I.values[0]

定義した関数をリストアップします。

tools = [calc_load_no1, ..., select_moment_of_inertia]

LLM の API を呼び出します。
ここでは GPT-4o を指定して、Azure OpenAI を使っています。

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")

model_name="gpt-4o"

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

ツールと LLM の準備がそれぞれできました。

次にこれらを合わせてエージェントを作ります。
LangChain には、create_tool_calling_agentという function calling に特化したエージェントを作る機能が用意されています。これは、GPT、Gemini、Claude などあらゆるモデルでエージェントを構築できます。

from langchain_core.prompts import ChatPromptTemplate
from langchain.agents import create_tool_calling_agent, AgentExecutor

system_message = "you're a mechanical design API assistant to help with serching products and calculating load capacities based on user preferences. "
"Answer the following questions as best you can. You have access to the following APIs:
Please choose the appropriate tool according to the user's question. If you don't need to call it, please reply directly to the user's question. When you have enough information from the tool results, respond directly to the user with a text message without having to call the tool again."
    
prompt = ChatPromptTemplate.from_messages([
    ("system", system_message), 
    ("human", "{input}"), 
    ("placeholder", "{agent_scratchpad}"),
])

agent = create_tool_calling_agent(llm, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

ユーザープロンプトを渡して、エージェントを実行します。

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

messages = agent_executor.invoke({"input": query})
print (messages["output"])

すると、GPT が関数を使いながら以下のように推論を進めていき、最終的に結果が生成されました。
この例では、以下の処理が行われました:

  1. 商品の候補をデータから取得する関数を読び、
  2. 各候補について荷重計算(候補の数だけ繰り返し)
  3. 計算結果をまとめて最終的な応答を生成
関数の呼び出しと応答結果
Invoking: `get_aef_candidates` with `{'aeftype': None, 'series': None, 'extrusion': None, 'H': None, 'W': None}`

    type  series  extrusion
0    EFS       6       3030
1    EFS       8       4040
2    EFS       8       8080
3    EFS       8       4545
...

Invoking: `select_moment_of_inertia` with `{'product_type': 'EFS6-3030'}`
28500
Invoking: `select_moment_of_inertia` with `{'product_type': 'EFS8-4040'}`
104900
...

Invoking: `calc_load_no4` with `{'L': 770, 'I': 28500}`
0.20967092411720512
Invoking: `calc_load_no4` with `{'L': 770, 'I': 104900}`
0.7717361382419233
...

> Finished chain.
以下のアルミフレームは、長さ770mmで100kgの装置を載せることができます:

1. **EFS8-8080**: 最大許容荷重は約9563.94N
2. **GFS8-9090**: 最大許容荷重は約23556.71N
3. **GFS8-100100**: 最大許容荷重は約34268.32N
4. **HFS6-6060**: 最大許容荷重は約3042.07N
5. **HFS6-100100**: 最大許容荷重は約19525.15N
6. **HFS8-8080**: 最大許容荷重は約9549.22N
7. **HFS8-9090**: 最大許容荷重は約15486.22N
8. **HFSG6-6060**: 最大許容荷重は約3818.95N
9. **HFSH8-8080**: 最大許容荷重は約13485.15N

これらのフレームはすべて100kgの荷重(約981N)をしっかり支えることができます。

長いので一部省略しています。

試行錯誤

約 10 個のプロンプトを何度か試し、回答を見ながら関数や説明文を調整しました。ただし、様々なプロンプトを用意して数百回試すような定量評価はまだしていません。

関数や引数の説明文

GPT-4o は試行錯誤をしなくてもかなり関数を使い分けられました。
商品の型番について言葉で説明するのは難しいですが、例をいくつか挙げるだけで、ユーザープロンプトから該当部分を抜き出すことができました。
ただし、例に挙げていない型番は抽出できないことが多かったので、可能性のあるパターンを列挙する必要がありました。パターンが膨大な場合は工夫が必要そうです。

関数のまとめ方

一つの指示に複数の関数を順番に使うとき、関数出力を次の関数に渡すところで失敗することがありました。

失敗例

  • 関数 select_moment_of_inertia の出力を calc_load_no1 の引数 I にそのまま渡すべきところで、None や 1/2 した値を誤って渡す。
  • 関数 get_aef_candidates で候補が多く出力されると一部が欠落。

想定される一連の処理を 1 個の関数にまとめるとエラーが減りました。また、関数の候補が減ると、関数選択の間違いも減ると期待できます。一方で、関数 1 個あたりの引数が増えがちで、引数の予測でのエラーが心配になります。
一般的なベストプラクティスでは、関数の数を減らすことが推奨されますが、引数を減らすことについては言及されていないので、関数の数を減らす方がより効果的だと思われます。

引数の指定をどこまで LLM に任すか

ミスミではアルミフレームのたわみを長さの 1/1000 までを許容しているので、システムプロンプトに "MISUMI defines the Load Capacity (Max Allowable Load) to be a deflection 1/1000 of the extrusion length." と書いて、関数の引数に指定させることも検討しました。ユーザーが条件を明記したらその値を使うなど融通が利くためです。
間違った値が渡されるのを完全に防げなかった一方、ほぼ常に 1/1000 を使えばいいので、前節のようにコード内に記述しました。

LLM にどこまで数値を扱わせるか

数値の単位変換
長さ (e.g. mm, m, インチ、フィート) や荷重 (e.g. N, kgf, ポンド力) など、工学で使われる数値には単位がいろいろあります。前節の関数の例では、アルミフレームの長さを mm の単位で受け取る設定なので、cm やインチで表記されると変換が必要になります。桁を変えるだけの単位変換なら LLM は間違えませんでした
ただし、LLM に単位変換を任せるのは数値計算は関数が担う趣旨にそぐわないので、関数内で行うようにしました。

条件の判定 (荷重の許容可否)
最大許容荷重の計算結果と積載物の荷重とを比較する判定を LLM に任せた場合、単一の値の判定では間違いは見られませんでした。しかし、複数の計算結果を LLM が処理するとき、条件を満たす一部の結果が抜け落ちることがありました。また、安全率を適用する計算も必要です。
これらの理由から、条件の判定と、条件を満たした商品の選定を同じ関数内で行うようにしました。

この実験・検討を通して、よく使う商品選定の機能を一つの関数に集約しました。この関数は、希望する荷重やアルミフレームの長さを入力として受け取り、それに適した商品の一覧を出力します。引数として、長さと荷重はそれぞれ値と単位を指定できるようになっています。

これにより、関数の入出力を LLM が扱っていた部分で起こるエラーが解消されました。

{
"name": "get_load_bulk",
"description": "Choose all aluminum extrusions whose max load is larger than the given load.\
                Use this function when you want to compare the max load of multiple aluminum extrusions.\
                This function returns all the aluminum extrusions  that can withstand the specified load and its maximum load.",
"parameters": {"type": "object",
               "properties": {
                  "L": {"type": "number", "description": "Beam length"},
                  "load": {"type": "number", "description": "Load to be applied to the beam"},
                  "L_unit": {"type": "string", "description": "Unit of the beam length (L). e.g. mm, cm"},
                  "load_unit": {"type": "string", "description": "Unit of the load. e.g. N, kgf"},
                  "product_type": {"type": "string", "description": "e.g. EFS5-2525, HFS6-3030, GFS8-4040"},
                  "W": {"type": "string", "description": "Short side width of the extrusion cross section (mm)"},
                  "safety_factor": {"type": "number", "description": "Safety factor for the load. The default value is 5.0."},
                  },
"required": ["L", "load"]}
}

この関数一個を使うだけでは AI エージェントとは呼べないでしょう。

アルミフレームでは、商品の重量やたわみ量の値を知りたい場合や、複数の計算結果をもとに一商品を選ぶ場合もあります。また、取扱商品ごとに異なる計算が求められるので、より多くの関数が必要になってきます。

複数のツールを使う場合でも、各ツールを独立して使って結果をまとめるだけなら ModularRAG や LLM workflow のような考え方でもよさそうです。
一方で、複数ツールを出力に応じて適切な順序で組合せる場合にはエージェントが効果的です。

おわりに

アルミフレームの商品選定において Function Calling を活用して LLM エージェントを作りました。

GPT-4o では単位変換やたわみ計算はネイティブにできています。また reasoning model (e.g. OpenAI o3, DeepSeek-R1) で推論能力も向上しており、計算を LLM に任せることも今後ある程度できるようになりそうです。
しかし、社内の構造化されたデータベースや API が既にあるなら、これらを外部ツールとして Function Calling で活用する方が正確で低コストという状況は変わらないはずです。推論能力の向上に伴いツールを使う能力も上がり、外部ツールとの組み合わせがさらに進むと考えられます。OpenAI o1 や o3-mini でも Function Calling が利用可能です。

今回はアルミフレームに限り、シングルエージェントの構成で実験しました。他の商品では、異なるスペック計算が必要となります。そのため、あらゆる商品に対応するには、専門の assistant エージェントを作り、それらを統合する親エージェントを立てるマルチエージェントシステムが必要かもしれません。研究業界でも LLM エージェントに関するテーマが盛り上がっており、その知見を活かして、より良い構成を検討中です。また今後、エージェントの構成を明示的に実装するときは LangGraph の使用を検討すると思います。

Version 情報

  • LangChain 0.3.7
  • GPT-4o (version:2024-08-06)
脚注
  1. 最近はエージェントシステムを LangGraphDify で作ることが多いと思います。 ↩︎

ミスミ DataTech ブログ

Discussion

ログインするとコメントできます