Published on

LiteLLM mit LLM Guard und NeMo Guardrails verketten

Authors

Guardrails im Gateway verketten statt in jeder Anwendung

TL;DR

Wer Guardrails in die Anwendung baut, hat sie nach der dritten Anwendung dreimal — in drei Versionsständen. LiteLLM erlaubt über eigene Hooks eine zentrale Kette: Maskierung, Eingabeprüfung, Modell, Ausgabeprüfung, Demaskierung. Der Preis ist Latenz, und die sollten Sie kennen, bevor Sie ausrollen.


Warum das ins Gateway gehört

Ein realistischer Verlauf: Die erste Anwendung bekommt eine Eingabeprüfung, weil jemand darauf besteht. Die zweite bekommt eine ähnliche, weil ein anderes Team sie baut. Die dritte bekommt keine, weil es schnell gehen musste.

Ein halbes Jahr später fragt jemand, welche Regeln gelten. Die Antwort ist: drei verschiedene, und niemand weiß welche.

Die Regeln gehören an eine Stelle, und diese Stelle ist das Gateway — weil dort ohnehin jede Anfrage vorbeikommt. Welche Gateways dafür in Frage kommen, behandelt der Vergleich der vier LLM-Gateways; dieser Artikel zeigt den Aufbau mit LiteLLM.

Die Kette

Fünf Stationen, in dieser Reihenfolge:

Anfrage
Presidio: personenbezogene Daten maskieren
LLM Guard: Injektion, Geheimnisse, Länge prüfen
NeMo Guardrails: zulässiges Thema?
MODELL
LLM Guard: sensible Inhalte, schädliche Links
Presidio: für den berechtigten Empfänger demaskieren
Antwort

Nicht jede Installation braucht alle fünf. Die Mindestausstattung für eine Plattform, die Kunden- oder Personaldaten berührt, sind Maskierung, Eingabeprüfung und Demaskierung. Themenregeln kommen dazu, wenn der Assistent nach außen sichtbar wird. Was die einzelnen Werkzeuge leisten, steht im Guardrail-Vergleich.

Der Einhängepunkt in LiteLLM

LiteLLM bietet eine Klasse mit Rückrufmethoden, die vor und nach dem Modellaufruf greifen. Das ist der saubere Weg — kein Proxy vor dem Proxy.

# guardrails_hook.py
from litellm.integrations.custom_logger import CustomLogger
from litellm.proxy._types import UserAPIKeyAuth
from fastapi import HTTPException

from llm_guard import scan_prompt, scan_output
from llm_guard.input_scanners import PromptInjection, Secrets, TokenLimit
from llm_guard.output_scanners import Sensitive, MaliciousURLs
from presidio_analyzer import AnalyzerEngine
from presidio_anonymizer import AnonymizerEngine

# Scanner einmal beim Start laden, nicht pro Anfrage —
# die Modelle brauchen mehrere Sekunden zum Initialisieren
INPUT_SCANNERS = [PromptInjection(threshold=0.85), Secrets(), TokenLimit(limit=16384)]
OUTPUT_SCANNERS = [Sensitive(), MaliciousURLs()]
ANALYZER, ANONYMIZER = AnalyzerEngine(), AnonymizerEngine()


class GuardrailKette(CustomLogger):

    async def async_pre_call_hook(self, user_api_key_dict: UserAPIKeyAuth,
                                  cache, data: dict, call_type: str):
        nachrichten = data.get("messages", [])
        if not nachrichten:
            return data

        letzte = nachrichten[-1]["content"]

        # Station 1: personenbezogene Daten maskieren
        treffer = ANALYZER.analyze(text=letzte, language="de")
        maskiert = ANONYMIZER.anonymize(text=letzte,
                                        analyzer_results=treffer).text

        # Station 2: Eingabe prüfen
        bereinigt, bestanden, scores = scan_prompt(INPUT_SCANNERS, maskiert)
        if not all(bestanden.values()):
            # Ablehnen, aber protokollieren — sonst merken Sie nie,
            # ob der Schwellenwert zu scharf steht
            raise HTTPException(
                status_code=400,
                detail={"error": "Eingabe abgelehnt", "scanner": bestanden},
            )

        nachrichten[-1]["content"] = bereinigt
        data["messages"] = nachrichten
        return data

    async def async_post_call_success_hook(self, data: dict,
                                           user_api_key_dict: UserAPIKeyAuth,
                                           response):
        text = response.choices[0].message.content
        _, bestanden, _ = scan_output(OUTPUT_SCANNERS, "", text)
        if not all(bestanden.values()):
            response.choices[0].message.content = (
                "Die Antwort wurde zurückgehalten, weil sie eine "
                "Sicherheitsprüfung nicht bestanden hat."
            )
        return response


guardrails = GuardrailKette()

Eingebunden wird das in der Gateway-Konfiguration:

litellm_settings:
  callbacks: guardrails_hook.guardrails

Zwei Dinge, die in diesem Code bewusst so stehen:

Die Scanner werden beim Start geladen, nicht pro Anfrage. Ein PromptInjection() im Anfragepfad kostet mehrere Sekunden Initialisierung — ein Fehler, der in Beispielen häufig steht und in Produktion sofort auffällt.

Die abgelehnte Eingabe wirft einen Fehler mit Details zum auslösenden Scanner. Was der Nutzer sieht, sollte weniger verraten; was im Protokoll landet, muss vollständig sein, sonst können Sie Fehlalarme nie nachjustieren.

Die Demaskierung, und warum sie heikel ist

Der Teil, den die meisten Anleitungen weglassen. Wenn Sie in der Eingabe "Frau Schmidt" durch <PERSON> ersetzt haben, steht in der Antwort ebenfalls <PERSON> — und das ist für den Nutzer unbrauchbar.

Presidio kann über eine Zuordnungstabelle reversibel anonymisieren. Diese Tabelle müssen Sie über den Modellaufruf hinweg halten, und zwar pro Anfrage:

from presidio_anonymizer.entities import OperatorConfig

# In der Vorprüfung: reversibel ersetzen und Zuordnung merken
ergebnis = ANONYMIZER.anonymize(
    text=letzte, analyzer_results=treffer,
    operators={"DEFAULT": OperatorConfig("replace",
                                         {"new_value": "<ENTITAET>"})},
)
# ergebnis.items enthält Position und ursprünglichen Wert —
# im Anfragekontext ablegen, NICHT global

Zwei Warnungen dazu, und beide sind ernst gemeint.

Legen Sie die Zuordnung niemals in einem globalen Speicher ab. Bei parallelen Anfragen bekommen Sie sonst die Daten des einen Nutzers in der Antwort des anderen — ein Fehler, den wir tatsächlich schon in einer Installation gesehen haben.

Und: Die Demaskierung ist nur zulässig, wenn der Empfänger die Daten ohnehin sehen dürfte. Sie ersetzt keine Berechtigungsprüfung. Wenn ein Nutzer eine Personalakte nicht sehen darf, hilft es nicht, dass das Modell sie maskiert bekommen hat — die Antwort enthält am Ende Klarnamen.

Was das an Latenz kostet

Die Zahl, die vor dem Ausrollen auf dem Tisch liegen sollte. Größenordnungen aus unseren Messungen, bei kurzen Anfragen und CPU-Betrieb der Scanner:

StationAufschlag
Presidio maskieren30–100 ms
LLM Guard Eingabe (mit Injektions-Scanner)100–300 ms
NeMo Guardrails Themenprüfung200–800 ms
LLM Guard Ausgabe80–200 ms
Presidio demaskieren20–50 ms

Die vollständige Kette liegt damit bei 400 bis 1.400 Millisekunden zusätzlich — vor der ersten Modellantwort. Bei einem Chat-Assistenten, dessen Antwort ohnehin zwei Sekunden braucht, ist das spürbar, aber tragbar. Bei einem Verarbeitungsschritt in einer getakteten Kette ist es das nicht.

Drei Stellschrauben, in der Reihenfolge ihrer Wirkung:

  1. Nicht jede Route braucht jede Station. Definieren Sie im Gateway logische Modellnamen mit unterschiedlichen Ketten — standard mit voller Prüfung, intern-schnell nur mit Maskierung.
  2. Scanner auf die GPU. Die Klassifikationsmodelle laufen dort deutlich schneller, wenn ohnehin eine Karte im System ist.
  3. NeMo Guardrails weglassen, wenn keine Themenregeln nötig sind. Es ist die teuerste Station der Kette.

Häufig gestellte Fragen

Kann LiteLLM Guardrails ohne eigenen Code einbinden?

Für einige Anbieter gibt es vorkonfigurierte Integrationen, die per Konfigurationsdatei aktiviert werden. Sobald Sie mehrere Werkzeuge verketten oder eigene Regeln brauchen — etwa deutsche Erkennungsmuster in Presidio —, führt der Weg über einen eigenen Hook. Der Aufwand dafür liegt bei ein bis zwei Personentagen.

Wo werden abgelehnte Anfragen protokolliert?

Dort, wo Sie es im Hook festlegen. Wichtig ist, dass das Protokoll den auslösenden Scanner und die Bewertung enthält, nicht nur die Tatsache der Ablehnung. Ohne diese Information können Sie Schwellenwerte nicht nachjustieren, und ein zu scharf eingestellter Filter, der legitime Anfragen blockiert, wird von Nutzern schnell umgangen.

Ist die Kette auch bei gestreamten Antworten wirksam?

Bei der Ausgabeprüfung nur eingeschränkt. Wenn Sie Token für Token ausliefern, ist ein Teil der Antwort beim Nutzer, bevor die vollständige Prüfung möglich ist. Praktikable Wege: Streaming für geprüfte Routen abschalten, oder abschnittsweise prüfen und bei einem Treffer abbrechen — beides verschlechtert die wahrgenommene Geschwindigkeit.

Wie viel Latenz kostet eine vollständige Guardrail-Kette?

In unseren Messungen 400 bis 1.400 Millisekunden zusätzlich, je nach Umfang und ob die Scanner auf CPU oder GPU laufen. Die teuerste Station ist die Themenprüfung, weil sie zusätzliche Modellaufrufe auslöst. Definieren Sie deshalb unterschiedliche Ketten für unterschiedliche Anwendungsfälle, statt eine Konfiguration für alles zu verwenden.

Ersetzt die Maskierung eine Berechtigungsprüfung?

Nein, und diese Verwechslung ist gefährlich. Maskierung verhindert, dass personenbezogene Daten das Modell erreichen. Sie sagt nichts darüber, ob der anfragende Nutzer die Information sehen darf — nach der Demaskierung stehen die Klarnamen wieder in der Antwort. Die Zugriffskontrolle muss davor greifen, im Abruf.


Der nächste Schritt

Bauen Sie die Kette mit einer einzigen Station: Maskierung in der Eingabe, Demaskierung in der Ausgabe. Das deckt die häufigste Anforderung ab und lässt sich in zwei Tagen produktiv nehmen. Alles Weitere ergänzen Sie, wenn ein konkretes Risiko dazukommt. Beim Aufbau unterstützen wir gern.

📖 Verwandte Artikel

Weitere interessante Beiträge zu ähnlichen Themen