こんにちは、かつコーチです。
前回は構造化出力でJSON形式の応答を得る方法を紹介しました。
今回はさらに一歩進んで、LLM自身に「この関数を呼び出したい」と判断させ、実際にアプリの処理を実行させる仕組みを解説します。
ツール呼び出し(Function Calling)を使うと、「注文状況を調べて」といった曖昧な依頼にAIが自律的に対応できるようになります。
実装時に権限周りで冷や汗をかいた経験も含めて、実践的にまとめます。
Function Callingとは?
LLMが「関数を呼びたい」と判断する仕組み
Function Callingとは、あらかじめアプリ側で「使える関数(ツール)」をLLMに教えておき、必要に応じてLLMがその関数の呼び出しを要求してくる仕組みです。
LLM自体はDBにアクセスしたり外部APIを叩いたりできません。
あくまで「この関数を、この引数で呼んでほしい」という指示を返すだけで、実際に実行するのはアプリ側の役割です。
この「判断はAI、実行はアプリ」という役割分担が、Function Callingの本質です。
一般的なやり取りの流れ
- アプリがLLMに、ユーザーの質問と「使えるツール一覧」を送る
- LLMが「この関数を、この引数で呼びたい」と応答する
- アプリ側がその関数を実際に実行し、結果をLLMに返す
- LLMが実行結果を踏まえて、最終的な回答を生成する
この往復があるため、Function Callingは構造化出力よりも一段階複雑な実装になります。
基本の書き方 / 実装手順
Laravel AI SDKでのツールクラス定義
PrismPHPでは、ツールをクラスとして定義できます。
DB検索を行うツールを例に見てみましょう。
// app/AiTools/SearchOrdersTool.php
namespace App\AiTools;
use App\Models\Order;
use Prism\Prism\Tool;
class SearchOrdersTool extends Tool
{
public function __construct()
{
$this
->as('search_orders')
->for('ユーザーIDと注文ステータスから注文一覧を検索する')
->withStringParameter('status', '注文ステータス(pending, shipped, completed のいずれか)')
->using($this);
}
public function __invoke(string $status): string
{
// 認可済みユーザー自身の注文だけに絞り込む(後述)
$orders = Order::query()
->where('user_id', auth()->id())
->where('status', $status)
->limit(10)
->get(['id', 'status', 'total_amount', 'created_at']);
return $orders->toJson();
}
}
ツールを使ったリクエスト
// app/Http/Controllers/AssistantController.php
namespace App\Http\Controllers;
use App\AiTools\SearchOrdersTool;
use Illuminate\Http\Request;
use Prism\Prism\Prism;
use Prism\Prism\Enums\Provider;
class AssistantController extends Controller
{
public function ask(Request $request)
{
$question = $request->input('question');
$response = Prism::text()
->using(Provider::OpenAI, 'gpt-4o-mini')
->withTools([new SearchOrdersTool()])
->withMaxSteps(3) // ツール呼び出し〜最終応答までの最大往復回数
->withPrompt($question)
->asText();
return response()->json(['answer' => $response->text]);
}
}
withMaxSteps() は、LLMがツールを呼び出してから最終的な回答を返すまでの往復回数の上限です。
ここを設定しておかないと、ツール呼び出しのループが想定外に長引くことがあるので必ず指定しておきましょう。
よくあるつまずきポイント・エラー対処
権限範囲を絞らずに実装して肝を冷やした話
最初にこの機能を試作したとき、SearchOrdersTool の中で user_id による絞り込みを入れずに、単純に status だけで全ユーザーの注文を検索するコードを書いていました。
動作確認中、テストユーザーで質問しただけなのに、他のダミーユーザーの注文まで一覧に含まれて返ってきたのです。
エラーにはならなかったものの、「これ本番でやったら個人情報漏洩事故だ」と血の気が引きました。
LLMは「悪意なく」ツールを呼び出しますが、渡した関数の権限範囲がそのままAIの行動範囲になる、ということを痛感した瞬間でした。
❌Before → ✅After
❌Before:ツール内でuser_idによる絞り込みを行わず、認可済みユーザー以外のデータまで検索できてしまった。
✅After:ツールの実行コードで必ずauth()->id()を条件に含め、AIが呼び出せる範囲をユーザー自身のデータに限定した。
// ❌Before:全ユーザーのデータにアクセスできてしまう
$orders = Order::where('status', $status)->get();
// ✅After:認可済みユーザー自身のデータに限定する
$orders = Order::where('user_id', auth()->id())
->where('status', $status)
->get();
ツールに渡す権限範囲を絞る重要性
Function Callingを実装する際は、「このツールが実行できる操作は最小限か」を必ず確認してください。
- 検索系ツールは、必ず認可済みユーザーのスコープ内に絞る
- 更新・削除系ツールは、追加の確認ステップを挟むか、そもそも自動実行させない
- 外部APIを叩くツールは、レートリミットやタイムアウトを明示的に設定する
これはセキュリティ全般に関わる重要な考え方なので、次回以降の記事でも改めて深掘りする予定です。
応用・一歩先の使い方
複数ツールの組み合わせ
withTools() には複数のツールクラスを配列で渡せます。
「注文検索ツール」と「在庫確認ツール」を両方渡しておけば、LLMは質問内容に応じて適切なツールを自分で選んで呼び出します。
ツール呼び出しのログを残す
どのツールが、誰によって、どんな引数で呼ばれたかは、__invoke の中でログに残しておくと、後から挙動を追跡しやすくなります。
public function __invoke(string $status): string
{
logger()->info('AIツール呼び出し', [
'tool' => 'search_orders',
'user_id' => auth()->id(),
'status' => $status,
]);
// 以下、検索処理
}
AIに操作を任せる以上、監査ログは事故対応の生命線になります。
まとめ
この記事のポイント
- Function CallingはLLMが「判断」、アプリが「実行」という役割分担の仕組み
- Laravel AI SDKではツールをクラスとして定義し、
withTools()で渡す - ツールの権限範囲を絞らないと、認可外のデータにアクセスできてしまう危険がある
次に読むべき記事
- 前の記事「構造化出力(Structured Output)でJSON形式の応答を得る」も合わせてどうぞ
- 次は「embeddingとは?文章の意味を数値化する仕組み」で、RAG実装の土台となる考え方を学びましょう
コメント