简介
Laravel AI SDK 提供了一个统一且富有表现力的 API,用于与 OpenAI、Anthropic、Gemini 等 AI 提供商进行交互。借助 AI SDK,你可以构建具备工具调用和结构化输出的智能代理,生成图像,合成与转写音频,创建向量嵌入,等等——所有操作都使用一致且符合 Laravel 风格的接口。
安装
你可以通过 Composer 安装 Laravel AI SDK:
composer require laravel/ai接下来,你应该使用 vendor:publish Artisan 命令发布 AI SDK 的配置文件和迁移文件:
php artisan vendor:publish --provider="Laravel\Ai\AiServiceProvider"最后,你应该运行应用程序的数据库迁移。这将创建 agent_conversations 和 agent_conversation_messages 表,AI SDK 会使用这些表来实现对话存储功能:
php artisan migrate配置
你可以在应用程序的 config/ai.php 配置文件中,或在应用程序的 .env 文件中通过环境变量定义 AI 提供商的凭据:
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=用于文本、图像、音频、转写和嵌入的默认模型,也可以在应用程序的 config/ai.php 配置文件中进行配置。
自定义基础 URL
默认情况下,Laravel AI SDK 会直接连接到每个提供商的公共 API 端点。然而,你可能需要将请求路由到其他端点——例如,在使用代理服务集中管理 API 密钥、实现速率限制,或通过企业网关路由流量时。
你可以通过在提供商配置中添加 url 参数来配置自定义基础 URL:
'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 令牌发送:
'providers' => [
'local' => [
'driver' => 'openai-compatible',
'url' => env('LOCAL_AI_URL'),
'key' => env('LOCAL_AI_API_KEY'),
],
],配置完成后,你可以像使用其他任何提供商一样使用该命名提供商:
agent()->prompt('What is Laravel?', provider: 'local', model: 'local-model');你也可以为该提供商配置默认的文本模型,这样就无需显式传递模型:
'local' => [
'driver' => 'openai-compatible',
'url' => env('LOCAL_AI_URL'),
'key' => env('LOCAL_AI_API_KEY'),
'models' => [
'text' => [
'default' => env('LOCAL_AI_MODEL'),
],
],
],你可以通过在提供商配置中定义 headers 数组,为该提供商的每个传出请求添加自定义 HTTP 头部。当端点除 Bearer 令牌外还需要额外的标识或身份验证头部时,这非常有用:
'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 兼容提供商支持文本生成、流式传输、工具调用、结构化输出、图像附件、嵌入、转写和分类。如果你的端点需要额外的请求体字段,请通过提供商选项提供。
OpenAI 兼容嵌入
由于任意端点没有已知的模型,在使用 OpenAI 兼容提供商调用 embeddings() 时,你必须配置一个默认的嵌入模型。你也可以配置固定的维度值;如果省略,请求将不发送 dimensions 参数,而是使用模型的原生维度。
'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, // optional
],
],
],OpenAI 兼容转写
同样,在使用 OpenAI 兼容提供商调用 Transcription 时,你必须配置一个默认的转写模型。音频将作为标准的多部分请求上传到端点的 /audio/transcriptions 路由:
'local' => [
'driver' => 'openai-compatible',
'url' => env('LOCAL_AI_URL'),
'key' => env('LOCAL_AI_API_KEY'),
'models' => [
'transcription' => [
'default' => 'whisper-1',
],
],
], OpenAI 兼容提供商和 Groq 提供商不支持说话人分离。在使用这些提供商时调用 diarize 方法会抛出异常。
OpenAI 兼容分类
如果你的端点实现了 OpenAI 的 decisions 端点,你可以使用 OpenAI 兼容提供商进行分类。SDK 会将问题发送到端点的 /decisions 路由,并支持图像附件。由于 SDK 不知道你的端点支持哪些模型,请配置一个默认的分类模型,或向 classify 方法传递一个模型:
'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 配置文件中的提供商条目一致:
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,
]),
]);在需要提供商的任何位置都可以使用按需提供商,包括故障转移列表,以及在生成图像、音频、转写和嵌入时。若要为按需提供商指定模型,请在其配置中包含 models 数组:
$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']],
]),
]);当代理应始终使用按需提供商时,你可以从代理的 provider 方法返回该提供商。由于代理被使用时会重建提供商,这种方式也适用于队列中的代理:
use Laravel\Ai\Ai;
use Laravel\Ai\Providers\Provider;
public function provider(): Provider
{
return Ai::build($this->user->aiConfiguration());
} 你应该像上面的示例所示,将按需提供商放在数组中传递。如果你在按需提供商的配置数组中为其指定了 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 |
| 嵌入向量 | 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 枚举来引用提供商,而无需使用纯字符串:
use Laravel\Ai\Enums\Lab;
Lab::Anthropic;
Lab::OpenAI;
Lab::OpenAiCompatible;
Lab::Gemini;
// ...代理
代理是 Laravel AI SDK 中与 AI 提供商交互的基础构建块。每个代理都是一个专用的 PHP 类,封装了与大语言模型交互所需的指令、对话上下文、工具和输出模式。可以将代理看作一个专门的助手——比如销售教练、文档分析器、支持机器人——你只需配置一次,然后在应用程序中根据需要随时向其发送提示。
你可以通过 make:agent Artisan 命令创建代理:
php artisan make:agent SalesCoach
php artisan make:agent SalesCoach --structured在生成的代理类中,你可以定义系统提示 / 指令、消息上下文、可用工具和输出模式(如果适用):
<?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) {}
/**
* Get the instructions that the agent should follow.
*/
public function instructions(): Stringable|string
{
return 'You are a sales coach, analyzing transcripts and providing feedback and an overall sales strength score.';
}
/**
* Get the list of messages comprising the conversation so far.
*/
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();
}
/**
* Get the tools available to the agent.
*
* @return Tool[]
*/
public function tools(): iterable
{
return [
new RetrievePreviousTranscripts,
];
}
/**
* Get the agent's structured output schema definition.
*/
public function schema(JsonSchema $schema): array
{
return [
'feedback' => $schema->string()->required(),
'score' => $schema->integer()->min(1)->max(10)->required(),
];
}
}发送提示
要向代理发送提示,首先使用 make 方法或标准实例化方式创建实例,然后调用 prompt:
$response = (new SalesCoach)
->prompt('Analyze this sales transcript...');
return (string) $response;make 方法会从容器中解析你的代理,从而支持自动依赖注入。你也可以向代理的构造函数传递参数:
$agent = SalesCoach::make(user: $user);通过向 prompt 方法传递额外参数,你可以在发送提示时覆盖默认的提供商、模型或 HTTP 超时时间:
$response = (new SalesCoach)->prompt(
'Analyze this sales transcript...',
provider: Lab::Anthropic,
model: 'claude-sonnet-5',
timeout: 120,
);原始 HTTP 响应
文本生成代理返回的每个响应,都会通过 raw 属性暴露底层提供商 API 调用的原始 HTTP 响应。这样你就可以访问 AI SDK 通用响应中不包含的提供商特定信息,如速率限制响应头、请求 ID 或其他确切的负载字段:
$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');在工具调用循环中,每一步都会保留其自身请求的原始响应:
foreach ($response->steps as $step) {
$step->raw?->header('X-RateLimit-Remaining-Requests');
} 在流式响应时,使用 Bedrock 提供商时(该提供商通过 AWS SDK 而非 HTTP 客户端执行 API 调用),以及在模拟响应上,raw 属性均为 null,除非通过 withRawResponse 显式提供了原始响应。
对话上下文
如果你的代理实现了 Conversational 接口,你可以使用 messages 方法返回之前的对话上下文(如果适用):
/**
* Get the list of messages comprising the conversation so far.
*/
public function messages(): iterable
{
return $this->user->history()
->latest()
->limit(50)
->get()
->reverse()
->map(fn ($message) => new Message(
$message->role, $message->content,
))->all();
}如果你的代理未实现 Conversational 接口,可以使用 withMessages 方法为单次运行提供对话历史,例如应用程序前端提交的历史记录:
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 接口的代理会自行加载其历史记录,因此将两种方法结合使用会抛出 LogicException。
记住对话
在使用 RemembersConversations 特性之前,你应该使用 vendor:publish Artisan 命令发布并运行 AI SDK 的迁移。这些迁移会创建用于存储对话的必要数据库表。
如果你希望 Laravel 自动为代理存储和检索对话历史,可以使用 RemembersConversations 特性。该特性提供了一种将对话消息持久化到数据库的简单方法,无需手动实现 Conversational 接口:
<?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;
/**
* Get the instructions that the agent should follow.
*/
public function instructions(): string
{
return 'You are a sales coach...';
}
}使用 RemembersConversations 特性时,不要在代理类中手动定义 messages 方法。如果存在 messages 方法,它会优先于该特性的实现,导致不会从数据库中加载对话历史。
要为用户开启新对话,请在发送提示前调用 forUser 方法:
$response = (new SalesCoach)->forUser($user)->prompt('Hello!');
$conversationId = $response->conversationId;对话 ID 会在响应中返回,可存储起来供日后参考。如果你想使用 Eloquent 检索用户的所有对话,可以在用户模型中添加 HasConversations 特性:
<?php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Ai\Concerns\HasConversations;
class User extends Authenticatable
{
use HasConversations;
}将该特性添加到模型后,你可以通过 conversations 关联关系检索和查询用户的对话:
$conversations = $user->conversations()
->latest('updated_at')
->paginate(20);要继续已有对话,请使用 continue 方法:
$response = (new SalesCoach)
->continue($conversationId, as: $user)
->prompt('Tell me more about that.');可以使用 continueOrStart 方法继续给定的对话,或者在给定 ID 为 null 时开启新对话:
$response = (new SalesCoach)
->continueOrStart($conversationId, as: $user)
->prompt('Hello!');当使用 RemembersConversations 特性时,在发起提示时,之前的消息会自动加载并纳入对话上下文。每次交互后,新消息(包括用户消息和助手消息)都会自动存储。每个响应还会包含已存储的对话和消息的 ID:
$response->conversationId;
$response->userMessageId;
$response->assistantMessageId;对话参与者
尽管用户是最常见的对话参与者,但对话可以属于任何 Eloquent 模型。可以使用 forParticipant 方法为其他类型的模型开启对话:
$response = (new SalesCoach)
->forParticipant($team)
->prompt('Review our latest sales results.');参与者的多态类和主键会随对话一起存储。因此,即使具有相同主键的不同类型模型,例如 User ID 1 和 Team ID 1,也拥有各自独立的对话历史。forUser 方法是 forParticipant 的别名。
可以使用 continueLastConversation 方法继续参与者与智能体的最近一次对话。对话是针对智能体限定范围的,因此只有智能体参与的对话才会被继续:
$response = (new SalesCoach)
->continueLastConversation($team)
->prompt('Tell me more about that.');若要继续特定的对话,请将参与者传递给 continue 方法:
$response = (new SalesCoach)
->continue($conversationId, as: $team)
->prompt('Tell me more about that.');可以向任何参与对话的 Eloquent 模型添加 HasConversations 特性。由此产生的 conversations 关系是一个以该模型的类型和主键为范围的多态关系。还可以通过其反向关系访问拥有该对话的参与者:
$conversations = $team->conversations;
$participant = $conversation->participant;如果你的应用使用了多种参与者模型类型,建议考虑定义一个 Eloquent 多态映射(Eloquent morph map),这样存储的参与者类型就不会与模型类名绑定。
continue 和 continueOrStart 方法不会验证给定的参与者是否拥有该对话。在继续对话之前,你的应用应当对访问该对话的操作进行授权。
检查已存储的对话
向用户展示对话时,通常需要消息 ID、时间戳和工具调用等详细信息。你可以从服务容器中解析对话存储,从而读取已存储的消息,无需直接查询 AI SDK 的表:
use Laravel\Ai\Contracts\ConversationStore;
$store = app(ConversationStore::class);消息采用游标分页,按从新到旧排序,并作为 StoredMessage 实例返回,其中包含每条消息的 ID、时间戳、用量、元数据和附件:
$messages = $store->paginateConversationMessages($conversationId, perPage: 25);
foreach ($messages as $message) {
$message->id;
$message->role;
$message->content;
$message->createdAt;
$message->usage;
$message->status;
}每一轮对话(包含用户提示和助手的回复)会以步骤列表的形式存储。步骤是对提供程序的一次单独请求,因此如果模型调用了工具,一轮对话会包含多个步骤。每个工具结果都会记录在生成它的工具调用上。toolCalls、providerToolCalls 和 toolResults 方法会按顺序将这些步骤扁平化,因此你无需自行遍历它们:
$message->steps;
$message->toolCalls();
$message->providerToolCalls();
$message->toolResults();工具调用在执行完成后会包含一个 result。包含 approval_reason 但没有 result 的工具调用仍在等待工具审批。
status 属性包含一个 Laravel\Ai\Enums\MessageStatus 实例。中途失败的一轮对话会以 Failed 状态存储,同时保留其已完成的步骤,因此失败前执行的工具调用仍保留在历史记录中。当对话继续时,任何未记录结果的工具调用都会被标记为中断并发送给模型,因为 Laravel 无法确定该调用是否已执行。
在通过应用前端提供的 ID 继续对话之前,你应该验证该对话是否确实属于给定的参与者:
abort_unless($store->conversationBelongsTo(
$conversationId, $user->getMorphClass(), $user->getKey()
), 403);如果最近的一轮对话处于暂停状态,正在等待工具审批,你可以在页面重新加载后渲染其待处理的工具调用,而无需继续运行:
foreach ($store->pendingApprovalsFor($conversationId) as $approval) {
// $approval->id, $approval->tool, $approval->arguments, $approval->reason...
}这些方法由 PaginatesConversations、VerifiesConversationOwnership 和 ResolvesPendingApprovals 契约定义。内置的数据库存储实现了全部三个契约,而自定义存储可以仅实现它所需的契约。
结构化输出
如果你希望智能体返回结构化输出,请实现 HasStructuredOutput 接口,该接口要求你的智能体定义一个 schema 方法:
<?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;
// ...
/**
* 获取智能体的结构化输出 schema 定义。
*/
public function schema(JsonSchema $schema): array
{
return [
'score' => $schema->integer()->required(),
];
}
}当向返回结构化输出的智能体发起提示时,你可以像访问数组一样访问返回的 StructuredAgentResponse:
$response = (new SalesCoach)->prompt('Analyze this sales transcript...');
return $response['score'];嵌套对象
要定义嵌套的结构化输出,请结合闭包使用 object 方法:
<?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;
// ...
/**
* 获取智能体的结构化输出 schema 定义。
*/
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(),
];
}
}对象数组
如果智能体应当返回结构化项目的列表,请组合使用 array 和 object 方法:
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 方法:
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(),
];
}附件
发起提示时,你还可以在提示中传递附件,以便让模型查看图片和文档:
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 类可用于在提示中附加图片:
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 方法来流式传输智能体的响应。返回的 StreamableAgentResponse 可以从路由中返回,以自动向客户端发送流式响应(SSE):
use App\Ai\Agents\SalesCoach;
Route::get('/coach', function () {
return (new SalesCoach)->stream('Analyze this sales transcript...');
});可以使用 then 方法提供一个闭包,当整个响应已流式传输到客户端时,该闭包将被调用:
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...
});
});或者,你也可以手动遍历流式事件:
$stream = (new SalesCoach)->stream('Analyze this sales transcript...');
foreach ($stream as $event) {
// ...
}响应还包含模型的推理过程及其引用的来源。当使用 RemembersConversations trait 时,这两者都会与助手消息一起存储:
use Laravel\Ai\Responses\StreamedAgentResponse;
(new SalesCoach)
->stream('Analyze this sales transcript...')
->then(function (StreamedAgentResponse $response) {
$response->reasoning; // '' unless the model returned reasoning text...
$response->meta->citations;
});通过 prompt 方法返回的响应也可获取推理内容。
流式协议
默认情况下,流式响应使用 AI SDK 自身的事件格式。不过,你也可以使用前端流式协议,这样可以将智能体与现有的聊天界面搭配使用,而无需自行构建。
你可以通过在可流式传输的响应上调用 usingVercelDataProtocol 方法,使用Vercel AI SDK 流式协议来流式传输事件:
use App\Ai\Agents\SalesCoach;
Route::get('/coach', function () {
return (new SalesCoach)
->stream('Analyze this sales transcript...')
->usingVercelDataProtocol();
});如果你的应用前端自行分配了消息 ID,你可以传入该 ID:
->usingVercelDataProtocol($request->string('messageId'));此外,也可以使用 usingAgentUserInteractionProtocol 方法,通过智能体用户交互(AG-UI)协议进行流式传输:
Route::post('/coach', function (Request $request) {
return (new SalesCoach)
->forUser($request->user())
->stream($request->string('prompt'))
->usingAgentUserInteractionProtocol();
});threadId 和 runId 参数是可选的,默认值为会话 ID 和调用 ID:
->usingAgentUserInteractionProtocol(
threadId: $request->input('threadId'),
runId: $request->input('runId'),
);若要使用 AI SDK 未实现的协议,你可以将自定义的 Laravel\Ai\Streaming\Protocols\StreamProtocol 实现传递给 usingProtocol 方法:
use App\Ai\Protocols\CustomProtocol;
return (new SalesCoach)
->stream('Analyze this sales transcript...')
->usingProtocol(new CustomProtocol);前端集成
使用 Vercel 的 useChat 或 CopilotKit 等库构建的聊天界面,已经可以渲染消息、工具调用和审批提示,因此你的应用只需处理它们发送的请求。每个请求包含对话历史、最新的用户消息以及任何工具审批响应。
Vercel::chat 和 AgentUserInteraction::chat 方法会将此类请求转换为一个对象,该对象可直接传递给智能体的 stream 方法:
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());
});如果请求包含审批决策,智能体会使用这些决策继续执行。否则,智能体将根据请求中最新的用户消息和附件进行提示。protocol 方法返回客户端所使用的协议。
实现了 Conversational 接口的智能体会自动加载自身历史记录,因此可以省略 withMessages 方法。
AgentUserInteraction::chat 方法为 AG-UI 客户端提供了相同的 API,此外还会提供请求的线程 ID 和运行 ID:
use Laravel\Ai\AgentUserInteraction\AgentUserInteraction;
$chat = AgentUserInteraction::chat($request);
$chat->threadId();
$chat->runId();你也可以将存储的消息转换回客户端期望的格式,以便客户端展示之前的对话,例如在页面重新加载后:
$messages = $conversation->messages()->oldest()->get();
return ['messages' => Vercel::toUiMessages($messages)];AgentUserInteraction::toClientState 方法为 AG-UI 客户端执行相同的转换,同时还会返回任何待处理的审批中断:
return AgentUserInteraction::toClientState($messages);广播
你可以通过几种不同的方式广播流式事件。首先,你可以在流式事件上直接调用 broadcast 或 broadcastNow 方法:
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'));
}或者,你可以调用智能体的 broadcastOnQueue 方法,将智能体操作加入队列,并在流式事件可用时进行广播:
(new SalesCoach)->broadcastOnQueue(
'Analyze this sales transcript...',
new Channel('channel-name'),
);跳过过大的事件
某些广播平台将 WebSocket 消息限制在约 10KB。数据量大的流式事件,如大型工具结果,可能会超过此限制并导致广播失败。你可以使用 WithoutBroadcasting 特性将特定事件类型排除在广播之外:
<?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)。
队列处理
通过使用智能体的 queue 方法,你可以向智能体发送提示,但允许它在后台处理响应,从而保持应用快速响应。then 和 catch 方法可用于注册闭包,当响应可用或发生异常时,这些闭包将被调用:
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();
});工具
工具可用于为智能体提供额外功能,以便其在响应提示词时加以利用。可以使用 make:tool Artisan 命令创建工具:
php artisan make:tool RandomNumberGenerator生成的工具将放置在应用程序的 app/Ai/Tools 目录中。每个工具都包含一个 handle 方法,当智能体需要使用该工具时,会调用此方法:
<?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']);
}
/**
* 获取工具的 schema 定义。
*/
public function schema(JsonSchema $schema): array
{
return [
'min' => $schema->integer()->min(0)->required(),
'max' => $schema->integer()->required(),
];
}
}定义好工具后,可以在任意智能体的 tools 方法中返回它:
use App\Ai\Tools\RandomNumberGenerator;
/**
* 获取智能体可用的工具。
*
* @return Tool[]
*/
public function tools(): iterable
{
return [
new RandomNumberGenerator,
];
}运行时工具覆盖
withTools 方法可用于替换智能体实例声明的工具。这对于按租户或特性开关的工具集非常有用:
$response = (new SupportAgent)
->withTools([new LookupOrder])
->prompt('Where is order 12345?');你也可以传入一个闭包,该闭包会接收智能体声明的工具,从而允许你追加或过滤工具:
$response = (new SupportAgent)
->withTools(fn (array $tools) => [...$tools, new LookupOrder])
->prompt('Where is order 12345?');验证工具参数
虽然工具的 schema 会约束模型可能提供的参数,但你可以使用请求的 validate 方法对传入的参数进行验证:
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 特性,可在模型调用未知的本地工具时让智能体进行恢复。Laravel 会将失败的调用连同可用本地工具的名称一起返回给模型,使其能够修正调用:
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 自动推导最大步数时,此特性会为修复后的调用增加一步。显式设置的 MaxSteps 限制不受影响。
相似度搜索
SimilaritySearch 工具允许智能体使用数据库中存储的向量嵌入,搜索与给定查询相似的文档。当你希望智能体能够搜索应用程序的数据时,这对检索增强生成(RAG)非常有用。
创建相似度搜索工具最简单的方式,是使用 usingModel 方法搭配具有向量嵌入的 Eloquent 模型:
use App\Models\Document;
use Laravel\Ai\Tools\SimilaritySearch;
public function tools(): iterable
{
return [
SimilaritySearch::usingModel(Document::class, 'embedding'),
];
}第一个参数是 Eloquent 模型类,第二个参数是包含向量嵌入的列。
你也可以在 0.0 到 1.0 之间设置最低相似度阈值,并传入一个闭包来自定义查询:
SimilaritySearch::usingModel(
model: Document::class,
column: 'embedding',
minSimilarity: 0.7,
limit: 10,
query: fn ($query) => $query->where('published', true),
),若需要更多控制,你可以创建一个自定义闭包来返回搜索结果的相似度搜索工具:
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 方法自定义工具的描述:
SimilaritySearch::usingModel(Document::class, 'embedding')
->withDescription('Search the knowledge base for relevant articles.'),延迟加载工具
默认情况下,智能体暴露的每个工具都会随每次请求发送给服务提供商。当智能体提供大量工具时,这会消耗 token,并可能降低模型选择工具的准确性。在 OpenAI、Azure 或 Anthropic 平台使用 ToolSearch 提供商工具时,你可以延迟定义工具,使提供商仅在需要时才加载它们:
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,
]),
];
}被包裹的工具无需任何修改。当工具与提示词相关时,提供商会搜索并加载它们,之后智能体就可以像调用其他工具一样调用它们。
使用 Anthropic 时,可通过 strategy 参数确定提供商应如何搜索延迟加载的工具。支持的策略有 regex(默认值)和 bm25:
new ToolSearch(tools: [new SearchInvoices], strategy: 'bm25'),使用 Anthropic 时,可通过 withProviderOptions 方法为搜索工具传入额外的提供商专属选项:
(new ToolSearch(tools: [new SearchInvoices]))
->withProviderOptions(['cache_control' => ['type' => 'ephemeral']]), 不支持工具搜索的提供商会抛出异常,而不是静默丢弃延迟加载的工具。此外,Anthropic 要求在 ToolSearch 包裹之外至少提供一个工具。
文件存储工具
FileStorage 工具工厂可让你为智能体提供对 Laravel 文件系统磁盘的访问权限。all 方法返回的工具,可让智能体在指定磁盘上列出、读取、检查、生成 URL、写入、删除、复制和移动文件:
use Laravel\Ai\Tools\FileStorage;
public function tools(): iterable
{
return FileStorage::all('local');
}如果你的智能体仅应具备检查文件的权限,请使用 readOnly 方法:
return FileStorage::readOnly('local');这些方法会返回一个 Illuminate\Support\Collection,以便你进一步筛选提供给智能体的工具:
use Laravel\Ai\Tools\Filesystem\DeleteFile;
return FileStorage::all('s3')
->reject(fn ($tool) => $tool instanceof DeleteFile);MCP 工具
如果你的应用程序使用了 Laravel MCP,你可以为智能体提供由 Model Context Protocol 服务器暴露的工具。借助 Laravel MCP 客户端,你可以连接到远程或本地的 MCP 服务器,并将其工具直接传递给智能体。
MCP 工具需要在应用程序中安装 Laravel MCP 包。
由于 MCP 客户端的 tools 方法返回的是一个集合,你可以使用 ... 操作符将其展开到智能体的 tools 数组中:
use App\Ai\Tools\RandomNumberGenerator;
use Laravel\Mcp\Client;
/**
* 获取智能体可用的工具。
*
* @return Tool[]
*/
public function tools(): iterable
{
return [
...Client::web('https://mcp.example.com')
->withToken($token)
->tools(),
new RandomNumberGenerator,
];
}AI SDK 会自动封装每个 MCP 工具,使智能体(agent)能够像调用其他任何工具一样调用它。你也可以使用命名 MCP 客户端(named MCP client):
use Laravel\Mcp\Facades\Mcp;
public function tools(): iterable
{
return [
...Mcp::client('github')->tools(),
];
}或者连接到本地 MCP 服务器(local MCP server):
use Laravel\Mcp\Client;
public function tools(): iterable
{
return [
...Client::local('php', ['artisan', 'mcp:start'])->tools(),
];
}如需了解更多有关创建和认证 MCP 客户端(包括 bearer 令牌和 OAuth)的信息,请参阅MCP 客户端文档。
技能(Skills)
智能体技能(Agent Skills)是用于指示和辅助文件的文件夹,用于教导智能体如何执行特定任务。由于技能遵循开放标准,同一个技能可以在你的应用智能体以及你用于构建应用的编码智能体之间共享。
每个技能都是应用resources/skills目录下的一个目录,其中包含一个SKILL.md文件。目录中的任何其他文件(如参考文档)都会与该技能打包在一起:
resources/skills/
└── refund-policy/
├── SKILL.md
└── references/EDGE-CASES.mdSKILL.md文件以包含技能name和说明何时应使用该技能的description的 YAML frontmatter 开头,其后为技能的具体说明:
---
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,则会使用技能目录的名称。没有description的技能将被忽略。
若要让智能体能够访问你的技能,请实现HasSkills接口,并从skills方法中返回你的技能源:
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实例或返回技能的闭包,这些闭包仅在智能体需要时才会被调用。当两个技能的名称相同时,排在第一位的源优先。
智能体会收到一个LoadSkill工具,该工具会列出每个技能的名称和描述,仅在提示词需要时才会加载技能的完整指令和打包的文本文件。二进制文件以及大于 256 KB 的文件不会被返回。
提供方工具(Provider Tools)
提供方工具是由 AI 提供方原生实现的特殊工具,提供网页搜索、URL 获取和文件搜索等功能。与常规工具不同,提供方工具由提供方自身执行,而非由你的应用执行。
提供方工具可以由智能体的tools方法返回。
网页搜索(Web Search)
WebSearch提供方工具允许智能体在网络上搜索实时信息。这对于回答有关时事、近期数据或可能在模型训练截止日期后发生变化的主题的问题非常有用。
支持的提供方:Anthropic、OpenAI、Azure、Gemini、xAI、Groq、OpenRouter
use Laravel\Ai\Providers\Tools\WebSearch;
public function tools(): iterable
{
return [
new WebSearch,
];
}你可以对网页搜索工具进行配置,以限制搜索次数或将结果限制在特定域名内:
(new WebSearch)->max(5)->allow(['laravel.com', 'php.net']),若要基于用户位置优化搜索结果,请使用location方法:
(new WebSearch)->location(
city: 'New York',
region: 'NY',
country: 'US'
); Groq 仅在其 GPT-OSS 模型上支持网页搜索,并且会忽略max、allow和location方法。
网页获取(Web Fetch)
WebFetch提供方工具允许智能体获取并读取网页内容。当你需要智能体分析特定 URL 或从已知网页获取详细信息时,这非常有用。
支持的提供方:Anthropic、Gemini、OpenRouter
use Laravel\Ai\Providers\Tools\WebFetch;
public function tools(): iterable
{
return [
new WebFetch,
];
}你可以对网页获取工具进行配置,以限制获取次数或将其限制在特定域名内:
(new WebFetch)->max(3)->allow(['docs.laravel.com']),文件搜索(File Search)
FileSearch提供方工具允许智能体在向量存储(vector stores)中搜索文件。这允许智能体在你上传的文档中搜索相关信息,从而实现检索增强生成(RAG)。
支持的提供方:OpenAI、Azure、Gemini、xAI
use Laravel\Ai\Providers\Tools\FileSearch;
public function tools(): iterable
{
return [
new FileSearch(stores: ['store_id']),
];
}你可以提供多个向量存储 ID,以便在多个存储中进行搜索:
new FileSearch(stores: ['store_1', 'store_2']);如果你的文件包含元数据,你可以通过提供where参数来过滤搜索结果。对于简单的相等过滤,请传递一个数组:
new FileSearch(stores: ['store_id'], where: [
'author' => 'Taylor Otwell',
'year' => 2026,
]);对于更复杂的过滤,你可以传递一个接收FileSearchQuery实例的闭包:
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'])
);代码执行(Code Execution)
CodeExecution提供方工具允许智能体在由 AI 提供方托管的沙盒中运行代码。这对于执行计算和分析数据非常有用。
支持的提供方:Anthropic、OpenAI、Azure、Gemini、xAI、Groq
use Laravel\Ai\Providers\Tools\CodeExecution;
public function tools(): iterable
{
return [new CodeExecution];
}使用 OpenAI 或 Azure 时,你可以通过提供方选项使存储的文件在沙盒中可用:
(new CodeExecution)->withProviderOptions([
'container' => ['type' => 'auto', 'file_ids' => ['file_123']],
]); Groq 仅在其 GPT-OSS 模型上支持代码执行。
子智能体(Sub-Agents)
智能体也可以从另一个智能体的tools方法中返回。当一个智能体作为工具返回时,父智能体可以将特定任务委托给子智能体,并在回答原始提示词时使用子智能体的响应。当通用智能体需要访问具有自己的指令、工具、模型配置或提供方偏好的专用智能体时,这非常有用。
例如,客户支持智能体可以将退款资格问题委托给专门的退款智能体:
若要自定义子智能体如何向父智能体暴露,请在子智能体上实现CanActAsTool接口,并定义面向工具的名称和描述:
use Laravel\Ai\Attributes\Provider;
use Laravel\Ai\Contracts\CanActAsTool;
use Laravel\Ai\Enums\Lab;
#[Provider(Lab::Anthropic)]
class RefundsAgent implements Agent, CanActAsTool, HasTools
{
/**
* Get the agent's tool name.
*/
public function name(): string
{
return 'refunds_specialist';
}
/**
* Get the agent's tool description.
*/
public function description(): string
{
return 'Determine whether an order is eligible for a refund and explain the next step.';
}
// ...
}如果子代理未实现 CanActAsTool,Laravel 会使用该代理的类名作为工具名称,并使用一条通用描述,要求父代理传递清晰且独立的任务描述。每次子代理调用均在隔离环境中运行,且不会接收父代理的对话历史。
子代理目前不支持需要人工审批的工具。如果子代理的某个工具需要审批,子代理会暂停运行。然而,父代理会收到正常的工具结果并继续运行,且不会向应用程序告知存在待处理的审批请求。请在顶层代理上注册需要审批的工具。
当父代理进行流式输出时,其子代理也会流式输出,除非子代理产生了结构化输出。对于流式输出的子代理,父代理会发出初步的 ToolResult 事件,其中包含子代理当前已生成的文本。工具调用的最终结果在这些事件之后,因此在手动遍历事件时,您可以跳过这些初步事件:
use Laravel\Ai\Streaming\Events\ToolResult;
foreach ($stream as $event) {
if ($event instanceof ToolResult && $event->preliminary) {
continue;
}
// ...
}text、usage 和 toolResults 等响应值会忽略初步事件。Vercel 协议会将其渲染为原生流式工具输出,因此 useChat 无需自定义代码即可显示进度;而 AG-UI 协议会将其报告为活动快照。完整响应的文本、推理、引用和用量统计包含子代理的相关内容。
中间件
代理支持中间件,允许在将每个生成步骤发送给提供商之前拦截并修改该步骤。中间件在每个步骤中仅被调用一次,因此一个包含三个步骤的运行会调用中间件三次。可以使用 make:agent-middleware Artisan 命令创建中间件:
php artisan make:agent-middleware LogPrompts生成的中间件将放置在应用程序的 app/Ai/Middleware 目录下。若要为代理添加中间件,需实现 HasMiddleware 接口,并定义一个返回中间件类数组的 middleware 方法:
<?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,
];
}
}每个中间件类应定义一个 handle 方法,该方法接收一个 PendingStep 和一个将步骤传递给下一个中间件的 Closure:
<?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 外,该步骤还公开了已完成的步骤、这些步骤的累计用量以及运行进度:
$step->steps;
$step->usage;
$step->number;
$step->isFirstStep();
$step->isFinalStep;withModel、withInstructions、withMessages、withTools、onlyTools、withoutTools、withToolChoice、withMaxTokens 和 withProviderOptions 方法均会返回该步骤的一个副本。例如,您可以在代理使用完某个开销较大的工具后将其移除:
public function handle(PendingStep $step, Closure $next)
{
if (! $step->isFirstStep()) {
$step = $step->withoutTools('SearchDocumentation');
}
return $next($step);
}或者,您可以通过总结对话中间部分的内容,使较长的工具调用循环保持在上下文窗口内:
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 方法,在模型对步骤作出响应后、执行其工具调用之前执行代码。该方法适用于同步响应和流式响应:
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]);
});
}中间件必须返回 $next 的结果、一个 StepResponse 或 StepResult::stop()。若要在不调用模型的情况下响应步骤(例如提供缓存响应),请返回一个 StepResponse。若要结束运行,请返回 StepResult::stop()。任何其他返回值都会引发 LogicException。
停止运行
若要在代理下一次调用模型前结束其运行,请使用 Stop 中间件。例如,当用户按下“停止”按钮或运行达到输入 token 预算时,您可以停止运行。响应将保留已完成的步骤及其累计用量:
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 the user has pressed the "stop" button...
Stop::when(fn (PendingStep $step) => Cache::has("chat:{$this->user->id}:stopped")),
// Stop when the run has used at least 50,000 input tokens...
Stop::unless(fn (PendingStep $step) => $step->usage->inputTokens < 50_000),
];
}在每个步骤前,若 when 中的条件为真,则停止运行;若 unless 中的条件为假,则停止运行。被停止的流式运行具有 cancelled 结束原因。
自定义中间件也可以通过返回 StepResult::stop() 而非 $next 的结果来结束运行:
use Laravel\Ai\Gateway\StepResult;
public function handle(PendingStep $step, Closure $next)
{
return $this->cancelled($step) ? StepResult::stop() : $next($step);
}匿名代理
有时您可能希望快速与模型交互,而无需创建专门的代理类。您可以使用 agent 函数创建一个临时的匿名代理:
use function Laravel\Ai\{agent};
$response = agent(
instructions: 'You are an expert at software development.',
messages: [],
tools: [],
)->prompt('Tell me about Laravel');匿名代理也可以产生结构化输出:
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');代理配置
您可以使用 PHP 属性为代理配置文本生成选项。以下属性可用:
MaxSteps:代理在使用工具时可执行的最大步骤数。MaxTokens:模型可生成的最大 token 数。Model:代理应使用的模型。Provider:代理要使用的 AI 提供商(或用于故障转移的提供商)。Temperature:生成时使用的采样温度(0.0 到 1.0)。Timeout:代理请求的 HTTP 超时时间(秒),默认值为 60。TopP:生成时使用的核采样概率(0.0 到 1.0)。UseCheapestModel:使用提供商最经济的文本模型以优化成本。UseSmartestModel:使用提供商功能最强的文本模型以处理复杂任务。
<?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 属性允许你在不指定模型名称的情况下,自动为给定提供商选择最具性价比或能力最强的模型。当你希望在不同提供商之间优化成本或能力时,这非常有用:
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;
// Will use the cheapest model (e.g., Haiku)...
}
#[UseSmartestModel]
class ComplexReasoner implements Agent
{
use Promptable;
// Will use the most capable model (e.g., Opus)...
} UseCheapestModel 和 UseSmartestModel 选择的基础模型可能会随着提供商发布新模型,在 Laravel AI SDK 的不同版本之间发生变化。切换模型可能会带来行为变化、弃用参数以及显著的成本差异。如果你需要稳定、可预测的模型和定价,请使用 Model 属性显式指定模型。
提供商选项
如果你的智能体需要传递提供商特定的选项(例如 OpenAI 的推理强度或惩罚设置),请实现 HasProviderOptions 契约并定义一个 providerOptions 方法:
<?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 启用了提示词缓存。
图像、音频、转录、嵌入和重排序构建器也接受提供商选项:
use Laravel\Ai\Audio;
$audio = Audio::of('I love coding with Laravel.')
->withProviderOptions(['speed' => 1.25])
->generate();你也可以传入一个闭包而非数组,该闭包会接收当前使用的提供商。
自定义 HTTP 请求头
在应用程序的 config/ai.php 配置文件中为某个提供商配置的请求头,会在该提供商发起的每个请求中一并发送。若需在单次请求的基础上发送请求头,例如用于 AI 网关的元数据,可以使用 withHeaders 方法,该方法在图像、音频、转录、嵌入和重排序构建器以及文件上传功能中均可用:
use Laravel\Ai\Embeddings;
$embeddings = Embeddings::for($chunks)
->withHeaders(['cf-aig-metadata' => json_encode(['team' => $team->id])])
->withProviderOptions(['dimensions' => 1024])
->generate();请求头也可以以闭包的形式提供,该闭包会接收当前使用的提供商。请求头不会包含在请求体中,也不会影响嵌入缓存键。
提示词缓存
大多数提供商会自动缓存重复的提示词前缀,并对缓存部分给予折扣计费。OpenAI、Gemini、Groq、DeepSeek 和 xAI 无需额外配置,你可以通过响应的 usage 查看节省情况:
$response->usage->cacheReadInputTokens;
$response->usage->cacheWriteInputTokens;这两个计数值都是输入 token 总量的子集,关于这一点会在 usage 文档中进一步讨论。
anthropic 和 bedrock 提供商仅在有明确要求时才会进行缓存。CacheInstructions 和 CacheToolDefinitions 属性会在智能体的指令和工具定义末尾放置一个缓存断点,这样每次对话都会从缓存中读取该前缀,而无需再次写入:
use Laravel\Ai\Attributes\CacheInstructions;
use Laravel\Ai\Attributes\CacheToolDefinitions;
#[CacheInstructions]
#[CacheToolDefinitions]
class SalesCoach implements Agent
{
use Promptable;
// ...
}如果你的指令在每次请求时都会变化,例如嵌入了当前日期,请单独使用 CacheToolDefinitions。缓存每次请求都会发生变化的前缀,会导致每次生成新的缓存条目,你需要为写入缓存付费,却永远无法复用。
不支持这些属性的提供商会忽略它们,因此智能体在使用故障转移时,可以放心声明这些属性。
缓存的前缀默认保留五分钟。如果你向属性传入 TTL,Anthropic 可能会将它们保留一小时:
#[CacheInstructions('1h')]
#[CacheToolDefinitions('1h')]另外,也可以通过顶层的 cache_control 提供商选项启用 Anthropic 的自动缓存功能。这会在请求的最后一个块之后放置一个断点,因此随着对话增长,断点会向前推进,每一轮都会从缓存中读取之前的轮次内容。这两种机制可以结合使用。
由于提供商按照工具、指令、消息的顺序构建提示词,因此将指令缓存一小时,也需要将工具定义缓存一小时。混用两者会抛出 InvalidArgumentException。
人工工具审批
工具审批要求在运行恢复时,暂停轮次的历史记录是可用的。你应该使用 Conversational 智能体,例如使用 RemembersConversations trait 的智能体,或者使用 withMessages 方法,从应用程序的前端提供历史记录。既未使用其一的智能体,在工具暂停时会抛出 ApprovalNotResumableException。
执行敏感或不可逆操作的工具,在执行前可能需要人工审批。要让工具可审批,请实现 Approvable 契约并使用 InteractsWithApprovals trait。可审批的工具默认需要审批:
<?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 实例:
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.');
}在从智能体的 tools 方法返回工具时,你可以覆盖该工具的审批要求:
public function tools(): iterable
{
return [
(new SendNotification)->withoutApproval(),
(new DeleteFile)->requireApproval('Deletion review required.'),
];
}当调用可审批的工具时,智能体会在执行前暂停。你可以检查响应中的待审批项,其中包含每个工具调用的 ID、工具名称、参数和审批原因:
$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
}
}若要恢复智能体,请继续对话并提供一个 Decisions 实例,其中包含针对每个待处理工具调用的决策。决策可以批准调用、拒绝调用,或在执行前编辑其参数:
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 方法,为没有显式决策的调用设置默认值:
$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 方法均支持工具审批功能。
在流式传输和广播过程中,暂停操作由 tool_approval_request 事件表示。使用 Vercel AI SDK 流协议时,审批请求和结果通过协议的原生工具审批部分发出,而智能体用户交互协议则将其报告为中断。
使用这两种协议的客户端会将决策与其他对话内容一起提交,因此聊天请求可以直接传递给智能体:
$chat = Vercel::chat($request);
return (new FileAssistant)
->continue($conversationId, as: $request->user())
->stream($chat)
->usingProtocol($chat->protocol());当暂停的轮次被恢复时,恢复后的步骤会合并到该轮次中,因此每一轮次都会存储为一条单独的智能体消息。响应的 assistantMessageId 包含被暂停消息的 ID,该消息的使用量会同时包含暂停和恢复操作。
对于排队执行的智能体,最终响应会传递给 then 回调,Laravel 还会派发一个 ToolApprovalRequested 事件。
Laravel 会在要求模型继续之前存储已批准工具的结果。如果后续生成失败,该审批已处理完成。此时请通过普通的文本提示继续对话,无需再次提交相同的审批决策。
完整审批流程
以下路由演示了一个完整的审批流程,可从聊天界面接收新的文本提示或审批决策。此示例假设应用的 User 模型使用了 HasConversations 特性:
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 值:
{
"decisions": {
"call_abc": {
"action": "approve"
},
"call_def": {
"action": "reject",
"result": "The invoice must be retained."
}
}
}图像
Laravel\Ai\Image 类可用于通过 openai、gemini 或 xai 提供商生成图像:
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 超时时间(单位:秒):
use Laravel\Ai\Image;
$image = Image::of('A donut sitting on the kitchen counter')
->quality('high')
->landscape()
->timeout(120)
->generate();可以使用 attachments 方法附加参考图像:
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();部分提供商可在单个请求中生成多张图像。OpenAI、Azure 和 xAI 支持 n 提供商选项,响应中会包含返回的所有图像:
$response = Image::of('A donut sitting on the kitchen counter')
->withProviderOptions(['n' => 4])
->generate();
count($response); // 4
$response->images; // A collection of generated images...
$response->firstImage(); // The first generated image...生成的图像可以轻松存储到应用 config/filesystems.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');图像生成也可以排队执行:
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();
// ...
});音频
使用 Laravel\Ai\Audio 类可以从给定文本生成音频:
use Laravel\Ai\Audio;
$audio = Audio::of('I love coding with Laravel.')->generate();
$rawContent = (string) $audio;也可以通过 Laravel Stringable 类提供的 toAudio 方法,将字符串转换为音频:
use Illuminate\Support\Str;
$audio = Str::of('I love coding with Laravel.')->toAudio();可以使用 male、female 和 voice 方法来指定生成音频的声音:
$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 方法可用于动态指导模型应如何生成音频:
$audio = Audio::of('I love coding with Laravel.')
->female()
->instructions('Said like a pirate')
->generate();生成的音频可以轻松存储在应用程序 config/filesystems.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');音频生成也可以加入队列:
use Laravel\Ai\Audio;
use Laravel\Ai\Responses\AudioResponse;
Audio::of('I love coding with Laravel.')
->queue()
->then(function (AudioResponse $audio) {
$path = $audio->store();
// ...
});转录
可以使用 Laravel\Ai\Transcription 类生成给定音频的转录文本:
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 方法指示除了原始文本转录外,还希望响应包含说话人分离的转录,从而可以按说话人访问分段转录:
$transcript = Transcription::fromStorage('audio.mp3')
->diarize()
->generate();转录生成也可以加入队列:
use Laravel\Ai\Transcription;
use Laravel\Ai\Responses\TranscriptionResponse;
Transcription::fromStorage('audio.mp3')
->queue()
->then(function (TranscriptionResponse $transcript) {
// ...
});文本摘要
你可以使用 Laravel Stringable 类提供的 summarize 方法来总结文本。默认情况下,摘要不超过三句话,并且使用所配置提供商最廉价的文本模型生成:
use Illuminate\Support\Str;
$summary = Str::of($article)->summarize();你可以指定用于生成摘要的最大句子数、提供商、模型和超时时间。Str 类还提供了该方法的静态版本:
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);向量嵌入
你可以使用 Laravel Stringable 类提供的新方法 toEmbeddings,轻松为任意给定字符串生成向量嵌入:
use Illuminate\Support\Str;
$embeddings = Str::of('Napa Valley has great wine.')->toEmbeddings();或者,你也可以使用 Embeddings 类同时为多个输入生成嵌入:
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, ...]]你可以为嵌入指定维度和提供商:
$response = Embeddings::for(['Napa Valley has great wine.'])
->dimensions(1536)
->generate(Lab::OpenAI, 'text-embedding-3-small');多模态嵌入
除了字符串之外,Embeddings::for 方法还接受图像、音频、文档和视频输入,允许你为非文本内容生成嵌入。Gemini 支持图像、音频、文档和视频嵌入,而 VoyageAI 支持图像和视频嵌入:
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);多模态输入使用与附件相同的文件类。这些文件可以由本地路径、文件系统磁盘、远程 URL 或 Base64 编码的内容创建。图像、文档和视频也可以由上传的文件创建,而文档可以由原始字符串内容创建:
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')); VoyageAI 不允许在同一请求中混合使用远程 URL 媒体和 Base64 编码的媒体。本地、已存储和上传的文件会作为 Base64 编码的内容发送,文本输入可与任一媒体源组合使用。请查阅提供商的文档,以确定可用的多模态模型和输入。
查询嵌入
生成嵌入后,通常会将其存储在数据库的 vector 列中,以便后续查询。Laravel 通过 pgvector 扩展为 PostgreSQL 上的向量列提供原生支持,也支持 MariaDB。开始之前,请在迁移中定义一个 vector 列,并指定维度数量:
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 索引:
$table->vector('embedding', dimensions: 1536)->index();在你的 Eloquent 模型中,应使用 AsVector 类型转换来转换向量列:
use Illuminate\Database\Eloquent\Casts\AsVector;
protected function casts(): array
{
return [
'embedding' => AsVector::class,
];
}要查询相似记录,请使用 whereVectorSimilarTo 方法。该方法按最小余弦相似度(介于 0.0 和 1.0 之间,1.0 表示完全相同)过滤结果,并按相似度对结果排序:
use App\Models\Document;
$documents = Document::query()
->whereVectorSimilarTo('embedding', $queryEmbedding, minSimilarity: 0.4)
->limit(10)
->get();$queryEmbedding 可以是浮点数数组或纯字符串。当提供字符串时,Laravel 会自动为其生成嵌入:
$documents = Document::query()
->whereVectorSimilarTo('embedding', 'best wineries in Napa Valley')
->limit(10)
->get();如果需要更精细的控制,你可以独立使用更底层的 whereVectorDistanceLessThan、selectVectorDistance 和 orderByVectorDistance 方法:
$documents = Document::query()
->select('*')
->selectVectorDistance('embedding', $queryEmbedding, as: 'distance')
->whereVectorDistanceLessThan('embedding', $queryEmbedding, maxDistance: 0.3)
->orderByVectorDistance('embedding', $queryEmbedding)
->limit(10)
->get();如果你想让智能体能够将相似性搜索作为一种工具来执行,请查阅相似性搜索工具的文档。
向量查询目前支持使用 pgvector 扩展的 PostgreSQL 连接,或使用原生向量支持的 MariaDB 11.7 及以上版本。
缓存嵌入
嵌入生成可以进行缓存,以避免对相同输入进行冗余的 API 调用。要启用缓存,请将 ai.caching.embeddings.cache 配置选项设置为 true:
'caching' => [
'embeddings' => [
'cache' => true,
'store' => env('CACHE_STORE', 'database'),
'individually' => true,
// ...
],
],当启用缓存时,嵌入向量会被缓存 30 天。缓存键基于提供商、模型、维度和输入内容生成,确保相同的请求返回缓存结果,而不同的配置则生成新的嵌入向量。
默认情况下,每个输入的嵌入向量都在其各自的键下进行缓存,因此即使输入集合或其顺序发生变化,后续请求仍可能命中之前见过的输入的缓存。若要改为将所有输入作为一个整体在单个键下进行缓存,可将ai.caching.embeddings.individually配置选项设置为false。
即使全局缓存处于禁用状态,您也可以使用cache方法为特定请求启用缓存:
$response = Embeddings::for(['Napa Valley has great wine.'])
->cache()
->generate();您可以指定自定义的缓存时长(以秒为单位):
$response = Embeddings::for(['Napa Valley has great wine.'])
->cache(seconds: 3600) // 缓存 1 小时
->generate();toEmbeddings可字符串化方法也接受cache参数:
// 使用默认时长进行缓存...
$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类对文档进行重排序:
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:
$response = Reranking::of($documents)
->limit(5)
->timeout(60)
->rerank('search query');重排序集合
为方便起见,可以使用rerank宏对 Laravel 集合进行重排序。第一个参数指定用于重排序的字段,第二个参数为查询:
// 按单个字段重排序...
$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'
);您还可以限制结果数量并指定提供商:
$reranked = $posts->rerank(
by: 'content',
query: 'Laravel tutorials',
limit: 10,
provider: Lab::Cohere,
timeout: 60,
);分类
分类功能目前处于实验阶段,其 API 在未来的 AI SDK 小版本中可能会发生变化。
分类功能允许您针对给定的字符串或数据数组提出一组固定的问题,并针对每个问题获取带有概率支持的定型答案,而非自由格式的文本。这对于路由、内容审核和评分等场景非常有用,在这些场景中,您需要将答案与阈值进行比较,或在测试中对答案进行断言。
可以使用Laravel\Ai\Classification类对内容进行分类。每个问题都有一个对应的键,可通过该键从响应中获取相应答案:
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:
$result['urgent']->probability; // 0.94
$result['urgent']->isTrue(threshold: 0.8); // trueChoice问题返回给定选项之一以及每个选项的概率。confidence属性表示提供商对全部概率集合的确定程度,当提供商无法衡量时,该值为null:
$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方法则描述最可能的等级:
$result['frustration']->score; // 1.24, the probability-weighted level
$result['frustration']->level(); // 1, the most probable level
$result['frustration']->label(); // 'Frustrated'
$result['frustration']->normalized(); // 0.62, the score as a fraction of the highest level
$result['frustration']->probabilities; // [0.12, 0.52, 0.36]当单一语句不足以描述时,传递给Boolean问题的判定标准、传递给Choice问题的选项描述以及传递给Score问题的等级均可设为数组。Choice问题至少需要两个选项,而Score问题至少需要两个等级。
响应可以像数组一样进行迭代、计数和访问。此外,answer方法可用于获取单个答案,而collect方法会以集合形式返回所有答案:
$result->answer('urgent');
$result->collect();
$result->usage;
$result->meta->provider;对图像进行分类
当使用 OpenAI、兼容 OpenAI、TypeSafe 或 OpenRouter 等提供商时,可以通过将图像作为of方法的第二个参数传入,将图像与给定内容一起进行分类。图像可以使用与附件相同的文件类来创建:
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 会将每个图像的键作为其名称发送给提供商:
$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方法,该方法返回布尔值而非完整响应。您可以描述“是”和“否”的含义,并指定答案必须达到的概率,该概率默认值为0.5:
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 参数提供字段、字段数组或闭包,以便为模型提供每个选项的更多详细信息:
$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 参数传入闭包时,该闭包会接收每个项,并应返回用于描述它的字符串或数组:
$priority = collect(Priority::cases())->decide(
'How urgent is this request?',
$ticket->body,
describe: fn (Priority $priority) => $priority->description(),
);当字符串集合以字符串作为键时,会将键作为选项、将值作为对应的描述,并返回所选的键。若设置了 threshold,且所选选项的概率低于该阈值,则会返回 null:
$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 提供商处,以便后续在对话中使用。对于大型文档,或者你希望多次引用而无需重复上传的文件而言,这一功能非常有用:
use Laravel\Ai\Files\Document;
use Laravel\Ai\Files\Image;
// Store a file from a local path...
$response = Document::fromPath('/home/laravel/document.pdf')->put();
$response = Image::fromPath('/home/laravel/photo.jpg')->put();
// Store a file that is stored on a filesystem disk...
$response = Document::fromStorage('document.pdf', disk: 'local')->put();
$response = Image::fromStorage('photo.jpg', disk: 'local')->put();
// Store a file that is stored on a remote URL...
$response = Document::fromUrl('https://example.com/document.pdf')->put();
$response = Image::fromUrl('https://example.com/photo.jpg')->put();
return $response->id;你也可以存储原始内容或已上传的文件:
use Laravel\Ai\Files;
use Laravel\Ai\Files\Document;
// Store raw content...
$stored = Document::fromString('Hello, World!', 'text/plain')->put();
// Store an uploaded file...
$stored = Document::fromUpload($request->file('document'))->put();文件存储完成后,你可以在通过 agent 生成文本时引用该文件,无需再次上传:
use App\Ai\Agents\SalesCoach;
use Laravel\Ai\Files;
$response = (new SalesCoach)->prompt(
'Analyze the attached sales transcript...',
attachments: [
Files\Document::fromId('file-id') // Attach a stored document...
]
);若要获取之前存储的文件,可在文件实例上使用 get 方法:
use Laravel\Ai\Files\Document;
$file = Document::fromId('file-id')->get();
$file->id;
$file->mimeType();要从提供商处删除文件,可使用 delete 方法:
Document::fromId('file-id')->delete();默认情况下,Files 类使用的是你的应用配置文件 config/ai.php 中配置的默认 AI 提供商。对于大多数操作,你可以通过 provider 参数指定其他提供商:
$response = Document::fromPath(
'/home/laravel/document.pdf'
)->put(provider: Lab::Anthropic);你可以使用 withProviderOptions 方法传入提供商专属的上传选项。例如,你可以设置 OpenAI 文件的 purpose:
use Laravel\Ai\Files\Document;
$response = Document::fromPath('/home/laravel/knowledge.txt')
->withProviderOptions(['purpose' => 'assistants'])
->put();若要按提供商限定选项,可以传入一个接收当前提供商的闭包:
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 对话中引用该文件:
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 类进行引用:
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 类提供了创建、获取和删除向量存储的方法:
use Laravel\Ai\Stores;
// Create a new vector store...
$store = Stores::create('Knowledge Base');
// Create a store with additional options...
$store = Stores::create(
name: 'Knowledge Base',
description: 'Documentation and reference materials.',
expiresWhenIdleFor: days(30),
);
return $store->id;若要通过 ID 获取已有的向量存储,请使用 get 方法:
use Laravel\Ai\Stores;
$store = Stores::get('store_id');
$store->id;
$store->name;
$store->fileCounts;
$store->ready;若要删除向量存储,可以在 Stores 类或存储实例上使用 delete 方法:
use Laravel\Ai\Stores;
// Delete by ID...
Stores::delete('store_id');
// Or delete via a store instance...
$store = Stores::get('store_id');
$store->delete();向存储中添加文件
拥有向量存储之后,可以使用 add 方法向其中添加文件。添加到存储中的文件会自动通过文件搜索提供商工具进行索引,以便进行语义搜索:
use Laravel\Ai\Files\Document;
use Laravel\Ai\Stores;
$store = Stores::get('store_id');
// Add a file that has already been stored with the provider...
$document = $store->add('file_id');
$document = $store->add(Document::fromId('file_id'));
// Or, store and add a file in one step...
$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 文件。
在将文件添加到存储时,你可以附加元数据。之后在使用文件搜索提供商工具时,可以使用这些元数据来筛选搜索结果:
$store->add(Document::fromPath('/path/to/document.pdf'), metadata: [
'author' => 'Taylor Otwell',
'department' => 'Engineering',
'year' => 2026,
]);若要从存储中移除文件,请使用 remove 方法:
$store->remove('file_id');从向量存储中移除文件,并不会将其从提供商的文件存储中删除。若要同时从向量存储中移除文件并从文件存储中永久删除,请使用 deleteFile 参数:
$store->remove('file_abc123', deleteFile: true);用法
每个响应都包含一个 usage 属性,其中含有提供方报告的令牌计数。输入和输出计数均为总数,因此被计为缓存或推理的令牌也包含在它们所属的总数中:
$response = (new SalesCoach)->prompt('Analyze this sales transcript...');
$response->usage->inputTokens;
$response->usage->outputTokens;
$response->usage->totalTokens();文本生成会返回一个 Laravel\Ai\Responses\Data\TextUsage 实例,它会将这些总数进一步细分。当提供方未报告这些值时,每个值将为 null,而非 0:
$response->usage->cacheReadInputTokens; // 从提示缓存中读取的输入令牌子集...
$response->usage->cacheWriteInputTokens; // 写入提示缓存的输入令牌子集...
$response->usage->reasoningTokens; // 用于推理的输出令牌子集...
$response->usage->uncachedInputTokens(); // 既未从缓存读取也未写入缓存的输入令牌...缓存读取、缓存写入和非缓存输入的计费费率不同,因此应分别对这三项计数定价,而非仅使用输入总数。
其余功能会返回一个包含其专属计数的使用对象:
| 功能 | 使用对象 | 新增内容 |
|---|---|---|
| 文本、分类 | TextUsage | 缓存读取、缓存写入和推理令牌 |
| 图像 | ImageUsage | imageInputTokens 和 imageOutputTokens |
| 转录 | TranscriptionUsage | audioSeconds,即转录音频的时长 |
| 重排序 | RerankingUsage | searchUnits,部分提供方按此而非按令牌计费 |
| 音频、嵌入 | Usage |
并非所有提供方都会报告每一项计数,提供方未报告的计数将显示为 null:
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;故障转移
在发起提示或生成其他媒体时,可以提供一个提供方/模型数组,当主提供方出现服务中断或速率限制时,将自动故障转移到备用提供方/模型:
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 作为键(枚举实例不能直接作为 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',
],
);测试
在模拟队列化的图像、音频、转录或嵌入生成时,在队列化生成上注册的任何 then 回调都会以模拟响应被调用,以便测试回调中包含的逻辑。若不希望调用这些回调,也可使用 Queue::fake() 模拟队列。
智能体
若要在测试中模拟智能体的响应,可在智能体类上调用 fake 方法。可选择性地提供响应数组或闭包:
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;
});当模拟返回结构化输出的智能体时,可提供数组作为响应。智能体会返回包含指定数据的结构化响应:
SalesCoach::fake([
['score' => 87],
]);还可模拟一个等待工具审批的响应:
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此外,可模拟包含推理的响应。模拟会触发推理事件,因此在流式运行中也会报告推理内容:
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::fake() 且未显式提供模拟输出时,Laravel 会自动生成与智能体定义的输出 schema 匹配的模拟数据。
在向智能体发起提示后,可对收到的提示进行断言:
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();在断言审批续接时,可检查提示的审批决策:
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();
});对于队列化的智能体调用,请使用队列化断言方法:
use Laravel\Ai\QueuedAgentPrompt;
SalesCoach::assertQueued('Analyze this...');
SalesCoach::assertQueued(function (QueuedAgentPrompt $prompt) {
return $prompt->contains('Analyze');
});
SalesCoach::assertNotQueued('Missing prompt');
SalesCoach::assertNeverQueued();为确保所有智能体调用都有对应的模拟响应,可使用 preventStrayPrompts。若智能体在未定义模拟响应的情况下被调用,将抛出异常:
SalesCoach::fake()->preventStrayPrompts();图像
可通过在 Image 类上调用 fake 方法来模拟图像生成。图像被模拟后,可对记录的图像生成提示进行多种断言:
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('...');
});生成图像后,可对收到的提示进行断言:
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();为了确保所有图像生成都有对应的模拟响应,你可以使用 preventStrayImages。如果生成图像时未定义模拟响应,将抛出异常:
Image::fake()->preventStrayImages();音频
可以通过调用 Audio 类上的 fake 方法来模拟音频生成。一旦音频被模拟,就可以针对记录的音频生成提示执行各种断言:
use Laravel\Ai\Audio;
use Laravel\Ai\Prompts\AudioPrompt;
use Laravel\Ai\Prompts\QueuedAudioPrompt;
// Automatically generate a fixed response for every prompt...
Audio::fake();
// Provide a list of prompt responses...
Audio::fake([
base64_encode($firstAudio),
base64_encode($secondAudio),
]);
// Dynamically handle prompt responses based on the incoming prompt...
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();为了确保所有音频生成都有对应的模拟响应,你可以使用 preventStrayAudio。如果生成音频时未定义模拟响应,将抛出异常:
Audio::fake()->preventStrayAudio();转录
可以通过调用 Transcription 类上的 fake 方法来模拟转录生成。一旦转录被模拟,就可以针对记录的转录生成提示执行各种断言:
use Laravel\Ai\Transcription;
use Laravel\Ai\Prompts\TranscriptionPrompt;
use Laravel\Ai\Prompts\QueuedTranscriptionPrompt;
// Automatically generate a fixed response for every prompt...
Transcription::fake();
// Provide a list of prompt responses...
Transcription::fake([
'First transcription text.',
'Second transcription text.',
]);
// Dynamically handle prompt responses based on the incoming prompt...
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();为了确保所有转录生成都有对应的模拟响应,你可以使用 preventStrayTranscriptions。如果生成转录时未定义模拟响应,将抛出异常:
Transcription::fake()->preventStrayTranscriptions();嵌入
可以通过调用 Embeddings 类上的 fake 方法来模拟嵌入生成。一旦嵌入被模拟,就可以针对记录的嵌入生成提示执行各种断言:
use Laravel\Ai\Embeddings;
use Laravel\Ai\Prompts\EmbeddingsPrompt;
use Laravel\Ai\Prompts\QueuedEmbeddingsPrompt;
// Automatically generate fake embeddings of the proper dimensions for every prompt...
Embeddings::fake();
// Provide a list of prompt responses...
Embeddings::fake([
[$firstEmbeddingVector],
[$secondEmbeddingVector],
]);
// Dynamically handle prompt responses based on the incoming prompt...
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();为了确保所有嵌入生成都有对应的模拟响应,你可以使用 preventStrayEmbeddings。如果生成嵌入时未定义模拟响应,将抛出异常:
Embeddings::fake()->preventStrayEmbeddings();重排序
可以通过调用 Reranking 类上的 fake 方法来模拟重排序操作:
use Laravel\Ai\Reranking;
use Laravel\Ai\Prompts\RerankingPrompt;
use Laravel\Ai\Responses\Data\RankedDocument;
// Automatically generate a fake reranked responses...
Reranking::fake();
// Provide custom responses...
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();分类
可以通过调用 Classification 类上的 fake 方法来模拟分类。如果未提供自定义响应,Laravel 会自动生成与每个问题结构相匹配的答案:
use Laravel\Ai\Classification;
use Laravel\Ai\Prompts\ClassificationPrompt;
use Laravel\Ai\Responses\Data\BooleanAnswer;
use Laravel\Ai\Responses\Data\ChoiceAnswer;
// Automatically generate fake answers...
Classification::fake();
// Provide answers for specific questions...
Classification::fake([
[
'urgent' => new BooleanAnswer(0.94),
'department' => new ChoiceAnswer('technical', [
'billing' => 0.08,
'technical' => 0.87,
'sales' => 0.05,
], confidence: 0.82),
],
]);
// Build answers from the prompt...
Classification::fake(fn (ClassificationPrompt $prompt) => [
'urgent' => new BooleanAnswer($prompt->contains('ASAP') ? 1.0 : 0.0),
]);未在模拟响应中列出的问题也会收到生成的答案,因此你的测试只需提供用于进行断言的答案。
分类完成后,你可以对已执行的操作进行断言:
Classification::assertClassified(function (ClassificationPrompt $prompt) {
return $prompt->contains('refund') && $prompt->asks('department');
});
Classification::assertNotClassified(
fn (ClassificationPrompt $prompt) => $prompt->asks('sentiment')
);
Classification::assertNothingClassified();文件
可以通过调用 Files 类上的 fake 方法来模拟文件操作:
use Laravel\Ai\Files;
Files::fake();一旦文件操作被模拟,你就可以对发生的上传和删除操作进行断言:
use Laravel\Ai\Contracts\Files\StorableFile;
use Laravel\Ai\Files\Document;
// Store files...
Document::fromString('Hello, Laravel!', mimeType: 'text/plain')
->as('hello.txt')
->put();
// Make assertions...
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:
Files::assertDeleted('file-id');
Files::assertNotDeleted('file-id');
Files::assertNothingDeleted();向量存储
可以通过调用 Stores 类上的 fake 方法来模拟向量存储操作。模拟存储操作的同时,也会自动模拟文件操作:
use Laravel\Ai\Stores;
Stores::fake();对存储操作进行模拟后,你可以对创建或删除的存储进行断言:
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:
Stores::assertDeleted('store_id');
Stores::assertNotDeleted('other_store_id');
Stores::assertNothingDeleted();若要断言文件已从存储中添加或移除,请对给定的 Store 实例使用断言方法:
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 方法传递一个闭包,以便根据添加文件的内容进行断言:
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 的使用信息。