Elk Laravel-project dat ik tegenwoordig aanraak heeft ergens op de backlog dezelfde wens staan: "voeg AI toe." Meestal bedoelen mensen daarmee "roep de OpenAI API aan en hoop op het beste." Maar als je ooit een kale Http::post() naar een LLM-endpoint in productie hebt moeten onderhouden, weet je hoe snel dat uit elkaar valt. Geen structuur, geen type safety, geen manier om van provider te wisselen, en prompts verspreid over je codebase als broodkruimels.
Laravel heeft nu fatsoenlijke tooling hiervoor. Twee opties die je tijd waard zijn: Prism, het beproefde community-pakket, en de gloednieuwe officiële Laravel AI SDK. Hier is hoe ik ze gebruik, en de patronen die daadwerkelijk standhouden.
1. Kies je wapen
Prism (prism-php/prism) bestaat al langer en voelt heel Laravel-native. Fluent API, multi-provider ondersteuning (OpenAI, Anthropic, Gemini, Ollama), gestructureerde output met schema-objecten, en een solide tool-systeem. Als je vandaag productiestabiliteit nodig hebt, is dit de veilige keuze.
Laravel AI SDK (laravel/ai) is Taylors officiële antwoord. Het is nog 0.x, maar de architectuur is strak: agent-klassen die contracts implementeren, artisan-generators, middleware-ondersteuning, en gestructureerde output via JSON Schema. Als je helemaal opnieuw begint en het niet erg vindt om op de early wave mee te surfen, is dit waar het ecosysteem naartoe gaat.
Installeer een van beide:
composer require prism-php/prism
# of
composer require laravel/aiIk laat voorbeelden van beide zien. De patronen zijn overdraagbaar.
2. Tekstgeneratie die je niet voor schut zet
De simpelste use case: tekst genereren met een system prompt. Dit is waar de meeste teams stoppen, en waar de meeste teams de fout in gaan. Een kale string-concatenatie is geen promptstrategie.
Met Prism:
// app/Services/ProductDescriptionService.php
use Prism\Prism\Facades\Prism;
class ProductDescriptionService
{
public function generate(string $name, string $features): string
{
$response = Prism::text()
->using('anthropic', 'claude-sonnet-4-6')
->withSystemPrompt(
'Je schrijft bondige productbeschrijvingen voor een webshop. '
. 'Maximaal 2 zinnen. Geen opvulling. Geen uitroeptekens.'
)
->withPrompt("Product: {$name}\nKenmerken: {$features}")
->asText();
return $response->text;
}
}Met Laravel AI SDK:
// app/Agents/ProductWriter.php
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Promptable;
class ProductWriter implements Agent
{
use Promptable;
public function instructions(): string
{
return 'Je schrijft bondige productbeschrijvingen voor een webshop. '
. 'Maximaal 2 zinnen. Geen opvulling. Geen uitroeptekens.';
}
}
// Gebruik
$description = ProductWriter::make()
->prompt("Product: {$name}\nKenmerken: {$features}")
->text;Beide zijn schoon. Beide houden de prompt uit je controller. Het kernpatroon: wikkel elke AI-aanroep in een dedicated service- of agent-klasse. Als het model wijzigt, als de prompt bijgesteld moet worden, als je caching moet toevoegen, wijzig je één bestand.
3. Gestructureerde output: stop met strings parsen
Op het moment dat je de AI data nodig hebt in plaats van proza, heb je gestructureerde output nodig. Dit is het verschil tussen een prototype en een productiefunctie.
Stel dat je een support-ticket classifier bouwt:
// app/Services/TicketClassifier.php
use Prism\Prism\Facades\Prism;
use Prism\Prism\Schema\ObjectSchema;
use Prism\Prism\Schema\StringSchema;
use Prism\Prism\Schema\NumberSchema;
class TicketClassifier
{
public function classify(string $ticketBody): array
{
$schema = new ObjectSchema(
name: 'ticket_classification',
description: 'Classificatie van een support-ticket',
properties: [
new StringSchema('category', 'De ticketcategorie: billing, technical, account, other'),
new StringSchema('priority', 'Prioriteit: low, medium, high, critical'),
new NumberSchema('confidence', 'Betrouwbaarheidsscore tussen 0 en 1'),
new StringSchema('summary', 'Samenvatting in één zin'),
],
requiredFields: ['category', 'priority', 'confidence', 'summary']
);
$response = Prism::structured()
->using('openai', 'gpt-4o')
->withSchema($schema)
->withPrompt("Classificeer dit support-ticket:\n\n{$ticketBody}")
->asStructured();
return $response->structured;
// ['category' => 'billing', 'priority' => 'high', 'confidence' => 0.92, 'summary' => '...']
}
}Geen regex. Geen json_decode en hopen. Het model wordt beperkt tot je schema. Als je een enum nodig hebt, definieer een enum. Als je een getal in een bereik nodig hebt, beperk het. Dit maakt AI-features betrouwbaar genoeg om in je businesslogica te voeden.
Het Laravel AI SDK equivalent gebruikt het HasStructuredOutput contract:
// app/Agents/TicketClassifier.php
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasStructuredOutput;
use Laravel\Ai\Promptable;
use Illuminate\Contracts\JsonSchema\JsonSchema;
class TicketClassifier implements Agent, HasStructuredOutput
{
use Promptable;
public function instructions(): string
{
return 'Je classificeert support-tickets op categorie en prioriteit.';
}
public function schema(JsonSchema $schema): array
{
return [
'category' => $schema->string()->enum(['billing', 'technical', 'account', 'other'])->required(),
'priority' => $schema->string()->enum(['low', 'medium', 'high', 'critical'])->required(),
'confidence' => $schema->number()->minimum(0)->maximum(1)->required(),
'summary' => $schema->string()->required(),
];
}
}4. Tools: laat het model je code aanroepen
Hier wordt het echt krachtig. Tools laten de AI functies in je applicatie aanroepen, waardoor het contextbewust wordt zonder alles in de prompt te proppen.
use Prism\Prism\Facades\Prism;
use Prism\Prism\Facades\Tool;
$orderLookup = Tool::as('lookup_order')
->for('Zoek een bestelling op basis van ordernummer')
->withStringParameter('order_number', 'Het ordernummer om op te zoeken')
->using(function (string $order_number): string {
$order = Order::where('number', $order_number)->first();
if (!$order) {
return "Bestelling {$order_number} niet gevonden.";
}
return json_encode([
'status' => $order->status,
'total' => $order->total,
'shipped_at' => $order->shipped_at?->toDateString(),
'items' => $order->items->count(),
]);
});
$response = Prism::text()
->using('anthropic', 'claude-sonnet-4-6')
->withSystemPrompt('Je bent een klantenservice-assistent. Gebruik de tools om echte data op te zoeken. Gok nooit.')
->withTools([$orderLookup])
->withMaxSteps(3)
->withPrompt("Klant vraagt: Waar is mijn bestelling #A1234?")
->asText();Het model bepaalt zelf wanneer het de tool aanroept, verwerkt het resultaat en formuleert het antwoord. Je Order model blijft de single source of truth. De AI verzint geen data omdat het een echte functie heeft om aan te roepen.
5. Prompts zijn code, behandel ze ook zo
De grootste fout die ik zie: prompts als inline strings. Op het moment dat je meer dan één AI-feature hebt, heb je een promptmanagementstrategie nodig. Dit is wat werkt:
// app/Prompts/TicketClassifierPrompt.php
class TicketClassifierPrompt
{
public static function system(): string
{
return <<<'PROMPT'
Je classificeert support-tickets voor een SaaS-platform.
Regels:
- Categorie moet een van zijn: billing, technical, account, other
- Prioriteit is gebaseerd op bedrijfsimpact, niet op klantemoties
- Critical: dienst down of dataverlies. High: geblokkeerde workflow. Medium: verslechterde ervaring. Low: vragen.
- Betrouwbaarheid onder 0.7 betekent dat je onzeker bent. Markeer het.
PROMPT;
}
public static function user(string $body): string
{
return "Classificeer dit ticket:\n\n{$body}";
}
}Dedicated klassen. Version-controlled. Testbaar. Als je je prompts bijstelt (en dat ga je doen, constant), krijg je een schone diff in je PR.
De scherpe randjes
Een paar dingen die je bijten als je niet oplet:
Rate limits. Elke provider heeft ze. Wikkel je AI-aanroepen in een queue job met retry_after en exponential backoff. Roep geen LLM synchroon aan in een webrequest, tenzij het achter een laadstatus zit.
Kosten. Gestructureerde output met tools kan meerdere API-calls ketenen. Een enkele withMaxSteps(5) kan 5 round-trips triggeren. Monitor je gebruik. Stel harde limieten in.
Testen. Zowel Prism als de Laravel AI SDK ondersteunen het faken van responses in tests. Gebruik het. Raak geen echte APIs aan in je testsuite. Prism heeft Prism::fake(), de officiële SDK heeft eigen testhelpers.
Prompt injection. Als je prompt gebruikersinvoer bevat, ga er dan vanuit dat ze proberen uit je instructies te breken. Scheid system prompts van gebruikersinhoud. Interpoleer nooit gebruikersinvoer in system-instructies.
AI in Laravel is geen magie. Het is gewoon vakwerk. Goed vakwerk: getypeerde responses, dedicated serviceklassen, schema-beperkte output, versiebeheerde prompts. Slecht vakwerk: Http::post('openai...') in een controller met een string-prompt. De tooling is eindelijk goed genoeg om op te bouwen. Gebruik het op de juiste manier.