【vLLM】OpenAI互換APIでFunction Callingを実装する方法と「Invalid tools format」エラーの解決策

問題の概要:vLLMのOpenAI APIでFunction Callingが機能しない

vLLMは、その高速な推論エンジンとして広く利用されています。vLLM 0.4.0以降では、OpenAI形式のAPIサーバーを起動し、OpenAIクライアントライブラリと互換性のある方法でFunction Calling(関数呼び出し)を利用できるようになりました。しかし、設定方法を誤ると、Function Callingが全く動作せず、モデルが通常のチャット応答のみを返したり、あるいは以下のようなエラーメッセージがAPIから返されることがあります。

{
  "error": {
    "message": "Invalid tools format. Expected a list of tools.",
    "type": "invalid_request_error",
    "param": "tools",
    "code": 400
  }
}

また、サーバーログには WARNING:root:Failed to parse tools input. のような警告が出力される場合もあります。この記事では、vLLMのOpenAI互換APIサーバーを正しく設定し、Function Callingを成功させるための具体的な手順と、発生するエラーの解決方法を解説します。

原因の解説:なぜFunction Callingが失敗するのか

vLLMでFunction Callingが失敗する主な原因は、以下の3点に集約されます。

1. モデルの対応状況

Function Callingは、モデル自体がこの機能をサポートしている必要があります。vLLMはあくまで推論エンジンであり、モデルがFunction Calling用の訓練(あるいはファインチューニング)を受けていなければ、適切なJSON形式の応答を生成できません。一般的に、gpt-3.5-turboやgpt-4をはじめ、多くの最近のLLM(Llama 3.1, Qwen 2.5, Command R+など)はFunction Callingをサポートしています。

2. APIサーバーの起動オプション

vLLMのOpenAI互換APIサーバーを起動する際に、--served-model-name オプションを指定しない場合、デフォルトではモデルのパス名がそのまま使用されます。しかし、OpenAIクライアントは特定のモデル名(例: “gpt-3.5-turbo”)を期待していることがあり、これが不一致を起こす可能性があります。

3. リクエストのフォーマット

OpenAI APIのFunction Callingは、リクエストボディの tools パラメータに関数の仕様をリスト形式で渡します。この構造が正しいJSON形式でない場合、前述の「Invalid tools format」エラーが発生します。特に、ツールのリストを直接JSON文字列として渡してしまったり、ネストが誤っているケースが多く見られます。

解決方法:ステップバイステップでのFunction Calling実装

ここからは、vLLMでFunction Callingを動作させるための完全な手順を説明します。

ステップ1: vLLMのインストールとAPIサーバー起動

まず、最新版のvLLMをインストールし、Function Calling対応モデルを指定してAPIサーバーを起動します。

# vLLMのインストール(既に入っている場合はアップグレード)
pip install -U vLLM

# OpenAI互換APIサーバーの起動
# `--served-model-name` でクライアントが参照するモデル名を明示的に指定することが重要
vllm serve meta-llama/Llama-3.1-8B-Instruct 
  --served-model-name gpt-3.5-turbo 
  --api-key token-abc123 # オプション: 簡単な認証を追加

サーバーはデフォルトで http://localhost:8000 で起動します。--served-model-name を「gpt-3.5-turbo」に設定することで、OpenAIクライアントライブラリが期待するモデル名との互換性を確保します。

ステップ2: クライアントコードの準備 (Python)

次に、PythonのOpenAIクライアントライブラリを使用して、vLLMサーバーにリクエストを送信するコードを書きます。ベースURLとAPIキーをvLLMサーバーの設定に合わせる点がポイントです。

from openai import OpenAI
import json

# クライアントの初期化。base_urlとapi_keyはvLLMサーバー起動時の設定に合わせる。
client = OpenAI(
    base_url="http://localhost:8000/v1", # vLLMのOpenAIエンドポイント
    api_key="token-abc123" # `vllm serve` で指定したもの
)

# Function Callingで使用する「関数」(ツール)の定義
# これは天気情報を取得する架空の関数の仕様です。
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_current_weather",
            "description": "指定された場所の現在の天気を取得します",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "都市名と国名、例:'東京, 日本'",
                    },
                    "unit": {
                        "type": "string",
                        "enum": ["celsius", "fahrenheit"],
                        "description": "温度の単位",
                    },
                },
                "required": ["location"],
            },
        },
    }
]

# ユーザーのメッセージ
messages = [{"role": "user", "content": "東京都の天気はどうですか?"}]

# vLLMサーバーへのリクエスト送信
try:
    response = client.chat.completions.create(
        model="gpt-3.5-turbo", # --served-model-name で指定した名前
        messages=messages,
        tools=tools, # ここに関数仕様のリストを渡す
        tool_choice="auto", # "auto", "none", または特定の関数を指定
    )
except Exception as e:
    print(f"APIリクエストエラー: {e}")
    exit(1)

# レスポンスの解析
print("=== 生のレスポンス ===")
print(json.dumps(response.model_dump(), indent=2, ensure_ascii=False))

chat_completion = response.choices[0].message
print(f"n=== モデルの返答 ===")
print(f"Role: {chat_completion.role}")
print(f"Content: {chat_completion.content}")

# Function Callingが提案されたか確認
if chat_completion.tool_calls:
    print("n=== モデルが呼び出しを提案した関数 ===")
    for tool_call in chat_completion.tool_calls:
        func_name = tool_call.function.name
        func_args = json.loads(tool_call.function.arguments)
        print(f"関数名: {func_name}")
        print(f"引数: {func_args}")

        # ここで、実際の関数 `get_current_weather` を実行するロジックを記述
        # 例: weather_info = get_current_weather(**func_args)

ステップ3: 実行と動作確認

上記コードを実行すると、モデルはユーザーの質問「東京都の天気はどうですか?」を解釈し、定義された get_current_weather 関数を呼び出すべきだと判断します。成功すると、レスポンスの tool_calls フィールドに関数名と引数(例: {"location": "東京都, 日本", "unit": "celsius"})が含まれます。この引数を用いて、開発者はバックエンドで実際の天気APIを呼び出すなどの処理を行うことができます。

コード例・コマンド例:よくあるエラーとその対処法

エラー1: 「Invalid tools format」が返る

現象: リクエストを送信すると、400エラーとともに上記メッセージが返る。
原因と解決策: tools パラメータの形式が不正。OpenAIクライアントライブラリに渡す前に、自分でJSON文字列に変換していないか確認してください。ライブラリが自動的にシリアライズします。

# ❌ 間違い: 文字列として渡している
tools = '[{"type": "function", ...}]' # JSON文字列はダメ
response = client.chat.completions.create(..., tools=tools)

# ✅ 正解: Pythonのリスト/ディクショナリとして渡す
tools = [{"type": "function", ...}] # リストが正解
response = client.chat.completions.create(..., tools=tools)

エラー2: モデルが関数を呼び出さず、普通のチャット応答をする

現象: エラーは出ないが、tool_calls が空で、content に「天気はわかりません」などの通常応答が返る。
原因と解決策1: モデルがFunction Callingに対応していない可能性があります。よりFunction Callingに特化して訓練されたモデル(例: “Qwen2.5-7B-Instruct”)を試してください。
原因と解決策2: 関数の説明(description)が不十分で、モデルがいつ関数を呼び出すべきか判断できない。説明文をより具体的に書き直しましょう。

# ✅ 関数の説明を具体的にする例
"description": "ユーザーが現在の天気、今日の天気、明日の天気について尋ねた時に、この関数を呼び出して地点と単位を取得してください。",

サーバー起動のその他の有用なオプション

# トークン生成の詳細を表示(デバッグ用)
vllm serve meta-llama/Llama-3.1-8B-Instruct 
  --served-model-name gpt-3.5-turbo 
  --api-key token-abc123 
  --log-requests # リクエストログを出力

# 別のポートで起動する場合
vllm serve meta-llama/Llama-3.1-8B-Instruct 
  --served-model-name gpt-3.5-turbo 
  --port 8888

# Tensor Parallelismを利用して大型モデルを効率的に実行
vllm serve meta-llama/Llama-3.1-70B-Instruct 
  --served-model-name gpt-3.5-turbo 
  --tensor-parallel-size 4

まとめ・補足情報

vLLMのOpenAI互換APIを使用したFunction Callingは、高性能なオープンソースLLMに強力な機能呼び出し能力を追加する優れた方法です。成功の鍵は、(1) 対応モデルの選択(2) --served-model-name を用いた正しいAPIサーバーの起動、そして(3) OpenAIクライアントライブラリ規約に沿った正しいtoolsパラメータの渡し方の3点にあります。

本番環境で使用する際は、APIキーによる認証の強化、サーバーをバックグラウンドで安定稼動させるためのプロセス管理(systemd, Docker)、そしてモデルが返した関数引数のバリデーションとサニタイズを必ず実施してください。Function Callingを活用することで、LLMを単なるチャットボットから、実際のシステムやAPIと連携する自律的なエージェントへと進化させることができます。

vLLMは現在も活発に開発が続いており、Function Callingに関する機能も日々強化されています。最新の情報は公式ドキュメントで常に確認することをお勧めします。

この記事は役に立ちましたか?