主题
Laravel AI SDK
简介
Laravel AI SDK 提供了一套统一且富有表现力的 API,用于与 OpenAI、Anthropic、Gemini 等 AI 提供商进行交互。借助 AI SDK,你可以构建拥有工具和结构化输出的智能 agent,生成图像,合成和转录音频,创建向量 embeddings 等等——所有这些都使用一致的、Laravel 风格的接口。
安装
你可以通过 Composer 安装 Laravel AI SDK:
shell
composer require laravel/ai接下来,你应该使用 vendor:publish Artisan 命令发布 AI SDK 的配置和迁移文件:
shell
php artisan vendor:publish --provider="Laravel\Ai\AiServiceProvider"最后,你应该运行应用程序的数据库迁移。这将创建 AI SDK 用于存储对话的 agent_conversations 和 agent_conversation_messages 表:
shell
php artisan migrate配置
你可以在应用程序的 config/ai.php 配置文件中或在 .env 文件中以环境变量的形式定义 AI 提供商凭据:
ini
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=
OPENAI_COMPATIBLE_API_KEY=
OPENAI_COMPATIBLE_URL=
OPENROUTER_API_KEY=
JINA_API_KEY=
TYPESAFE_API_KEY=
VOYAGEAI_API_KEY=
XAI_API_KEY=用于文本、图像、音频、转录和 embeddings 的默认模型也可以在应用程序的 config/ai.php 配置文件中进行配置。
自定义基础 URL
默认情况下,Laravel AI SDK 直接连接到每个提供商的公共 API 端点。但是,你可能需要通过不同的端点路由请求——例如,当使用代理服务来集中管理 API 密钥、实施速率限制或通过企业网关路由流量时。
你可以通过在提供商配置中添加 url 参数来配置自定义基础 URL:
php
'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'),
],
],当通过代理服务(如 LiteLLM 或 Azure OpenAI Gateway)路由请求或使用替代端点时,这非常有用。
以下提供商支持自定义基础 URL:OpenAI、Anthropic、Gemini、Groq、Cohere、DeepSeek、xAI、OpenRouter 和 TypeSafe。
OpenAI 兼容提供商
如果你使用的是 OpenAI 兼容 API,例如 LM Studio、vLLM、Together、Fireworks 或本地网关,则可以配置一个 openai-compatible 提供商。url 选项是必需的,而 key 选项是可选的;提供该选项时,它会作为 bearer token 发送:
php
'providers' => [
'local' => [
'driver' => 'openai-compatible',
'url' => env('LOCAL_AI_URL'),
'key' => env('LOCAL_AI_API_KEY'),
],
],配置完成后,你可以像使用其他提供商一样使用这个具名提供商:
php
agent()->prompt('What is Laravel?', provider: 'local', model: 'local-model');你也可以为该提供商配置默认文本模型,这样就无需显式传入模型:
php
'local' => [
'driver' => 'openai-compatible',
'url' => env('LOCAL_AI_URL'),
'key' => env('LOCAL_AI_API_KEY'),
'models' => [
'text' => [
'default' => env('LOCAL_AI_MODEL'),
],
],
],你可以在提供商配置中定义 headers 数组,为该提供商的每个传出请求添加自定义 HTTP header。当端点除了 bearer token 之外还需要额外的标识或认证 header 时,这会很有用:
php
'local' => [
'driver' => 'openai-compatible',
'url' => env('LOCAL_AI_URL'),
'key' => env('LOCAL_AI_API_KEY'),
'headers' => [
'X-Tenant-Id' => env('LOCAL_AI_TENANT_ID'),
],
],OpenAI 兼容提供商支持文本生成、流式传输、工具、结构化输出、图像附件、embeddings、转录和分类。如果你的端点需要额外的请求体字段,请使用提供商选项提供它们。
OpenAI 兼容 Embeddings
由于任意端点没有已知模型,你必须配置默认 embeddings 模型,才能在 OpenAI 兼容提供商中使用 embeddings()。你也可以配置固定的 dimensions 值;如果省略,请求会在没有 dimensions 参数的情况下发送,并使用模型的原生 dimensions。
php
'local' => [
'driver' => 'openai-compatible',
'url' => env('LOCAL_AI_URL'),
'key' => env('LOCAL_AI_API_KEY'),
'models' => [
'embeddings' => [
'default' => 'text-embedding-qwen3-embedding-0.6b',
'dimensions' => 1024, // 可选
],
],
],OpenAI 兼容转录
同样,要通过 OpenAI 兼容提供商使用 Transcription,你必须配置默认转录模型。音频会通过标准 multipart 请求上传到该端点的 /audio/transcriptions 路由:
php
'local' => [
'driver' => 'openai-compatible',
'url' => env('LOCAL_AI_URL'),
'key' => env('LOCAL_AI_API_KEY'),
'models' => [
'transcription' => [
'default' => 'whisper-1',
],
],
],NOTE
OpenAI 兼容和 Groq 提供商不支持说话人分离。使用这些提供商时调用 diarize 方法会抛出异常。
OpenAI 兼容分类
如果你的端点实现了 OpenAI 的 decisions 端点,就可以通过 OpenAI 兼容提供商使用分类功能。SDK 会将问题发送到端点的 /decisions 路由,并支持图像附件。由于 SDK 无法获知你的端点支持哪些模型,请配置默认分类模型,或向 classify 方法传入模型:
php
'local' => [
'driver' => 'openai-compatible',
'url' => env('LOCAL_AI_URL'),
'key' => env('LOCAL_AI_API_KEY'),
'models' => [
'classification' => [
'default' => env('LOCAL_AI_MODEL'),
],
],
],按需提供商
有时,你可能需要使用应用配置文件中未定义的提供商凭据,例如多租户应用中为各个租户存储在数据库中的 API 密钥。你可以使用 Ai::build 方法,通过配置数组创建提供商。该数组的结构应与应用 config/ai.php 配置文件中的提供商条目相同:
php
use App\Ai\Agents\SalesCoach;
use Laravel\Ai\Ai;
$response = (new SalesCoach)->prompt('Analyze this sales transcript...', provider: [
Ai::build([
'driver' => 'anthropic',
'key' => $user->anthropic_key,
]),
]);按需提供商可用于所有接受提供商的地方,包括故障转移列表,以及生成图像、音频、转录和 embeddings 时。要为按需提供商指定模型,请在其配置中添加 models 数组:
php
$response = (new SalesCoach)->prompt('Analyze this sales transcript...', provider: [
Ai::build(['driver' => 'anthropic', 'key' => $user->anthropic_key]),
Ai::build([
'driver' => 'openai',
'key' => $user->openai_key,
'models' => ['text' => ['default' => 'gpt-6']],
]),
]);当 agent 应始终使用按需提供商时,你可以从 agent 的 provider 方法返回该提供商。提供商会在使用 agent 时重新构建,因此这种方式也适用于队列 agent:
php
use Laravel\Ai\Ai;
use Laravel\Ai\Providers\Provider;
public function provider(): Provider
{
return Ai::build($this->user->aiConfiguration());
}NOTE
应像上面的示例一样,将按需提供商放在数组中传入。如果你在按需提供商的配置数组中指定了 name,该名称必须与内置提供商及 config/ai.php 配置文件中定义的提供商名称不同。
提供商支持
AI SDK 在其各项功能中支持多种提供商。下表总结了每项功能可用的提供商:
| 功能 | 提供商 |
|---|---|
| 文本 | OpenAI, OpenAI Compatible, Anthropic, Gemini, Azure, Bedrock, Groq, xAI, DeepSeek, Mistral, Ollama, OpenRouter, Cohere |
| 图像 | OpenAI, Gemini, xAI, Azure, Bedrock, OpenRouter |
| TTS | OpenAI, ElevenLabs, Gemini, Mistral, OpenRouter |
| STT | OpenAI, OpenAI Compatible, ElevenLabs, Groq, Mistral, Gemini, OpenRouter |
| Embeddings | OpenAI, OpenAI Compatible, Gemini, Azure, Bedrock, Cohere, Mistral, Jina, VoyageAI, Ollama, OpenRouter |
| 重排序 | Cohere, Jina, VoyageAI, Bedrock, OpenRouter |
| 分类 | OpenAI, OpenAI Compatible, TypeSafe, OpenRouter |
| 文件 | OpenAI, Anthropic, Gemini, Azure, OpenRouter |
Laravel\Ai\Enums\Lab 枚举可用于在代码中引用提供商,而不是使用普通字符串:
php
use Laravel\Ai\Enums\Lab;
Lab::Anthropic;
Lab::OpenAI;
Lab::OpenAiCompatible;
Lab::Gemini;
// ...Agent
Agent 是 Laravel AI SDK 中与 AI 提供商交互的基本构建块。每个 agent 都是一个专用的 PHP 类,封装了与大语言模型交互所需的指令、对话上下文、工具和输出模式。可以将 agent 视为一个专业助手——销售教练、文档分析器、客服机器人——你只需配置一次,即可在整个应用程序中按需使用。
你可以通过 make:agent Artisan 命令创建一个 agent:
shell
php artisan make:agent SalesCoach
php artisan make:agent SalesCoach --structured在生成的 agent 类中,你可以定义系统提示词/指令、消息上下文、可用工具和输出模式(如果适用):
php
<?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) {}
/**
* 获取 agent 应遵循的指令。
*/
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();
}
/**
* 获取 agent 可用的工具。
*
* @return Tool[]
*/
public function tools(): iterable
{
return [
new RetrievePreviousTranscripts,
];
}
/**
* 获取 agent 的结构化输出模式定义。
*/
public function schema(JsonSchema $schema): array
{
return [
'feedback' => $schema->string()->required(),
'score' => $schema->integer()->min(1)->max(10)->required(),
];
}
}提示
要提示一个 agent,首先使用 make 方法或标准实例化创建实例,然后调用 prompt:
php
$response = (new SalesCoach)
->prompt('Analyze this sales transcript...');
return (string) $response;make 方法从容器中解析你的 agent,允许自动依赖注入。你也可以向 agent 的构造函数传递参数:
php
$agent = SalesCoach::make(user: $user);通过向 prompt 方法传递额外参数,你可以在提示时覆盖默认的提供商、模型或 HTTP 超时时间:
php
$response = (new SalesCoach)->prompt(
'Analyze this sales transcript...',
provider: Lab::Anthropic,
model: 'claude-sonnet-5',
timeout: 120,
);原始 HTTP 响应
文本生成 agent 返回的每个响应都会通过 raw 属性暴露底层提供商 API 调用的原始 HTTP 响应。这样你可以访问 AI SDK 通用响应中未包含的提供商特定信息,例如速率限制 header、请求 ID 或其他精确载荷字段:
php
$response = (new SalesCoach)->prompt('Analyze this sales transcript...');
$response->raw; // Illuminate\Http\Client\Response|null
$response->raw->header('X-RateLimit-Remaining-Requests');
$response->raw->json('id');在工具调用循环中,每个步骤都会保留其自身请求的原始响应:
php
foreach ($response->steps as $step) {
$step->raw?->header('X-RateLimit-Remaining-Requests');
}注意: 当响应以流式传输返回、使用 Bedrock 提供商(它通过 AWS SDK 而非 HTTP client 执行 API 调用),以及伪造响应未通过
withRawResponse显式提供原始响应时,raw属性为null。
对话上下文
如果你的 agent 实现了 Conversational 接口,你可以使用 messages 方法返回之前的对话上下文(如果适用):
php
/**
* 获取迄今为止的对话消息列表。
*/
public function messages(): iterable
{
return $this->user->history()
->latest()
->limit(50)
->get()
->reverse()
->map(fn ($message) => new Message(
$message->role, $message->content,
))->all();
}如果你的 agent 未实现 Conversational 接口,可以使用 withMessages 方法为单次运行提供对话历史,例如由应用前端提交的历史记录:
php
use Laravel\Ai\Messages\Message;
$response = (new SalesCoach)
->withMessages([
new Message('user', 'Analyze this sales transcript...'),
new Message('assistant', 'The rep never asked for the close.'),
])
->prompt('What should they say next time?');实现 Conversational 接口的 agent 会自行加载历史记录,同时使用这两种方式将抛出 LogicException。
记忆对话
警告: 在使用
RemembersConversationstrait 之前,你应该使用vendor:publishArtisan 命令发布并运行 AI SDK 迁移。这些迁移将创建用于存储对话的必要数据库表。
如果你希望 Laravel 自动存储和检索 agent 的对话历史,你可以使用 RemembersConversations trait。该 trait 提供了一种简单的方式将对话消息持久化到数据库,无需手动实现 Conversational 接口:
php
<?php
namespace App\Ai\Agents;
use Laravel\Ai\Concerns\RemembersConversations;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\Conversational;
use Laravel\Ai\Promptable;
class SalesCoach implements Agent, Conversational
{
use Promptable, RemembersConversations;
/**
* 获取 agent 应遵循的指令。
*/
public function instructions(): string
{
return 'You are a sales coach...';
}
}使用 RemembersConversations trait 时,让该 trait 提供 messages 方法实现。如果 agent 类中存在 messages 方法,该方法会优先于 trait 的实现,导致对话历史无法从数据库加载。
要为用户开始新对话,请在提示之前调用 forUser 方法:
php
$response = (new SalesCoach)->forUser($user)->prompt('Hello!');
$conversationId = $response->conversationId;对话 ID 会在响应中返回,可以存储以供将来引用。如果你想使用 Eloquent 检索用户的所有对话,可以将 HasConversations trait 添加到你的用户模型:
php
<?php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Ai\Concerns\HasConversations;
class User extends Authenticatable
{
use HasConversations;
}将该 trait 添加到模型后,你可以通过 conversations 关联检索和查询用户的对话:
php
$conversations = $user->conversations()
->latest('updated_at')
->paginate(20);要继续现有对话,请使用 continue 方法:
php
$response = (new SalesCoach)
->continue($conversationId, as: $user)
->prompt('Tell me more about that.');continueOrStart 方法可继续给定对话;当给定 ID 为 null 时,它会开始新对话:
php
$response = (new SalesCoach)
->continueOrStart($conversationId, as: $user)
->prompt('Hello!');使用 RemembersConversations trait 时,之前的消息会在提示时自动加载并包含在对话上下文中。每次交互后,新消息(用户和助手消息)都会自动存储。每个响应还包含已存储的对话 ID 和消息 ID:
php
$response->conversationId;
$response->userMessageId;
$response->assistantMessageId;对话参与者
虽然用户是最常见的对话参与者,但对话可以属于任何 Eloquent model。使用 forParticipant 方法为另一种类型的 model 开始对话:
php
$response = (new SalesCoach)
->forParticipant($team)
->prompt('Review our latest sales results.');参与者的 morph class 和主键会随对话一起存储。因此,具有相同主键的不同类型 model,例如 User ID 1 和 Team ID 1,会拥有各自独立的对话历史。forUser 方法是 forParticipant 的别名。
你可以使用 continueLastConversation 方法继续该参与者最近与此 agent 的对话。对话范围限定于 agent,因此只会继续该 agent 参与过的对话:
php
$response = (new SalesCoach)
->continueLastConversation($team)
->prompt('Tell me more about that.');继续特定对话时,请将参与者传递给 continue 方法:
php
$response = (new SalesCoach)
->continue($conversationId, as: $team)
->prompt('Tell me more about that.');HasConversations trait 可以添加到任何参与对话的 Eloquent model。生成的 conversations 关系是一个按该 model 类型和主键限定范围的多态关系。你也可以通过其反向关系访问拥有某个对话的参与者:
php
$conversations = $team->conversations;
$participant = $conversation->participant;如果你的应用使用多种参与者 model 类型,应考虑定义 Eloquent morph map,使存储的参与者类型与 model 类名解耦。
WARNING
continue 和 continueOrStart 方法不会验证给定参与者是否拥有该对话。你的应用应在继续对话前授权访问该对话。
检查已存储的对话
向用户展示对话时,通常需要消息 ID、时间戳和工具调用等详细信息。你可以从 service container 解析对话存储,无需直接查询 AI SDK 的数据表即可读取已存储的消息:
php
use Laravel\Ai\Contracts\ConversationStore;
$store = app(ConversationStore::class);消息通过 cursor 按最新消息优先分页,并以 StoredMessage 实例返回,其中包含每条消息的 ID、时间戳、usage、元数据和附件:
php
$messages = $store->paginateConversationMessages($conversationId, perPage: 25);
foreach ($messages as $message) {
$message->id;
$message->role;
$message->content;
$message->createdAt;
$message->usage;
$message->status;
}每一轮由用户 prompt 和助手回复组成,并存储为步骤列表。一个步骤代表向 provider 发出的一次请求,因此模型调用工具的一轮会包含多个步骤。每个工具结果都记录在产生该结果的工具调用上。toolCalls、providerToolCalls 和 toolResults 方法会按顺序展平这些步骤,你无需自行遍历:
php
$message->steps;
$message->toolCalls();
$message->providerToolCalls();
$message->toolResults();工具调用执行后会包含 result。包含 approval_reason 但缺少 result 的工具调用仍在等待工具审批。
status 属性包含一个 Laravel\Ai\Enums\MessageStatus 实例。执行过程中失败的一轮会连同已经完成的步骤存储为 Failed,因此失败前运行的工具调用仍会保留在历史记录中。继续对话时,任何没有已记录结果的工具调用都会以中断状态发送给模型,因为 Laravel 无法确定该调用是否运行过。
通过应用前端提供的 ID 继续对话前,应验证该对话属于给定参与者:
php
abort_unless($store->conversationBelongsTo(
$conversationId, $user->getMorphClass(), $user->getKey()
), 403);如果最近一轮暂停并等待工具审批,页面重新加载后可以渲染其待审批工具调用,无需恢复运行:
php
foreach ($store->pendingApprovalsFor($conversationId) as $approval) {
// $approval->id、$approval->tool、$approval->arguments、$approval->reason...
}这些方法由 PaginatesConversations、VerifiesConversationOwnership 和 ResolvesPendingApprovals contract 定义。内置数据库存储实现了全部三个 contract,自定义存储可以只实现所需的 contract。
结构化输出
如果你希望 agent 返回结构化输出,请实现 HasStructuredOutput 接口,该接口要求你的 agent 定义一个 schema 方法:
php
<?php
namespace App\Ai\Agents;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasStructuredOutput;
use Laravel\Ai\Promptable;
class SalesCoach implements Agent, HasStructuredOutput
{
use Promptable;
// ...
/**
* 获取 agent 的结构化输出模式定义。
*/
public function schema(JsonSchema $schema): array
{
return [
'score' => $schema->integer()->required(),
];
}
}当提示一个返回结构化输出的 agent 时,你可以像数组一样访问返回的 StructuredAgentResponse:
php
$response = (new SalesCoach)->prompt('Analyze this sales transcript...');
return $response['score'];嵌套对象
要定义嵌套的结构化输出,可以结合闭包使用 object 方法:
php
<?php
namespace App\Ai\Agents;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasStructuredOutput;
use Laravel\Ai\Promptable;
class SalesCoach implements Agent, HasStructuredOutput
{
use Promptable;
// ...
/**
* 获取 agent 的结构化输出模式定义。
*/
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(),
];
}
}对象数组
如果你的 agent 应返回由结构化条目组成的列表,可以组合 array 和 object 方法:
php
public function schema(JsonSchema $schema): array
{
return [
'feedback' => $schema->array()
->items(
$schema->object(fn ($schema) => [
'comment' => $schema->string()->required(),
'score' => $schema->integer()->required(),
])
)
->required(),
];
}如果某个值可能匹配多个 schema 中的任意一个,请使用 anyOf 方法:
php
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(),
];
}附件
在提示时,你还可以传递附件以允许模型检查图像和文档:
php
use App\Ai\Agents\SalesCoach;
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'), // 附加上传的文件...
]
);同样,Laravel\Ai\Files\Image 类可用于将图像附加到提示:
php
use App\Ai\Agents\ImageAnalyzer;
use Laravel\Ai\Files;
$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 方法流式传输 agent 的响应。返回的 StreamableAgentResponse 可以从路由返回,以自动向客户端发送流式响应(SSE):
php
use App\Ai\Agents\SalesCoach;
Route::get('/coach', function () {
return (new SalesCoach)->stream('Analyze this sales transcript...');
});then 方法可用于提供一个闭包,该闭包将在整个响应流式传输到客户端后被调用:
php
use App\Ai\Agents\SalesCoach;
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
$stream = (new SalesCoach)->stream('Analyze this sales transcript...');
foreach ($stream as $event) {
// ...
}响应还包含模型的推理内容及其引用的来源。使用 RemembersConversations trait 时,这两项内容都会随助手消息一起存储:
php
use Laravel\Ai\Responses\StreamedAgentResponse;
(new SalesCoach)
->stream('Analyze this sales transcript...')
->then(function (StreamedAgentResponse $response) {
$response->reasoning; // 模型未返回推理文本时为 ''...
$response->meta->citations;
});prompt 方法返回的响应中也提供推理内容。
流协议
默认情况下,流式响应使用 AI SDK 自身的事件格式。你也可以使用前端流协议,将 agent 与现有聊天界面配合使用。
你可以通过在流式响应上调用 usingVercelDataProtocol 方法,使用 Vercel AI SDK 流协议 来流式传输事件:
php
use App\Ai\Agents\SalesCoach;
Route::get('/coach', function () {
return (new SalesCoach)
->stream('Analyze this sales transcript...')
->usingVercelDataProtocol();
});如果应用前端自行分配消息 ID,可以将它传入:
php
->usingVercelDataProtocol($request->string('messageId'));也可以使用 usingAgentUserInteractionProtocol 方法,通过 Agent User Interaction(AG-UI)协议进行流式传输:
php
Route::post('/coach', function (Request $request) {
return (new SalesCoach)
->forUser($request->user())
->stream($request->string('prompt'))
->usingAgentUserInteractionProtocol();
});threadId 和 runId 参数均为可选参数,默认分别使用对话 ID 和调用 ID:
php
->usingAgentUserInteractionProtocol(
threadId: $request->input('threadId'),
runId: $request->input('runId'),
);要使用 AI SDK 尚未实现的协议,可以将自己的 Laravel\Ai\Streaming\Protocols\StreamProtocol 实现传给 usingProtocol 方法:
php
use App\Ai\Protocols\CustomProtocol;
return (new SalesCoach)
->stream('Analyze this sales transcript...')
->usingProtocol(new CustomProtocol);前端集成
使用 Vercel useChat 或 CopilotKit 等库构建的聊天界面已具备消息、工具调用和审批提示的渲染能力,因此应用只需处理它们发送的请求。每个请求都包含对话历史、最新用户消息及所有工具审批响应。
Vercel::chat 和 AgentUserInteraction::chat 方法会将这类请求转换成可直接传给 agent stream 方法的对象:
php
use Laravel\Ai\Vercel\Vercel;
Route::post('/chat', function (Request $request) {
$chat = Vercel::chat($request);
return (new SupportAgent)
->withMessages($chat->history())
->stream($chat)
->usingProtocol($chat->protocol());
});如果请求包含审批决定,agent 会使用这些决定恢复运行。其余请求会使用最新的用户消息和附件提示 agent。protocol 方法返回 client 使用的协议。
NOTE
实现 Conversational 接口的 agent 会自行加载历史记录,因此可以省略 withMessages 方法。
AgentUserInteraction::chat 方法为 AG-UI client 提供相同的 API,并额外提供请求的 thread ID 和 run ID:
php
use Laravel\Ai\AgentUserInteraction\AgentUserInteraction;
$chat = AgentUserInteraction::chat($request);
$chat->threadId();
$chat->runId();你还可以将已存储的消息转换回 client 需要的格式,让 client 能够展示之前的对话,例如页面重新加载后的对话:
php
$messages = $conversation->messages()->oldest()->get();
return ['messages' => Vercel::toUiMessages($messages)];AgentUserInteraction::toClientState 方法会为 AG-UI client 执行相同转换,并返回所有待处理的审批中断:
php
return AgentUserInteraction::toClientState($messages);广播
你可以通过几种不同的方式广播流式事件。首先,你可以简单地在流式事件上调用 broadcast 或 broadcastNow 方法:
php
use App\Ai\Agents\SalesCoach;
use Illuminate\Broadcasting\Channel;
$stream = (new SalesCoach)->stream('Analyze this sales transcript...');
foreach ($stream as $event) {
$event->broadcast(new Channel('channel-name'));
}或者,你可以调用 agent 的 broadcastOnQueue 方法将 agent 操作加入队列,并在流式事件可用时广播它们:
php
(new SalesCoach)->broadcastOnQueue(
'Analyze this sales transcript...',
new Channel('channel-name'),
);跳过过大的事件
某些广播平台会将 WebSocket 消息限制在约 10KB。包含大量数据的流式事件(例如大型工具结果)可能会超过该限制并导致广播失败。你可以使用 WithoutBroadcasting 属性从广播中排除特定事件类型:
php
<?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 表,因此你的前端可以在流结束后加载完整的工具数据。这同时适用于队列广播(broadcastOnQueue)和同步广播(broadcast / broadcastNow)。
队列
使用 agent 的 queue 方法,你可以提示 agent,但允许它在后台处理响应,使你的应用程序保持快速和响应。then 和 catch 方法可用于注册闭包,这些闭包将在响应可用时或发生异常时被调用:
php
use Illuminate\Http\Request;
use Laravel\Ai\Responses\AgentResponse;
use Throwable;
Route::post('/coach', function (Request $request) {
(new SalesCoach)
->queue($request->input('transcript'))
->then(function (AgentResponse $response) {
// ...
})
->catch(function (Throwable $e) {
// ...
});
return back();
});工具
工具可用于为 agent 提供额外的功能,agent 在响应提示时可以使用这些功能。可以使用 make:tool Artisan 命令创建工具:
shell
php artisan make:tool RandomNumberGenerator生成的工具将放置在应用程序的 app/Ai/Tools 目录中。每个工具包含一个 handle 方法,当 agent 需要使用该工具时会调用此方法:
php
<?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 方法中返回它:
php
use App\Ai\Tools\RandomNumberGenerator;
/**
* 获取 agent 可用的工具。
*
* @return Tool[]
*/
public function tools(): iterable
{
return [
new RandomNumberGenerator,
];
}运行时覆盖工具
withTools 方法可替换 agent 实例声明的工具。这适用于按租户或功能标志设置工具集:
php
$response = (new SupportAgent)
->withTools([new LookupOrder])
->prompt('Where is order 12345?');你也可以传入一个接收 agent 已声明工具的闭包,以追加或过滤工具:
php
$response = (new SupportAgent)
->withTools(fn (array $tools) => [...$tools, new LookupOrder])
->prompt('Where is order 12345?');验证工具参数
尽管工具的 schema 会约束模型可提供的参数,你仍可使用 request 的 validate 方法验证传入参数:
php
public function handle(Request $request): Stringable|string
{
$validated = $request->validate([
'city' => 'required|string',
'days' => 'required|integer|max:7',
]);
return $this->forecast($validated['city'], $validated['days']);
}验证失败时,验证消息会作为工具的结果返回给模型,使其能够修正参数并再次调用工具。
修复工具调用
使用 RepairToolCalls attribute,可让 agent 在模型调用未知本地工具时恢复。Laravel 会将失败的调用以及可用本地工具名称返回给模型,使其能够修正该调用:
php
use Laravel\Ai\Attributes\RepairToolCalls;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasTools;
use Laravel\Ai\Promptable;
#[RepairToolCalls]
class SupportAgent implements Agent, HasTools
{
use Promptable;
// ...
}Laravel 自动推导最大步骤数时,此 attribute 会为修复后的调用增加一个步骤。显式的 MaxSteps 限制保持不变。
相似性搜索
SimilaritySearch 工具允许 agent 使用存储在数据库中的向量 embeddings 搜索与给定查询相似的文档。当你想让 agent 能够搜索应用程序数据时,这对于检索增强生成(RAG)非常有用。
创建相似性搜索工具最简单的方法是使用 usingModel 方法,传入一个具有向量 embeddings 的 Eloquent 模型:
php
use App\Models\Document;
use Laravel\Ai\Tools\SimilaritySearch;
public function tools(): iterable
{
return [
SimilaritySearch::usingModel(Document::class, 'embedding'),
];
}第一个参数是 Eloquent 模型类,第二个参数是包含向量 embeddings 的列。
你还可以提供 0.0 到 1.0 之间的最小相似度阈值和一个闭包来自定义查询:
php
SimilaritySearch::usingModel(
model: Document::class,
column: 'embedding',
minSimilarity: 0.7,
limit: 10,
query: fn ($query) => $query->where('published', true),
),如需更多控制,你可以使用自定义闭包创建相似性搜索工具来返回搜索结果:
php
use App\Models\Document;
use Laravel\Ai\Tools\SimilaritySearch;
public function tools(): iterable
{
return [
new SimilaritySearch(using: function (string $query) {
return Document::query()
->where('user_id', $this->user->id)
->whereVectorSimilarTo('embedding', $query)
->limit(10)
->get();
}),
];
}你可以使用 withDescription 方法自定义工具的描述:
php
SimilaritySearch::usingModel(Document::class, 'embedding')
->withDescription('Search the knowledge base for relevant articles.'),延迟加载工具
默认情况下,agent 会随每次请求将其公开的所有工具发送给 provider。当 agent 提供大量工具时,这会消耗 token,并可能降低模型选择工具的准确性。通过将 ToolSearch provider 工具用于 OpenAI、Azure 或 Anthropic,你可以延迟发送工具定义,使 provider 仅在需要时加载它们:
php
use App\Ai\Tools\RefundOrder;
use App\Ai\Tools\SearchInvoices;
use App\Ai\Tools\Weather;
use Laravel\Ai\Providers\Tools\ToolSearch;
public function tools(): iterable
{
return [
new Weather,
new ToolSearch(tools: [
new SearchInvoices,
new RefundOrder,
]),
];
}被包装的工具无需修改。provider 会在它们与 prompt 相关时搜索并加载,之后 agent 可以像调用其他工具一样调用它们。
使用 Anthropic 时,可通过 strategy 参数决定 provider 搜索延迟工具的方式。支持的策略是 regex(默认)和 bm25:
php
new ToolSearch(tools: [new SearchInvoices], strategy: 'bm25'),使用 Anthropic 时,可通过 withProviderOptions 方法将更多 provider 专属选项传递给搜索工具:
php
(new ToolSearch(tools: [new SearchInvoices]))
->withProviderOptions(['cache_control' => ['type' => 'ephemeral']]),WARNING
缺少 tool search 支持的 provider 会抛出异常,避免延迟工具被静默丢弃。此外,Anthropic 要求至少有一个工具位于 ToolSearch wrapper 外。
文件存储工具
FileStorage 工具工厂允许你为 agent 提供访问 Laravel filesystem disk 的能力。all 方法会返回一组工具,使 agent 能够列出、读取、检查、生成 URL、写入、删除、复制和移动给定 disk 上的文件:
php
use Laravel\Ai\Tools\FileStorage;
public function tools(): iterable
{
return FileStorage::all('local');
}如果你的 agent 只应检查文件,请使用 readOnly 方法:
php
return FileStorage::readOnly('local');这些方法会返回一个 Illuminate\Support\Collection,让你可以进一步过滤提供给 agent 的工具:
php
use Laravel\Ai\Tools\Filesystem\DeleteFile;
return FileStorage::all('s3')
->reject(fn ($tool) => $tool instanceof DeleteFile);MCP 工具
如果你的应用使用 Laravel MCP,你可以为 agent 提供由 Model Context Protocol 服务器公开的工具。使用 Laravel MCP client,你可以连接到远程或本地 MCP 服务器,并将其工具直接传递给你的 agent。
NOTE
MCP 工具要求你的应用中已安装 Laravel MCP 包。
由于 MCP client 的 tools 方法会返回一个集合,因此请使用 ... 运算符将其展开到 agent 的 tools 数组中:
php
use App\Ai\Tools\RandomNumberGenerator;
use Laravel\Mcp\Client;
/**
* 获取 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:
php
use Laravel\Mcp\Facades\Mcp;
public function tools(): iterable
{
return [
...Mcp::client('github')->tools(),
];
}或者连接到本地 MCP 服务器:
php
use Laravel\Mcp\Client;
public function tools(): iterable
{
return [
...Client::local('php', ['artisan', 'mcp:start'])->tools(),
];
}如需了解如何创建和认证 MCP client,包括 bearer tokens 和 OAuth,请查阅 MCP client 文档。
Skills(技能)
Agent Skills 是包含指令和辅助文件的文件夹,用于指导 agent 完成特定任务。Skills 遵循开放标准,因此同一个 skill 可以在应用的 agent 与用于构建应用的编程 agent 之间共享。
每个 skill 都是应用 resources/skills 目录下的一个目录,其中包含 SKILL.md 文件。该目录中的其他文件,例如参考文档,会随 skill 一同打包:
text
resources/skills/
└── refund-policy/
├── SKILL.md
└── references/EDGE-CASES.mdSKILL.md 文件以 YAML frontmatter 开头,其中包含 skill 的 name 和说明其使用时机的 description,后面则是 skill 的指令:
markdown
---
name: refund-policy
description: Use when a customer asks for a refund or disputes a charge.
---
# Refund Policy
Customers may request a full refund within 30 days of purchase...省略 name 时,会使用 skill 所在目录的名称。缺少 description 的 skill 会被忽略。
要让 agent 访问你的 skills,请实现 HasSkills 接口,并在 skills 方法中返回 skill 来源:
php
use Laravel\Ai\Contracts\HasSkills;
use Laravel\Ai\Skills\Skill;
class SupportAgent implements Agent, HasSkills
{
use Promptable;
public function skills(): iterable
{
return [
resource_path('skills'),
base_path('.agents/skills'),
new Skill('house-style', 'Use when you write copy for a customer.', view('skills.house-style')),
fn () => $this->user->team->skills->map(
fn ($skill) => new Skill($skill->name, $skill->description, $skill->instructions)
),
];
}
}来源可以是目录、Skill 实例,或返回 skills 的闭包;闭包只会在 agent 需要这些 skills 时调用。当两个 skills 名称相同时,优先采用列表中排在前面的来源。
agent 会获得一个 LoadSkill 工具,该工具列出每个 skill 的名称和描述,并仅在 prompt 需要时加载 skill 的完整指令及其附带的文本文件。二进制文件和大于 256 KB 的文件会被排除。
提供商工具
提供商工具是由 AI 提供商原生实现的特殊工具,提供网页搜索、URL 抓取和文件搜索等功能。与常规工具不同,提供商工具由提供商自身执行,而非你的应用程序。
提供商工具可以从 agent 的 tools 方法中返回。
网页搜索
WebSearch 提供商工具允许 agent 搜索网页以获取实时信息。这对于回答有关时事、最新数据或自模型训练截止后可能已发生变化的主题的问题非常有用。
支持的提供商: Anthropic、OpenAI、Azure、Gemini、xAI、Groq、OpenRouter
php
use Laravel\Ai\Providers\Tools\WebSearch;
public function tools(): iterable
{
return [
new WebSearch,
];
}你可以配置网页搜索工具以限制搜索次数或将结果限制到特定域名:
php
(new WebSearch)->max(5)->allow(['laravel.com', 'php.net']),要根据用户位置细化搜索结果,请使用 location 方法:
php
(new WebSearch)->location(
city: 'New York',
region: 'NY',
country: 'US'
);NOTE
Groq 仅在其 GPT-OSS 模型上支持网页搜索,并会忽略 max、allow 和 location 方法。
网页抓取
WebFetch 提供商工具允许 agent 抓取和读取网页内容。当你需要 agent 分析特定 URL 或从已知网页检索详细信息时,这非常有用。
支持的提供商: Anthropic、Gemini、OpenRouter
php
use Laravel\Ai\Providers\Tools\WebFetch;
public function tools(): iterable
{
return [
new WebFetch,
];
}你可以配置网页抓取工具以限制抓取次数或限制到特定域名:
php
(new WebFetch)->max(3)->allow(['docs.laravel.com']),文件搜索
FileSearch 提供商工具允许 agent 搜索存储在向量存储中的文件。这通过允许 agent 搜索你上传的文档中的相关信息来实现检索增强生成(RAG)。
支持的提供商: OpenAI、Azure、Gemini、xAI
php
use Laravel\Ai\Providers\Tools\FileSearch;
public function tools(): iterable
{
return [
new FileSearch(stores: ['store_id']),
];
}你可以提供多个向量存储 ID 以跨多个存储进行搜索:
php
new FileSearch(stores: ['store_1', 'store_2']);如果你的文件有元数据,你可以通过提供 where 参数来过滤搜索结果。对于简单的等值过滤,传递一个数组:
php
new FileSearch(stores: ['store_id'], where: [
'author' => 'Taylor Otwell',
'year' => 2026,
]);对于更复杂的过滤,你可以传递一个接收 FileSearchQuery 实例的闭包:
php
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'])
);代码执行
CodeExecution provider 工具允许 agent 在 AI provider 托管的沙箱中运行代码。这适用于执行计算和分析数据。
支持的提供商: Anthropic、OpenAI、Azure、Gemini、xAI、Groq
php
use Laravel\Ai\Providers\Tools\CodeExecution;
public function tools(): iterable
{
return [new CodeExecution];
}使用 OpenAI 或 Azure 时,可以通过 provider 选项让沙箱访问已存储的文件:
php
(new CodeExecution)->withProviderOptions([
'container' => ['type' => 'auto', 'file_ids' => ['file_123']],
]);NOTE
Groq 仅在其 GPT-OSS 模型上支持代码执行。
子 Agent
Agent 也可以从另一个 agent 的 tools 方法中返回。当一个 agent 作为工具返回时,父 agent 可以将特定任务委派给子 agent,并在回答原始提示时使用子 agent 的响应。当通用型 agent 需要访问拥有独立指令、工具、模型配置或提供商偏好的专用 agent 时,这会很有用。
例如,客服 agent 可以将退款资格问题委派给专门的退款 agent:
php
<?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;
/**
* 获取 agent 应遵循的指令。
*/
public function instructions(): string
{
return 'You help customers with account, order, and billing questions. Delegate refund policy questions to the refunds specialist.';
}
/**
* 获取 agent 可用的工具。
*
* @return Tool[]
*/
public function tools(): iterable
{
return [
new RefundsAgent,
];
}
}要自定义子 agent 暴露给父 agent 的方式,请在子 agent 上实现 CanActAsTool 接口,并定义面向工具的名称和描述:
php
use Laravel\Ai\Attributes\Provider;
use Laravel\Ai\Contracts\CanActAsTool;
use Laravel\Ai\Enums\Lab;
#[Provider(Lab::Anthropic)]
class RefundsAgent implements Agent, CanActAsTool, HasTools
{
/**
* 获取 agent 的工具名称。
*/
public function name(): string
{
return 'refunds_specialist';
}
/**
* 获取 agent 的工具描述。
*/
public function description(): string
{
return 'Determine whether an order is eligible for a refund and explain the next step.';
}
// ...
}如果子 agent 未实现 CanActAsTool,Laravel 会使用 agent 的类 basename 作为工具名称,并提供一段通用描述,要求父 agent 传入清晰且自包含的任务说明。每次子 agent 调用都独立运行,并且不会接收父 agent 的对话历史。
WARNING
子 agent 目前不支持需要人工审批的工具。如果子 agent 的工具需要审批,子 agent 会暂停。不过,父 agent 会收到普通的工具结果并继续运行,你的应用也不会收到待审批通知。请将需要审批的工具注册到顶层 agent。
父 agent 进行流式传输时,生成普通文本的子 agent 也会进行流式传输;生成结构化输出的子 agent 则采用非流式方式。对于流式子 agent,父 agent 会发出 preliminary ToolResult 事件,其中包含子 agent 截至当前生成的文本。工具调用的最终结果会在这些事件之后发出,因此手动遍历事件时可以跳过 preliminary 事件:
php
use Laravel\Ai\Streaming\Events\ToolResult;
foreach ($stream as $event) {
if ($event instanceof ToolResult && $event->preliminary) {
continue;
}
// ...
}text、usage 和 toolResults 等响应值会忽略 preliminary 事件。Vercel 协议会将其渲染为原生流式工具输出,因此 useChat 无需自定义代码即可显示进度;AG-UI 协议则将其报告为活动快照。完成响应中的文本、推理、引用和 usage 都包含子 agent 的对应内容。
中间件
Agent 支持中间件,允许你在每个生成步骤发送到 provider 之前对其进行拦截和修改。中间件会在每个步骤调用一次,因此包含三个步骤的运行会调用中间件三次。可以使用 make:agent-middleware Artisan 命令创建中间件:
shell
php artisan make:agent-middleware LogPrompts生成的中间件将放置在应用程序的 app/Ai/Middleware 目录中。要向 agent 添加中间件,请实现 HasMiddleware 接口并定义一个返回中间件类数组的 middleware 方法:
php
<?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;
// ...
/**
* 获取 agent 的中间件。
*/
public function middleware(): array
{
return [
new LogPrompts,
];
}
}每个中间件类应定义一个 handle 方法,该方法接收 PendingStep 和一个将步骤传给下一个中间件的 Closure:
php
<?php
namespace App\Ai\Middleware;
use Closure;
use Illuminate\Support\Facades\Log;
use Laravel\Ai\PendingStep;
class LogPrompts
{
/**
* 处理待生成的步骤。
*/
public function handle(PendingStep $step, Closure $next)
{
Log::info('Prompting agent', ['model' => $step->model]);
return $next($step);
}
}除了即将发送的 provider、model、instructions、messages 和 tools,步骤还会公开已经完成的步骤、它们的合并 usage 和运行进度:
php
$step->steps;
$step->usage;
$step->number;
$step->isFirstStep();
$step->isFinalStep;withModel、withInstructions、withMessages、withTools、onlyTools、withoutTools、withToolChoice、withMaxTokens 和 withProviderOptions 方法都会返回步骤的副本。例如,agent 使用过某个高成本工具后,可以移除该工具:
php
public function handle(PendingStep $step, Closure $next)
{
if (! $step->isFirstStep()) {
$step = $step->withoutTools('SearchDocumentation');
}
return $next($step);
}也可以通过概括对话中段,让较长的工具调用循环保持在上下文窗口内:
php
use App\Ai\Agents\Summarizer;
use Laravel\Ai\Messages\UserMessage;
public function handle(PendingStep $step, Closure $next)
{
if (count($step->messages) > 40) {
$summary = (new Summarizer)->prompt(
collect(array_slice($step->messages, 1, -10))->map->content->implode("\n"),
)->text;
$step = $step->withMessages([
$step->messages[0],
new UserMessage("Summary of the conversation so far: {$summary}"),
...array_slice($step->messages, -10),
]);
}
return $next($step);
}传给 withMessages 方法的消息只会改变当前步骤发送的内容。后续步骤和已存储的对话仍会使用完整且未经概括的历史记录。
你可以使用 then 方法在模型回答当前步骤之后、执行其工具调用之前运行代码。同步响应和流式响应均支持此方式:
php
use Laravel\Ai\Gateway\StepResponse;
public function handle(PendingStep $step, Closure $next)
{
return $next($step)->then(function (StepResponse $response) {
Log::info('Agent responded', ['text' => $response->text]);
});
}middleware 必须返回 $next 的结果、StepResponse 或 StepResult::stop()。返回 StepResponse 可以直接回答该步骤,例如从缓存提供响应时可以省去模型调用。返回 StepResult::stop() 可以结束运行。其他返回值会导致 LogicException。
停止运行
要在下一次模型调用之前结束 agent 的运行,可以使用 Stop middleware。例如,你可以在用户按下“停止”按钮或运行达到输入 token 预算时停止运行。响应会保留已完成的步骤及其合计用量:
php
use Illuminate\Support\Facades\Cache;
use Laravel\Ai\Contracts\HasMiddleware;
use Laravel\Ai\Middleware\Stop;
use Laravel\Ai\PendingStep;
public function middleware(): array
{
return [
// 用户按下“停止”按钮时停止运行...
Stop::when(fn (PendingStep $step) => Cache::has("chat:{$this->user->id}:stopped")),
// 运行已使用至少 50,000 个输入 token 时停止...
Stop::unless(fn (PendingStep $step) => $step->usage->inputTokens < 50_000),
];
}每个步骤开始前,when 会在条件为 true 时停止运行,unless 会在条件为 false 时停止运行。停止的流式运行会以 cancelled 作为结束原因。
自定义 middleware 也可以通过返回 StepResult::stop() 结束运行:
php
use Laravel\Ai\Gateway\StepResult;
public function handle(PendingStep $step, Closure $next)
{
return $this->cancelled($step) ? StepResult::stop() : $next($step);
}匿名 Agent
有时你可能希望快速与模型交互而无需创建专用的 agent 类。你可以使用 agent 函数创建一个临时的匿名 agent:
php
use function Laravel\Ai\{agent};
$response = agent(
instructions: 'You are an expert at software development.',
messages: [],
tools: [],
)->prompt('Tell me about Laravel');匿名 agent 也可以生成结构化输出:
php
use Illuminate\Contracts\JsonSchema\JsonSchema;
use function Laravel\Ai\{agent};
$response = agent(
schema: fn (JsonSchema $schema) => [
'number' => $schema->integer()->required(),
],
)->prompt('Generate a random number less than 100');Agent 配置
你可以使用 PHP 属性为 agent 配置文本生成选项。以下属性可用:
MaxSteps:agent 使用工具时可以执行的最大步骤数。MaxTokens:模型可以生成的最大 token 数。Model:agent 应使用的模型。Provider:用于 agent 的 AI 提供商(或用于故障转移的多个提供商)。Temperature:用于生成的采样温度(0.0 到 1.0)。Timeout:agent 请求的 HTTP 超时秒数(默认:60)。TopP:用于生成的 nucleus sampling 概率(0.0 到 1.0)。UseCheapestModel:使用提供商最便宜的文本模型以优化成本。UseSmartestModel:使用提供商最强大的文本模型以处理复杂任务。
php
<?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-sonnet-5')]
#[MaxSteps(10)]
#[MaxTokens(4096)]
#[Temperature(0.7)]
#[Timeout(120)]
#[TopP(0.9)]
class SalesCoach implements Agent
{
use Promptable;
// ...
}UseCheapestModel 和 UseSmartestModel 属性允许你自动选择给定提供商中最具成本效益或最强大的模型,而无需指定模型名称。当你想在不同提供商之间优化成本或能力时,这非常有用:
php
use Laravel\Ai\Attributes\UseCheapestModel;
use Laravel\Ai\Attributes\UseSmartestModel;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Promptable;
#[UseCheapestModel]
class SimpleSummarizer implements Agent
{
use Promptable;
// 将使用最便宜的模型(例如 Haiku)...
}
#[UseSmartestModel]
class ComplexReasoner implements Agent
{
use Promptable;
// 将使用最强大的模型(例如 Opus)...
}NOTE
UseCheapestModel 和 UseSmartestModel 选择的底层模型可能会随着 Laravel AI SDK 发布版本和提供商推出新模型而变化。切换模型可能引入行为变化、已弃用参数和显著的成本差异。如果你需要稳定、可预测的模型和定价,请使用 Model 属性明确指定模型。
提供商选项
如果你的 agent 需要传递提供商特定的选项(如 OpenAI 推理强度或惩罚设置),请实现 HasProviderOptions 契约并定义一个 providerOptions 方法:
php
<?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],
'cache_control' => ['type' => 'ephemeral'],
],
default => [],
};
}
}providerOptions 方法接收当前正在使用的提供商(Lab 枚举或字符串),允许你为每个提供商返回不同的选项。当使用故障转移时,这特别有用,因为每个备用提供商可以接收自己的配置。
上面的 Anthropic 示例还通过 cache_control 启用了提示词缓存。
图像、音频、转录、embedding和重排序 builder 同样接受 provider 选项:
php
use Laravel\Ai\Audio;
$audio = Audio::of('I love coding with Laravel.')
->withProviderOptions(['speed' => 1.25])
->generate();你也可以传入闭包代替数组,闭包会接收当前使用的 provider。
自定义 HTTP header
在应用 config/ai.php 配置文件中为 provider 配置的 header 会随该 provider 的每个请求发送。若要按请求发送 header,例如 AI gateway 使用的元数据,可以使用 withHeaders 方法。该方法适用于图像、音频、转录、embedding 和重排序 builder,也适用于文件上传:
php
use Laravel\Ai\Embeddings;
$embeddings = Embeddings::for($chunks)
->withHeaders(['cf-aig-metadata' => json_encode(['team' => $team->id])])
->withProviderOptions(['dimensions' => 1024])
->generate();header 也可以通过接收当前 provider 的闭包提供。header 不包含在请求体中,也不会影响 embedding 缓存键。
提示词缓存
大多数提供商会自动缓存重复的提示词前缀,并对缓存部分按折扣计费。OpenAI、Gemini、Groq、DeepSeek 和 xAI 无需配置;你可以通过响应的使用统计查看节省量:
php
$response->usage->cacheReadInputTokens;
$response->usage->cacheWriteInputTokens;这两个计数都是输入 token 总数的子集,详见 usage 文档。
anthropic 和 bedrock 提供商仅在明确请求时进行缓存。CacheInstructions 和 CacheToolDefinitions attribute 会在 agent 指令和工具定义的末尾设置缓存断点,让每段对话都从缓存读取该前缀,无需再次写入:
php
use Laravel\Ai\Attributes\CacheInstructions;
use Laravel\Ai\Attributes\CacheToolDefinitions;
#[CacheInstructions]
#[CacheToolDefinitions]
class SalesCoach implements Agent
{
use Promptable;
// ...
}当指令在每次请求中变化,例如嵌入当前日期时,请单独使用 CacheToolDefinitions。每次变化的前缀都会创建新的缓存条目,产生写入成本且无法复用。
这些 attribute 可与故障转移安全共用;未提供支持的提供商会忽略它们。
缓存前缀默认保留五分钟。向 attribute 传入 TTL 后,Anthropic 可将保留期延长至一小时:
php
#[CacheInstructions('1h')]
#[CacheToolDefinitions('1h')]你也可通过顶层 cache_control 提供商选项启用 Anthropic 自动缓存。这会在请求最后一个区块之后设置单一断点;对话增长时,断点会随之推进,每一轮都从缓存读取此前的对话内容。两种机制可以组合使用。
WARNING
提供商会按工具、指令和消息的顺序构建提示词。将指令缓存设为一小时,要求工具定义缓存同样设为一小时。混用会抛出 InvalidArgumentException。
人工工具审批
WARNING
恢复运行时必须能够获取已暂停轮次的历史记录,工具审批才能正常工作。你可以使用 Conversational agent,例如使用 RemembersConversations trait 的 agent,也可以通过应用前端使用 withMessages 方法提供历史记录。两种方式都未使用的 agent 在工具暂停时会抛出 ApprovalNotResumableException。
执行敏感或不可逆操作的工具在执行前可能需要人工审批。要让工具支持审批,请实现 Approvable contract 并使用 InteractsWithApprovals trait。可审批工具默认需要审批:
php
<?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 'Delete a file from storage.';
}
/**
* 执行工具。
*/
public function handle(Request $request): Stringable|string
{
Storage::delete($request['path']);
return "Deleted [{$request['path']}].";
}
/**
* 获取工具的 schema 定义。
*/
public function schema(JsonSchema $schema): array
{
return [
'path' => $schema->string()->required(),
];
}
}要根据工具调用的参数判断是否需要审批,请在工具上定义 needsApproval 方法。此方法可以返回布尔值,也可以返回包含审批请求原因的 Approval 实例:
php
use Laravel\Ai\Approvals\Approval;
/**
* 判断该工具是否需要针对给定请求进行审批。
*/
protected function needsApproval(Request $request): Approval|bool
{
return str_starts_with($request['path'], 'temporary/')
? false
: Approval::required('This will permanently delete a file.');
}从 agent 的 tools 方法返回工具时,你可以覆盖工具的审批要求:
php
public function tools(): iterable
{
return [
(new SendNotification)->withoutApproval(),
(new DeleteFile)->requireApproval('Deletion review required.'),
];
}当调用可审批工具时,agent 会在执行前暂停。你可以检查响应中的待审批项,其中包含每个工具调用的 ID、工具名称、参数和审批原因:
php
$response = (new FileAssistant)
->forUser($user)
->prompt('Delete the old invoice.');
if ($response->hasPendingApprovals()) {
foreach ($response->pendingApprovals as $approval) {
// $approval->id
// $approval->tool
// $approval->arguments
// $approval->reason
}
}要恢复 agent,请继续该对话,并提供一个 Decisions 实例,为每个待处理工具调用包含一项决定。决定可以批准调用、拒绝调用,或在执行前编辑其参数:
php
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('The invoice must be retained.'),
]));IMPORTANT
暂停的轮次通过其对话和待处理工具调用进行匹配。你的应用应在恢复运行前授权访问该对话,如完整审批流程所示;也可以使用对话存储的 conversationBelongsTo 方法验证访问权限。
布尔值 true 和 false 可分别作为批准和拒绝的简写。每个待处理工具调用都必须收到一项决定。未知、缺失或之前已解决的工具调用 ID 会导致抛出 ApprovalMismatchException。你可以使用 approveRemaining 或 rejectRemaining 方法为缺少显式决定的调用提供默认决定:
php
$decisions = Decisions::from([
'call_abc' => true,
])->rejectRemaining('Not approved.');
$response = (new FileAssistant)
->continue($conversationId, as: $user)
->prompt($decisions);带结果的拒绝,例如 Decision::reject('Not approved.'),会返回给模型,使其可以继续响应。无结果的拒绝会在记录拒绝后停止生成循环。
prompt、stream、queue、broadcast、broadcastNow 和 broadcastOnQueue 方法都支持工具审批。
在 streaming 和 broadcasting 期间,暂停会表示为 tool_approval_request 事件。使用 Vercel AI SDK stream protocol 时,审批请求和结果会使用该协议原生的工具审批片段发出;Agent User Interaction 协议则会将它们报告为中断。
使用任一协议的 client 都会随对话其余内容一起提交决定,因此可以将聊天请求直接传给 agent:
php
$chat = Vercel::chat($request);
return (new FileAssistant)
->continue($conversationId, as: $request->user())
->stream($chat)
->usingProtocol($chat->protocol());恢复已暂停的轮次时,恢复后产生的步骤会合并到该轮次,因此每一轮都存储为一条助手消息。响应的 assistantMessageId 包含已暂停消息的 ID,该消息的 usage 同时包含暂停和恢复两部分。
对于 queued agent,生成的响应会传递给 then 回调,Laravel 也会分发 ToolApprovalRequested 事件。
Laravel 会在要求模型继续之前存储已批准工具的结果。如果随后生成失败,该审批已经解决。请使用普通文本 prompt 继续对话,而不是再次提交相同的审批决定。
完整审批流程
以下路由演示了完整的审批流程,它接受来自聊天界面的新文本 prompt 或审批决定。此示例假设应用的 User model 使用了 HasConversations trait:
php
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::post('/chat/{conversation}', function (Request $request, Conversation $conversation) {
Gate::authorize('view', $conversation);
$validated = $request->validate([
'message' => ['nullable', 'string', 'required_without:decisions', 'prohibits:decisions'],
'decisions' => ['nullable', 'array', 'required_without:message', 'prohibits:message'],
'decisions.*.action' => ['required_with:decisions', Rule::in(['approve', 'reject'])],
'decisions.*.result' => ['nullable', 'string'],
]);
$prompt = isset($validated['decisions'])
? Decisions::from(collect($validated['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 作为每项决定的键,将用户选择提交到同一个端点。其他情况下,界面可以提交普通的 message 值:
json
{
"decisions": {
"call_abc": {
"action": "approve"
},
"call_def": {
"action": "reject",
"result": "The invoice must be retained."
}
}
}图像
Laravel\Ai\Image 类可用于使用 openai、gemini 或 xai 提供商生成图像:
php
use Laravel\Ai\Image;
$image = Image::of('A donut sitting on the kitchen counter')->generate();
$rawContent = (string) $image;square、portrait 和 landscape 方法可用于控制图像的纵横比,而 quality 方法可用于指导模型生成最终图像质量(high、medium、low)。timeout 方法可用于指定 HTTP 超时秒数:
php
use Laravel\Ai\Image;
$image = Image::of('A donut sitting on the kitchen counter')
->quality('high')
->landscape()
->timeout(120)
->generate();你可以使用 attachments 方法附加参考图像:
php
use Laravel\Ai\Files;
use Laravel\Ai\Image;
$image = Image::of('Update this photo of me to be in the style of an impressionist painting.')
->attachments([
Files\Image::fromStorage('photo.jpg'),
// Files\Image::fromPath('/home/laravel/photo.jpg'),
// Files\Image::fromUrl('https://example.com/photo.jpg'),
// $request->file('photo'),
])
->landscape()
->generate();部分 provider 可以在单个请求中生成多张图像。OpenAI、Azure 和 xAI 接受 n provider 选项,响应会包含返回的每张图像:
php
$response = Image::of('A donut sitting on the kitchen counter')
->withProviderOptions(['n' => 4])
->generate();
count($response); // 4
$response->images; // 生成图像的集合...
$response->firstImage(); // 第一张生成的图像...生成的图像可以轻松存储到应用程序 config/filesystems.php 配置文件中配置的默认磁盘上:
php
$image = Image::of('A donut sitting on the kitchen counter');
$path = $image->store();
$path = $image->storeAs('image.jpg');
$path = $image->storePublicly();
$path = $image->storePubliclyAs('image.jpg');图像生成也可以加入队列:
php
use Laravel\Ai\Image;
use Laravel\Ai\Responses\ImageResponse;
Image::of('A donut sitting on the kitchen counter')
->portrait()
->queue()
->then(function (ImageResponse $image) {
$path = $image->store();
// ...
});音频(TTS)
Laravel\Ai\Audio 类可用于从给定文本生成音频:
php
use Laravel\Ai\Audio;
$audio = Audio::of('I love coding with Laravel.')->generate();
$rawContent = (string) $audio;你也可以通过 Laravel 的 Stringable 类提供的 toAudio 方法从字符串生成音频:
php
use Illuminate\Support\Str;
$audio = Str::of('I love coding with Laravel.')->toAudio();male、female 和 voice 方法可用于确定生成音频的声音:
php
$audio = Audio::of('I love coding with Laravel.')
->female()
->generate();
$audio = Audio::of('I love coding with Laravel.')
->voice('voice-id-or-name')
->generate();同样,instructions 方法可用于动态指导模型生成音频的听觉效果:
php
$audio = Audio::of('I love coding with Laravel.')
->female()
->instructions('Said like a pirate')
->generate();生成的音频可以轻松存储到应用程序 config/filesystems.php 配置文件中配置的默认磁盘上:
php
$audio = Audio::of('I love coding with Laravel.')->generate();
$path = $audio->store();
$path = $audio->storeAs('audio.mp3');
$path = $audio->storePublicly();
$path = $audio->storePubliclyAs('audio.mp3');音频生成也可以加入队列:
php
use Laravel\Ai\Audio;
use Laravel\Ai\Responses\AudioResponse;
Audio::of('I love coding with Laravel.')
->queue()
->then(function (AudioResponse $audio) {
$path = $audio->store();
// ...
});转录(STT)
Laravel\Ai\Transcription 类可用于生成给定音频的转录文本:
php
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;diarize 方法可用于指示你希望响应中除了原始文本转录外还包含说话人分离的转录,允许你按说话人访问分段转录:
php
$transcript = Transcription::fromStorage('audio.mp3')
->diarize()
->generate();转录生成也可以加入队列:
php
use Laravel\Ai\Transcription;
use Laravel\Ai\Responses\TranscriptionResponse;
Transcription::fromStorage('audio.mp3')
->queue()
->then(function (TranscriptionResponse $transcript) {
// ...
});文本摘要
你可以使用 Laravel 的 Stringable 类提供的 summarize 方法对文本进行摘要。默认情况下,摘要最多包含三个句子,并使用已配置 provider 中最便宜的文本模型生成:
php
use Illuminate\Support\Str;
$summary = Str::of($article)->summarize();你可以指定用于生成摘要的最大句子数、provider、model 和 timeout。Str 类也提供了该方法的静态版本:
php
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);Embeddings
你可以使用 Laravel Stringable 类提供的新 toEmbeddings 方法轻松地为任何给定字符串生成向量 embeddings:
php
use Illuminate\Support\Str;
$embeddings = Str::of('Napa Valley has great wine.')->toEmbeddings();或者,你可以使用 Embeddings 类一次为多个输入生成 embeddings:
php
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, ...]]你可以指定 embeddings 的维度和提供商:
php
$response = Embeddings::for(['Napa Valley has great wine.'])
->dimensions(1536)
->generate(Lab::OpenAI, 'text-embedding-3-small');多模态 Embeddings
除了字符串,Embeddings::for 方法还接受图像、音频、文档和视频输入,让你可以为非文本内容生成 embeddings。Gemini 支持图像、音频、文档和视频 embeddings,而 VoyageAI 支持图像和视频 embeddings:
php
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);多模态输入使用与附件所用文件类相同的文件类。这些文件可以从本地路径、filesystem disk、远程 URL 或 Base64 编码内容创建。图像、文档和视频也可以从上传文件创建,而文档还可以从原始字符串内容创建:
php
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'));NOTE
VoyageAI 不允许在同一个请求中混用远程 URL 媒体和 Base64 编码媒体。本地文件、已存储文件和上传文件会作为 Base64 编码内容发送,而文本输入可以与任一媒体来源组合使用。请查阅你的 provider 文档,确认可用的多模态模型和输入类型。
查询 Embeddings
生成 embeddings 后,你通常会将它们存储在数据库的 vector 列中以供后续查询。Laravel 原生支持 PostgreSQL(通过 pgvector 扩展)和 MariaDB 的向量列。首先,在迁移中定义一个 vector 列,指定维度数:
php
Schema::ensureVectorExtensionExists();
Schema::create('documents', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('content');
$table->vector('embedding', dimensions: 1536);
$table->timestamps();
});你还可以添加向量索引以加速相似性搜索。在向量列上调用 index 时,Laravel 将自动创建一个使用余弦距离的 HNSW 索引:
php
$table->vector('embedding', dimensions: 1536)->index();在你的 Eloquent 模型上,你应使用 AsVector cast 转换向量列:
php
use Illuminate\Database\Eloquent\Casts\AsVector;
protected function casts(): array
{
return [
'embedding' => AsVector::class,
];
}要查询相似记录,请使用 whereVectorSimilarTo 方法。此方法按最小余弦相似度(0.0 到 1.0 之间,1.0 表示完全相同)过滤结果,并按相似度排序:
php
use App\Models\Document;
$documents = Document::query()
->whereVectorSimilarTo('embedding', $queryEmbedding, minSimilarity: 0.4)
->limit(10)
->get();$queryEmbedding 可以是浮点数组或纯字符串。当给定字符串时,Laravel 会自动为其生成 embeddings:
php
$documents = Document::query()
->whereVectorSimilarTo('embedding', 'best wineries in Napa Valley')
->limit(10)
->get();如果你需要更多控制,可以独立使用底层的 whereVectorDistanceLessThan、selectVectorDistance 和 orderByVectorDistance 方法:
php
$documents = Document::query()
->select('*')
->selectVectorDistance('embedding', $queryEmbedding, as: 'distance')
->whereVectorDistanceLessThan('embedding', $queryEmbedding, maxDistance: 0.3)
->orderByVectorDistance('embedding', $queryEmbedding)
->limit(10)
->get();如果你想让 agent 能够作为工具执行相似性搜索,请查看相似性搜索工具文档。
NOTE
向量查询目前支持使用 pgvector 扩展的 PostgreSQL 连接,或使用原生向量支持的 MariaDB 11.7 及更高版本。
缓存 Embeddings
Embedding 生成可以被缓存以避免对相同输入的冗余 API 调用。要启用缓存,请将 ai.caching.embeddings.cache 配置选项设置为 true:
php
'caching' => [
'embeddings' => [
'cache' => true,
'store' => env('CACHE_STORE', 'database'),
'individually' => true,
// ...
],
],启用缓存后,embeddings 将被缓存 30 天。缓存键基于提供商、模型、维度和输入内容,确保相同的请求返回缓存结果,而不同的配置会生成新的 embeddings。
默认情况下,每个输入的 embedding 都会使用独立缓存键,因此后续请求即使输入集合或顺序发生变化,仍可命中已见过的输入。要让整个输入集合共用一个缓存键,请将 ai.caching.embeddings.individually 配置选项设为 false。
你也可以使用 cache 方法为特定请求启用缓存,即使全局缓存已禁用:
php
$response = Embeddings::for(['Napa Valley has great wine.'])
->cache()
->generate();你可以指定自定义的缓存持续时间(秒):
php
$response = Embeddings::for(['Napa Valley has great wine.'])
->cache(seconds: 3600) // 缓存 1 小时
->generate();toEmbeddings Stringable 方法也接受 cache 参数:
php
// 使用默认持续时间缓存...
$embeddings = Str::of('Napa Valley has great wine.')->toEmbeddings(cache: true);
// 缓存指定持续时间...
$embeddings = Str::of('Napa Valley has great wine.')->toEmbeddings(cache: 3600);重排序
重排序允许你根据文档与给定查询的相关性重新排列文档列表。这对于通过语义理解来改善搜索结果非常有用:
Laravel\Ai\Reranking 类可用于重排序文档:
php
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 方法可用于限制返回的结果数量;timeout 方法可用于指定 HTTP 超时秒数,默认值为 30:
php
$response = Reranking::of($documents)
->limit(5)
->timeout(60)
->rerank('search query');重排序集合
为方便起见,Laravel 集合可以使用 rerank 宏进行重排序。第一个参数指定用于重排序的字段,第二个参数是查询:
php
// 按单个字段重排序...
$posts = Post::all()
->rerank('body', 'Laravel tutorials');
// 按多个字段重排序(以 JSON 形式发送)...
$reranked = $posts->rerank(['title', 'body'], 'Laravel tutorials');
// 使用闭包构建文档进行重排序...
$reranked = $posts->rerank(
fn ($post) => $post->title.': '.$post->body,
'Laravel tutorials'
);你还可以限制结果数量并指定提供商:
php
$reranked = $posts->rerank(
by: 'content',
query: 'Laravel tutorials',
limit: 10,
provider: Lab::Cohere,
timeout: 60,
);分类
WARNING
分类功能目前处于实验阶段,其 API 可能会在 AI SDK 的后续次要版本中发生变化。
分类功能允许你针对给定字符串或数据数组提出一组固定问题,并为每个问题接收带有概率的类型化答案,而非自由格式文本。这适用于路由、内容审核和评分等场景,你可以将答案与阈值比较,也可以在测试中对答案进行断言。
Laravel\Ai\Classification 类可用于对内容进行分类。每个问题都有一个键,可以通过该键从响应中获取相应答案:
php
use Laravel\Ai\Classification;
use Laravel\Ai\Classification\Boolean;
use Laravel\Ai\Classification\Choice;
use Laravel\Ai\Classification\Score;
$result = Classification::of($supportRequest)
->questions([
'urgent' => new Boolean('Does this request need an immediate response?', [
'true' => 'Explicitly time-sensitive',
'false' => 'No urgency expressed',
]),
'department' => new Choice('Which team should handle this request?', [
'billing' => 'Payments, invoices, and refunds',
'technical' => 'Bugs, outages, and integrations',
'sales' => 'Pricing, plans, and upgrades',
]),
'frustration' => new Score('How frustrated is the customer?', [
'Calm',
'Frustrated',
'Very angry',
]),
])
->classify();Boolean 问题返回答案为 "true" 的概率。isTrue 方法可用于判断该概率是否达到给定阈值,阈值默认为 0.5:
php
$result['urgent']->probability; // 0.94
$result['urgent']->isTrue(threshold: 0.8); // trueChoice 问题返回给定选项之一,以及每个选项的概率。confidence 属性表示 provider 对完整概率集合的确定程度;provider 无法测量时,该值为 null:
php
$result['department']->choice; // 'technical'
$result['department']->probabilityOf('technical'); // 0.87
$result['department']->probabilities; // ['billing' => 0.08, 'technical' => 0.87, 'sales' => 0.05]
$result['department']->confidence; // 0.82Score 问题返回给定有序等级中的位置。score 属性按概率加权,因此可能位于两个等级之间;level 和 label 方法描述概率最高的等级:
php
$result['frustration']->score; // 1.24,按概率加权的等级
$result['frustration']->level(); // 1,概率最高的等级
$result['frustration']->label(); // 'Frustrated'
$result['frustration']->normalized(); // 0.62,分数占最高等级的比例
$result['frustration']->probabilities; // [0.12, 0.52, 0.36]当一句话不足以表达时,传给 Boolean 问题的 criteria、传给 Choice 问题的选项描述,以及传给 Score 问题的等级都可以是数组。Choice 问题至少需要两个选项,Score 问题至少需要两个等级。
响应支持遍历、计数和数组方式访问。此外,answer 方法可获取单个答案,collect 方法则将全部答案作为集合返回:
php
$result->answer('urgent');
$result->collect();
$result->usage;
$result->meta->provider;图像分类
使用 OpenAI、OpenAI 兼容、TypeSafe 或 OpenRouter 提供商时,你可以将图像作为 of 方法的第二个参数传入,与给定内容一同进行分类。图像可以通过附件所用的文件类创建:
php
use Laravel\Ai\Classification;
use Laravel\Ai\Classification\Boolean;
use Laravel\Ai\Files\Image;
$result = Classification::of('Inspect the product in this photo.', [
Image::fromPath($photo),
])
->question('damaged', new Boolean('Does the product have visible damage?'))
->classify(provider: 'openai');要在问题中引用特定图像,可以使用字符串键来附加图像。SDK 会将每张图像的键作为其名称发送给提供商:
php
$result = Classification::of('Inspect the products in these photos.', [
'front' => Image::fromPath($front),
'back' => Image::fromPath($back),
])
->question('damaged', new Boolean('Is the product in `front` or `back` damaged?'))
->classify();是非判断
对于单个是非决定,可以使用 Laravel Stringable 类提供的 decide 方法,它返回布尔值而非完整响应。你可以描述 "yes" 和 "no" 的含义,并指定答案必须达到的概率,默认值为 0.5:
php
use Illuminate\Support\Str;
if (Str::of($message)->decide('Is this spam?')) {
// ...
}
$spam = Str::of($message)->decide('Is this spam?', criteria: [
'true' => 'Unsolicited bulk mail.',
'false' => 'A genuine message from a customer.',
], threshold: 0.9);从集合中选择
要从一组选项中快速选择一个项,可以使用 Laravel Collection 类提供的 decide 方法。该方法接收一个问题和待分类文本,并返回集合中选定的项。
字符串和枚举集合可以直接使用,其他类型的项应通过 by 参数指定名称。你还可以通过 describe 参数提供字段、字段数组或闭包,为模型提供每个选项的更多详细信息:
php
$department = collect(['billing', 'technical', 'sales'])
->decide('Which team should handle this request?', $ticket->body);
$priority = collect(Priority::cases())
->decide('How urgent is this request?', $ticket->body);
$department = Department::all()->decide(
'Which department should handle this request?',
$ticket->body,
by: 'name',
describe: 'description',
);通过 describe 参数定义的描述,会作为模型判断文本是否匹配该选项的标准。当选项名称本身存在歧义时,这尤其有用。向 describe 参数提供闭包时,闭包会接收每个项,并应返回描述该项的字符串或数组:
php
$priority = collect(Priority::cases())->decide(
'How urgent is this request?',
$ticket->body,
describe: fn (Priority $priority) => $priority->description(),
);当字符串集合使用字符串作为键时,键会作为选项,值会作为选项描述,并返回选定的键。如果指定了 threshold,且选定选项的概率低于该阈值,则返回 null:
php
$department = collect([
'billing' => 'Payments, invoices, and refunds',
'technical' => 'Bugs, outages, and integrations',
])->decide('Which team should handle this request?', $ticket->body, threshold: 0.6) ?? 'triage';文件
Laravel\Ai\Files 类或各个文件类可用于将文件存储到你的 AI 提供商,以便稍后在对话中使用。这对于大型文档或你想多次引用而不需要重新上传的文件非常有用:
php
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();
// 存储文件系统磁盘上的文件...
$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
use Laravel\Ai\Files;
use Laravel\Ai\Files\Document;
// 存储原始内容...
$stored = Document::fromString('Hello, World!', 'text/plain')->put();
// 存储上传的文件...
$stored = Document::fromUpload($request->file('document'))->put();文件存储后,你可以在通过 agent 生成文本时引用该文件,而无需重新上传:
php
use App\Ai\Agents\SalesCoach;
use Laravel\Ai\Files;
$response = (new SalesCoach)->prompt(
'Analyze the attached sales transcript...',
attachments: [
Files\Document::fromId('file-id') // 附加已存储的文档...
]
);要检索之前存储的文件,请在文件实例上使用 get 方法:
php
use Laravel\Ai\Files\Document;
$file = Document::fromId('file-id')->get();
$file->id;
$file->mimeType();要从提供商删除文件,请使用 delete 方法:
php
Document::fromId('file-id')->delete();默认情况下,Files 类使用应用程序 config/ai.php 配置文件中配置的默认 AI 提供商。对于大多数操作,你可以使用 provider 参数指定不同的提供商:
php
$response = Document::fromPath(
'/home/laravel/document.pdf'
)->put(provider: Lab::Anthropic);你可以使用 withProviderOptions 方法传入提供商特定的上传选项。例如,你可以设置 OpenAI 文件的 purpose:
php
use Laravel\Ai\Files\Document;
$response = Document::fromPath('/home/laravel/knowledge.txt')
->withProviderOptions(['purpose' => 'assistants'])
->put();要按提供商限定选项,请传入一个接收当前提供商的闭包:
php
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();在对话中使用已存储的文件
文件存储到提供商后,你可以使用 Document 或 Image 类的 fromId 方法在 agent 对话中引用它:
php
use App\Ai\Agents\DocumentAnalyzer;
use Laravel\Ai\Files;
use Laravel\Ai\Files\Document;
$stored = Document::fromPath('/path/to/report.pdf')->put();
$response = (new DocumentAnalyzer)->prompt(
'Summarize this document.',
attachments: [
Document::fromId($stored->id),
],
);同样,已存储的图像可以使用 Image 类引用:
php
use Laravel\Ai\Files;
use Laravel\Ai\Files\Image;
$stored = Image::fromPath('/path/to/photo.jpg')->put();
$response = (new ImageAnalyzer)->prompt(
'What is in this image?',
attachments: [
Image::fromId($stored->id),
],
);向量存储
向量存储允许你创建可搜索的文件集合,用于检索增强生成(RAG)。Laravel\Ai\Stores 类提供了创建、检索和删除向量存储的方法:
php
use Laravel\Ai\Stores;
// 创建新的向量存储...
$store = Stores::create('Knowledge Base');
// 使用额外选项创建存储...
$store = Stores::create(
name: 'Knowledge Base',
description: 'Documentation and reference materials.',
expiresWhenIdleFor: days(30),
);
return $store->id;要通过 ID 检索现有的向量存储,请使用 get 方法:
php
use Laravel\Ai\Stores;
$store = Stores::get('store_id');
$store->id;
$store->name;
$store->fileCounts;
$store->ready;要删除向量存储,请在 Stores 类或存储实例上使用 delete 方法:
php
use Laravel\Ai\Stores;
// 通过 ID 删除...
Stores::delete('store_id');
// 或通过存储实例删除...
$store = Stores::get('store_id');
$store->delete();向存储添加文件
拥有向量存储后,你可以使用 add 方法向其中添加文件。添加到存储中的文件会自动索引,以便使用文件搜索提供商工具进行语义搜索:
php
use Laravel\Ai\Files\Document;
use Laravel\Ai\Stores;
$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;注意: 通常,将之前存储的文件添加到向量存储时,返回的文档 ID 将与文件先前分配的 ID 匹配;但是,某些向量存储提供商可能会返回一个新的、不同的"文档 ID"。因此,建议你始终在数据库中存储这两个 ID 以供将来引用。
向 Gemini 存储添加文件时,Laravel 会等待导入完成,确保调用返回后即可搜索该文档。如果导入失败或超过五分钟,将抛出 Laravel\Ai\Exceptions\AiException;因此可以考虑通过队列任务添加 Gemini 文件。
你可以在将文件添加到存储时附加元数据。此元数据稍后可用于在使用文件搜索提供商工具时过滤搜索结果:
php
$store->add(Document::fromPath('/path/to/document.pdf'), metadata: [
'author' => 'Taylor Otwell',
'department' => 'Engineering',
'year' => 2026,
]);要从存储中移除文件,请使用 remove 方法:
php
$store->remove('file_id');从向量存储中移除文件不会将其从提供商的文件存储中删除。要从向量存储中移除文件并从文件存储中永久删除它,请使用 deleteFile 参数:
php
$store->remove('file_abc123', deleteFile: true);用量
每个响应都包含一个 usage 属性,其中包含 provider 报告的 token 计数。输入和输出计数均为总数,因此被计为缓存 token 或推理 token 的部分也包含在相应总数中:
php
$response = (new SalesCoach)->prompt('Analyze this sales transcript...');
$response->usage->inputTokens;
$response->usage->outputTokens;
$response->usage->totalTokens();文本生成返回 Laravel\Ai\Responses\Data\TextUsage 实例,提供这些总数的进一步明细。provider 未报告某项值时,该值为 null,而非 0:
php
$response->usage->cacheReadInputTokens; // 从 prompt 缓存读取的输入 token 子集...
$response->usage->cacheWriteInputTokens; // 写入 prompt 缓存的输入 token 子集...
$response->usage->reasoningTokens; // 用于推理的输出 token 子集...
$response->usage->uncachedInputTokens(); // 既未从缓存读取也未写入缓存的输入 token...缓存读取、缓存写入和未缓存输入采用不同费率,因此应分别计算这三项费用。
其余功能会返回包含其特定计数的 usage 对象:
| 功能 | 用量对象 | 附加字段 |
|---|---|---|
| 文本、分类 | TextUsage | 缓存读取、缓存写入和推理 token |
| 图像 | ImageUsage | imageInputTokens 和 imageOutputTokens |
| 转录 | TranscriptionUsage | audioSeconds,即转录音频的时长 |
| 重排序 | RerankingUsage | searchUnits,部分 provider 使用它代替 token 计费 |
| 音频、embeddings | Usage |
并非每个 provider 都会报告所有计数。provider 未报告的计数值为 null:
php
use Laravel\Ai\Image;
use Laravel\Ai\Transcription;
Image::of('A donut sitting on the kitchen counter')->generate()->usage->imageOutputTokens;
Transcription::fromPath('/home/laravel/meeting.mp3')->generate()->usage->audioSeconds;故障转移
在提示或生成其他媒体时,你可以提供一个提供商/模型数组,以便在主提供商遇到服务中断或速率限制时自动故障转移到备用提供商/模型:
php
use App\Ai\Agents\SalesCoach;
use Laravel\Ai\Enums\Lab;
use Laravel\Ai\Image;
$response = (new SalesCoach)->prompt(
'Analyze this sales transcript...',
provider: [Lab::OpenAI, Lab::Anthropic],
);
$image = Image::of('A donut sitting on the kitchen counter')
->generate(provider: [Lab::Gemini, Lab::xAI]);只有在抛出 FailoverableException 时才会触发故障转移,例如速率限制(RateLimitedException)、提供商过载或不可用(ProviderOverloadedException),或额度不足(InsufficientCreditsException)。普通错误,例如验证错误或错误请求错误,不会触发故障转移。
当你传入普通的提供商列表(例如 [Lab::OpenAI, Lab::Anthropic])时,每个提供商都会使用其默认模型。要为故障转移链中的每个提供商指定特定模型,请传入以提供商为键的关联数组,并使用 Lab 枚举的 value 作为键(enum case 不能直接用作 PHP 数组键):
php
use Laravel\Ai\Enums\Lab;
$response = (new SalesCoach)->prompt(
'Analyze this sales transcript...',
provider: [
Lab::Gemini->value => 'gemini-3-flash-preview',
Lab::DeepSeek->value => 'deepseek-v4-pro',
],
);测试
在模拟已加入队列的图像、音频、转录或 embeddings 生成时,已在该队列生成上注册的任何 then callback 都会接收模拟响应,从而让你能够测试 callback 内部的逻辑。若要避免调用这些 callback,也可同时使用 Queue::fake() 模拟队列。
Agent
要在测试中模拟 agent 的响应,请在 agent 类上调用 fake 方法。你可以选择性地提供响应数组或闭包:
php
use App\Ai\Agents\SalesCoach;
use Laravel\Ai\Prompts\AgentPrompt;
// 自动为每个提示生成固定响应...
SalesCoach::fake();
// 提供提示响应列表...
SalesCoach::fake([
'First response',
'Second response',
]);
// 根据传入的提示动态处理响应...
SalesCoach::fake(function (AgentPrompt $prompt) {
return 'Response for: '.$prompt->prompt;
});模拟返回结构化输出的 agent 时,你可以提供数组作为响应。agent 将返回包含给定数据的结构化响应:
php
SalesCoach::fake([
['score' => 87],
]);你也可以伪造一个正在等待工具审批的响应:
php
use Laravel\Ai\Approvals\PendingApproval;
use Laravel\Ai\Responses\AgentResponse;
FileAssistant::fake([
AgentResponse::fakeWithPendingApprovals([
new PendingApproval(
id: 'call_abc',
tool: 'DeleteFile',
arguments: ['path' => 'invoice.pdf'],
reason: 'This will permanently delete a file.',
),
]),
]);
$response = (new FileAssistant)->prompt('Delete the invoice.');
$response->hasPendingApprovals(); // true还可以伪造包含推理内容的响应。该伪响应会发出推理事件,因此流式运行也会报告推理内容:
php
use Laravel\Ai\Responses\AgentResponse;
SalesCoach::fake([
AgentResponse::fakeWithReasoning('They asked about pricing.', 'Plans start at $10.'),
]);
$response = (new SalesCoach)->stream('What does it cost?');
foreach ($response as $event) {
// ...
}
$response->reasoning; // 'They asked about pricing.'注意: 当在返回结构化输出的 agent 上调用
Agent::fake()且未显式提供假输出时,Laravel 会自动生成与 agent 定义的输出模式匹配的假数据。
提示 agent 后,你可以对接收到的提示进行断言:
php
use Laravel\Ai\Prompts\AgentPrompt;
SalesCoach::assertPrompted('Analyze this...');
SalesCoach::assertPrompted(function (AgentPrompt $prompt) {
return $prompt->contains('Analyze');
});
SalesCoach::assertPromptedTimes(3);
SalesCoach::assertNotPrompted('Missing prompt');
SalesCoach::assertNeverPrompted();断言审批继续流程时,你可以检查 prompt 的审批决定:
php
use Laravel\Ai\Approvals\Decisions;
use Laravel\Ai\Prompts\AgentPrompt;
FileAssistant::fake();
(new FileAssistant)->prompt(Decisions::from([
'call_abc' => true,
]));
FileAssistant::assertPrompted(function (AgentPrompt $prompt) {
return $prompt->hasApprovalDecisions()
&& $prompt->approvalDecisions->get('call_abc')->isApproved();
});对于加入队列的 agent 调用,使用队列断言方法:
php
use Laravel\Ai\QueuedAgentPrompt;
SalesCoach::assertQueued('Analyze this...');
SalesCoach::assertQueued(function (QueuedAgentPrompt $prompt) {
return $prompt->contains('Analyze');
});
SalesCoach::assertNotQueued('Missing prompt');
SalesCoach::assertNeverQueued();要确保所有 agent 调用都有对应的假响应,你可以使用 preventStrayPrompts。如果 agent 在没有定义假响应的情况下被调用,将抛出异常:
php
SalesCoach::fake()->preventStrayPrompts();图像
可以通过在 Image 类上调用 fake 方法来模拟图像生成。模拟图像后,可以对记录的图像生成提示执行各种断言:
php
use Laravel\Ai\Image;
use Laravel\Ai\Prompts\ImagePrompt;
use Laravel\Ai\Prompts\QueuedImagePrompt;
// 自动为每个提示生成固定响应...
Image::fake();
// 提供提示响应列表...
Image::fake([
base64_encode($firstImage),
base64_encode($secondImage),
]);
// 根据传入的提示动态处理响应...
Image::fake(function (ImagePrompt $prompt) {
return base64_encode('...');
});生成图像后,你可以对接收到的提示进行断言:
php
Image::assertGenerated(function (ImagePrompt $prompt) {
return $prompt->contains('sunset') && $prompt->isLandscape();
});
Image::assertNotGenerated('Missing prompt');
Image::assertNothingGenerated();对于加入队列的图像生成,使用队列断言方法:
php
Image::assertQueued(
fn (QueuedImagePrompt $prompt) => $prompt->contains('sunset')
);
Image::assertNotQueued('Missing prompt');
Image::assertNothingQueued();要确保所有图像生成都有对应的假响应,你可以使用 preventStrayImages。如果在没有定义假响应的情况下生成图像,将抛出异常:
php
Image::fake()->preventStrayImages();音频
可以通过在 Audio 类上调用 fake 方法来模拟音频生成。模拟音频后,可以对记录的音频生成提示执行各种断言:
php
use Laravel\Ai\Audio;
use Laravel\Ai\Prompts\AudioPrompt;
use Laravel\Ai\Prompts\QueuedAudioPrompt;
// 自动为每个提示生成固定响应...
Audio::fake();
// 提供提示响应列表...
Audio::fake([
base64_encode($firstAudio),
base64_encode($secondAudio),
]);
// 根据传入的提示动态处理响应...
Audio::fake(function (AudioPrompt $prompt) {
return base64_encode('...');
});生成音频后,你可以对接收到的提示进行断言:
php
Audio::assertGenerated(function (AudioPrompt $prompt) {
return $prompt->contains('Hello') && $prompt->isFemale();
});
Audio::assertNotGenerated('Missing prompt');
Audio::assertNothingGenerated();对于加入队列的音频生成,使用队列断言方法:
php
Audio::assertQueued(
fn (QueuedAudioPrompt $prompt) => $prompt->contains('Hello')
);
Audio::assertNotQueued('Missing prompt');
Audio::assertNothingQueued();要确保所有音频生成都有对应的假响应,你可以使用 preventStrayAudio。如果在没有定义假响应的情况下生成音频,将抛出异常:
php
Audio::fake()->preventStrayAudio();转录
可以通过在 Transcription 类上调用 fake 方法来模拟转录生成。模拟转录后,可以对记录的转录生成提示执行各种断言:
php
use Laravel\Ai\Transcription;
use Laravel\Ai\Prompts\TranscriptionPrompt;
use Laravel\Ai\Prompts\QueuedTranscriptionPrompt;
// 自动为每个提示生成固定响应...
Transcription::fake();
// 提供提示响应列表...
Transcription::fake([
'First transcription text.',
'Second transcription text.',
]);
// 根据传入的提示动态处理响应...
Transcription::fake(function (TranscriptionPrompt $prompt) {
return 'Transcribed text...';
});生成转录后,你可以对接收到的提示进行断言:
php
Transcription::assertGenerated(function (TranscriptionPrompt $prompt) {
return $prompt->language === 'en' && $prompt->isDiarized();
});
Transcription::assertNotGenerated(
fn (TranscriptionPrompt $prompt) => $prompt->language === 'fr'
);
Transcription::assertNothingGenerated();对于加入队列的转录生成,使用队列断言方法:
php
Transcription::assertQueued(
fn (QueuedTranscriptionPrompt $prompt) => $prompt->isDiarized()
);
Transcription::assertNotQueued(
fn (QueuedTranscriptionPrompt $prompt) => $prompt->language === 'fr'
);
Transcription::assertNothingQueued();要确保所有转录生成都有对应的假响应,你可以使用 preventStrayTranscriptions。如果在没有定义假响应的情况下生成转录,将抛出异常:
php
Transcription::fake()->preventStrayTranscriptions();Embeddings
可以通过在 Embeddings 类上调用 fake 方法来模拟 embeddings 生成。模拟 embeddings 后,可以对记录的 embeddings 生成提示执行各种断言:
php
use Laravel\Ai\Embeddings;
use Laravel\Ai\Prompts\EmbeddingsPrompt;
use Laravel\Ai\Prompts\QueuedEmbeddingsPrompt;
// 自动为每个提示生成适当维度的假 embeddings...
Embeddings::fake();
// 提供提示响应列表...
Embeddings::fake([
[$firstEmbeddingVector],
[$secondEmbeddingVector],
]);
// 根据传入的提示动态处理响应...
Embeddings::fake(function (EmbeddingsPrompt $prompt) {
return array_map(
fn () => Embeddings::fakeEmbedding($prompt->dimensions),
$prompt->inputs
);
});生成 embeddings 后,你可以对接收到的提示进行断言:
php
Embeddings::assertGenerated(function (EmbeddingsPrompt $prompt) {
return $prompt->contains('Laravel') && $prompt->dimensions === 1536;
});
Embeddings::assertNotGenerated(
fn (EmbeddingsPrompt $prompt) => $prompt->contains('Other')
);
Embeddings::assertNothingGenerated();对于加入队列的 embeddings 生成,使用队列断言方法:
php
Embeddings::assertQueued(
fn (QueuedEmbeddingsPrompt $prompt) => $prompt->contains('Laravel')
);
Embeddings::assertNotQueued(
fn (QueuedEmbeddingsPrompt $prompt) => $prompt->contains('Other')
);
Embeddings::assertNothingQueued();要确保所有 embeddings 生成都有对应的假响应,你可以使用 preventStrayEmbeddings。如果在没有定义假响应的情况下生成 embeddings,将抛出异常:
php
Embeddings::fake()->preventStrayEmbeddings();重排序
可以通过在 Reranking 类上调用 fake 方法来模拟重排序操作:
php
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),
],
]);重排序后,你可以对执行的操作进行断言:
php
Reranking::assertReranked(function (RerankingPrompt $prompt) {
return $prompt->contains('Laravel') && $prompt->limit === 5;
});
Reranking::assertNotReranked(
fn (RerankingPrompt $prompt) => $prompt->contains('Django')
);
Reranking::assertNothingReranked();分类
可以通过在 Classification 类上调用 fake 方法来模拟分类。未提供自定义响应时,Laravel 会自动生成符合每个问题结构的答案:
php
use Laravel\Ai\Classification;
use Laravel\Ai\Prompts\ClassificationPrompt;
use Laravel\Ai\Responses\Data\BooleanAnswer;
use Laravel\Ai\Responses\Data\ChoiceAnswer;
// 自动生成假答案...
Classification::fake();
// 为特定问题提供答案...
Classification::fake([
[
'urgent' => new BooleanAnswer(0.94),
'department' => new ChoiceAnswer('technical', [
'billing' => 0.08,
'technical' => 0.87,
'sales' => 0.05,
], confidence: 0.82),
],
]);
// 根据 prompt 构建答案...
Classification::fake(fn (ClassificationPrompt $prompt) => [
'urgent' => new BooleanAnswer($prompt->contains('ASAP') ? 1.0 : 0.0),
]);假响应中省略的问题仍会获得生成的答案,因此测试只需提供需要断言的答案。
分类后,你可以对执行的操作进行断言:
php
Classification::assertClassified(function (ClassificationPrompt $prompt) {
return $prompt->contains('refund') && $prompt->asks('department');
});
Classification::assertNotClassified(
fn (ClassificationPrompt $prompt) => $prompt->asks('sentiment')
);
Classification::assertNothingClassified();文件
可以通过在 Files 类上调用 fake 方法来模拟文件操作:
php
use Laravel\Ai\Files;
Files::fake();模拟文件操作后,你可以对发生的上传和删除进行断言:
php
use Laravel\Ai\Contracts\Files\StorableFile;
use Laravel\Ai\Files\Document;
// 存储文件...
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();对于文件删除的断言,你可以传递文件 ID:
php
Files::assertDeleted('file-id');
Files::assertNotDeleted('file-id');
Files::assertNothingDeleted();向量存储
可以通过在 Stores 类上调用 fake 方法来模拟向量存储操作。模拟存储也会自动模拟文件操作:
php
use Laravel\Ai\Stores;
Stores::fake();模拟存储操作后,你可以对创建或删除的存储进行断言:
php
use Laravel\Ai\Stores;
// 创建存储...
$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();对于存储删除的断言,你可以提供存储 ID:
php
Stores::assertDeleted('store_id');
Stores::assertNotDeleted('other_store_id');
Stores::assertNothingDeleted();要断言文件是否已添加到存储或从存储中移除,请在给定的 Store 实例上使用断言方法:
php
Stores::fake();
$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');如果文件在同一请求中被存储到提供商的文件存储并添加到向量存储,你可能不知道文件的提供商 ID。在这种情况下,你可以向 assertAdded 方法传递一个闭包来对添加文件的内容进行断言:
php
use Laravel\Ai\Contracts\Files\StorableFile;
use Laravel\Ai\Files\Document;
$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 会调度多种事件,包括:
AddingFileToStoreAddingFileToStoreFailedAgentFailedAgentFailedOverAgentPromptedAgentStreamedAudioFailedAudioGeneratedClassificationFailedClassifiedClassifyingCreatingStoreCreatingStoreFailedEmbeddingsFailedEmbeddingsGeneratedFileAddedToStoreFileDeletedFileRemovedFromStoreFileStoredGeneratingAudioGeneratingEmbeddingsGeneratingImageGeneratingTranscriptionImageFailedImageGeneratedInvokingToolPromptingAgentProviderFailedOverRemovingFileFromStoreRemovingFileFromStoreFailedRerankedRerankingRerankingFailedStartingStepStepCompletedStepFailedStoreCreatedStoreDeletedStoringFileStoringFileFailedStreamingAgentToolApprovalRequestedToolApprovalResolvedToolFailedToolInvokedTranscriptionFailedTranscriptionGenerated
你可以监听这些事件中的任何一个来记录或存储 AI SDK 使用信息。