TH
Zurück zum Blog

LLMs in Software integrieren

6 Min. LesezeitTill Heller

Im Vorgängerbeitrag „Was ist ein LLM?“ wurde ein Large Language Model als das beschrieben, was es aus IT-Sicht ist: eine Komponente mit benennbaren Kosten, einem begrenzten Kontextfenster und, das ist der springende Punkt, nicht-deterministischem Verhalten. Dieselbe Eingabe kann verschiedene Ausgaben liefern, und manche davon sind schlicht falsch. Die Frage dieses Beitrags lautet: Wie baut man eine solche Komponente sauber in bestehende Software ein?

Die zentrale These vorweg: Integration heißt, das Unsichere zu kapseln. Das LLM ist die eine nicht-deterministische, unzuverlässige Stelle im System. Ziel der Architektur ist es, diese Stelle so einzuhegen, dass der Rest des Systems deterministisch, testbar und vorhersagbar bleibt.

Das Modell hinter eine klare Schnittstelle legen

Der erste Schritt ist eine schmale, eigene Schnittstelle. In der Sprache des Domain-Driven Design ist das ein Port mit Adapter, oder allgemeiner ein Anti-Corruption-Layer. Die Businesslogik ruft eine domänenspezifische Funktion auf, etwa extrahiereRechnungsdaten(text), und weiß nichts über den Anbieter, das konkrete Modell oder das API-Format dahinter. Der Adapter übersetzt zwischen dieser Domänenfunktion und dem jeweiligen SDK.

Das bringt gleich mehrere Vorteile. Der Anbieter wird austauschbar, weil seine spezifischen Typen nicht durch die gesamte Codebasis lecken und damit keinen Lock-in erzeugen. Die Businesslogik wird testbar, weil man den Adapter im Test durch ein Mock ersetzen kann, statt bei jedem Testlauf ein echtes Modell anzusprechen. Und der Nicht-Determinismus ist klar lokalisiert: Es gibt genau eine Stelle im Code, an der Unsicherheit ins System eintritt.

Strukturierte Ausgaben statt Freitext-Parsing

Der zweite Baustein betrifft das Format der Antwort. Es ist verlockend, sich vom Modell Freitext geben zu lassen und diesen dann mit regulären Ausdrücken oder String-Suche auseinanderzunehmen. Das ist brüchig: Formulierung, Reihenfolge und Zeichensetzung schwanken, und jede kleine Änderung im Antwortstil bricht das Parsing.

Besser sind strukturierte Ausgaben nach einem Schema, üblicherweise JSON. Dabei lohnt es sich, drei Stufen zu unterscheiden, die unterschiedliche Garantien geben:

  • Reines Prompten auf JSON: Man bittet das Modell im Prompt um JSON. Das Modell versucht es, aber es gibt keinerlei Garantie. Fehlende Klammern oder ein erklärender Satz davor sind möglich.
  • JSON-Modus: Der Anbieter garantiert syntaktisch gültiges JSON, sofern die Generierung vollständig abschließt. Bricht die Ausgabe am Token-Limit ab (Truncation), kann auch dieser Modus unvollständiges und damit ungültiges JSON liefern. Und ob das JSON dem von uns erwarteten Schema entspricht, ist damit ohnehin nicht gesagt.
  • Schema- oder grammatikgeführtes Decoding: Der Decoder maskiert beim Generieren jedes Token, das die vorgegebene Grammatik verletzen würde. Das garantiert syntaktische Konformität zum Schema.

Entscheidend ist eine Klarstellung, die man leicht überliest: Keine dieser Stufen garantiert die semantische Korrektheit des Inhalts. Ein Feld betrag kann schema-konform eine Zahl enthalten und trotzdem frei erfunden sein. Syntaktisch gültig heißt nicht fachlich richtig.

Tool-Calling als Brücke zum deterministischen Code

Damit das Modell nicht nur Text produziert, sondern etwas bewirkt, dient Tool- oder Function-Calling als Brücke. Hier hält sich ein hartnäckiges Missverständnis: Das Modell führe selbst Code aus. Das tut es nicht.

Der Ablauf ist folgender. Die Anwendung deklariert Werkzeuge samt Parameter-Schema, etwa sucheKunde(id). Das Modell gibt daraufhin keine Aktion aus, sondern eine strukturierte Aufforderung: den Werkzeugnamen und die Argumente. Die Anwendung validiert diese Aufforderung, führt das Werkzeug in deterministischem Code aus und gibt das Ergebnis an das Modell zurück. Das Modell antwortet dann final oder fordert das nächste Werkzeug an.

Der Tool-Call ist also nur eine spezielle Form strukturierter Ausgabe. Merksatz: Das Modell entscheidet, der Code handelt. Ausführung und Vertrauensgrenze liegen ausschließlich in der Anwendung, nie im Modell.

Fehlerbehandlung bei unsicheren Ausgaben

Weil das Modell irren kann, gilt eine Grundregel: Eine LLM-Ausgabe ist eine unsichere Eingabe und wird wie jede fremde Eingabe behandelt. Daraus folgt eine mehrschichtige Fehlerbehandlung.

Zuerst die syntaktische Validierung gegen das Schema. Dann die semantische Validierung: Liegen Werte im erlaubten Bereich, sind referenzierte IDs plausibel und existieren sie tatsächlich? Schlägt etwas fehl, folgen Retries mit Backoff, wobei man dem Modell die konkrete Fehlermeldung zurückgeben kann, damit es korrigiert. Dazu kommen Timeouts und definierte Fallbacks: ein degradierter Modus, ein deterministischer Ersatzpfad oder ein Mensch im Loop.

Bei Retries ist eine Feinheit zentral. Sie sind nur bei idempotenten Operationen unbedenklich. Hat ein Tool-Call bereits eine Nebenwirkung ausgelöst, eine Bestellung, eine Zahlung, einen Mailversand, darf ein Wiederholungsversuch diese nicht ein zweites Mal auslösen. Hier helfen Idempotenzschlüssel und die konsequente Trennung von „Modell schlägt vor“ und „Code führt aus“.

Der Prompt ist Code

Ein Prompt ist kein Konfigurationsdetail, das man beiläufig inline verstreut. Er ist versionierter Teil des Codes: Er gehört in die Versionskontrolle, ins Code-Review und unter Test.

Klassische Unit-Tests reichen allerdings nicht, denn identische Eingaben liefern nicht garantiert identische Ausgaben. Stattdessen braucht man Evaluierungen, also Regressionstests gegen eine Menge von Beispielfällen, die eine Erfolgsquote messen statt exakte Gleichheit zu prüfen. Und man muss zwei Quellen stillen Verhaltensdrifts beachten: Sowohl eine Prompt-Änderung als auch ein Modellwechsel können das Verhalten unbemerkt verschieben. Deshalb sollte man die Modellversion pinnen, statt automatisch der jeweils neuesten zu folgen.

Prompt, Kontext und Businesslogik trennen

Zuletzt eine saubere Dreiteilung. Stabile Anweisungen (der System-Prompt) gehören getrennt von den dynamisch zur Laufzeit eingefügten Daten (dem Kontext) und von der deterministischen Entscheidungslogik (dem Code). Template und Daten trennt man wie bei jedem anderen Templating auch.

Das hat einen Sicherheitsaspekt. Vermischt man nicht vertrauenswürdigen Kontext mit Anweisungen, öffnet man die Tür für Prompt-Injection: Eingespeiste Daten, etwa der Text einer fremden E-Mail, können selbst Instruktionen enthalten, die das Modell zu übernehmen versucht. Deshalb zieht man Vertrauensgrenzen, kennzeichnet fremde Inhalte klar als Daten und nicht als Anweisung, und sichert Werkzeuge mit Nebenwirkungen ab, statt dem Output blind zu vertrauen.

Daraus folgt auch eine Abgrenzung: Businessregeln, die verlässlich und prüfbar sein müssen, gehören in den Code, nicht in den Prompt. Und Aufgaben, die deterministischer Code zuverlässiger und billiger löst, etwa Rechnen oder exaktes Nachschlagen, überlässt man nicht dem Modell.

Fazit

Ein LLM sauber zu integrieren bedeutet, das Unsichere zu kapseln, sodass der Rest deterministisch bleibt. Die Bausteine dafür sind eine klare, anbieterunabhängige Schnittstelle, strukturierte Ausgaben mit dem Bewusstsein für ihre Grenzen, Tool-Calling als Brücke bei klarer Vertrauensgrenze, eine mehrschichtige Fehlerbehandlung, der Prompt als versionierter und getesteter Teil des Codes und die Trennung von Anweisung, Kontext und Logik. Wer das beherzigt, bekommt eine austauschbare Komponente statt eines unberechenbaren Kerns.

Damit ist der Rahmen gesteckt. Anschlussfragen, die auf diesem Fundament aufbauen, sind naheliegend: Wie führt man mit RAG kontrolliert externes Wissen zu, und was ändert sich beim lokalen Betrieb eines Modells? Beides sind Themen für eigene Beiträge.