はじめに
こんにちは、ヱビスです。
Laracon US 2026で、Laravel AI SDKにHuman-in-the-loop(HITL)の仕組みが追加されたことが発表されました。Laravelの公式ドキュメントでは、この機能は Human Tool Approval という名前で紹介されています。
発表を見たとき、Xでは「AIに操作を任せるなら、人が確認する境界をどう設計するかが大切になりそう」という短い所感を投稿しました。
AIエージェントは、文章を生成するだけの存在ではありません。ツールを与えれば、メールの送信、データの更新、記事の公開、ファイルの削除といった処理まで実行できます。
便利になる一方で、AIが判断した操作をそのまま実行してよいのか、どこで人が確認するべきなのかは、業務システムへ組み込む前に考えておきたいところです。
そこで今回は、Laravel AI SDKのHuman Tool Approvalを確認しながら、次の点について整理します。
業務システムへAIエージェントを組み込むとき、どの操作を自動化し、どの操作に人間の承認を挟むべきか。
この記事で紹介する業務例は、Human Tool Approvalを業務システムへ組み込む場合の設計例であり、特定サービスの実装済み機能を紹介するものではありません。
Human-in-the-loopとは
Human-in-the-loopは、AIだけで処理を完結させず、判断や実行の途中へ人間を参加させる考え方です。
今回のLaravel AI SDKでは、AIエージェントがツールを呼び出そうとした時点で処理を止め、利用者が次の判断を返せます。
- 承認する
- 拒否する
- ツールへ渡す引数を変更する
たとえば、AIが障害情報の文章を作るところまでは自動で進め、実際に公開する直前で管理者の承認を求める、といった流れを作れます。
ここで大切なのは、AIの回答生成と、システムへ影響を与える操作を分けて考えることです。
文章の下書きが少し不自然でも、修正すれば済みます。一方で、誤った宛先へのメール送信や、必要なデータの削除は、実行後の取り消しが難しい場合があります。
AI活用の安全性は、モデルの性能だけでは決まりません。AIが実行できる操作と、人間の承認が必要な操作の境界設計も重要です。
Laravel AI SDKのHuman Tool Approval
Laravel AI SDKでは、承認待ちで停止した処理を後から再開するため、会話履歴を永続化できるConversationalエージェントを使用します。
公式ドキュメントでは、RemembersConversationsトレイトを使って会話履歴を保存する構成が紹介されています。
<?php
namespace App\Ai\Agents;
use App\Ai\Tools\PublishIncidentNotice;
use Laravel\Ai\Concerns\RemembersConversations;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\Conversational;
use Laravel\Ai\Contracts\HasTools;
use Laravel\Ai\Promptable;
use Stringable;
class OperationsAssistant implements Agent, Conversational, HasTools
{
use Promptable, RemembersConversations;
public function instructions(): Stringable|string
{
return '問い合わせ対応や障害告知を支援します。公開前には必ずツールの承認要件に従ってください。';
}
public function tools(): iterable
{
return [
new PublishIncidentNotice(),
];
}
}
RemembersConversationsを利用する前に、Laravel AI SDKの設定ファイルとマイグレーションを公開し、会話保存用のテーブルを作成しておく必要があります。
composer require laravel/ai
php artisan vendor:publish --provider="Laravel\Ai\AiServiceProvider"
php artisan migrate
会話を開始するときはforUser()を使い、レスポンスから会話IDを取得します。承認後は、その会話IDを使って処理を再開します。
AIの回答生成とツール実行は分けて考える
AIエージェントへ何を任せるか考えるとき、私は処理を次の2つに分けて考えます。
- 情報を作る処理
- 作った情報を使って外部へ影響を与える処理
たとえば、顧客向けメールを考える場合は次のようになります。
- 返信文の下書きを作る
- 顧客へメールを送信する
前者は、画面上へ候補を表示するだけであれば、失敗しても利用者が修正できます。後者は、宛先や内容を間違えると顧客へ直接影響します。
AIにメール作成を任せることと、メール送信まで任せることは、同じ「AIによるメール対応」でもリスクが異なります。
Human Tool Approvalは、この境界へ人間の確認を入れるための仕組みとして使えます。
Approvableでツール実行前に停止する
承認対象にするツールは、Approvableコントラクトを実装し、InteractsWithApprovalsトレイトを使用します。
次の例では、障害情報を公開するツールを承認対象にしています。
<?php
namespace App\Ai\Tools;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Concerns\InteractsWithApprovals;
use Laravel\Ai\Contracts\Approvable;
use Laravel\Ai\Contracts\Tool;
use Laravel\Ai\Tools\Request;
use Stringable;
class PublishIncidentNotice implements Approvable, Tool
{
use InteractsWithApprovals;
public function description(): Stringable|string
{
return '障害情報をサービスのお知らせとして公開します。';
}
public function handle(Request $request): Stringable|string
{
// 実際には認可、入力値の再検証、重複実行防止を行ってから公開します。
$notice = IncidentNotice::create([
'title' => $request['title'],
'body' => $request['body'],
'visibility' => $request['visibility'],
'published_at' => now(),
]);
return "障害情報 {$notice->id} を公開しました。";
}
public function schema(JsonSchema $schema): array
{
return [
'title' => $schema->string()->required(),
'body' => $schema->string()->required(),
'visibility' => $schema->string()->required(),
];
}
}
Approvableツールは、初期状態では実行前の承認が必要です。AIがこのツールを選んでも、handle()はすぐに呼ばれず、承認待ちのレスポンスが返ります。
なお、サンプルを簡潔にするため、モデルのuse文など一部を省略しています。
needsApproval()で条件付き承認にする
すべての操作を一律に承認制へすると、安全に見えるかもしれません。しかし、毎回同じような確認が続くと、内容を読まずに承認ボタンを押す「承認疲れ」が起きやすくなります。
リスクの低い操作は自動実行し、金額、件数、公開範囲、対象者などに応じて承認を求める方が実用的です。
needsApproval()を実装すると、ツールへ渡された引数に応じて承認の要否を切り替えられます。
use Laravel\Ai\Approvals\Approval;
use Laravel\Ai\Tools\Request;
protected function needsApproval(Request $request): Approval|bool
{
$ticketIds = $request['ticket_ids'] ?? [];
if (count($ticketIds) <= 5) {
return false;
}
return Approval::required(
sprintf('%d件の問い合わせステータスを一括変更します。', count($ticketIds))
);
}
この例では、5件以下なら自動実行し、6件以上なら承認を求めています。
ただし、AIから渡された件数や金額をそのまま信用してはいけません。承認後のhandle()でも、対象データをサーバー側で取得し直し、件数、権限、現在の状態を再確認する必要があります。
エージェントのtools()内で、個別に承認要件を上書きすることもできます。
public function tools(): iterable
{
return [
(new CreateReplyDraft())->withoutApproval(),
(new PublishIncidentNotice())->requireApproval('外部公開前の確認が必要です。'),
];
}
ツール側の標準設定と、エージェントごとの利用方針を分けられるため、同じツールでも用途に応じて扱いを変えられます。
保留中のツール名・引数・理由を確認する
承認対象のツールが呼び出されると、レスポンスのpendingApprovalsから保留中の操作を取得できます。
$response = (new OperationsAssistant())
->forUser($user)
->prompt('現在の障害状況をまとめて、利用者向けに公開してください。');
if ($response->hasPendingApprovals()) {
foreach ($response->pendingApprovals as $approval) {
logger()->info('AIツールが承認待ちになりました。', [
'tool_call_id' => $approval->id,
'tool' => $approval->tool,
'arguments' => $approval->arguments,
'reason' => $approval->reason,
]);
}
}
確認できる主な情報は次のとおりです。
- ツール呼び出しID
- ツール名
- 引数
- 承認が必要な理由
承認画面では、少なくとも「何を」「どの値で」「なぜ実行するのか」が分かるようにします。
障害情報の公開であれば、単に「公開を承認しますか」と表示するだけでは不十分です。タイトル、本文、公開範囲、公開対象サービスなど、実行結果へ影響する値を確認できるようにします。
承認・拒否後に会話を再開する
利用者が判断した後は、continue()で会話を再開し、Decisionsへ各ツール呼び出しの判断を渡します。
use Laravel\Ai\Approvals\Decision;
use Laravel\Ai\Approvals\Decisions;
$decisions = Decisions::from([
'call_publish_incident' => Decision::approve(),
]);
$response = (new OperationsAssistant())
->continue($conversationId, as: $user)
->prompt($decisions);
拒否するときは、理由を返すこともできます。
$decisions = Decisions::from([
'call_publish_incident' => Decision::reject(
'影響範囲がまだ確定していないため、公開せず下書きに戻してください。'
),
]);
拒否理由をモデルへ返した場合、モデルは理由を踏まえて別の提案を考え、会話を続けられます。理由を付けずに拒否した場合は、拒否を記録した時点で生成ループが停止します。
引数を修正してから実行する場合は、Decision::edit()を使用します。
$decisions = Decisions::from([
'call_publish_incident' => Decision::edit([
'title' => '一部機能で発生している接続障害について',
'body' => $reviewedBody,
'visibility' => 'affected_users',
]),
]);
引数変更のAPIはパッケージ更新で変わる可能性があるため、実装時には使用しているlaravel/aiのバージョンと公式ドキュメント、パッケージソースを確認してください。
すべての保留中ツール呼び出しに判断を返す必要があります。存在しないID、判断漏れ、すでに処理済みのIDが含まれる場合は、ApprovalMismatchExceptionが発生します。
複数のツールが保留になり得る画面では、表示時点のIDをそのまま保持し、承認送信時に勝手に再生成しないようにします。
非同期処理でも承認を挟める
Human Tool Approvalは、次の実行形式で利用できます。
promptstreamqueuebroadcastbroadcastNowbroadcastOnQueue
ストリーミングやブロードキャストでは、停止がtool_approval_requestイベントとして表現されます。キュー実行では、ToolApprovalRequestedイベントも発行されます。
承認待ちを画面へリアルタイム通知したい場合や、管理者向けの承認一覧を作りたい場合は、このイベントを起点に通知や監査ログを作れます。
AIに任せてよい操作と承認が必要な操作
私は、AIへ渡す操作を次の4段階に分けると整理しやすいと考えています。
| 区分 | 操作例 | 方針 |
|---|---|---|
| 自動実行 | 要約、分類、下書き、候補抽出 | 原則として自動実行します |
| 条件付き承認 | 少額更新、少人数への通知、限定範囲の一括変更 | 金額、件数、対象、公開範囲で判断します |
| 必須承認 | 公開、メール送信、削除、返金、権限変更 | 実行前に必ず人が確認します |
| AIに許可しない | 秘密情報の無制限取得、管理者権限の付与 | ツール自体をエージェントへ渡しません |
この分類で特に重要なのは、最後の「AIに許可しない」です。
承認を付ければ、どの操作でもAIへ提供してよいわけではありません。用途として不要な操作や、影響範囲を限定できない操作は、ツールとして登録しない方が安全です。
AIエージェントは、与えられていないツールを実行できません。承認条件を細かくする前に、まずツールの種類と権限を絞ることが基本になります。
公開・通知・削除を想定した設計例
公開
障害情報、メンテナンス情報、記事などの公開は、利用者や取引先から見える状態を変える操作です。
下書き作成は自動化しても、公開前には次の項目を確認した方がよいです。
- タイトルと本文
- 公開範囲
- 対象サービス
- 公開日時
- 個人情報や社内情報が含まれていないか
通知とメール送信
メールやサービス内通知は、送信後に内容を差し替えられません。
少なくとも次の値を承認画面へ表示します。
- 送信先
- 対象件数
- 件名
- 本文
- 配信チャネル
大量送信では、承認とは別に送信上限、分割実行、配信停止、重複除外も必要です。
削除
削除は不可逆になりやすいため、原則として必須承認にします。
可能であれば、いきなり物理削除せず、論理削除や一定期間の保留を挟みます。承認画面には、対象IDだけでなく、人が判断できる名称や現在の状態も表示した方が確認しやすくなります。
二重実行と承認ログに注意する
承認画面を作るときに気を付けたいのが、二重実行です。
Laravel AI SDKは、承認されたツールの実行結果をモデルへ処理を戻す前に保存します。その後の文章生成でエラーが発生しても、ツールの承認と実行はすでに完了している場合があります。
この状態で同じ承認情報を再送すると、すでに解決済みのツール呼び出しとして扱われます。再開するときは、通常のメッセージとして会話を続ける必要があります。
さらに、メール送信、返金、外部API呼び出しなどは、アプリケーション側でも冪等にしておきます。
実務では、次の対策を組み合わせます。
- ツール呼び出しIDへ一意制約を付ける
- 承認ボタンを押した後は再送を防止する
- 同じ冪等キーによる外部処理を1回に限定する
- 承認とツール実行を別々の状態として記録する
- タイムアウト時に、外部処理の成否を照会してから再試行する
監査ログには、少なくとも次の情報を残しておきたいところです。
conversation_id
agent
ツール呼び出しID
ツール名
変更前の引数
承認後の引数
承認・拒否の結果
拒否理由
承認者
承認日時
ツール実行日時
実行結果またはエラー
tool_call_idへ一意制約を設けておくと、二重クリックやジョブのリトライによる重複実行を防ぐ材料になります。
Human Tool Approvalは認証・認可の代わりではない
承認画面があるからといって、通常の認証・認可を省略してはいけません。
公式ドキュメントの承認フロー例でも、会話を参照するルートでGate::authorize()と認証ミドルウェアが使われています。
承認処理では、次の3段階を分けて確認します。
- 承認画面を開ける利用者か
- そのツールを承認できる権限があるか
- 承認後の引数で実際の操作を実行できるか
AIが作った引数は、承認された後も信頼済みの値にはなりません。Form Request、Policy、ドメインサービスなどを使い、通常の画面操作と同じようにサーバー側で検証します。
問い合わせ管理サービスへ応用する場合
たとえば、小規模な問い合わせ管理サービスへHuman Tool Approvalの考え方を取り入れる場合、次のように分けられそうです。
自動実行してよい処理
- 問い合わせ内容の分類
- 顧客向け返信の下書き
- 障害情報や更新情報の下書き
- 過去の対応履歴の要約
- FAQ候補の作成
これらは、結果を人が確認してから使えるため、基本的には自動化しやすい処理です。
承認が必要な処理
- 顧客へのメール送信
- 障害情報の公開
- サービス内通知の配信
- 顧客データの更新
- 問い合わせステータスの一括変更
- データの削除
たとえば、障害情報を公開する流れは次のようになります。
- AIが障害告知の下書きを作ります
- AIが公開ツールを呼び出そうとします
- Laravelがツール実行前に処理を止めます
- 管理者がタイトル、本文、公開範囲を確認します
- 必要であれば引数を修正します
- 承認後に公開します
- 承認者と公開結果を監査ログへ残します
AIは文章作成の時間を短縮し、人は外部へ影響する最終判断に集中できます。
すべてを自動化するのでも、すべてを人が処理するのでもなく、失敗したときの影響に応じて役割を分ける形です。
承認待ち状態をテストする
承認機能は画面と非同期処理をまたぐため、正常系だけでなく、承認漏れや再送もテストしておきたいところです。
Laravel AI SDKでは、AgentResponse::fakeWithPendingApprovals()を使って承認待ちのレスポンスを作れます。
use Laravel\Ai\Approvals\PendingApproval;
use Laravel\Ai\Responses\AgentResponse;
OperationsAssistant::fake([
AgentResponse::fakeWithPendingApprovals([
new PendingApproval(
id: 'call_publish_incident',
tool: 'PublishIncidentNotice',
arguments: [
'title' => '接続障害について',
'body' => '現在、一部機能へ接続しづらい状態です。',
'visibility' => 'public',
],
reason: '外部公開前の確認が必要です。',
),
]),
]);
$response = (new OperationsAssistant())
->prompt('障害情報を公開してください。');
expect($response->hasPendingApprovals())->toBeTrue();
expect($response->pendingApprovals)->toHaveCount(1);
公開前には、少なくとも次の項目を確認しておくと安心です。
-
Approvableによって実行前に停止すること -
needsApproval()の条件どおりに分岐すること -
pendingApprovalsからID、ツール名、引数、理由を取得できること - 承認後にツールが1回だけ実行されること
- 引数を変更した場合、変更後の値が再検証されること
- 拒否理由を返した場合、モデルが別の提案を返すこと
- 理由なしで拒否した場合、生成ループが停止すること
- 承認漏れ、不正なID、処理済みIDで例外になること
- 二重送信やジョブのリトライで外部処理が重複しないこと
まとめ
Laravel AI SDKのHuman Tool Approvalを使うと、AIエージェントがツールを実行する直前で処理を止め、人間の承認、拒否、引数変更を挟めます。
ただし、承認機能を追加するだけで、安全なAI機能になるわけではありません。
- リスクの低い作業は自動化する
- 外部への影響や不可逆性がある操作は承認対象にする
- 必要のない危険なツールはAIへ提供しない
- 承認画面にも通常の認証・認可を適用する
- 承認後も引数をサーバー側で再検証する
- 外部処理を冪等にし、監査ログを残す
こうした境界を設計して初めて、AIの便利さを業務システムの中で安全に使いやすくなります。
AIに何ができるかだけでなく、AIにどこまでさせるかを決めることが、これからの業務システム開発では重要になりそうです。