コンテンツにスキップ

8.3.4 関数呼び出し 入門

関数呼び出し ワークフロー図

  • 自然言語の出力だけでは、なぜ安定してツールを呼び出しにくいのかを理解する
  • 関数 スキーマ、パラメータ、呼び出し結果という 3 つの核心概念を理解する
  • 最小限の関数呼び出しの一連の流れを読み取れるようになる
  • 関数呼び出し がどのような場面に最も向いているかを知る

初心者はまず押さえる / 上級者はあとで理解する

Section titled “初心者はまず押さえる / 上級者はあとで理解する”

もしあなたが初心者なら、この節ではまず 1 つのことを押さえてください。Function Calling は、モデルに本当にコードを実行させるものではありません。まず構造化された「呼び出し意図」を出力させ、その後でプログラムがそれを検査・実行・結果返却します。

すでに LLM アプリを作ったことがあるなら、次の点をさらに意識するとよいでしょう。ツール schema は十分に明確か、パラメータ検証は完全か、ツール失敗時にどう再試行またはフォールバックするか、呼び出しログはデバッグや評価に十分使えるか。


一、なぜ純テキスト出力だけでは足りないのか?

Section titled “一、なぜ純テキスト出力だけでは足りないのか?”

たとえば、ユーザーがこう聞いたとします。

「北京の今日の気温は?」

モデルに 1 文で返させます。

get_weather(city='Beijing') を呼び出すのがおすすめです」

一見使えそうに見えますが、実はかなり脆いです。

  • フォーマットが安定しない
  • パラメータ名を間違える可能性がある
  • 都市名が「北京」「Beijing」「北京市」のように揺れる
  • さらに説明文を大量に付け足すこともある

問題は、モデルがタスクを理解できないことではありません。むしろ、

自然言語は自由すぎて、安定したプログラムインターフェースには向かない。

という点にあります。

プログラムが好むのは次のようなものです。

  • 固定されたフィールド
  • 明確なパラメータ
  • 検証可能な構造

ここに Function Calling の価値があります。


関数呼び出し = モデルに自由文ではなく、構造化されたツール呼び出しを出力させること。

通常、含まれるのは次の 2 つです。

  • どのツールを呼ぶか
  • どんなパラメータを渡すか

たとえば、こんな形です。

{
"name": "get_weather",
"arguments": {
"city": "Beijing"
}
}

自由文より何が優れているのか?

Section titled “自由文より何が優れているのか?”

プログラムインターフェースに近いからです。雑談文ではありません。

プログラムはこの構造を受け取ったら、次のようなことができます。

  • フィールドを検証する
  • 自動で実行する
  • 失敗時に再試行する
  • ログを残す

つまり、Function Calling はモデルとプログラムの間に橋を架けるものです。


三、まず最小の一連の流れを見てみよう

Section titled “三、まず最小の一連の流れを見てみよう”
import ast
import operator
OPS = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.Div: operator.truediv,
}
def safe_calculate(expression):
def visit(node):
if isinstance(node, ast.Expression):
return visit(node.body)
if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
return node.value
if isinstance(node, ast.BinOp) and type(node.op) in OPS:
return OPS[type(node.op)](visit(node.left), visit(node.right))
if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub):
return -visit(node.operand)
raise ValueError("unsupported_expression")
return visit(ast.parse(expression, mode="eval"))
def get_weather(city):
data = {
"Beijing": {"temperature": 22, "condition": "sunny"},
"Shanghai": {"temperature": 25, "condition": "cloudy"}
}
return data.get(city, {"error": "city_not_found"})
def calculate(expression):
return {"result": safe_calculate(expression)}

「モデルの出力」を表す呼び出し構造を定義する

Section titled “「モデルの出力」を表す呼び出し構造を定義する”
tool_call = {
"name": "get_weather",
"arguments": {
"city": "Beijing"
}
}
print(tool_call)

想定出力:

{'name': 'get_weather', 'arguments': {'city': 'Beijing'}}

この呼び出しを実際に実行する

Section titled “この呼び出しを実際に実行する”
def dispatch(call):
if call["name"] == "get_weather":
return get_weather(**call["arguments"])
if call["name"] == "calculate":
return calculate(**call["arguments"])
return {"error": "unknown_tool"}
tool_call = {
"name": "get_weather",
"arguments": {"city": "Beijing"}
}
result = dispatch(tool_call)
print(result)

想定出力:

{'temperature': 22, 'condition': 'sunny'}

これが関数呼び出しの最小閉ループです。

  1. タスクを識別する
  2. 構造化された呼び出しを出力する
  3. プログラムが実行する
  4. 結果を受け取る

スキーマ は「ツールの説明書」と考えればよい

Section titled “スキーマ は「ツールの説明書」と考えればよい”

モデルが正しくツールを呼び出すには、次の情報が必要です。

  • ツール名
  • 各パラメータ名
  • パラメータの型
  • 必須かどうか

これが schema の役割です。

weather_schema = {
"name": "get_weather",
"description": "指定した都市の天気を取得する",
"parameters": {
"city": {
"type": "string",
"description": "都市の英語名。例: Beijing"
}
},
"required": ["city"]
}
print(weather_schema)

想定出力:

{'name': 'get_weather', 'description': '指定した都市の天気を取得する', 'parameters': {'city': {'type': 'string', 'description': '都市の英語名。例: Beijing'}}, 'required': ['city']}

schema は「見た目を整えるための文」ではありません。モデルとプログラムにこう伝えています。

このツールは、このように呼び出せる。


五、なぜパラメータ検証が重要なのか?

Section titled “五、なぜパラメータ検証が重要なのか?”

モデルがいつも正しいパラメータを返すとは限らない

Section titled “モデルがいつも正しいパラメータを返すとは限らない”

モデルがツールの選択を正しくできても、次のようなミスは起こりえます。

  • フィールドが抜ける
  • 型が違う
  • パラメータ値が無効

たとえば、

bad_call = {
"name": "get_weather",
"arguments": {"city_name": "Beijing"}
}

もしプログラムが検証しなければ、実行時にそのまま落ちてしまいます。

def validate_weather_call(call):
if call.get("name") != "get_weather":
return False, "wrong_tool"
args = call.get("arguments", {})
if "city" not in args:
return False, "missing_city"
if not isinstance(args["city"], str):
return False, "city_must_be_string"
return True, "ok"
good_call = {"name": "get_weather", "arguments": {"city": "Beijing"}}
bad_call = {"name": "get_weather", "arguments": {"city_name": "Beijing"}}
print(validate_weather_call(good_call))
print(validate_weather_call(bad_call))

想定出力:

(True, 'ok')
(False, 'missing_city')

六、より完全な例:天気と計算機

Section titled “六、より完全な例:天気と計算機”

まず「モデルがどのツールを呼ぶか」をまねる

Section titled “まず「モデルがどのツールを呼ぶか」をまねる”

ここでは本物の大規模モデルは使いません。まずは学習用のルール関数を書き、「ツール呼び出しの構造」を見やすくします。

def mock_llm_tool_selector(user_query):
if "天気" in user_query:
city = "Beijing" if "北京" in user_query else "Shanghai"
return {
"name": "get_weather",
"arguments": {"city": city}
}
if "計算" in user_query:
expression = user_query.replace("計算", "").strip()
return {
"name": "calculate",
"arguments": {"expression": expression}
}
return None
import ast
import operator
OPS = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.Div: operator.truediv,
}
def safe_calculate(expression):
def visit(node):
if isinstance(node, ast.Expression):
return visit(node.body)
if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
return node.value
if isinstance(node, ast.BinOp) and type(node.op) in OPS:
return OPS[type(node.op)](visit(node.left), visit(node.right))
if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub):
return -visit(node.operand)
raise ValueError("unsupported_expression")
return visit(ast.parse(expression, mode="eval"))
def get_weather(city):
data = {
"Beijing": {"temperature": 22, "condition": "sunny"},
"Shanghai": {"temperature": 25, "condition": "cloudy"}
}
return data.get(city, {"error": "city_not_found"})
def calculate(expression):
return {"result": safe_calculate(expression)}
def dispatch(call):
if call["name"] == "get_weather":
return get_weather(**call["arguments"])
if call["name"] == "calculate":
return calculate(**call["arguments"])
return {"error": "unknown_tool"}
queries = [
"北京今天天気はどうですか",
"計算 3 * (4 + 5)"
]
for q in queries:
call = mock_llm_tool_selector(q)
result = dispatch(call)
print("ユーザーの質問:", q)
print("ツール呼び出し:", call)
print("実行結果:", result)
print("-" * 40)

想定出力:

ユーザーの質問: 北京今天天気はどうですか
ツール呼び出し: {'name': 'get_weather', 'arguments': {'city': 'Beijing'}}
実行結果: {'temperature': 22, 'condition': 'sunny'}
----------------------------------------
ユーザーの質問: 計算 3 * (4 + 5)
ツール呼び出し: {'name': 'calculate', 'arguments': {'expression': '3 * (4 + 5)'}}
実行結果: {'result': 27}
----------------------------------------

関数呼び出し の天気と計算機の実行結果図

この例は、実際のシステムの骨組みにかなり近いです。


七、関数呼び出し はどんなタスクに向いているのか?

Section titled “七、関数呼び出し はどんなタスクに向いているのか?”
  • 天気の取得
  • ナレッジベース検索
  • データベース検索
  • 数学計算
  • 検索 API の呼び出し
  • チケット起票

つまり、

モデルは「何をするか」を決め、プログラムが実際に実行する。

タスクの本質が次のような場合です。

  • 文章を書く
  • 自由な生成を行う
  • ただの雑談をする

このような場合は、必ずしも Function Calling が必要とは限りません。


八、知識ベース駆動の SOP 文書アシスタントを作るなら、最小ツールセットはどうあるべきか?

Section titled “八、知識ベース駆動の SOP 文書アシスタントを作るなら、最小ツールセットはどうあるべきか?”

この種のプロジェクトを初めて作るとき、最初から何十個ものツールを用意する必要はありません。
より安定した最小ツールセットは、通常たった 4 つで十分です。

  1. retrieve_internal_docs(topic)
    社内 SOP、ポリシー、ケース文書を検索する

  2. check_policy_case_coverage(materials) ドラフトに必要なポリシー根拠とケース根拠が足りているか確認する

  3. build_sop_draft_schema(materials) 資料を policy、case、checklist、citations の欄に整理する

  4. export_word(schema)
    SOP テンプレートを当てて Word に出力する

まずはこう考えるとよいです。

  • モデルが直接 Word を書くわけではない
  • モデルは「次にどの工程を呼ぶか」を決めている

小さなツール定義の例は、次のように書けます。

tools = [
{
"name": "retrieve_internal_docs",
"description": "テーマに基づいて社内 SOP とポリシー文書を検索する",
"parameters": {"topic": {"type": "string"}},
},
{
"name": "export_word",
"description": "構造化された SOP ドラフトを Word 文書として出力する",
"parameters": {"title": {"type": "string"}, "sections": {"type": "array"}},
},
]
print(tools)

想定出力:

[{'name': 'retrieve_internal_docs', 'description': 'テーマに基づいて社内 SOP とポリシー文書を検索する', 'parameters': {'topic': {'type': 'string'}}}, {'name': 'export_word', 'description': '構造化された SOP ドラフトを Word 文書として出力する', 'parameters': {'title': {'type': 'string'}, 'sections': {'type': 'array'}}}]

九、最もよくある実装上の問題

Section titled “九、最もよくある実装上の問題”

本当はナレッジベースを検索すべきなのに、計算機を呼んでしまう例です。

たとえば次のように揺れます。

  • city
  • city_name
  • location

モデルは混ぜて使ってしまうことがあります。

ツール呼び出しの構造が正しくても、次のような失敗は起こります。

  • API タイムアウト
  • パラメータが不正
  • 都市が存在しない

つまり、

関数呼び出し は「モデルがツールを呼べるようになれば終わり」ではありません。その後ろに、必ず実装上の安全網が必要です。


十、初心者がよくやってしまうミス

Section titled “十、初心者がよくやってしまうミス”

関数呼び出し を「モデルが直接コードを実行するもの」と思う

Section titled “関数呼び出し を「モデルが直接コードを実行するもの」と思う”

違います。
モデルは構造化された呼び出し意図を出力するだけで、実際に実行するのはあなたのプログラムです。

ツール スキーマ があいまいすぎる

Section titled “ツール スキーマ があいまいすぎる”

ツール説明が不十分で、パラメータ定義も不明確だと、モデルは誤った呼び出しをしやすくなります。

本番環境に入ると、これは非常に危険な習慣です。


このページを終えたら、この証拠カードを残します。

要求
入力、状態、tools/context、期待される出力の契約
検証済み出力
パーサー/スキーマ、または業務ルール確認の結果
追跡記録
モデル呼び出し、ツール/関数呼び出し、文書解析、または対話状態
失敗確認
フォーマット不正、必須フィールド不足、古い状態、または誤ったツール
次の行動
prompt、schema、state、API、または parsing の改善

まとめを見る前に:関数呼び出し の工程ループ

Section titled “まとめを見る前に:関数呼び出し の工程ループ”
flowchart LR
A["ユーザーの質問"] --> B["モデルがツールが必要か判断する"]
B --> C["構造化された tool call を出力する"]
C --> D["パラメータ検証"]
D --> E{"検証に通るか"}
E -- はい --> F["プログラムがツールを実行する"]
E -- いいえ --> G["エラーを返す / パラメータを再生成する"]
F --> H["ツールの結果"]
H --> I["モデルが最終回答をまとめる"]
style C fill:#e3f2fd,stroke:#1565c0,color:#333
style D fill:#fff3e0,stroke:#e65100,color:#333
style F fill:#e8f5e9,stroke:#2e7d32,color:#333

このループはとても重要です。なぜなら、Function Calling の難しさは「モデルが関数名を言えるかどうか」ではなく、モデル、schema、検証、実行器、エラー処理がそろって安定したシステムになるかどうかにあるからです。

関数呼び出し 検証と実行の閉ループ図

レベルできるようになること
直感自由文がなぜそのままプログラムインターフェースに向かないか説明できる
コード最小の tool call、dispatch、パラメータ検証関数を書ける
工学スキーマ、検証、エラー処理、ログがそれぞれ何を担当するか説明できる
次のステップとのつながり関数呼び出し が Agent のツール呼び出しの前提になる理由を理解できる

この節でいちばん大事なのは、namearguments の 2 つのフィールドを覚えることではありません。本質は次の一点です。

関数呼び出し は、モデルの自然言語理解能力を、プログラムの構造化された実行能力につなぐ仕組みである。

この点を理解できると、次に Agent、ツール戦略、複数ツールの協調を学ぶときに、かなりスムーズになります。


  1. この節の例に search_docs(keyword) という新しいツールを追加してみましょう。
  2. calculate のパラメータ検証関数を書いて、危険な式を防いでみましょう。
  3. もしモデルが「北京の天気」を何度も calculate に誤ルーティングするなら、まず prompt、スキーマ、実行器のどれを直しますか?
  4. 自分の言葉で説明してみましょう。関数呼び出し は、なぜ「モデルにコマンド文を直接返させる」より安定しているのでしょうか?
操作例と確認ポイント
  1. search_docs(keyword) には入力 schema、検証ルール、実行器の戻り値、失敗時の挙動を定義します。
  2. calculate の検証では、数字、安全な演算子、括弧だけを許可するか、AST whitelist を使います。任意文字列を eval() してはいけません。
  3. まず tool schema と説明文を直し、必要なら prompt 例を追加します。実行器は不正引数を拒否できますが、routing を単独で教えることはできません。
  4. Function Calling は構造化引数、型付き schema、検証点、監査可能な tool call を持つため、自由形式のコマンド文字列より安定します。