KI-Agenten in Laravel integrieren — Praxis-Guide für 2026
Wie du KI-Agenten produktionsreif in deine Laravel-App einbaust: Service-Pattern, Streaming via SSE, Queue-Architektur, Rate-Limiting, DSGVO. Mit Code.
Vom 30-Zeilen-Prototyp zum produktiven Agenten
Jeder zweite Laravel-Dev hat 2026 entweder schon einen LLM-Aufruf in einer App produktiv oder einen Pull Request für genau das auf dem Tisch. Der Standard-Prototyp sieht so aus:
public function chat(Request $request)
{
$response = OpenAI::chat()->create([
'model' => 'gpt-4o',
'messages' => [['role' => 'user', 'content' => $request->message]],
]);
return $response->choices[0]->message->content;
}
Funktioniert lokal. Stirbt in Produktion. Eine LLM-Antwort braucht 3 bis 15 Sekunden — der Request blockiert eine ganze PHP-Worker-Slot, der Browser läuft in den Timeout, und beim ersten echten Use Case fehlen Conversation-Memory, Tool-Calls, Rate-Limits und Cost-Tracking.
Was du nach diesem Artikel hast: ein Architektur-Pattern, das diese Probleme adressiert — getestet auf Laravel 11/12, mit prism-php als Multi-Provider-Layer, Redis als Conversation-Store und SSE für die UI.
Was „KI-Agent" eigentlich heißt
Wichtige Unterscheidung vor allem anderen: ein LLM-Aufruf ist kein Agent. Ein Agent ist:
- ein LLM,
- mit einem System-Prompt für eine konkrete Aufgabe,
- Zugriff auf Tools — Funktionen, die der Agent eigenständig aufrufen darf,
- einer Loop, in der das LLM Tools nutzt, deren Output verarbeitet und ggf. weitere Tools aufruft, bis es eine Antwort hat,
- und persistierter Kontext: Konversations-Historie, Benutzer-Zustand.
Beispiel: ein Support-Agent bekommt die Frage „Wo ist meine Bestellung 4711?" — ruft das Tool search_orders auf, bekommt das Ergebnis, formuliert die Antwort. Drei LLM-Calls, ein API-Endpoint, eine geöffnete UI-Bubble.
Architektur-Überblick
Drei Schichten halten den Stack sauber:
- Controller — dünn, validiert, schiebt Job in die Queue.
- Service-Klasse (
SupportAgent) — kapselt LLM-Stack, Tool-Registrierung und Konversations-Cache. - Job (
RunSupportAgentJob) — Hintergrund-Verarbeitung, Retries, Backoff.
Plus zwei Querschnitts-Themen: SSE-Streaming für Live-Antworten in der UI, und Rate-Limiting auf User- und Token-Ebene.
Composer-Setup
composer require prism-php/prism
php artisan vendor:publish --tag=prism-config
config/prism.php:
return [
'using' => env('PRISM_DEFAULT_PROVIDER', 'anthropic'),
'providers' => [
'anthropic' => [
'api_key' => env('ANTHROPIC_API_KEY'),
'url' => env('ANTHROPIC_URL', 'https://api.anthropic.com/v1'),
],
'openai' => [
'api_key' => env('OPENAI_API_KEY'),
'url' => env('OPENAI_URL', 'https://api.openai.com/v1'),
],
'mistral' => [
'api_key' => env('MISTRAL_API_KEY'),
],
],
];
prism-php abstrahiert über die Provider — du wechselst per Env-Variable von Anthropic auf Mistral, der Code bleibt gleich. Wichtig für DSGVO-Wechsel (siehe unten).
Service-Klasse — der eigentliche Agent
<?php
namespace App\Services\Agents;
use Illuminate\Contracts\Cache\Repository as Cache;
use Prism\Prism\Prism;
use Prism\Prism\Enums\Provider;
use Prism\Prism\Tool;
use App\Repositories\OrderRepository;
class SupportAgent
{
public function __construct(
private OrderRepository $orders,
private Cache $cache,
) {}
public function reply(int $userId, string $message): string
{
$history = $this->loadHistory($userId);
$history[] = ['role' => 'user', 'content' => $message];
$response = Prism::text()
->using(Provider::Anthropic, 'claude-sonnet-4-6')
->withSystemPrompt($this->systemPrompt())
->withMessages($history)
->withTools($this->tools($userId))
->withMaxSteps(5)
->asText();
$history[] = ['role' => 'assistant', 'content' => $response->text];
$this->saveHistory($userId, $history);
return $response->text;
}
private function tools(int $userId): array
{
return [
Tool::as('search_orders')
->for('Findet Bestellungen des aktuellen Kunden')
->withStringParameter('query', 'Bestellnummer oder Suchbegriff')
->using(fn (string $query) => $this->orders->searchForUser($userId, $query)),
];
}
private function systemPrompt(): string
{
return <<<TXT
Du bist Support-Agent für ACME GmbH. Antworte knapp, auf Deutsch.
Wenn du eine Bestellnummer oder Suchanfrage brauchst, nutze das Tool search_orders.
Erfinde keine Bestelldaten. Bei Unsicherheit: frage nach.
TXT;
}
private function loadHistory(int $userId): array
{
return $this->cache->get("agent:support:{$userId}", []);
}
private function saveHistory(int $userId, array $history): void
{
$this->cache->put("agent:support:{$userId}", array_slice($history, -20), now()->addHour());
}
}
Drei Punkte tragen in dem Code mehr Wert, als sie auf den ersten Blick zeigen:
withMaxSteps(5)— limitiert die Tool-Loop. Ohne diesen Schutz kann ein verwirrtes LLM in Endlos-Tool-Calls gehen und in 30 Sekunden 80.000 Tokens verbrennen.array_slice($history, -20)— nur die letzten 20 Nachrichten persistieren. Conversation-Historie ist quadratisch in Tokens (alles wird bei jedem Call mitgeschickt). 20 ist ein sinnvoller Default.- Tool-Closure greift auf
$userIdaus dem Scope zu. Damit kann das LLM nie versehentlich Bestellungen anderer User sehen — der Agent hat kein Argument für „User wechseln", weil das Tool den User schon hardgecodet hat.
Job für asynchrone Verarbeitung
<?php
namespace App\Jobs;
use App\Events\AgentReplied;
use App\Models\AgentMessage;
use App\Services\Agents\SupportAgent;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class RunSupportAgentJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
public int $timeout = 60;
public function __construct(
public int $userId,
public string $sessionId,
public string $message,
) {}
public function handle(SupportAgent $agent): void
{
$reply = $agent->reply($this->userId, $this->message);
AgentMessage::create([
'session_id' => $this->sessionId,
'role' => 'assistant',
'content' => $reply,
]);
broadcast(new AgentReplied($this->sessionId, $reply));
}
public function backoff(): array
{
return [10, 30, 90];
}
}
Drei Werte, die du nicht auf Default lassen solltest:
tries = 3— LLM-APIs haben Spikes von 5xx-Fehlern; drei Versuche fangen 95 % davon weg.timeout = 60— beiMaxSteps=5plus Tool-Calls kannst du auf 45-60 Sekunden hochkommen. Setze nicht 30.backoff = [10, 30, 90]— exponentiell, weil Provider-Outages selten unter 60 s vorbei sind.
Im Controller wird das Ganze ein Drei-Zeiler:
public function send(SendMessageRequest $request)
{
$session = $request->session_id ?? Str::uuid()->toString();
AgentMessage::create([
'session_id' => $session,
'role' => 'user',
'content' => $request->validated('message'),
]);
RunSupportAgentJob::dispatch(
$request->user()->id,
$session,
$request->validated('message'),
);
return response()->json(['session_id' => $session]);
}
Streaming via Server-Sent Events
Asynchrone Job-Verarbeitung ist robust, aber UX-mäßig nicht ideal — der User wartet 5 Sekunden auf eine Antwort. Für interaktive Chats willst du Streaming: Tokens fließen während der Generierung in die UI.
Route::get('/agents/support/stream', function (Request $request) {
$request->validate(['prompt' => 'required|string|max:2000']);
return response()->stream(function () use ($request) {
$stream = Prism::text()
->using(Provider::Anthropic, 'claude-sonnet-4-6')
->withSystemPrompt('Du bist ein Support-Agent. Knapp, auf Deutsch.')
->withPrompt($request->prompt)
->asStream();
foreach ($stream as $chunk) {
echo "data: " . json_encode(['delta' => $chunk->text]) . "\n\n";
ob_flush();
flush();
}
echo "data: [DONE]\n\n";
ob_flush();
flush();
}, 200, [
'Content-Type' => 'text/event-stream',
'Cache-Control' => 'no-cache',
'X-Accel-Buffering' => 'no',
]);
})->middleware(['auth', 'throttle:agents']);
X-Accel-Buffering: no ist Pflicht hinter Nginx — sonst puffert der Reverse Proxy die Tokens und der Stream-Effekt verpufft.
UI-seitig drei Zeilen:
const sse = new EventSource(`/agents/support/stream?prompt=${encodeURIComponent(prompt)}`);
sse.onmessage = (e) => {
if (e.data === '[DONE]') return sse.close();
const { delta } = JSON.parse(e.data);
chatBubble.textContent += delta;
};
Rate-Limiting und Cost-Tracking
LLM-Calls sind teuer und werden gerne zu Open-Source-Geldverbrennern. Zwei Schichten:
// app/Providers/AppServiceProvider.php
RateLimiter::for('agents', function (Request $request) {
return [
Limit::perMinute(20)->by($request->user()->id),
Limit::perDay(500)->by($request->user()->id),
];
});
Plus Token-Tracking pro User in einer Tabelle:
AgentUsage::create([
'user_id' => $this->userId,
'input_tokens' => $response->usage->promptTokens,
'output_tokens' => $response->usage->completionTokens,
'cost_cents' => $this->calculateCost($response->usage),
]);
Damit kannst du pro User Quoten setzen, Top-Verbraucher erkennen, und die Cost-per-Customer berechnen — entscheidend für jede Pricing-Entscheidung.
DSGVO und Datenresidenz
Hier wird's für DACH-Mittelständler ernst. Vier Optionen, sortiert nach DSGVO-Aufwand:
| Provider | Datenresidenz | DPA-Aufwand | Modell-Qualität |
|---|---|---|---|
| OpenAI / Anthropic direkt | USA, kein Training auf API-Daten | DPA via Dashboard | Höchstmöglich |
| Azure OpenAI EU | EU (Frankfurt, Stockholm) | Microsoft-Mantel-DPA | Identisch |
| Mistral La Plateforme | Frankreich | DPA in 5 Min | Sehr gut, je nach Modell |
| Lokal via Ollama | On-Premise | keiner | Geringer (Llama, Mistral OS) |
Für viele DACH-Use-Cases ist Azure OpenAI Frankfurt der pragmatische Default: OpenAI-Qualität, Daten in der EU, AVV existiert. Mit prism-php kostet der Wechsel eine Config-Zeile.
Zusätzlich: PII-Filterung vor dem Prompt. Niemals IBANs, Bestellungen oder Klarnamen ungefiltert in den LLM-Call. Hash- oder Token-Replacement auf der Service-Schicht:
$message = preg_replace_callback(
'/\b[A-Z]{2}\d{2}[A-Z0-9]{16,30}\b/',
fn($m) => '[IBAN_REDACTED]',
$message,
);
Build vs. Buy
Den oberen Stack zu bauen, ist machbar — der Code in diesem Artikel deckt den Großteil ab. Was er nicht abdeckt:
- Knowledge-Base-Anbindung mit Embeddings und Vector-Search.
- Mehrere Agent-Vertikalen (Support + Akquise + Compliance + Phishing-Detection) im selben Stack.
- Audit-Logs für regulierte Branchen.
- Versionierung und A/B-Tests von System-Prompts.
- Modell-Switching pro Use-Case mit Cost-Optimierung.
Ab da steigt der Wartungsoverhead in eine Größenordnung, die du als Single-Dev-Team selten neben dem Hauptprodukt mitziehen kannst. Wenn dein Use-Case dort hingeht, lohnt der Vergleich mit fertigen Plattformen — z. B. aigeni.de mit 13 vertikal spezialisierten Agenten ab Tag 1 produktiv. Build-Aufwand für 13 äquivalente Agenten in-house: realistisch 4-6 Monate Engineering-Vollzeit.
Faustregel: Bau es selbst, wenn du genau einen Agent für genau einen Use-Case brauchst und volle Kontrolle willst. Kauf, wenn du eine Agent-Suite über mehrere Vertikalen brauchst und deine Zeit besser in dein Kernprodukt steckst.
Key-Takeaways
- Agent ≠ API-Call — ein KI-Agent braucht Tools, Loop und Kontext.
- Drei-Schichten-Pattern hält den Stack sauber: Controller dünn, Service-Klasse zentral, Job für async.
- prism-php abstrahiert Provider — DSGVO-Wechsel kostet eine Env-Variable.
- MaxSteps und Token-Budgets sind Pflicht, sonst verbrennst du Geld.
- SSE-Streaming schlägt Polling-UX, braucht aber
X-Accel-Buffering: nohinter Nginx. - Azure OpenAI Frankfurt ist der DSGVO-pragmatische Default für DACH.
- Build vs. Buy: Single-Use-Case selber, Multi-Vertikal kaufen.
Fazit
KI-Agenten in Laravel sind 2026 weder schwarze Magie noch Standard-Composer-Install. Mit dem Stack aus prism-php, Queue-Job und SSE-Streaming hast du ein Pattern, das von 5 Anfragen pro Tag bis zu mehreren Hundert pro Stunde skaliert. Kritisch sind nicht die LLM-Calls — kritisch sind Conversation-State, Cost-Limits und Datenresidenz.
Tools, die in diesem Stack helfen
Als Affiliate: Kostenlos registrieren und erste Links innerhalb von 5 Minuten setzen.
Als Merchant: Pläne ab 29 €/Monat ansehen.
Werbekennzeichnung: Einige der im Artikel genannten Produkte führen wir als Partner auf laraveltools.com. Wenn du über unsere Plattform buchst, erhalten wir eine Provision — ohne dass es für dich teurer wird. Die hier geäußerten Einschätzungen sind unabhängig davon.