1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Function Callingで自然言語から安全に業務APIを呼び出す方法

1
Last updated at Posted at 2026-10-06

「注文をキャンセルして」「先月の売上を確認して」のような自然言語を、業務APIの呼び出しにつなげると、利用者は複雑な画面操作やAPI仕様を覚えずに済みます。

一方で、LLMの出力をそのまま業務APIへ渡す設計は危険です。引数の取り違え、権限のないデータ参照、意図しない更新、重複実行などが起こり得ます。

この記事では、Function Callingを「LLMに実行権限を与える機能」ではなく、「呼び出したい処理の候補を構造化して提案させる機能」として扱います。実際の実装では、アプリケーション側で入力検証・認可・確認・監査を行うことが重要です。

Function Callingの責任範囲を分ける

処理全体の流れを図にすると次の通りです。

Function Callingの基本的な流れは次のとおりです。

  1. ユーザーの自然言語をLLMへ渡す
  2. LLMが呼び出す関数名と引数を返す
  3. アプリケーションが引数を検証する
  4. ユーザー権限を確認する
  5. 必要ならユーザーに確認する
  6. 業務APIを実行する
  7. 実行結果をLLMへ渡し、自然言語で応答する

ここで重要なのは、2の結果を信頼しすぎないことです。LLMはデータベースの権限管理者ではなく、業務ルールの最終判断者でもありません。

特に、参照系APIと更新系APIは分けて設計します。注文状況の取得は比較的安全ですが、注文キャンセルや返金は副作用を伴います。関数の説明にも、その違いを明示しておくと意図しない呼び出しを減らせます。

関数スキーマを小さく厳密に定義する

まず、Function Callingへ渡す関数定義を作ります。例として、注文状況の取得と注文キャンセルを定義します。

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_order_status",
            "description": "指定された注文の現在の状態を取得する。データを変更しない。",
            "parameters": {
                "type": "object",
                "properties": {
                    "order_id": {
                        "type": "string",
                        "description": "注文番号。例: ORD-12345"
                    }
                },
                "required": ["order_id"],
                "additionalProperties": False
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "cancel_order",
            "description": "注文をキャンセルする。実行前に必ずユーザー確認を要求する。",
            "parameters": {
                "type": "object",
                "properties": {
                    "order_id": {
                        "type": "string",
                        "description": "注文番号。例: ORD-12345"
                    },
                    "reason": {
                        "type": "string",
                        "enum": ["customer_request", "duplicate", "other"]
                    }
                },
                "required": ["order_id", "reason"],
                "additionalProperties": False
            }
        }
    }
]

additionalProperties: Falseを設定すると、想定していない引数を受け取りにくくなります。また、自由入力の文字列を減らし、可能なものはenumで限定します。

ただし、スキーマだけでは安全性は不十分です。たとえば、モデルがorder_idとして別ユーザーの注文番号を返す可能性はあります。必ず業務API側でも、ログインユーザーと注文の所有者を照合します。

アプリケーション側で引数を検証する

私が実装するときは、LLMの出力を直接APIクライアントへ渡さず、Pydanticなどで一度検証します。

python -m venv .venv
.venv\Scripts\activate
pip install pydantic pytest

次の例では、関数名の許可リスト、引数の形式、実行ユーザーを確認しています。

from typing import Literal
from pydantic import BaseModel, ConfigDict, Field, ValidationError


class GetOrderStatusArgs(BaseModel):
    model_config = ConfigDict(extra="forbid")
    order_id: str = Field(pattern=r"^ORD-[0-9]{5,}$")


class CancelOrderArgs(BaseModel):
    model_config = ConfigDict(extra="forbid")
    order_id: str = Field(pattern=r"^ORD-[0-9]{5,}$")
    reason: Literal["customer_request", "duplicate", "other"]


READ_TOOLS = {"get_order_status"}
WRITE_TOOLS = {"cancel_order"}


def execute_tool(tool_name: str, raw_args: dict, user: dict, order_api):
    if tool_name not in READ_TOOLS | WRITE_TOOLS:
        raise ValueError("許可されていない関数です")

    if tool_name == "get_order_status":
        args = GetOrderStatusArgs.model_validate(raw_args)

        # tenant_idやuser_idはLLMの引数ではなく認証情報から取得する
        return order_api.get_status(
            tenant_id=user["tenant_id"],
            user_id=user["user_id"],
            order_id=args.order_id,
        )

    if tool_name == "cancel_order":
        args = CancelOrderArgs.model_validate(raw_args)

        if "orders:write" not in user["scopes"]:
            raise PermissionError("注文変更権限がありません")

        # 更新系処理は、ここで即時実行せず確認待ちにする
        return {
            "requires_confirmation": True,
            "action": "cancel_order",
            "order_id": args.order_id,
            "reason": args.reason,
        }

tenant_idやuser_idをFunction Callingの引数に含めない点がポイントです。これらをモデルに指定させると、プロンプトインジェクションなどで別ユーザーになりすます余地が生まれます。

認証情報から取得した値を、サーバー側で必ず注入します。

更新系APIには確認と冪等性を入れる

参照系の呼び出しは自動実行しても、更新系は確認を挟む設計が安全です。

たとえば、ユーザーが「ORD-12345をキャンセルして」と入力した場合、次のような確認画面やメッセージを返します。

注文 ORD-12345 をキャンセルします。
理由: customer_request
この操作は取り消せません。実行してよろしいですか?

ユーザーが明示的に承認した後だけ、確定用の処理を呼び出します。

def confirm_cancel_order(
    order_id: str,
    reason: str,
    user: dict,
    order_api,
    request_id: str,
):
    if "orders:write" not in user["scopes"]:
        raise PermissionError("注文変更権限がありません")

    return order_api.cancel(
        tenant_id=user["tenant_id"],
        user_id=user["user_id"],
        order_id=order_id,
        reason=reason,
        idempotency_key=request_id,
    )

idempotency_keyは、通信の再試行や画面の二重送信に備えるためのキーです。同じキーで複数回リクエストされても、キャンセル処理が一度だけ実行されるように業務API側で制御します。

また、残高変更、返金、権限変更、削除などは、Function Callingだけで完結させない方がよいでしょう。金額や対象件数に上限を設け、必要に応じて人の承認ワークフローへ接続します。

監査ログとテストで確認する

実行した関数だけでなく、提案された関数、検証結果、認可結果、確認の有無も記録します。

def write_audit_log(event_store, *, user, tool_name, args, result, request_id):
    event_store.append({
        "request_id": request_id,
        "user_id": user["user_id"],
        "tenant_id": user["tenant_id"],
        "tool_name": tool_name,
        "args": args,
        "result_type": type(result).__name__,
    })

ログにはパスワードやアクセストークンなどの秘密情報を含めません。注文番号やユーザーIDも、運用上必要な範囲に絞ります。

手元で確認したテストケースは、正常系だけでは不十分でした。最低限、次のようなケースを自動テストにします。

def test_unknown_tool_is_rejected():
    with pytest.raises(ValueError):
        execute_tool("delete_all_orders", {}, user, order_api)


def test_extra_argument_is_rejected():
    with pytest.raises(ValidationError):
        execute_tool(
            "get_order_status",
            {"order_id": "ORD-12345", "user_id": "admin"},
            user,
            order_api,
        )


def test_write_tool_requires_scope():
    no_write_user = {
        "user_id": "u-1",
        "tenant_id": "t-1",
        "scopes": ["orders:read"],
    }

    with pytest.raises(PermissionError):
        execute_tool(
            "cancel_order",
            {"order_id": "ORD-12345", "reason": "duplicate"},
            no_write_user,
            order_api,
        )

あわせて、「別テナントの注文番号を指定する」「キャンセル済み注文を再度キャンセルする」「同じリクエストを再送する」「曖昧な注文番号を入力する」といった業務上の境界値も確認します。

まとめ

Function Callingは、自然言語と業務APIをつなぐ便利な仕組みです。しかし、安全性を担保するのはLLMではなく、周辺のアプリケーション実装です。

実装時は、次の点を守ると設計しやすくなります。

  • 関数名と引数を許可リストで管理する
  • スキーマとサーバー側の両方で入力検証する
  • ユーザーIDやテナントIDは認証情報から取得する
  • 更新系処理には確認、権限チェック、冪等性を入れる
  • 実行内容を監査ログへ残し、境界値をテストする

「LLMがAPIを呼ぶ」のではなく、「LLMが候補を提案し、アプリケーションが安全性を確認してからAPIを呼ぶ」と考えるのが、実運用での基本パターンです。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?