跳转到主要内容

何时需要自定义 Provider

Laravel AI SDK 内置支持 OpenAI、Anthropic、Gemini、Mistral 等主流 AI 服务。然而在以下场景,标准 Provider 无法满足需求:
  • 尚未获得官方支持的新兴 AI 服务
  • 想要经过公司内部的模型网关或计费管理层
  • 拥有独立协议或认证方式的本地部署推理服务器
在这些情况下,你可以实现自定义 Provider 并注册到 SDK 的 AiManager,就能像标准 Provider 一样通过同一 API 使用它。
OpenAI 兼容 API 请使用内置驱动从 SDK 0.9 开始,openai-compatible 驱动已内置提供。对于公司内部的 OpenAI 兼容推理服务器(例如 Ollama 的 OpenAI 兼容端点),只需在 config/ai.php 中添加配置即可使用,无需实现自定义 Provider。
'my-server' => [
    'driver' => 'openai-compatible',
    'key'    => env('MY_SERVER_API_KEY'),
    'url'    => env('MY_SERVER_URL', 'http://localhost:8080/v1'),
    'models' => [
        'text' => ['default' => 'llama3.3-70b'],
    ],
],

架构概览

两层结构

Laravel AI SDK 由 Provider 与 Gateway 两层构成。
职责示例
Provider面向应用的接口。负责模型名解析与配置的持有OpenAiProviderAnthropicProvider
Gateway发送一个步骤的实际 API 请求AnthropicGatewayOpenAiCompatibleGateway
所有 Provider 都继承抽象类 Laravel\Ai\Providers\Provider,并按功能实现对应的契约(接口)。包含工具调用的多步骤循环由 TextGenerationLoop 反复调用 Gateway 来实现。

契约一览

根据要支持的功能,实现所需的契约即可。
契约命名空间功能
TextProviderLaravel\Ai\Contracts\Providers\TextProvider文本生成、Agent
EmbeddingProviderLaravel\Ai\Contracts\Providers\EmbeddingProvider向量嵌入生成
ImageProviderLaravel\Ai\Contracts\Providers\ImageProvider图像生成
AudioProviderLaravel\Ai\Contracts\Providers\AudioProvider语音合成(TTS)
TranscriptionProviderLaravel\Ai\Contracts\Providers\TranscriptionProvider语音识别(STT)
多数情况下只需实现 TextProvider 即可。

TextProvider 契约

这是文本生成 Provider 所要实现的接口(src/Contracts/Providers/TextProvider.php)。
interface TextProvider extends Provider
{
    public function prompt(AgentPrompt $prompt): AgentResponse;
    public function stream(AgentPrompt $prompt): StreamableAgentResponse;
    public function useTextGateway(StepTextGateway $gateway): self;
    public function textGenerationLoop(): TextGenerationLoop;
    public function defaultTextModel(): string;
    public function cheapestTextModel(): string;
    public function smartestTextModel(): string;
}
prompt()stream() 的实现可交由现有 trait(GeneratesTextStreamsText)处理,因此实际需要实现的只有返回模型名的 3 个方法以及 textGateway(),合计 4 个方法。多步骤的工具循环由 TextGenerationLoop 管理,Gateway 只需处理一步的请求。

实现示例:自定义 Provider

以下示例将一个拥有独立 API 的推理服务作为 my-inference Provider 进行注册。
1

创建 Provider 类

创建 app/Ai/Providers/MyInferenceProvider.php
<?php

declare(strict_types=1);

namespace App\Ai\Providers;

use Illuminate\Contracts\Events\Dispatcher;
use Laravel\Ai\Contracts\Gateway\StepTextGateway;
use Laravel\Ai\Contracts\Providers\TextProvider;
use Laravel\Ai\Providers\Concerns\GeneratesText;
use Laravel\Ai\Providers\Concerns\HasTextGateway;
use Laravel\Ai\Providers\Concerns\StreamsText;
use Laravel\Ai\Providers\Provider;

class MyInferenceProvider extends Provider implements TextProvider
{
    use GeneratesText;
    use HasTextGateway;
    use StreamsText;

    public function __construct(protected array $config, protected Dispatcher $events)
    {
        //
    }

    /**
     * Get the credentials for the provider.
     */
    public function providerCredentials(): array
    {
        return ['key' => $this->config['key'] ?? null];
    }

    /**
     * Get the provider's text gateway.
     */
    public function textGateway(): StepTextGateway
    {
        return $this->textGateway ??= new \App\Ai\Gateway\MyInferenceGateway($this->events);
    }

    public function defaultTextModel(): string
    {
        return $this->config['models']['text']['default'] ?? 'my-model-v1';
    }

    public function cheapestTextModel(): string
    {
        return $this->config['models']['text']['cheapest'] ?? 'my-model-v1';
    }

    public function smartestTextModel(): string
    {
        return $this->config['models']['text']['smartest'] ?? 'my-model-v1';
    }
}
2

在 AppServiceProvider 中注册

App\Providers\AppServiceProviderboot 方法中使用 extend() 进行注册。
<?php

namespace App\Providers;

use App\Ai\Providers\MyInferenceProvider;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Support\ServiceProvider;
use Laravel\Ai\AiManager;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        $this->app->make(AiManager::class)->extend(
            'my-inference',
            fn (array $config) => new MyInferenceProvider(
                $config,
                $this->app->make(Dispatcher::class)
            )
        );
    }
}
3

在 config/ai.php 中添加 Provider

'providers' => [
    // ...已有的 Provider...

    'my-inference' => [
        'driver' => 'my-inference',
        'key'    => env('MY_INFERENCE_API_KEY'),
        'url'    => env('MY_INFERENCE_URL', 'http://localhost:8080'),
    ],
],
同时在 .env 中添加:
MY_INFERENCE_API_KEY=your-api-key
MY_INFERENCE_URL=https://inference.example.internal
4

在 Agent 中使用

注册后,只需将 Provider 名称传给 prompt()provider 参数,就能像使用标准 Provider 一样使用它。
use App\Ai\Agents\SummaryAgent;

$response = SummaryAgent::make()->prompt('请总结这篇文章。', provider: 'my-inference');

echo $response->text;
若要将其作为默认 Provider,请修改 config/ai.phpdefault 键。
'default' => 'my-inference',

自定义 Gateway 的实现

对于并非 OpenAI 兼容、拥有独立 API 的服务,需要实现 StepTextGateway 契约的自定义 Gateway。

StepTextGateway 契约

以下是 src/Contracts/Gateway/StepTextGateway.php 定义的接口。Gateway 负责处理会话的单步请求并返回 StepResponse,工具调用循环由调用方的 TextGenerationLoop 管理。
interface StepTextGateway
{
    public function generateTextStep(
        TextProvider $provider,
        string $model,
        ?string $instructions,
        array $messages,
        array $tools,
        ?array $schema,
        ?TextGenerationOptions $options,
        ?int $timeout,
        StepContext $stepContext,
    ): StepResponse;

    public function generateStreamStep(
        string $invocationId,
        TextProvider $provider,
        string $model,
        ?string $instructions,
        array $messages,
        array $tools,
        ?array $schema,
        ?TextGenerationOptions $options,
        ?int $timeout,
        StepContext $stepContext,
    ): Generator;
}
0.8 之前的 TextGateway 契约(generateText()stream()onToolInvocation())已在 0.9 中移除。如果你拥有自定义 Gateway,需要迁移到 StepTextGateway。工具调用的 onToolInvocation() 已被移动到 TextGenerationLoop

自定义 Gateway 实现示例

以下是向自定义推理 API 发起 HTTP 请求的简单 Gateway 骨架。
<?php

declare(strict_types=1);

namespace App\Ai\Gateway;

use Generator;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Support\Facades\Http;
use Laravel\Ai\Contracts\Gateway\StepTextGateway;
use Laravel\Ai\Contracts\Providers\TextProvider;
use Laravel\Ai\Gateway\StepContext;
use Laravel\Ai\Gateway\StepResponse;
use Laravel\Ai\Gateway\TextGenerationOptions;
use Laravel\Ai\Responses\Data\FinishReason;
use Laravel\Ai\Responses\Data\Meta;
use Laravel\Ai\Responses\Data\Usage;

class MyInferenceGateway implements StepTextGateway
{
    public function __construct(protected Dispatcher $events) {}

    public function generateTextStep(
        TextProvider $provider,
        string $model,
        ?string $instructions,
        array $messages,
        array $tools,
        ?array $schema,
        ?TextGenerationOptions $options,
        ?int $timeout,
        StepContext $stepContext,
    ): StepResponse {
        $credentials = $provider->providerCredentials();
        $config      = $provider->additionalConfiguration();

        $response = Http::withToken($credentials['key'])
            ->baseUrl($config['url'])
            ->timeout($timeout ?? 30)
            ->post('/generate', [
                'model'        => $model,
                'instructions' => $instructions,
                'messages'     => $this->formatMessages($messages),
            ]);

        $data = $response->json();

        return new StepResponse(
            text: $data['output']['text'] ?? '',
            toolCalls: [],
            finishReason: FinishReason::Stop,
            usage: new Usage(
                promptTokens: $data['usage']['input_tokens'] ?? 0,
                completionTokens: $data['usage']['output_tokens'] ?? 0,
            ),
            meta: new Meta(
                provider: $provider->name(),
                model: $model,
            ),
        );
    }

    public function generateStreamStep(
        string $invocationId,
        TextProvider $provider,
        string $model,
        ?string $instructions,
        array $messages,
        array $tools,
        ?array $schema,
        ?TextGenerationOptions $options,
        ?int $timeout,
        StepContext $stepContext,
    ): Generator {
        // 流式实现(略)
        yield from [];

        return null;
    }

    protected function formatMessages(array $messages): array
    {
        return array_map(fn ($message) => [
            'role'    => $message->role->value,
            'content' => $message->content,
        ], $messages);
    }
}
如需支持工具调用(function calling),请在 generateTextStep() 中将工具的调用结果加入 toolCalls,并将 finishReason 设为 FinishReason::ToolCalls。工具的执行以及进入下一步的过渡由 TextGenerationLoop 自动处理。实现参考可查看 AnthropicGateway.php

测试方法

使用 Agent 类的 fake()

要测试使用自定义 Provider 的 Agent,可使用 Agent 类的 fake() 方法。不论是否是自定义 Provider,都会将 fake Gateway 注入到 Provider 中。
<?php

namespace Tests\Feature;

use App\Ai\Agents\SummaryAgent;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class SummaryAgentTest extends TestCase
{
    use RefreshDatabase;

    public function test_summary_agent_returns_text(): void
    {
        SummaryAgent::fake(['这是摘要。']);

        $response = SummaryAgent::make()->prompt('长篇文章的文本...', provider: 'my-inference');

        $this->assertEquals('这是摘要。', $response->text);

        SummaryAgent::assertPrompted('长篇文章的文本...');
    }
}
从 0.9 开始,Agent::fake() 的响应会经过与真实 Provider 相同的 TextGenerationLoop。如果在没有注册工具的 Agent 上设置了 fake 工具调用,会抛出 NoSuchToolException

使用 extend() 的 Mock Provider

也可以通过 extend() 从容器注册测试用 Provider。
public function test_with_mock_provider(): void
{
    $this->app->make(AiManager::class)->extend(
        'my-inference',
        function (array $config) {
            $events = $this->app->make(\Illuminate\Contracts\Events\Dispatcher::class);

            return new MyInferenceProvider($config, $events);
        }
    );

    // 测试代码
}

参考链接

OllamaProvider.php — 简单的 Provider 实现示例

连接本地模型服务器的 Provider 最小实现。可作为自定义 Provider 实现的参考。

AnthropicGateway.php — Gateway 实现示例

实现了 StepTextGateway 的 Gateway 示例。可查看 generateTextStep()generateStreamStep() 的实现。

StepTextGateway Contract

文本生成 Gateway 所要实现的接口定义。

TextProvider Contract

文本生成 Provider 所要实现的接口定义。
最后修改于 2026年7月13日