KI-Features in ein bestehendes Symfony- oder Next.js-SaaS integrieren: Ein Retrofit-Playbook

#KI-Features in bestehendes SaaS integrieren
Sandor Farkas - Founder & Lead Developer at Wolf-Tech

Sandor Farkas

Gründer & Lead Developer

Experte für Softwareentwicklung und Legacy-Code-Optimierung

Der Product Manager möchte einen KI-Zusammenfassungs-Button. Der CTO will einen "Smart Assistant". Der Vertrieb hat eine Competitor-Demo gesehen, und jetzt gibt es ein Board-Level-Mandat, KI-Features dieses Quartal zu liefern. Kommt dir das bekannt vor?

Wenn du versuchst, KI-Features in ein bestehendes SaaS-Produkt einzubauen - anstatt eine KI-native App von Grund auf zu entwickeln - bist du in der Mehrheit. Die meisten Produktions-SaaS-Codebasen, die 2026 in Betrieb sind, sind ausgereifte Symfony- oder Next.js-Applikationen mit jahrelanger Business-Logik, etablierten Domain-Modellen und echten Kunden, die sich keine Ausfallzeiten leisten können. Das Nachrüsten von Large-Language-Model (LLM)-Fähigkeiten in diese Umgebung ist ein anderes Engineering-Problem als Greenfield-KI-Entwicklung - und die meisten Tutorials überspringen die schwierigen Teile.

Dieses Playbook behandelt, was wir beim Arbeiten mit Kunden in genau dieser Situation gelernt haben: Wo die LLM-Grenze zu ziehen ist, wie nicht-deterministische Outputs davon abgehalten werden, in deterministische Systeme einzudringen, Graceful Degradation wenn dein Model-Provider nicht erreichbar ist, Streaming-UX im Next.js App Router, und wie KI-Outputs mit dem Rest deines Datenmodells konsistent gehalten werden.

Entscheide, wo die LLM-Grenze liegt, bevor du eine Zeile Code schreibst

Die folgenreichste Architekturentscheidung beim Hinzufügen von KI-Features zu einem bestehenden SaaS ist die Wahl, wo KI endet und der Rest deines Systems beginnt. Triffst du diese Entscheidung falsch, wirst du Monate damit verbringen, LLM-Responses aus der Business-Logik zu entwirren.

Das Prinzip: LLM-Calls gehören an den Applikationsrand, nicht in das Domain-Modell. Dein Domain-Modell - Doctrine-Entities in Symfony, Prisma-Modelle in Next.js, deine Preisregeln, dein Berechtigungssystem - muss rein deterministisch bleiben. Es sollte niemals einen LLM aufrufen, eine LLM-Response parsen oder direkt auf einem LLM-Output branchen.

Behandle das LLM stattdessen als I/O-Adapter. In Symfony-Begriffen mappt das sauber auf eine dedizierte Service-Schicht: ein AiSummaryService, ein ContractAnalyzerService, ein SmartSearchService. Jeder Service ruft das LLM auf, normalisiert die Response in ein typisiertes DTO, validiert es und übergibt ein sauberes, stark typisiertes Ergebnis an die Domain. Die Domain sieht niemals ein rohes Completion.

Im Next.js-Kontext mit dem App Router besteht dieselbe Grenze zwischen deinen Server Actions (oder API Route Handlers) und deiner Datenzugriffsschicht. Server Actions sind ein vernünftiger Ort zum Orchestrieren eines LLM-Calls - sie laufen auf dem Server, können streamen und halten das LLM-Token aus dem Client heraus. Was sie nicht tun sollten: rohen LLM-Output direkt in deinen Prisma-Write schreiben. Parse, validiere und normalisiere zuerst.

Nicht-Determinismus von etablierter Business-Logik isolieren

LLMs sind nicht deterministisch. Bei gleicher Eingabe produzieren sie bei verschiedenen Aufrufen unterschiedliche Ausgaben. Das ist ein Feature für kreative Aufgaben und eine Haftungsquelle für transaktionale Systeme.

Das Gegenmittel ist ein enger, expliziter Vertrag zwischen der KI-Schicht und dem Rest deiner Applikation. Praktisch bedeutet das: strukturierter Output. Beide großen Provider (OpenAI, Anthropic) unterstützen JSON-Modus oder Tool-Use-Muster, die das Modell dazu zwingen, eine schemavalide Response zu emittieren. Nutze sie. Definiere das Schema im Code - eine PHP-Readonly-Klasse in Symfony, ein Zod-Schema in Next.js - und lehne jede Response ab, die nicht dagegen validiert.

In Symfony sieht ein typisches Muster so aus: Dein KI-Service sendet den Prompt, gibt ein JSON-Schema über den response_format-Parameter an, deserialisiert die Response über den Symfony Serializer in ein typisiertes DTO und validiert es dann mit der Validator-Komponente. Wenn die Validierung fehlschlägt, hast du ein Retry-Budget (meist ein oder zwei Versuche), bevor du einen Graceful Error ausgibst. Das Domain-Objekt, das letztendlich in deine Datenbank geschrieben wird, wird aus dem DTO erstellt, nicht aus dem rohen Completion.

In Next.js ist Zod die natürliche Lösung. Parse die LLM-Response mit z.safeParse(), behandle den Fehlerfall, bevor du einen State aktualisierst, und lass niemals ein nicht validiertes Completion deine Datenbank-Writes oder deinen React-State-Tree berühren.

Der praktische Vorteil jenseits der Korrektheit ist Testbarkeit. Da dein KI-Service typisierte DTOs zurückgibt, kannst du Unit-Tests schreiben, die den KI-Service mit deterministischen Fake-Responses mocken. Deine Business-Logik-Tests bleiben schnell, reproduzierbar und vollständig unabhängig davon, ob die Model-API erreichbar ist.

Graceful Fallback wenn der Model-Provider ausfällt

Model-Provider-Ausfälle sind keine Hypothese - sie passieren. Wenn du KI-Features in einen kritischen Pfad deines Produkts eingebaut hast, ohne eine Fallback-Strategie, hast du eine harte Abhängigkeit von einem Drittanbieter mit einem anderen SLA als deinem eigenen eingeführt. Deine User werden davon nicht begeistert sein.

Frage für jedes KI-Feature: Was macht das Produkt, wenn dieser Aufruf fehlschlägt oder das Timeout überschreitet? Die Antworten variieren je nach Feature-Typ. Eine KI-generierte Zusammenfassung, die fehlschlägt, kann auf die Anzeige der zugrunde liegenden Rohdaten mit einem Hinweis zurückfallen, dass die Zusammenfassung vorübergehend nicht verfügbar ist. Eine KI-gestützte Suche, die fehlschlägt, kann auf eine deterministische Keyword-Suche zurückfallen. Ein KI-Klassifizierungsschritt, der fehlschlägt, kann auf manuelle Klassifizierung zurückfallen - langsamer, aber funktional.

In Symfony gibt das Wrappen von LLM-Calls in einen Circuit Breaker (den symfony/http-client-Retry-Mechanismus oder eine Bibliothek wie Resiliency) und das Abfangen von Provider-Exceptions an der Service-Grenze einen sauberen Ort zum Einfügen von Fallback-Logik. Logge den Fehler mit Kontext (Modell, Prompt-Hash, Fehlercode), damit du die Provider-Zuverlässigkeit über deinen Observability-Stack im Zeitverlauf überwachen kannst.

In Next.js sollten Server Actions Provider-Fehler abfangen und eine strukturierte Fehlerantwort zurückgeben, die die UI explizit behandelt - kein unbehandelter Promise-Rejection, der den Component-Tree zum Absturz bringt. Erwäge ein { success: false, fallback: true, data: ... }-Shape, das der UI signalisiert, dass sie die Non-KI-Version der Komponente rendern soll.

Vermeide die Versuchung, KI-Features einfach hinter einem Feature-Flag zu verstecken und Fallbacks zu vergessen. Feature-Flags sind nützlich für die Rollout-Kontrolle; sie sind kein Ersatz für Graceful Degradation.

Streaming-UX im Next.js App Router

Streaming ist der Punkt, an dem KI-Features wahrnehmbar schnell werden. Eine Response, die acht Sekunden zur Generierung benötigt, ist frustrierend, wenn die UI acht Sekunden leer ist. Dieselben acht Sekunden Generierungszeit sind tolerierbar - sogar ansprechend - wenn der Text Wort für Wort erscheint, während er gestreamt wird.

Der Next.js App Router und die Streaming-Response-Primitives machen das ohne Drittanbieter-Bibliothek erreichbar, obwohl das Vercel AI SDK den Boilerplate erheblich reduziert, wenn du die Abhängigkeit hinzufügen möchtest.

Die wichtigste Architekturentscheidung ist die zwischen Streaming aus einer Server Action und Streaming aus einem API Route Handler. Server Actions sind für Mutationen ausgelegt, die einen finalen Zustand zurückgeben. Für Streaming-Text ist ein API Route Handler (route.ts im App Router) mit ReadableStream das sauberere Modell. Der Client fetcht den Stream, liest Chunks mit einem ReadableStreamDefaultReader und hängt jeden Chunk an den Component-State an, während er ankommt.

Ein wichtiges Detail: Streaming und Suspense-Boundaries interagieren auf nicht offensichtliche Weise. Wenn du eine Streaming-Komponente in eine Suspense-Boundary einwickelst, wartet React auf den ersten Chunk, bevor es den Fallback verlässt. Das ist normalerweise was du willst - es verhindert einen Flash des leeren Zustands. Aber es bedeutet, dass dein Stream seinen ersten Token schnell emittieren muss. Lange serverseitige Vorverarbeitung vor dem ersten gestreamten Token untergräbt den Zweck des Streamings.

Für Symfony-Applikationen, die ein Next.js-Frontend bedienen, ist Streaming zu einem Browser über Symfonys StreamedResponse im Backend und einen Standard-fetch-Stream-Reader auf der Next.js-Seite erreichbar. Halte die Streaming-Grenze auf der HTTP-Ebene. Das Symfony-Backend streamt rohen Text oder Newline-delimitiertes JSON; der Next.js-Client liest und rendert es. Versuche nicht, innerhalb des Symfony-Event-Loops zu streamen - lass die HTTP-Schicht die Arbeit machen.

KI-Outputs mit dem Rest des Systems synchron halten

Ein subtileres Problem taucht Wochen nach dem Launch auf: KI-generierte Inhalte driften aus der Synchronisation mit den zugrunde liegenden Daten, aus denen sie generiert wurden. Der User aktualisiert einen Record. Die KI-Zusammenfassung in der Seitenleiste spiegelt noch die alte Version wider. Das KI-generierte Tag ist jetzt falsch, weil die Kategorie-Taxonomie geändert wurde.

Das Standard-Fix ist, KI-Outputs zu versionieren und die Generierung neu auszulösen, wenn sich Quelldaten ändern. In Symfony funktioniert das gut mit Doctrine-Lifecycle-Events oder Messenger-Messages: Wenn eine Entity sich ändert, dispatch einen Event, der einen Regenerierungsjob plant. Speichere den KI-Output mit einer Referenz auf die Quell-Entity-Version oder einem Inhalts-Hash, und zeige einen "veraltet"-Indikator an, wenn der Hash nicht mehr übereinstimmt.

In Next.js mit einem Prisma-Backend verwendet ein ähnlicher Ansatz applikationsseitige Hooks auf Mutations-Pfaden. Wenn ein Record aktualisiert wird, invalidiere den zugehörigen KI-Output und stelle Regenerierung in die Warteschlange. Die UI kann den zuletzt generierten Output mit einem "Wird aktualisiert..."-Indikator anzeigen, während neue Generierung im Hintergrund läuft.

Das ist auch der Punkt, an dem die Caching-Strategie wichtig wird. Vermeide das Caching von rohen LLM-Completions - cache das geparste, validierte DTO oder den finalen gespeicherten Output. Das Cachen von rohen Completions koppelt dich an das genaue Prompt-Format und bricht lautlos, wenn du deine Prompts verfeinerst.

Ein Hinweis zu Prompt-Management in einer ausgereiften Codebase

Ein Aspekt der KI-Integration, den ausgereifte Codebasen besonders spüren, ist das Prompt-Management. Prompts sind Code. Sie gehören in die Versionskontrolle, sie müssen getestet werden, wenn sie sich ändern, und Änderungen an ihnen sollten denselben Review-Prozess durchlaufen wie Code-Änderungen.

Vermeide es, Prompts als rohe String-Literale zu definieren, die über deine Service-Klassen verstreut sind. Definiere sie stattdessen als benannte Konstanten an einem dedizierten Ort, referenziere sie namentlich, und logge den Prompt-Namen (nicht den vollständigen Prompt-Text) zusammen mit jedem LLM-Call in deinen strukturierten Logs. Das macht es handhabbar, eine Änderung in der KI-Output-Qualität mit einer bestimmten Prompt-Änderung in deiner Git-Historie zu korrelieren.

In Symfony ist ein PromptRegistry-Service, der Prompts aus der YAML-Konfiguration lädt (mit Env-Var-Interpolation für dynamische Werte), ein sauberer Ansatz, der Prompts ohne Code-Deploy editierbar hält. In Next.js gibt ein ähnliches Muster mit environment-variable-gestützten Konstanten oder einem leichtgewichtigen CMS für die Prompt-Speicherung betriebliche Flexibilität.

Wo anfangen

Das Hinzufügen von KI-Features zu einem bestehenden SaaS ist Engineering-Arbeit, kein Prompt-Engineering-Übung. Die Architekturentscheidungen - wo die LLM-Grenze liegt, wie du mit Nicht-Determinismus umgehst, was deine Fallback-Strategie ist, wie du Stale-ness managst - bestimmen, ob ein Feature in der Produktion standhält oder eine neue Klasse von Zuverlässigkeitsproblemen schafft.

Wenn du abwägst, wo du anfangen sollst: Wähle ein enges, risikoarmes Feature (eine Zusammenfassung, eine Klassifizierung, ein Draft-Generator für ein Formularfeld) und baue den vollständigen Stack korrekt: strukturierter Output, Validierung, Fallback-Behandlung und Stale-ness-Management. Bringe das bei einem einzigen Feature in Ordnung, bevor du KI-Abdeckung auf das gesamte Produkt ausdehnst.

Wenn deine Symfony- oder Next.js-Codebase erfahrene Hände für diese Art von Arbeit benötigt - ob das ein Architektur-Review vor dem Start ist, Design-Sessions zur Definition der LLM-Grenze, oder Implementierungsunterstützung - arbeitet Wolf-Tech mit SaaS-Teams in genau dieser Phase. Wir haben Teams dabei geholfen, KI-Fähigkeiten in komplexe PHP- und React-Codebasen nachzurüsten, ohne das zu beeinträchtigen, was bereits funktioniert. Melde dich unter hello@wolf-tech.io, um deine Situation zu besprechen.