Blog · 14.05.2026 · 12 Min. Lesezeit

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 $userId aus 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 — bei MaxSteps=5 plus 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 direktUSA, kein Training auf API-DatenDPA via DashboardHöchstmöglich
Azure OpenAI EUEU (Frankfurt, Stockholm)Microsoft-Mantel-DPAIdentisch
Mistral La PlateformeFrankreichDPA in 5 MinSehr gut, je nach Modell
Lokal via OllamaOn-PremisekeinerGeringer (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: no hinter 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.