Laravel AI SDK makes multi-step AI agents much easier to ship because your application no longer needs to care which LLM provider is behind the request. In this tutorial, I’ll show the pattern I use in production Laravel apps: keep provider choice, model names, timeouts and agent behaviour in config, while the application talks to one stable interface.
The goal is simple: switch from OpenAI to Anthropic or Gemini by changing .env or config/ai.php, not by rewriting controllers, jobs or service classes.
Why Laravel AI SDK changes the agent boundary
Most early GenAI codebases start with direct SDK calls inside application services. It feels fast for a week. Then you need a cheaper model for classification, a stronger model for reasoning, Gemini for a specific customer, or Anthropic for longer context windows.
That is when direct provider calls become technical debt.
A better boundary is:
- Laravel application code depends on an internal
AiGatewaycontract. - The gateway uses the Laravel AI SDK provider abstraction.
- Provider, model and options come from config.
- Agents are composed as small steps, not one giant prompt.
This gives you provider-agnostic AI without hiding the important trade-offs. OpenAI, Anthropic and Gemini still have different model behaviour, token windows and pricing, but your Laravel code gets one consistent integration point.
If you are new to Laravel configuration patterns, the official Laravel configuration documentation is worth revisiting before building this.
The architecture for config-driven provider switching
For a multi-step AI agent, I usually separate four layers:
- Controller or job: receives the business request.
- Agent service: owns the workflow and step order.
- AI gateway: normalises requests and responses.
- Provider adapter: handled by the Laravel AI SDK and selected from config.
A typical support-ticket agent might run these steps:
- Classify the ticket intent and urgency.
- Extract entities such as order ID, product, account email.
- Retrieve matching records from your database.
- Draft a response using retrieved context.
- Run a final safety or policy check before sending.
Each step can use a different model. For example, use a cheaper Gemini or OpenAI mini model for classification, then a stronger Anthropic or OpenAI model for final reasoning. The point is not provider loyalty. The point is reliable software design.
Step 1: Create AI provider config for OpenAI, Anthropic and Gemini
Start with a dedicated config file. This keeps provider switching boring, which is exactly what you want in production.
// config/ai.php
return [
'default_provider' => env('AI_PROVIDER', 'openai'),
'providers' => [
'openai' => [
'api_key' => env('OPENAI_API_KEY'),
'base_url' => env('OPENAI_BASE_URL', 'https://api.openai.com/v1'),
'default_model' => env('OPENAI_MODEL', 'gpt-4o-mini'),
'timeout' => env('OPENAI_TIMEOUT', 30),
],
'anthropic' => [
'api_key' => env('ANTHROPIC_API_KEY'),
'base_url' => env('ANTHROPIC_BASE_URL', 'https://api.anthropic.com'),
'default_model' => env('ANTHROPIC_MODEL', 'claude-3-5-sonnet-latest'),
'timeout' => env('ANTHROPIC_TIMEOUT', 30),
],
'gemini' => [
'api_key' => env('GEMINI_API_KEY'),
'base_url' => env('GEMINI_BASE_URL', 'https://generativelanguage.googleapis.com'),
'default_model' => env('GEMINI_MODEL', 'gemini-1.5-flash'),
'timeout' => env('GEMINI_TIMEOUT', 30),
],
],
'agents' => [
'support_ticket' => [
'classification_model' => env('AI_CLASSIFICATION_MODEL', null),
'reasoning_model' => env('AI_REASONING_MODEL', null),
'temperature' => 0.2,
'max_output_tokens' => 900,
],
],
];
Now your environment controls the active provider:
AI_PROVIDER=anthropic
ANTHROPIC_API_KEY=sk-ant-...
ANTHROPIC_MODEL=claude-3-5-sonnet-latest
Switching to Gemini becomes a config change:
AI_PROVIDER=gemini
GEMINI_API_KEY=...
GEMINI_MODEL=gemini-1.5-pro
The OpenAI API reference and Anthropic Messages API docs are useful when tuning provider-specific limits, but your application should not import those details everywhere.
Step 2: Build a provider-agnostic AI gateway
I like creating my own small contract even when the underlying SDK already has one. It protects the domain code from SDK churn and makes testing painless.
namespace App\AI;
final readonly class AiRequest
{
public function __construct(
public array $messages,
public ?string $model = null,
public float $temperature = 0.2,
public int $maxOutputTokens = 900,
) {}
}
final readonly class AiResponse
{
public function __construct(
public string $text,
public array $raw = [],
) {}
}
interface AiGateway
{
public function complete(AiRequest $request): AiResponse;
}
Then implement the contract using the Laravel AI SDK. The exact method names may differ slightly by SDK version, but the important part is that provider and model are resolved from config, not hardcoded in business logic.
namespace App\AI;
use Laravel\AI\AiManager;
final readonly class LaravelSdkAiGateway implements AiGateway
{
public function __construct(private AiManager $ai) {}
public function complete(AiRequest $request): AiResponse
{
$provider = config('ai.default_provider');
$providerConfig = config("ai.providers.{$provider}");
$result = $this->ai
->provider($provider)
->chat()
->model($request->model ?? $providerConfig['default_model'])
->messages($request->messages)
->temperature($request->temperature)
->maxOutputTokens($request->maxOutputTokens)
->send();
return new AiResponse(
text: $result->text(),
raw: $result->toArray(),
);
}
}
Bind it once in a service provider:
use App\AI\AiGateway;
use App\AI\LaravelSdkAiGateway;
public function register(): void
{
$this->app->bind(AiGateway::class, LaravelSdkAiGateway::class);
}
From this point, your controllers, jobs and agents only type-hint AiGateway.
Step 3: Compose multi-step AI agents in Laravel
Do not build agents as one massive prompt. It becomes hard to observe, hard to retry and hard to improve. Build a deterministic workflow where AI is used for specific steps.
Here is a simplified support-ticket agent:
namespace App\Agents;
use App\AI\AiGateway;
use App\AI\AiRequest;
final readonly class SupportTicketAgent
{
public function __construct(private AiGateway $ai) {}
public function handle(string $ticketBody): array
{
$classification = $this->classify($ticketBody);
$context = $this->retrieveContext($classification);
$draft = $this->draftReply($ticketBody, $classification, $context);
$review = $this->reviewReply($draft);
return [
'classification' => $classification,
'draft' => $draft,
'approved' => $review['approved'] ?? false,
];
}
private function classify(string $ticketBody): array
{
$response = $this->ai->complete(new AiRequest(
messages: [
['role' => 'system', 'content' => 'Classify the ticket. Return compact JSON with intent, urgency and entities.'],
['role' => 'user', 'content' => $ticketBody],
],
model: config('ai.agents.support_ticket.classification_model'),
temperature: 0.0,
maxOutputTokens: 250,
));
return json_decode($response->text, true) ?: ['intent' => 'unknown'];
}
private function retrieveContext(array $classification): array
{
// Replace this with Eloquent queries, Scout search or a vector index.
return [
'refund_policy' => 'Refunds are allowed within 7 days for unused plans.',
'account_status' => 'active',
];
}
private function draftReply(string $ticketBody, array $classification, array $context): string
{
$response = $this->ai->complete(new AiRequest(
messages: [
['role' => 'system', 'content' => 'Write a concise, helpful support reply. Use only the supplied context.'],
['role' => 'user', 'content' => json_encode(compact('ticketBody', 'classification', 'context'))],
],
model: config('ai.agents.support_ticket.reasoning_model'),
temperature: config('ai.agents.support_ticket.temperature'),
maxOutputTokens: config('ai.agents.support_ticket.max_output_tokens'),
));
return $response->text;
}
private function reviewReply(string $draft): array
{
$response = $this->ai->complete(new AiRequest(
messages: [
['role' => 'system', 'content' => 'Approve only if the reply is safe, factual and does not invent policy. Return JSON.'],
['role' => 'user', 'content' => $draft],
],
temperature: 0.0,
maxOutputTokens: 120,
));
return json_decode($response->text, true) ?: ['approved' => false];
}
}
Notice the design: the workflow is deterministic, but individual steps can use AI. That is much easier to debug than a fully autonomous loop that keeps calling tools until it runs out of tokens.
Operational considerations before production
Provider-agnostic does not mean provider-identical. I would check these before shipping:
- Token limits: long-context models behave differently across vendors.
- JSON reliability: use schema or structured output support when available.
- Latency: measure p95 latency per step, not just total request time.
- Cost: classification and review should usually use cheaper models.
- Retries: retry network failures, not bad prompts.
- Logging: store step name, provider, model, token usage and latency.
- Privacy: redact secrets and customer PII before sending prompts.
For user-facing flows, I prefer dispatching the agent to a queue and streaming status updates. For internal tools, synchronous execution is fine if the workflow stays under 10 to 15 seconds.
Testing provider-agnostic AI code
Your tests should not call OpenAI, Anthropic or Gemini for normal CI runs. Fake the AiGateway contract.
use App\AI\AiGateway;
use App\AI\AiRequest;
use App\AI\AiResponse;
$this->app->bind(AiGateway::class, function () {
return new class implements AiGateway {
public function complete(AiRequest $request): AiResponse
{
return new AiResponse('{"intent":"refund","urgency":"medium","approved":true}');
}
};
});
This is where the abstraction pays for itself. You can test workflow branching, database lookups and failure handling without paying for tokens or depending on provider uptime.
FAQ
Can I really switch providers using only config files?
Yes, if your application depends on a provider-neutral gateway and resolves provider, model and options from config. You may still tune prompts per model, but the code path should remain the same.
Should every step use the same model?
Usually no. Use small, cheap models for classification, extraction and policy checks. Reserve stronger models for reasoning-heavy drafting or planning steps.
Is a multi-step AI agent better than one prompt?
For production Laravel apps, yes in most cases. Multi-step agents give you observability, retries, partial failure handling and clearer cost control.
Where should I store prompts in Laravel?
Start with versioned PHP config or dedicated prompt classes. Once prompts become product-managed content, move them to the database with versioning and approval workflows.
Conclusion
The Laravel AI SDK is most valuable when you treat it as infrastructure, not as business logic. Put provider choice in config, hide vendor differences behind a small gateway, and build multi-step AI agents as observable Laravel services.
If you are building a serious Laravel GenAI product and want the architecture reviewed, reach out and I’ll help you make it production-ready.