> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Laravel AI SDK

> Laravel AI SDK 的完整指南，可以統一介面處理 OpenAI、Anthropic、Gemini 等多個 AI 供應商。涵蓋 Agent、圖片生成、語音、Embedding 及測試。

## 前言

[Laravel AI SDK](https://github.com/laravel/ai) 為與 OpenAI、Anthropic、Gemini 等 AI 供應商對話提供了統一且表達力豐富的 API。使用 AI SDK，可以以一致且富有 Laravel 風格的介面實現：具備工具與結構化輸出的智慧 Agent 建構、圖片生成、語音合成/文字轉錄、向量 Embedding 建立等多樣化 AI 功能。

<Info>
  Laravel AI SDK 是 Laravel 13 新增的官方套件。以 `laravel/ai` 提供，可以統一 API 處理多個 AI 供應商。
</Info>

## 供應商支援一覽

| 功能        | 對應供應商                                                                                                          |
| --------- | -------------------------------------------------------------------------------------------------------------- |
| 文字生成      | OpenAI, OpenAI Compatible, Anthropic, Gemini, Azure, Bedrock, Groq, xAI, DeepSeek, Mistral, Ollama, OpenRouter |
| 圖片生成      | OpenAI, Gemini, xAI, Azure, Bedrock, OpenRouter                                                                |
| 語音合成（TTS） | OpenAI, ElevenLabs, Gemini                                                                                     |
| 語音辨識（STT） | OpenAI, ElevenLabs, Mistral, Gemini                                                                            |
| Embedding | OpenAI, Gemini, Azure, Bedrock, Cohere, Mistral, Jina, VoyageAI, Ollama, OpenRouter                            |
| Reranking | Cohere, Jina, VoyageAI                                                                                         |
| 檔案        | OpenAI, Anthropic, Gemini, Azure                                                                               |

## 安裝

<Steps>
  <Step title="套件安裝">
    以 Composer 安裝 Laravel AI SDK。

    ```shell theme={null}
    composer require laravel/ai
    ```
  </Step>

  <Step title="公開設定檔與 Migration">
    以 `vendor:publish` Artisan 指令公開設定檔與 Migration。

    ```shell theme={null}
    php artisan vendor:publish --provider="Laravel\Ai\AiServiceProvider"
    ```
  </Step>

  <Step title="執行 Migration">
    執行資料庫 Migration。會建立 `agent_conversations` 與 `agent_conversation_messages` 表格，用於保存對話歷史。

    ```shell theme={null}
    php artisan migrate
    ```
  </Step>
</Steps>

## 設定

### 環境變數

於 `.env` 檔案設定要使用的 AI 供應商 API 金鑰。

```ini theme={null}
ANTHROPIC_API_KEY=
AZURE_OPENAI_API_KEY=
COHERE_API_KEY=
DEEPSEEK_API_KEY=
ELEVENLABS_API_KEY=
GEMINI_API_KEY=
GROQ_API_KEY=
MISTRAL_API_KEY=
OLLAMA_API_KEY=
OPENAI_API_KEY=
OPENROUTER_API_KEY=
JINA_API_KEY=
VOYAGEAI_API_KEY=
XAI_API_KEY=
```

文字、圖片、語音、轉錄、Embedding 所使用的預設模型也可於 `config/ai.php` 設定。

### 自訂 base URL

若要透過 Proxy 服務，可為各供應商設定自訂 URL。

```php theme={null}
'providers' => [
    'openai' => [
        'driver' => 'openai',
        'key' => env('OPENAI_API_KEY'),
        'url' => env('OPENAI_URL'),
    ],
    'anthropic' => [
        'driver' => 'anthropic',
        'key' => env('ANTHROPIC_API_KEY'),
        'url' => env('ANTHROPIC_BASE_URL'),
    ],
],
```

OpenAI、Anthropic、Gemini、Groq、Cohere、DeepSeek、xAI、OpenRouter 皆可使用自訂 base URL。

### OpenAI-Compatible 供應商

若使用 LM Studio、vLLM、Together、Fireworks、本地 Gateway 等 OpenAI 相容 API，可以 `openai-compatible` driver 設定供應商。`url` 為必要，若指定 `key` 則會作為 Bearer token 傳送。

```php theme={null}
'providers' => [
    'local' => [
        'driver' => 'openai-compatible',
        'url' => env('LOCAL_AI_URL'),
        'key' => env('LOCAL_AI_API_KEY'),
    ],
],
```

設定後可與其他供應商同樣以供應商名稱指定。

```php theme={null}
agent()->prompt('What is Laravel?', provider: 'local', model: 'local-model');
```

若設定預設文字模型，可以不必每次指定模型。

```php theme={null}
'local' => [
    'driver' => 'openai-compatible',
    'url' => env('LOCAL_AI_URL'),
    'key' => env('LOCAL_AI_API_KEY'),
    'models' => [
        'text' => [
            'default' => env('LOCAL_AI_MODEL'),
        ],
    ],
],
```

OpenAI-Compatible 供應商支援文字生成、串流、Tool、結構化輸出與圖片附加。若端點需要額外的請求 body 欄位，請使用[供應商選項](#供應商選項)。

### Lab enum

於程式碼中參照供應商可使用 `Lab` enum。

```php theme={null}
use Laravel\Ai\Enums\Lab;

Lab::Anthropic;
Lab::OpenAI;
Lab::Gemini;
```

***

## Agent

Agent 是 Laravel AI SDK 的基本組成要素。可以 `make:agent` 指令產生 Agent 類別。

```shell theme={null}
php artisan make:agent SalesCoach

# 產生附有結構化輸出的 Agent
php artisan make:agent SalesCoach --structured
```

產生的 Agent 位於 `app/Ai/Agents/` 目錄。以下為實作了主要介面的 Agent 範例。

```php theme={null}
<?php

namespace App\Ai\Agents;

use App\Ai\Tools\RetrievePreviousTranscripts;
use App\Models\History;
use App\Models\User;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\Conversational;
use Laravel\Ai\Contracts\HasStructuredOutput;
use Laravel\Ai\Contracts\HasTools;
use Laravel\Ai\Messages\Message;
use Laravel\Ai\Promptable;
use Stringable;

class SalesCoach implements Agent, Conversational, HasTools, HasStructuredOutput
{
    use Promptable;

    public function __construct(public User $user) {}

    public function instructions(): Stringable|string
    {
        return 'You are a sales coach, analyzing transcripts and providing feedback and an overall sales strength score.';
    }

    public function messages(): iterable
    {
        return History::where('user_id', $this->user->id)
            ->latest()
            ->limit(50)
            ->get()
            ->reverse()
            ->map(function ($message) {
                return new Message($message->role, $message->content);
            })->all();
    }

    public function tools(): iterable
    {
        return [new RetrievePreviousTranscripts];
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'feedback' => $schema->string()->required(),
            'score' => $schema->integer()->min(1)->max(10)->required(),
        ];
    }
}
```

```mermaid theme={null}
flowchart TD
    A["使用者的 prompt"] --> B["Agent::prompt()"]
    B --> C["附加選項"]
    C -->|"Conversational 時"| D["附加對話歷史"]
    C -->|"HasTools 時"| E["附加工具清單"]
    C -->|"HasStructuredOutput 時"| F["附加 JSON schema"]
    D --> G["送往 AI 供應商"]
    E --> G
    F --> G
    B --> G
    G --> H["Response"]
    H -->|"工具呼叫"| I["執行工具"]
    I --> G
    H -->|"完成"| J["AgentResponse"]
```

### Prompt

以 `prompt()` 方法對 Agent 送出訊息。

```php theme={null}
$response = (new SalesCoach)->prompt('Analyze this sales transcript...');
return (string) $response;
```

使用 `make()` 靜態方法可從容器解析相依並產生實例。

```php theme={null}
$agent = SalesCoach::make(user: $user);
```

供應商、模型、Timeout 可透過 `prompt()` 的引數覆寫。

```php theme={null}
$response = (new SalesCoach)->prompt(
    'Analyze this sales transcript...',
    provider: Lab::Anthropic,
    model: 'claude-haiku-4-5-20251001',
    timeout: 120,
);
```

### 對話 Context

若實作 `Conversational` 介面並定義 `messages()` 方法，可將過去的對話歷史傳給 AI。

使用 `RemembersConversations` trait，可將對話歷史自動保存至資料庫並取用。

```php theme={null}
use Laravel\Ai\Concerns\RemembersConversations;

class SalesCoach implements Agent, Conversational
{
    use Promptable, RemembersConversations;

    public function instructions(): string
    {
        return 'You are a sales coach...';
    }
}
```

以 `forUser()` 開始對話，並使用回傳的 `conversationId` 透過 `continue()` 繼續對話。

```php theme={null}
$response = (new SalesCoach)->forUser($user)->prompt('Hello!');
$conversationId = $response->conversationId;

$response = (new SalesCoach)->continue($conversationId, as: $user)->prompt('Tell me more about that.');
```

### 結構化輸出

實作 `HasStructuredOutput` 介面並於 `schema()` 方法定義 JSON schema，可以取得結構化資料形式的 AI 回應。

```php theme={null}
public function schema(JsonSchema $schema): array
{
    return ['score' => $schema->integer()->required()];
}

$response = (new SalesCoach)->prompt('Analyze this...');
return $response['score'];
```

#### 巢狀物件

```php theme={null}
public function schema(JsonSchema $schema): array
{
    return [
        'score' => $schema->integer()->required(),
        'metadata' => $schema->object(fn ($schema) => [
            'confidence' => $schema->string()->enum(['low', 'medium', 'high'])->required(),
            'language' => $schema->string()->required(),
        ])->required(),
    ];
}
```

#### 物件的陣列

```php theme={null}
public function schema(JsonSchema $schema): array
{
    return [
        'feedback' => $schema->array()->items(
            $schema->object(fn ($schema) => [
                'comment' => $schema->string()->required(),
                'score' => $schema->integer()->required(),
            ])
        )->required(),
    ];
}
```

#### anyOf（多個 schema 的選擇）

若值符合多個 schema 中任一者，可使用 `anyOf` 方法。

```php theme={null}
public function schema(JsonSchema $schema): array
{
    return [
        'content' => $schema->anyOf([
            $schema->object(fn ($schema) => [
                'type' => $schema->string()->enum(['article'])->required(),
                'title' => $schema->string()->required(),
            ]),
            $schema->object(fn ($schema) => [
                'type' => $schema->string()->enum(['image'])->required(),
                'url' => $schema->string()->required(),
            ]),
        ])->required(),
    ];
}
```

### 附加檔案

可透過 `attachments` 引數將文件或圖片交給 Agent。

```php theme={null}
use Laravel\Ai\Files;

$response = (new SalesCoach)->prompt(
    'Analyze the attached sales transcript...',
    attachments: [
        Files\Document::fromStorage('transcript.pdf'),
        Files\Document::fromPath('/home/laravel/transcript.md'),
        $request->file('transcript'),
    ]
);
```

圖片附加也同樣可行。

```php theme={null}
$response = (new ImageAnalyzer)->prompt('What is in this image?', attachments: [
    Files\Image::fromStorage('photo.jpg'),
    Files\Image::fromPath('/home/laravel/photo.jpg'),
    $request->file('photo'),
]);
```

### 串流

使用 `stream()` 方法可以以 chunk 為單位回傳回應。適合將長回應即時傳送至前端。

```php theme={null}
Route::get('/coach', function () {
    return (new SalesCoach)->stream('Analyze this sales transcript...');
});
```

以 `then()` callback 可以撰寫串流完成後的處理。

```php theme={null}
use Laravel\Ai\Responses\StreamedAgentResponse;

Route::get('/coach', function () {
    return (new SalesCoach)
        ->stream('Analyze this sales transcript...')
        ->then(function (StreamedAgentResponse $response) {
            // $response->text, $response->events, $response->usage...
        });
});
```

也可以手動迭代串流。

```php theme={null}
$stream = (new SalesCoach)->stream('Analyze this sales transcript...');

foreach ($stream as $event) {
    // ...
}
```

#### Vercel AI SDK 協定

若前端使用 Vercel AI SDK，可呼叫 `usingVercelDataProtocol()`。

```php theme={null}
Route::get('/coach', function () {
    return (new SalesCoach)->stream('Analyze...')->usingVercelDataProtocol();
});
```

### 廣播

可以將 stream 的事件傳送至 Laravel Echo 等 Broadcasting 頻道。

```php theme={null}
use Illuminate\Broadcasting\Channel;

$stream = (new SalesCoach)->stream('Analyze this sales transcript...');

foreach ($stream as $event) {
    $event->broadcast(new Channel('channel-name'));
}
```

使用 `broadcastOnQueue()` 可透過 Queue 進行廣播。

```php theme={null}
(new SalesCoach)->broadcastOnQueue(
    'Analyze this sales transcript...',
    new Channel('channel-name'),
);
```

#### 跳過巨大事件

某些廣播平台將 WebSocket 訊息限制在約 10KB。像大型工具結果等資料量大的 stream 事件可能超過此上限而廣播失敗。可使用 `WithoutBroadcasting` Attribute 將特定事件類型從廣播中排除。

```php theme={null}
<?php

namespace App\Ai\Agents;

use Laravel\Ai\Attributes\WithoutBroadcasting;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasTools;
use Laravel\Ai\Promptable;
use Laravel\Ai\Streaming\Events\ToolCall;
use Laravel\Ai\Streaming\Events\ToolResult;

#[WithoutBroadcasting(ToolCall::class, ToolResult::class)]
class SearchAgent implements Agent, HasTools
{
    use Promptable;

    // ...
}
```

被排除的事件不會廣播，但仍會持續保存至 `agent_conversation_messages` 表。因此前端可於 stream 完成後取得工具的全部資料。此機制於透過 Queue（`broadcastOnQueue`）與同步（`broadcast` / `broadcastNow`）均可運作。

### Queue

以 `queue()` 方法將 prompt 送入 Queue 可進行非同步處理。

```php theme={null}
use Laravel\Ai\Responses\AgentResponse;

Route::post('/coach', function (Request $request) {
    (new SalesCoach)
        ->queue($request->input('transcript'))
        ->then(function (AgentResponse $response) { /* ... */ })
        ->catch(function (Throwable $e) { /* ... */ });

    return back();
});
```

### Tool

使用 Tool 可讓 AI 呼叫程式碼中的函式。可以 `make:tool` 指令產生 Tool 類別。

```shell theme={null}
php artisan make:tool RandomNumberGenerator
```

```php theme={null}
<?php

namespace App\Ai\Tools;

use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Contracts\Tool;
use Laravel\Ai\Tools\Request;
use Stringable;

class RandomNumberGenerator implements Tool
{
    public function description(): Stringable|string
    {
        return 'This tool may be used to generate cryptographically secure random numbers.';
    }

    public function handle(Request $request): Stringable|string
    {
        return (string) random_int($request['min'], $request['max']);
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'min' => $schema->integer()->min(0)->required(),
            'max' => $schema->integer()->required(),
        ];
    }
}
```

於 Agent 的 `tools()` 方法註冊 Tool。

```php theme={null}
public function tools(): iterable
{
    return [new RandomNumberGenerator];
}
```

#### 相似度搜尋工具

可輕鬆加入使用向量 Embedding 的相似度搜尋工具。

```php theme={null}
use App\Models\Document;
use Laravel\Ai\Tools\SimilaritySearch;

public function tools(): iterable
{
    return [
        SimilaritySearch::usingModel(Document::class, 'embedding'),
    ];
}
```

亦可指定選項。

```php theme={null}
SimilaritySearch::usingModel(
    model: Document::class,
    column: 'embedding',
    minSimilarity: 0.7,
    limit: 10,
    query: fn ($query) => $query->where('published', true),
),
```

也可以 closure 定義自訂搜尋邏輯。

```php theme={null}
new SimilaritySearch(using: function (string $query) {
    return Document::query()
        ->where('user_id', $this->user->id)
        ->whereVectorSimilarTo('embedding', $query)
        ->limit(10)
        ->get();
}),
```

可以 `withDescription()` 自訂工具的描述。

```php theme={null}
SimilaritySearch::usingModel(Document::class, 'embedding')
    ->withDescription('Search the knowledge base for relevant articles.'),
```

### 檔案儲存工具

使用 `FileStorage` 工具 factory 可對 Agent 授予存取 Laravel [檔案系統 disk](/zh-TW/filesystem) 的權限。`all` 方法回傳一組可對指定 disk 上的檔案進行列示、讀取、URL 生成、寫入、刪除、複製的工具集。

```php theme={null}
use Laravel\Ai\Tools\FileStorage;

public function tools(): iterable
{
    return FileStorage::all('local');
}
```

若僅授予唯讀存取，可使用 `readOnly` 方法。

```php theme={null}
return FileStorage::readOnly('local');
```

由於這些方法回傳 `Illuminate\Support\Collection`，可以進一步縮限提供的工具。

```php theme={null}
use Laravel\Ai\Tools\Filesystem\DeleteFile;

return FileStorage::all('s3')
    ->reject(fn ($tool) => $tool instanceof DeleteFile);
```

### MCP 工具

若應用程式使用 [Laravel MCP](/zh-TW/mcp)，可將 [Model Context Protocol](https://modelcontextprotocol.io) 伺服器公開的工具提供給 Agent。可使用 [Laravel MCP Client](/zh-TW/mcp#mcp-client)連線至遠端或本地 MCP 伺服器，並將其工具直接交給 Agent。

<Info>
  要使用 MCP 工具，需於應用程式安裝 [Laravel MCP](/zh-TW/mcp) 套件。
</Info>

MCP client 的 `tools` 方法回傳 Collection，因此使用 `...` spread 運算子展開至 Agent 的 `tools` 陣列。

```php theme={null}
use App\Ai\Tools\RandomNumberGenerator;
use Laravel\Mcp\Client;

/**
 * Get the tools available to the agent.
 *
 * @return Tool[]
 */
public function tools(): iterable
{
    return [
        ...Client::web('https://mcp.example.com')
            ->withToken($token)
            ->tools(),

        new RandomNumberGenerator,
    ];
}
```

AI SDK 會自動包裝各 MCP 工具，讓 Agent 可以與其他工具同樣的方式呼叫。也可以使用[具名 MCP client](/zh-TW/mcp#具名-client)。

```php theme={null}
use Laravel\Mcp\Facades\Mcp;

public function tools(): iterable
{
    return [
        ...Mcp::client('github')->tools(),
    ];
}
```

或者也可以連線至[本地 MCP 伺服器](/zh-TW/mcp#本地伺服器)。

```php theme={null}
use Laravel\Mcp\Client;

public function tools(): iterable
{
    return [
        ...Client::local('php', ['artisan', 'mcp:start'])->tools(),
    ];
}
```

MCP client 的建立與驗證（Bearer token 或 OAuth 等）詳情請參考 [MCP client 文件](/zh-TW/mcp#mcp-client)。

### 供應商工具

由 AI 供應商原生實作的特殊工具。

#### Web Search

為 Agent 加入網頁搜尋。支援 Anthropic、OpenAI、Gemini、OpenRouter。

```php theme={null}
use Laravel\Ai\Providers\Tools\WebSearch;

public function tools(): iterable
{
    return [new WebSearch];
}
```

可以選項指定搜尋件數、限制網域、位置資訊。

```php theme={null}
(new WebSearch)->max(5)->allow(['laravel.com', 'php.net']),
(new WebSearch)->location(city: 'New York', region: 'NY', country: 'US');
```

#### Web Fetch

取得指定 URL 內容的工具。支援 Anthropic、Gemini。

```php theme={null}
use Laravel\Ai\Providers\Tools\WebFetch;

public function tools(): iterable
{
    return [new WebFetch];
}

(new WebFetch)->max(3)->allow(['docs.laravel.com']),
```

#### File Search

從向量儲存搜尋文件的工具。支援 OpenAI、Gemini。

```php theme={null}
use Laravel\Ai\Providers\Tools\FileSearch;

public function tools(): iterable
{
    return [new FileSearch(stores: ['store_id'])];
}

// 指定多個 Store
new FileSearch(stores: ['store_1', 'store_2']);

// 以 metadata 篩選
new FileSearch(stores: ['store_id'], where: ['author' => 'Taylor Otwell', 'year' => 2026]);
```

也可用 `FileSearchQuery` 指定複雜篩選。

```php theme={null}
use Laravel\Ai\Providers\Tools\FileSearchQuery;

new FileSearch(stores: ['store_id'], where: fn (FileSearchQuery $query) =>
    $query->where('author', 'Taylor Otwell')
          ->whereNot('status', 'draft')
          ->whereIn('category', ['news', 'updates'])
);
```

### Sub-Agent

Agent 也可從其他 Agent 的 `tools()` 方法回傳。若將 Agent 註冊為 Tool，父 Agent 可將特定任務委派給 Sub-Agent，並將其結果納入原本的回應。適用於通用 Agent 需存取具備專門指示、工具、模型設定、供應商設定的特化型 Agent 時。

例如客服 Agent 將關於退款政策的問題委派給退款專門 Agent 的範例。

```php theme={null}
<?php

namespace App\Ai\Agents;

use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasTools;
use Laravel\Ai\Promptable;

class CustomerSupportAgent implements Agent, HasTools
{
    use Promptable;

    public function instructions(): string
    {
        return 'You help customers with account, order, and billing questions. Delegate refund policy questions to the refunds specialist.';
    }

    public function tools(): iterable
    {
        return [
            new RefundsAgent,
        ];
    }
}
```

若要自訂 Sub-Agent 對父 Agent 的呈現方式，可於 Sub-Agent 實作 `CanActAsTool` 介面，並定義工具用的名稱與描述。

```php theme={null}
<?php

namespace App\Ai\Agents;

use App\Ai\Tools\LookupOrder;
use Laravel\Ai\Attributes\Provider;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\CanActAsTool;
use Laravel\Ai\Contracts\HasTools;
use Laravel\Ai\Enums\Lab;
use Laravel\Ai\Promptable;

#[Provider(Lab::Anthropic)]
class RefundsAgent implements Agent, CanActAsTool, HasTools
{
    use Promptable;

    public function instructions(): string
    {
        return 'You are a refunds specialist. Use order details and the refund policy to give concise eligibility guidance.';
    }

    public function name(): string
    {
        return 'refunds_specialist';
    }

    public function description(): string
    {
        return 'Determine whether an order is eligible for a refund and explain the next step.';
    }

    public function tools(): iterable
    {
        return [
            new LookupOrder,
        ];
    }
}
```

若未實作 `CanActAsTool` 的 Sub-Agent，Laravel 會使用類別名稱作為工具名稱，並自動產生通用的描述文字。每個 Sub-Agent 的呼叫獨立進行，不會沿用父 Agent 的對話歷史。

### 中介層

可為 Agent 加入中介層，攔截 prompt 或回應。

```shell theme={null}
php artisan make:agent-middleware LogPrompts
```

於 Agent 實作 `HasMiddleware` 介面，並於 `middleware()` 方法註冊中介層。

```php theme={null}
<?php

namespace App\Ai\Agents;

use App\Ai\Middleware\LogPrompts;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasMiddleware;
use Laravel\Ai\Promptable;

class SalesCoach implements Agent, HasMiddleware
{
    use Promptable;

    public function middleware(): array
    {
        return [new LogPrompts];
    }
}
```

以下為中介層類別的實作範例。

```php theme={null}
<?php

namespace App\Ai\Middleware;

use Closure;
use Laravel\Ai\Prompts\AgentPrompt;

class LogPrompts
{
    public function handle(AgentPrompt $prompt, Closure $next)
    {
        Log::info('Prompting agent', ['prompt' => $prompt->prompt]);
        return $next($prompt);
    }
}
```

使用 `then()` 也可加入回應後的處理。

```php theme={null}
public function handle(AgentPrompt $prompt, Closure $next)
{
    return $next($prompt)->then(function (AgentResponse $response) {
        Log::info('Agent responded', ['text' => $response->text]);
    });
}
```

### 匿名 Agent

不定義類別，可用 `agent()` helper 使用匿名 Agent。

```php theme={null}
use function Laravel\Ai\{agent};

$response = agent(
    instructions: 'You are an expert at software development.',
    messages: [],
    tools: [],
)->prompt('Tell me about Laravel');
```

也可建立附有結構化輸出的匿名 Agent。

```php theme={null}
use Illuminate\Contracts\JsonSchema\JsonSchema;

$response = agent(
    schema: fn (JsonSchema $schema) => ['number' => $schema->integer()->required()],
)->prompt('Generate a random number less than 100');
```

### Agent 設定（PHP Attributes）

可透過 PHP Attribute 宣告式地撰寫 Agent 的預設設定。

| Attribute             | 說明                      |
| --------------------- | ----------------------- |
| `#[Provider]`         | 指定使用的供應商                |
| `#[Model]`            | 指定使用的模型                 |
| `#[MaxSteps]`         | 工具呼叫的最大步驟數              |
| `#[MaxTokens]`        | 最大 token 數              |
| `#[Temperature]`      | temperature 參數（0.0〜1.0） |
| `#[TopP]`             | 核採樣的機率（0.0〜1.0）         |
| `#[Timeout]`          | Timeout（秒）              |
| `#[UseCheapestModel]` | 自動選擇最便宜的模型              |
| `#[UseSmartestModel]` | 自動選擇最高效能的模型             |

```php theme={null}
<?php

namespace App\Ai\Agents;

use Laravel\Ai\Attributes\MaxSteps;
use Laravel\Ai\Attributes\MaxTokens;
use Laravel\Ai\Attributes\Model;
use Laravel\Ai\Attributes\Provider;
use Laravel\Ai\Attributes\Temperature;
use Laravel\Ai\Attributes\Timeout;
use Laravel\Ai\Attributes\TopP;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Enums\Lab;
use Laravel\Ai\Promptable;

#[Provider(Lab::Anthropic)]
#[Model('claude-haiku-4-5-20251001')]
#[MaxSteps(10)]
#[MaxTokens(4096)]
#[Temperature(0.7)]
#[Timeout(120)]
#[TopP(0.9)]
class SalesCoach implements Agent
{
    use Promptable;
}
```

模型選擇的捷徑 Attribute 也已備妥。

```php theme={null}
// 使用最便宜的模型
#[UseCheapestModel]
class SimpleSummarizer implements Agent
{
    use Promptable;
}

// 使用最高效能的模型
#[UseSmartestModel]
class ComplexReasoner implements Agent
{
    use Promptable;
}
```

### 供應商選項

實作 `HasProviderOptions` 介面可傳遞供應商特有的選項。

```php theme={null}
<?php

namespace App\Ai\Agents;

use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasProviderOptions;
use Laravel\Ai\Enums\Lab;
use Laravel\Ai\Promptable;

class SalesCoach implements Agent, HasProviderOptions
{
    use Promptable;

    public function providerOptions(Lab|string $provider): array
    {
        return match ($provider) {
            Lab::OpenAI => [
                'reasoning' => ['effort' => 'low'],
                'frequency_penalty' => 0.5,
                'presence_penalty' => 0.3,
            ],
            Lab::Anthropic => [
                'thinking' => ['budget_tokens' => 1024],
            ],
            default => [],
        };
    }
}
```

***

## 人工承認（Human Tool Approval）

<Warning>
  使用工具承認需要對話歷史被永續化的 `Conversational` Agent。要恢復暫停的呼叫，`RemembersConversations` trait 提供所需的永續化功能。
</Warning>

對於檔案刪除或匯款等機敏或不可還原的操作，可於執行前要求人工承認。要將工具設為承認對象，實作 `Approvable` contract 並使用 `InteractsWithApprovals` trait。承認對象工具預設為必需承認。

```php theme={null}
<?php

namespace App\Ai\Tools;

use Illuminate\Contracts\JsonSchema\JsonSchema;
use Illuminate\Support\Facades\Storage;
use Laravel\Ai\Concerns\InteractsWithApprovals;
use Laravel\Ai\Contracts\Approvable;
use Laravel\Ai\Contracts\Tool;
use Laravel\Ai\Tools\Request;
use Stringable;

class DeleteFile implements Approvable, Tool
{
    use InteractsWithApprovals;

    /**
     * 取得工具目的的描述
     */
    public function description(): Stringable|string
    {
        return '從儲存刪除檔案。';
    }

    /**
     * 執行工具
     */
    public function handle(Request $request): Stringable|string
    {
        Storage::delete($request['path']);

        return "已刪除：[{$request['path']}]";
    }

    /**
     * 取得工具的 schema 定義
     */
    public function schema(JsonSchema $schema): array
    {
        return [
            'path' => $schema->string()->required(),
        ];
    }
}
```

若希望依工具呼叫的引數判斷是否需承認，於工具定義 `needsApproval` 方法。此方法可回傳布林值，或含承認理由的 `Approval` 實例。

```php theme={null}
use Laravel\Ai\Approvals\Approval;

/**
 * 判斷對指定 Request 工具是否需要承認
 */
protected function needsApproval(Request $request): Approval|bool
{
    return str_starts_with($request['path'], 'temporary/')
        ? false
        : Approval::required('此檔案將被完全刪除。');
}
```

於 Agent 的 `tools` 方法回傳工具時，也可覆寫承認要件。

```php theme={null}
public function tools(): iterable
{
    return [
        (new SendNotification)->withoutApproval(),
        (new DeleteFile)->requireApproval('刪除需要確認。'),
    ];
}
```

當承認對象工具被呼叫時，Agent 會於執行前暫停。透過檢查回應的 `pendingApprovals`，可確認各工具呼叫的 ID、工具名稱、引數、承認理由。

```php theme={null}
$response = (new FileAssistant)
    ->forUser($user)
    ->prompt('刪除舊的請款單。');

if ($response->hasPendingApprovals()) {
    foreach ($response->pendingApprovals as $approval) {
        // $approval->id
        // $approval->tool
        // $approval->arguments
        // $approval->reason
    }
}
```

要恢復 Agent，繼續對話並傳入包含對各保留中工具呼叫決定的 `Decisions` 實例。決定可以承認、拒絕呼叫，或於執行前編輯引數。

```php theme={null}
use Laravel\Ai\Approvals\Decision;
use Laravel\Ai\Approvals\Decisions;

$response = (new FileAssistant)
    ->continue($conversationId, as: $user)
    ->prompt(Decisions::from([
        'call_abc' => Decision::approve(),
        'call_ghi' => Decision::reject('此請款單需要保存。'),
    ]));
```

布林值 `true`、`false` 可分別作為承認、拒絕的簡略寫法使用。所有保留中的工具呼叫都必須要有決定。若指定未知、缺失、已解決的工具呼叫 ID，會拋出 `ApprovalMismatchException`。對於無明確決定的呼叫，可透過 `approveRemaining` 或 `rejectRemaining` 方法指定預設決定。

```php theme={null}
$decisions = Decisions::from([
    'call_abc' => true,
])->rejectRemaining('未獲承認。');

$response = (new FileAssistant)
    ->continue($conversationId, as: $user)
    ->prompt($decisions);
```

若如 `Decision::reject('未獲承認。')` 附結果拒絕，會回傳給模型並繼續回應。若無結果拒絕，於拒絕被記錄時生成迴圈停止。

工具承認於 `prompt`、`stream`、`queue`、`broadcast`、`broadcastNow`、`broadcastOnQueue` 方法皆受支援。

於 Streaming 及 Broadcasting 中，暫停會被表達為 `tool_approval_request` 事件。若使用 [Vercel AI SDK stream 協定](#vercel-ai-sdk-協定)，承認請求與結果會作為協定原生的工具承認部分送出。

於 Queue 化的 Agent 中，結果回應會傳入 `then` callback，Laravel 也會 dispatch `ToolApprovalRequested` 事件。

Laravel 於要求模型繼續前，會保存已承認工具的執行結果。若之後生成失敗，承認已解決。請以一般文字 prompt 繼續對話，而非重送相同的承認決定。

### 完整的承認流程

以下路由展示完整的承認流程。`GET` 路由回傳聊天畫面，`POST` 路由接收來自聊天畫面的新文字 prompt 或承認決定。此範例假設應用程式的 `User` 模型使用了 `HasConversations` trait。

```php theme={null}
use App\Ai\Agents\FileAssistant;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Route;
use Illuminate\Validation\Rule;
use Laravel\Ai\Approvals\Decision;
use Laravel\Ai\Approvals\Decisions;
use Laravel\Ai\Models\Conversation;

Route::get('/chat/{conversation}', function (Request $request, Conversation $conversation) {
    Gate::authorize('view', $conversation);

    return view('chat', [
        'conversation' => $conversation,
    ]);
})->middleware('auth');

Route::post('/chat/{conversation}', function (Request $request, Conversation $conversation) {
    Gate::authorize('view', $conversation);

    $validated = $request->validate([
        'message' => ['nullable', 'string', 'required_without:decisions', 'prohibited_with:decisions'],
        'decisions' => ['nullable', 'array', 'required_without:message', 'prohibited_with:message'],
        'decisions.*.action' => ['required_with:decisions', Rule::in(['approve', 'reject'])],
        'decisions.*.result' => ['nullable', 'string'],
    ]);

    $prompt = isset($validated['decisions'])
        ? Decisions::from($validated->collect('decisions')->map(
            fn (array $decision) => match ($decision['action']) {
                'approve' => Decision::approve(),
                'reject' => Decision::reject($decision['result'] ?? null),
            }
        )->all())
        : $validated['message'];

    $response = (new FileAssistant)
        ->continue($conversation->id, as: $request->user())
        ->prompt($prompt);

    return [
        'conversation_id' => $response->conversationId,
        'status' => $response->hasPendingApprovals() ? 'awaiting_approval' : 'complete',
        'message' => $response->text,
        'approvals' => $response->pendingApprovals,
    ];
})->middleware('auth');
```

當回應狀態為 `awaiting_approval` 時，聊天畫面應顯示保留中的承認，並以工具呼叫 ID 為 key 將使用者的選擇送至同一端點。

```json theme={null}
{
    "decisions": {
        "call_abc": {
            "action": "approve"
        },
        "call_def": {
            "action": "reject",
            "result": "此請款單需要保存。"
        }
    }
}
```

一般聊天訊息則改為傳送 `message` 值。

```json theme={null}
{
    "message": "刪除舊的請款單。"
}
```

<Tip>
  承認流程是同時授予 AI Agent 強力操作權限，同時可於執行前加入人工檢查的機制。於檔案刪除、支付處理、對外部 API 寫入等伴隨無法還原之操作的工具中，請積極活用。
</Tip>

***

## 圖片生成

可以 `Image` 類別產生圖片。支援 OpenAI、Gemini、xAI 供應商。

```php theme={null}
use Laravel\Ai\Image;

$image = Image::of('A donut sitting on the kitchen counter')->generate();
$rawContent = (string) $image;
```

可指定品質、Aspect Ratio、Timeout。

```php theme={null}
$image = Image::of('A donut sitting on the kitchen counter')
    ->quality('high')
    ->landscape()
    ->timeout(120)
    ->generate();
```

也可附加參考圖片進行加工。

```php theme={null}
use Laravel\Ai\Files;

$image = Image::of('Update this photo to be in the style of an impressionist painting.')
    ->attachments([Files\Image::fromStorage('photo.jpg')])
    ->landscape()
    ->generate();
```

### 圖片的儲存

```php theme={null}
$path = $image->store();
$path = $image->storeAs('image.jpg');
$path = $image->storePublicly();
$path = $image->storePubliclyAs('image.jpg');
```

### 以 Queue 生成圖片

```php theme={null}
use Laravel\Ai\Responses\ImageResponse;

Image::of('A donut sitting on the kitchen counter')
    ->portrait()
    ->queue()
    ->then(function (ImageResponse $image) {
        $path = $image->store();
    });
```

***

## 語音合成（TTS）

可以 `Audio` 類別將文字轉換為語音。支援 OpenAI、ElevenLabs 供應商。

```php theme={null}
use Laravel\Ai\Audio;

$audio = Audio::of('I love coding with Laravel.')->generate();
$rawContent = (string) $audio;
```

可指定聲音性別、具體 Voice ID、說話方式指示。

```php theme={null}
$audio = Audio::of('I love coding with Laravel.')->female()->generate();
$audio = Audio::of('I love coding with Laravel.')->voice('voice-id-or-name')->generate();
$audio = Audio::of('I love coding with Laravel.')->female()->instructions('Said like a pirate')->generate();
```

### 語音的儲存

```php theme={null}
$path = $audio->store();
$path = $audio->storeAs('audio.mp3');
$path = $audio->storePublicly();
$path = $audio->storePubliclyAs('audio.mp3');
```

### 以 Queue 生成語音

```php theme={null}
use Laravel\Ai\Responses\AudioResponse;

Audio::of('I love coding with Laravel.')
    ->queue()
    ->then(function (AudioResponse $audio) {
        $path = $audio->store();
    });
```

***

## 語音轉錄（STT）

可以 `Transcription` 類別將音訊檔轉換為文字。支援 OpenAI、ElevenLabs、Mistral 供應商。

```php theme={null}
use Laravel\Ai\Transcription;

$transcript = Transcription::fromPath('/home/laravel/audio.mp3')->generate();
$transcript = Transcription::fromStorage('audio.mp3')->generate();
$transcript = Transcription::fromUpload($request->file('audio'))->generate();

return (string) $transcript;
```

### 說話者分離（Diarization）

使用 `diarize()` 可取得依說話者分離的文字轉錄。

```php theme={null}
$transcript = Transcription::fromStorage('audio.mp3')->diarize()->generate();
```

### 以 Queue 進行文字轉錄

```php theme={null}
use Laravel\Ai\Responses\TranscriptionResponse;

Transcription::fromStorage('audio.mp3')
    ->queue()
    ->then(function (TranscriptionResponse $transcript) { /* ... */ });
```

***

## 文字摘要（Text Summarization）

透過 Laravel `Stringable` 類別提供的 `summarize` 方法，可摘要文字。預設會在 3 句內摘要，使用已設定供應商中最便宜的文字模型。

```php theme={null}
use Illuminate\Support\Str;

$summary = Str::of($article)->summarize();
```

也可以指定摘要使用的最大句數、供應商、模型、Timeout。`Str` 類別也備有靜態方法版本。

```php theme={null}
use Laravel\Ai\Enums\Lab;

$summary = Str::of($article)->summarize(
    sentences: 4,
    provider: Lab::Anthropic,
    model: 'claude-sonnet-5',
    timeout: 30,
);

$summary = Str::summarize($article, sentences: 4);
```

***

## Embedding

將文字轉換為向量表示，可用於相似度搜尋等。

```php theme={null}
use Illuminate\Support\Str;

// 使用 Stringable 的方式
$embeddings = Str::of('Napa Valley has great wine.')->toEmbeddings();
```

```php theme={null}
use Laravel\Ai\Embeddings;

// 一次處理多個文字
$response = Embeddings::for(['Napa Valley has great wine.', 'Laravel is a PHP framework.'])->generate();
$response->embeddings; // [[0.123, 0.456, ...], [0.789, 0.012, ...]]
```

也可指定供應商、模型、維度數。

```php theme={null}
$response = Embeddings::for(['Napa Valley has great wine.'])
    ->dimensions(1536)
    ->generate(Lab::OpenAI, 'text-embedding-3-small');
```

### 多模態 Embedding（Multimodal Embeddings）

`Embeddings::for` 方法不僅接受字串，也接受圖片、音訊、文件、影片的輸入，因此可對非文字內容生成 Embedding。Gemini 支援圖片、音訊、文件、影片的 Embedding，VoyageAI 支援圖片、影片的 Embedding。

```php theme={null}
use Laravel\Ai\Embeddings;
use Laravel\Ai\Enums\Lab;
use Laravel\Ai\Files\Image;
use Laravel\Ai\Files\Video;

$response = Embeddings::for([
    'A vineyard at sunset.',
    Image::fromStorage('vineyard.jpg'),
    Video::fromPath('/home/laravel/tour.mp4'),
])->generate(Lab::Gemini);
```

多模態輸入使用[與附加檔案相同的 File 類別](#附加檔案)。這些檔案可從本地路徑、檔案系統 disk、遠端 URL、Base64 編碼內容建立。圖片、文件、影片也可從上傳檔案建立，文件也可從原始字串內容建立。

```php theme={null}
use Laravel\Ai\Files\Audio;
use Laravel\Ai\Files\Document;
use Laravel\Ai\Files\Image;
use Laravel\Ai\Files\Video;

Image::fromPath('/home/laravel/photo.jpg');
Image::fromStorage('photo.jpg');
Image::fromUpload($request->file('photo'));

Audio::fromPath('/home/laravel/clip.mp3');
Audio::fromStorage('clip.mp3');
Audio::fromUpload($request->file('clip.mp3'));

Video::fromPath('/home/laravel/video.mp4');
Video::fromStorage('video.mp4');
Video::fromUpload($request->file('video'));

Document::fromUrl('https://example.com/report.pdf');
Document::fromString('Laravel is a PHP framework.', 'text/plain');
Document::fromUpload($request->file('report'));
```

<Info>
  VoyageAI 不允許遠端 URL 媒體與 Base64 編碼媒體於同一請求中混用。本地、Storage、上傳檔案會以 Base64 編碼內容送出，文字輸入可與任一媒體來源結合。可用的多模態模型與輸入請確認各供應商的文件。
</Info>

### 向量搜尋（pgvector）

以下為使用 PostgreSQL 與 pgvector 擴充的向量搜尋設定範例。

<Steps>
  <Step title="建立 Migration">
    ```php theme={null}
    Schema::ensureVectorExtensionExists();

    Schema::create('documents', function (Blueprint $table) {
        $table->id();
        $table->string('title');
        $table->text('content');
        $table->vector('embedding', dimensions: 1536);
        $table->timestamps();
    });

    // 加入 HNSW 索引
    $table->vector('embedding', dimensions: 1536)->index();
    ```
  </Step>

  <Step title="模型的設定">
    ```php theme={null}
    protected function casts(): array
    {
        return ['embedding' => 'array'];
    }
    ```
  </Step>

  <Step title="相似度搜尋查詢">
    ```php theme={null}
    // 以 Embedding 向量搜尋
    $documents = Document::query()
        ->whereVectorSimilarTo('embedding', $queryEmbedding, minSimilarity: 0.4)
        ->limit(10)
        ->get();

    // 傳入字串會自動產生 Embedding
    $documents = Document::query()
        ->whereVectorSimilarTo('embedding', 'best wineries in Napa Valley')
        ->limit(10)
        ->get();
    ```
  </Step>
</Steps>

亦可使用低階方法。

```php theme={null}
$documents = Document::query()
    ->select('*')
    ->selectVectorDistance('embedding', $queryEmbedding, as: 'distance')
    ->whereVectorDistanceLessThan('embedding', $queryEmbedding, maxDistance: 0.3)
    ->orderByVectorDistance('embedding', $queryEmbedding)
    ->limit(10)
    ->get();
```

### Embedding 的快取

可將相同文字的 Embedding 生成快取以避免重複執行。

於 `config/ai.php` 設定預設的快取。

```php theme={null}
'caching' => [
    'embeddings' => [
        'cache' => true,
        'store' => env('CACHE_STORE', 'database'),
    ],
],
```

也可以逐請求控制快取。

```php theme={null}
$response = Embeddings::for(['Napa Valley has great wine.'])->cache()->generate();
$response = Embeddings::for(['Napa Valley has great wine.'])->cache(seconds: 3600)->generate();

// Stringable 也同樣
$embeddings = Str::of('Napa Valley has great wine.')->toEmbeddings(cache: true);
$embeddings = Str::of('Napa Valley has great wine.')->toEmbeddings(cache: 3600);
```

***

## Reranking

可依查詢的相關度對搜尋結果進行 Rerank（重新排序）。支援 Cohere、Jina 供應商。

```php theme={null}
use Laravel\Ai\Reranking;

$response = Reranking::of([
    'Django is a Python web framework.',
    'Laravel is a PHP web application framework.',
    'React is a JavaScript library for building user interfaces.',
])->rerank('PHP frameworks');

$response->first()->document; // "Laravel is a PHP web application framework."
$response->first()->score;    // 0.95
$response->first()->index;    // 1
```

可以 `limit()` 縮限回傳件數。

```php theme={null}
$response = Reranking::of($documents)->limit(5)->rerank('search query');
```

### Collection 的 Reranking

可直接 Rerank Eloquent Collection。

```php theme={null}
// 單一欄位
$posts = Post::all()->rerank('body', 'Laravel tutorials');

// 多個欄位（作為 JSON 傳送）
$reranked = $posts->rerank(['title', 'body'], 'Laravel tutorials');

// 以 closure 自訂文字生成
$reranked = $posts->rerank(fn ($post) => $post->title.': '.$post->body, 'Laravel tutorials');

// 附選項
$reranked = $posts->rerank(
    by: 'content',
    query: 'Laravel tutorials',
    limit: 10,
    provider: Lab::Cohere,
);
```

***

## 檔案管理

可將檔案上傳至 AI 供應商並於稍後參照。

```php theme={null}
use Laravel\Ai\Files\Document;
use Laravel\Ai\Files\Image;

// 從路徑
$response = Document::fromPath('/home/laravel/document.pdf')->put();
$response = Image::fromPath('/home/laravel/photo.jpg')->put();

// 從 Storage
$response = Document::fromStorage('document.pdf', disk: 'local')->put();
$response = Image::fromStorage('photo.jpg', disk: 'local')->put();

// 從 URL
$response = Document::fromUrl('https://example.com/document.pdf')->put();
$response = Image::fromUrl('https://example.com/photo.jpg')->put();

return $response->id;
```

也可處理字串或表單上傳。

```php theme={null}
$stored = Document::fromString('Hello, World!', 'text/plain')->put();
$stored = Document::fromUpload($request->file('document'))->put();
```

### 參照已儲存的檔案

可用已上傳的檔案 ID 附加至 Agent。

```php theme={null}
use Laravel\Ai\Files;

$response = (new SalesCoach)->prompt(
    'Analyze the attached sales transcript...',
    attachments: [Files\Document::fromId('file-id')]
);
```

### 檔案的取得、刪除

```php theme={null}
// 取得
$file = Document::fromId('file-id')->get();
$file->id;
$file->mimeType();

// 刪除
Document::fromId('file-id')->delete();
```

### 指定供應商

```php theme={null}
$response = Document::fromPath('/home/laravel/document.pdf')->put(provider: Lab::Anthropic);
```

### 指定供應商特定選項

以 `withProviderOptions` 方法可傳遞供應商特定的上傳選項。例如可設定 OpenAI 的檔案 `purpose`。

```php theme={null}
use Laravel\Ai\Files\Document;

$response = Document::fromPath('/home/laravel/knowledge.txt')
    ->withProviderOptions(['purpose' => 'assistants'])
    ->put();
```

若要對不同供應商指定不同選項，可傳入 closure。

```php theme={null}
use Laravel\Ai\Enums\Lab;
use Laravel\Ai\Files\Document;

$response = Document::fromPath('/home/laravel/training.jsonl')
    ->withProviderOptions(fn (Lab|string $provider) => match ($provider) {
        Lab::OpenAI => ['purpose' => 'fine-tune'],
        default => [],
    })
    ->put();
```

***

## 向量儲存

使用向量儲存可以在供應商端管理文件。

```php theme={null}
use Laravel\Ai\Stores;

// 建立
$store = Stores::create('Knowledge Base');
$store = Stores::create(
    name: 'Knowledge Base',
    description: 'Documentation.',
    expiresWhenIdleFor: days(30),
);
return $store->id;

// 取得
$store = Stores::get('store_id');
$store->id;
$store->name;
$store->fileCounts;
$store->ready;

// 刪除
Stores::delete('store_id');
$store->delete();
```

### 將檔案加入 Store

```php theme={null}
$store = Stores::get('store_id');

// 以各種方式加入檔案
$document = $store->add('file_id');
$document = $store->add(Document::fromId('file_id'));
$document = $store->add(Document::fromPath('/path/to/document.pdf'));
$document = $store->add(Document::fromStorage('manual.pdf'));
$document = $store->add($request->file('document'));

$document->id;
$document->fileId;
```

也可加上 metadata。

```php theme={null}
$store->add(
    Document::fromPath('/path/to/document.pdf'),
    metadata: [
        'author' => 'Taylor Otwell',
        'department' => 'Engineering',
        'year' => 2026,
    ]
);
```

### 從 Store 刪除檔案

```php theme={null}
$store->remove('file_id');

// 若要同時刪除檔案本身
$store->remove('file_abc123', deleteFile: true);
```

***

## Failover

若以陣列指定多個供應商，於第一個供應商失敗時會自動 fallback 至下一個。

```php theme={null}
use App\Ai\Agents\SalesCoach;
use Laravel\Ai\Image;

// Agent 的 Failover
$response = (new SalesCoach)->prompt(
    'Analyze this sales transcript...',
    provider: [Lab::OpenAI, Lab::Anthropic],
);

// 圖片生成的 Failover
$image = Image::of('A donut sitting on the kitchen counter')
    ->generate(provider: [Lab::Gemini, Lab::xAI]);
```

***

## 測試

Laravel AI SDK 提供測試用的 Fake 功能，可以不呼叫實際 API 進行測試。

### Agent 的測試

```php theme={null}
use App\Ai\Agents\SalesCoach;
use Laravel\Ai\Prompts\AgentPrompt;

// 固定回應的 Fake
SalesCoach::fake();
SalesCoach::fake(['First response', 'Second response']);

// 以 closure 動態回應
SalesCoach::fake(function (AgentPrompt $prompt) {
    return 'Response for: '.$prompt->prompt;
});

// 斷言
SalesCoach::assertPrompted('Analyze this...');
SalesCoach::assertPrompted(function (AgentPrompt $prompt) {
    return $prompt->contains('Analyze');
});
SalesCoach::assertNotPrompted('Missing prompt');
SalesCoach::assertNeverPrompted();
```

也備有 Queue 化的斷言。

```php theme={null}
use Laravel\Ai\QueuedAgentPrompt;

SalesCoach::assertQueued('Analyze this...');
SalesCoach::assertQueued(function (QueuedAgentPrompt $prompt) {
    return $prompt->contains('Analyze');
});
SalesCoach::assertNotQueued('Missing prompt');
SalesCoach::assertNeverQueued();
```

使用 `preventStrayPrompts()`，若呼叫未於 Fake 定義的 prompt 會拋出例外。

```php theme={null}
SalesCoach::fake()->preventStrayPrompts();
```

若要 Fake 回傳結構化輸出的 Agent，可用陣列指定回應。Agent 會回傳含指定資料的結構化回應。

```php theme={null}
SalesCoach::fake([
    ['score' => 87],
]);
```

<Info>
  若對結構化輸出 Agent 呼叫 `fake()` 時未明確傳入 Fake 資料，Laravel 會自動生成符合 Agent 所定義 schema 的 Fake 資料。
</Info>

匿名 Agent 的測試使用 `AnonymousAgent::fake()`。

```php theme={null}
use Laravel\Ai\AnonymousAgent;

AnonymousAgent::fake(['Test response']);
```

### 圖片生成的測試

```php theme={null}
use Laravel\Ai\Image;
use Laravel\Ai\Prompts\ImagePrompt;

Image::fake();
Image::fake([base64_encode($firstImage), base64_encode($secondImage)]);
Image::fake(function (ImagePrompt $prompt) {
    return base64_encode('...');
});

Image::assertGenerated(function (ImagePrompt $prompt) {
    return $prompt->contains('sunset') && $prompt->isLandscape();
});
Image::assertNotGenerated('Missing prompt');
Image::assertNothingGenerated();

Image::assertQueued(fn (QueuedImagePrompt $prompt) => $prompt->contains('sunset'));
Image::assertNotQueued('Missing prompt');
Image::assertNothingQueued();

Image::fake()->preventStrayImages();
```

### 語音合成的測試

```php theme={null}
use Laravel\Ai\Audio;
use Laravel\Ai\Prompts\AudioPrompt;

Audio::fake();
Audio::fake([base64_encode($firstAudio), base64_encode($secondAudio)]);
Audio::fake(function (AudioPrompt $prompt) {
    return base64_encode('...');
});

Audio::assertGenerated(function (AudioPrompt $prompt) {
    return $prompt->contains('Hello') && $prompt->isFemale();
});
Audio::assertNotGenerated('Missing prompt');
Audio::assertNothingGenerated();

Audio::assertQueued(fn (QueuedAudioPrompt $prompt) => $prompt->contains('Hello'));
Audio::assertNotQueued('Missing prompt');
Audio::assertNothingQueued();

Audio::fake()->preventStrayAudio();
```

### 文字轉錄的測試

```php theme={null}
use Laravel\Ai\Transcription;
use Laravel\Ai\Prompts\TranscriptionPrompt;

Transcription::fake();
Transcription::fake(['First transcription text.', 'Second transcription text.']);
Transcription::fake(function (TranscriptionPrompt $prompt) {
    return 'Transcribed text...';
});

Transcription::assertGenerated(function (TranscriptionPrompt $prompt) {
    return $prompt->language === 'en' && $prompt->isDiarized();
});
Transcription::assertNotGenerated(fn (TranscriptionPrompt $prompt) => $prompt->language === 'fr');
Transcription::assertNothingGenerated();

Transcription::assertQueued(fn (QueuedTranscriptionPrompt $prompt) => $prompt->isDiarized());
Transcription::assertNotQueued(fn (QueuedTranscriptionPrompt $prompt) => $prompt->language === 'fr');
Transcription::assertNothingQueued();

Transcription::fake()->preventStrayTranscriptions();
```

### Embedding 的測試

```php theme={null}
use Laravel\Ai\Embeddings;
use Laravel\Ai\Prompts\EmbeddingsPrompt;

Embeddings::fake();
Embeddings::fake([[$firstEmbeddingVector], [$secondEmbeddingVector]]);
Embeddings::fake(function (EmbeddingsPrompt $prompt) {
    return array_map(
        fn () => Embeddings::fakeEmbedding($prompt->dimensions),
        $prompt->inputs
    );
});

Embeddings::assertGenerated(function (EmbeddingsPrompt $prompt) {
    return $prompt->contains('Laravel') && $prompt->dimensions === 1536;
});
Embeddings::assertNotGenerated(fn (EmbeddingsPrompt $prompt) => $prompt->contains('Other'));
Embeddings::assertNothingGenerated();

Embeddings::assertQueued(fn (QueuedEmbeddingsPrompt $prompt) => $prompt->contains('Laravel'));
Embeddings::assertNotQueued(fn (QueuedEmbeddingsPrompt $prompt) => $prompt->contains('Other'));
Embeddings::assertNothingQueued();

Embeddings::fake()->preventStrayEmbeddings();
```

### Reranking 的測試

```php theme={null}
use Laravel\Ai\Reranking;
use Laravel\Ai\Prompts\RerankingPrompt;
use Laravel\Ai\Responses\Data\RankedDocument;

Reranking::fake();
Reranking::fake([[
    new RankedDocument(index: 0, document: 'First', score: 0.95),
    new RankedDocument(index: 1, document: 'Second', score: 0.80),
]]);

Reranking::assertReranked(function (RerankingPrompt $prompt) {
    return $prompt->contains('Laravel') && $prompt->limit === 5;
});
Reranking::assertNotReranked(fn (RerankingPrompt $prompt) => $prompt->contains('Django'));
Reranking::assertNothingReranked();
```

### 檔案的測試

```php theme={null}
use Laravel\Ai\Files;
use Laravel\Ai\Contracts\Files\StorableFile;
use Laravel\Ai\Files\Document;

Files::fake();

Document::fromString('Hello, Laravel!', mimeType: 'text/plain')->as('hello.txt')->put();

Files::assertStored(fn (StorableFile $file) =>
    (string) $file === 'Hello, Laravel!' && $file->mimeType() === 'text/plain'
);
Files::assertNotStored(fn (StorableFile $file) => (string) $file === 'Hello, World!');
Files::assertNothingStored();

Files::assertDeleted('file-id');
Files::assertNotDeleted('file-id');
Files::assertNothingDeleted();
```

### 向量儲存的測試

```php theme={null}
use Laravel\Ai\Stores;

Stores::fake(); // 檔案操作也會 Fake

$store = Stores::create('Knowledge Base');

Stores::assertCreated('Knowledge Base');
Stores::assertCreated(fn (string $name, ?string $description) => $name === 'Knowledge Base');
Stores::assertNotCreated('Other Store');
Stores::assertNothingCreated();

Stores::assertDeleted('store_id');
Stores::assertNotDeleted('other_store_id');
Stores::assertNothingDeleted();
```

也可對 Store 的檔案操作進行斷言。

```php theme={null}
$store = Stores::get('store_id');
$store->add('added_id');
$store->remove('removed_id');

$store->assertAdded('added_id');
$store->assertRemoved('removed_id');
$store->assertNotAdded('other_file_id');
$store->assertNotRemoved('other_file_id');

// 以 closure 驗證內容
$store->add(Document::fromString('Hello, World!', 'text/plain')->as('hello.txt'));
$store->assertAdded(fn (StorableFile $file) => $file->name() === 'hello.txt');
$store->assertAdded(fn (StorableFile $file) => $file->content() === 'Hello, World!');
```

***

## 事件

Laravel AI SDK dispatch 以下事件。透過監聽這些事件，可活用於日誌記錄或監視。

<AccordionGroup>
  <Accordion title="Agent 相關">
    * `PromptingAgent` — 送出 prompt 前
    * `AgentPrompted` — 送出 prompt 後
    * `StreamingAgent` — 開始 Streaming 時
    * `AgentStreamed` — Streaming 完成後
    * `InvokingTool` — 呼叫工具前
    * `ToolInvoked` — 呼叫工具後
    * `ToolApprovalRequested` — 工具承認請求時
    * `ToolApprovalResolved` — 工具承認解決後
  </Accordion>

  <Accordion title="圖片、語音、轉錄相關">
    * `GeneratingImage` — 圖片生成前
    * `ImageGenerated` — 圖片生成後
    * `GeneratingAudio` — 語音生成前
    * `AudioGenerated` — 語音生成後
    * `GeneratingTranscription` — 文字轉錄前
    * `TranscriptionGenerated` — 文字轉錄後
  </Accordion>

  <Accordion title="Embedding、Reranking 相關">
    * `GeneratingEmbeddings` — Embedding 生成前
    * `EmbeddingsGenerated` — Embedding 生成後
    * `Reranking` — Reranking 前
    * `Reranked` — Reranking 後
  </Accordion>

  <Accordion title="檔案、Store 相關">
    * `StoringFile` — 檔案保存前
    * `FileStored` — 檔案保存後
    * `FileDeleted` — 檔案刪除後
    * `CreatingStore` — Store 建立前
    * `StoreCreated` — Store 建立後
    * `AddingFileToStore` — 檔案加入 Store 前
    * `FileAddedToStore` — 檔案加入 Store 後
    * `RemovingFileFromStore` — 從 Store 刪除檔案前
    * `FileRemovedFromStore` — 從 Store 刪除檔案後
  </Accordion>
</AccordionGroup>


## Related topics

- [Laravel 13 新功能彙總](/zh-TW/blog/laravel-13-new-features.md)
- [2026 年 3 月 Laravel 更新](/zh-TW/blog/changelog/202603.md)
- [Laravel AI SDK 整合](/zh-TW/packages/laravel-copilot-sdk/ai-sdk.md)
- [Laravel AI SDK 整合 - VOICEVOX for Laravel](/zh-TW/packages/laravel-voicevox/ai-sdk.md)
- [Laravel AI SDK 的 Amazon Bedrock 驅動](/zh-TW/packages/laravel-amazon-bedrock.md)
