0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LLMのTool Calling(Function Calling)失敗パターンと改善プラクティス

0
Posted at

はじめに

LLM(大規模言語モデル)を活用したAIエージェント構築において、Tool Calling(Function Calling)は外部システムとの連携や動的な処理を実行するための核となる技術です。しかし、本番環境や複雑なタスクにおいて「モデルがツールを呼び出してくれない」「誤ったパラメータを生成する」「出力フォーマットが崩れる」といった失敗が発生することがあります。

この記事では、Tool Callingが失敗する主な原因を整理し、実務で適用できる具体的な改善策と実装パターンについて解説します。

対象読者・前提条件

  • PythonおよびOpenAI API / LangChain等を用いてLLMアプリケーションを開発しているエンジニア
  • Tool Callingの基本構造を理解しており、エラー率の低減や精度向上を目指している方

※ 本記事のコードは2024年時点の一般的なOpenAI API仕様をベースに記述しています。利用時は最新の公式ドキュメントを確認してください。


Tool Callingが失敗する4つの主な原因

  1. スキーマ定義の曖昧さ
    • 引数の description や型定義が不十分で、モデルがどのような値を入力すべきか判断できない。
  2. ツール選択の混同(ツールの過多)
    • 類似した名前や役割のツールが同時に多数渡され、適切なツールを選択できない。
  3. モデルの文脈理解・指示追従の限界
    • プロンプトの指示とツールの役割が競合している、またはJSONの構造が複雑すぎる。
  4. エラーハンドリング不足
    • ツール側でエラー(API制限やValidation Error)が発生した際、LLM側にフィードバックされずにループや停止が発生する。

改善アプローチと実装例

1. Schema(Pydantic / JSON Schema)の堅牢化

LLMは型情報だけでなく description を強く参照します。Field に対して「何を入力すべきか」「どのようなフォーマットか(例: YYYY-MM-DD)」を明記します。

悪い例と良い例の比較

from pydantic import BaseModel, Field
from typing import Optional

# 悪い例: 説明が不足しており、フォーマットや想定値が不明確
class BadUserQuery(BaseModel):
    user_id: str
    date: str

# 良い例: 具体的な制約や例をdescriptionに記載
class GoodUserQuery(BaseModel):
    user_id: str = Field(..., description="ユーザーの一意識別子。例: 'USR-12345'")
    date: str = Field(..., description="検索対象の処理日付。フォーマットは 'YYYY-MM-DD'。例: '2024-04-01'")
    status: Optional[str] = Field(None, description="ステータスフィルタ。指定可能値: 'ACTIVE', 'PENDING', 'CANCELLED'")

2. OpenAI APIを用いた堅牢なTool Calling呼び出し実装

以下は、ツールの定義とエラーハンドリングを含めた基本的な実装パターンです。

import json
from openai import OpenAI

client = OpenAI()

# 独自定義のツール関数例
def fetch_user_data(user_id: str, date: str, status: str = None):
    # 実際のデータベース検索などの処理(独自実装例)
    if not user_id.startswith("USR-"):
        raise ValueError("無効なユーザーIDフォーマットです。")
    return {"user_id": user_id, "status": status or "ACTIVE", "result": "データ取得成功"}

# ツール定義のスキーマ
tools = [
    {
        "type": "function",
        "function": {
            "name": "fetch_user_data",
            "description": "指定したユーザーIDと日付に基づいてユーザー情報を取得します。",
            "parameters": {
                "type": "object",
                "properties": {
                    "user_id": {"type": "string", "description": "ユーザー識別子(例: USR-1001)"},
                    "date": {"type": "string", "description": "取得日付(YYYY-MM-DD)"},
                    "status": {"type": "string", "enum": ["ACTIVE", "PENDING", "CANCELLED"], "description": "状態フィルタ"}
                },
                "required": ["user_id", "date"]
            }
        }
    }
]

def run_agent(user_prompt: str):
    messages = [{"role": "user", "content": user_prompt}]
    
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=messages,
        tools=tools,
        tool_choice="auto"
    )
    
    response_message = response.choices[0].message
    tool_calls = response_message.tool_calls

    if tool_calls:
        messages.append(response_message)
        for tool_call in tool_calls:
            function_name = tool_call.function.name
            function_args = json.loads(tool_call.function.arguments)
            
            if function_name == "fetch_user_data":
                try:
                    # ツール実行
                    tool_output = fetch_user_data(
                        user_id=function_args.get("user_id"),
                        date=function_args.get("date"),
                        status=function_args.get("status")
                    )
                    content = json.dumps(tool_output, ensure_ascii=False)
                except Exception as e:
                    # エラーが発生した場合もLLMにコンテキストとしてフィードバックする
                    content = json.dumps({"error": str(e)}, ensure_ascii=False)
                
                messages.append({
                    "tool_call_id": tool_call.id,
                    "role": "tool",
                    "name": function_name,
                    "content": content,
                })
        
        # ツール実行結果を踏まえて最終応答を生成
        final_response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=messages
        )
        return final_response.choices[0].message.content
    
    return response_message.content

トラブル例と対処方法

トラブル現象 主な原因 対処方法
存在しない引数が生成される スキーマの additionalProperties 未設定、または説明不足 スキーマで additionalProperties: false を指定し、必須項目(required)を明示する。
ツールが呼び出されず文章だけ返る tool_choice が auto でモデルが不要と判断した 必要に応じて tool_choice={"type": "function", "function": {"name": "..."}} で強制するか、システムプロンプトで条件を明確化する。
似たツールを誤って呼び出す ツールの役割定義(description)が重複している ツールの役割分担を整理し、「どのような時に使うか / 使わないか」を description に明記する。必要のないツールは渡さない。
無限ループが発生する ツールエラー時の出力フォーマットが不正 ツール実行時の例外をキャッチし、エラー内容を role: "tool" のレスポンスとしてLLMに返却して再試行・停止を判断させる。

導入前・運用時のチェックリスト

  • スキーマの明確化: 全てのパラメータに description が記述されているか?
  • フォーマット指定: 日付や列挙型(enum)などの形式が明確に指示されているか?
  • ツールの選別: 1回の呼び出しで渡却するツール数が多すぎないか(推奨: 数個程度に制限)?
  • エラーフィードバック: ツール側で例外が発生した際、LLMにエラー内容をレスポンスとして渡す設計になっているか?
  • バリデーション: LLMから受け取った引数をそのまま外部APIに渡さず、プログラム側で事前検証を行っているか?

まとめ

Tool Callingの精度向上には、モデルの性能向上だけに頼るのではなく、スキーマ定義の明確化、適切なツールの絞り込み、そして例外発生時のフィードバック設計が不可欠です。本記事の改善パターンやチェックリストを活用し、堅牢なAIエージェントシステムの構築を進めてみてください。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?