Python & AI Tutorials Logo
LangChain & LangGraph

7. Strukturierte Ausgabe mit Pydantic

In den vorherigen Kapiteln haben wir mit LLM-Ausgaben als rohen Textstrings gearbeitet. Das funktioniert gut für Chatbots, bei denen Menschen die Antworten lesen, aber beim Erstellen von AI-Agents, bei denen Programme LLM-Ausgaben parsen und interpretieren müssen, benötigen wir vorhersehbare, strukturierte Daten. In diesem Kapitel lernen Sie, wie Sie Pydantic-Schemas verwenden, um das LLM strukturierte Python-Objekte zurückgeben zu lassen.

7.1) Warum strukturierte Ausgabe?

Das Problem mit Freitext-LLM-Ausgabe

Beginnen wir damit zu verstehen, warum rohe Textantworten in realen Anwendungen Probleme verursachen. Betrachten Sie dieses häufige Szenario:

python
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
 
llm = ChatOpenAI(model="gpt-4o-mini")
 
# Fragen Sie das LLM nach einem Produkt
message = HumanMessage(content="""
Extrahieren Sie die Produktinformationen aus diesem Text:
"Das UltraWidget Pro kostet 299,99 $ und ist derzeit auf Lager."
""")
 
response = llm.invoke([message])
print(response.content)

Ausgabe:

Produktname: UltraWidget Pro
Preis: 299,99 $
Verfügbarkeit: Auf Lager

Die Ausgabe sieht gut aus. Aber nehmen Sie nun an, Sie müssen diese Daten in Ihrer Python-Anwendung verwenden. Wie extrahieren Sie den Preis als Zahl? Wie prüfen Sie die Verfügbarkeit programmatisch? Sie könnten versuchen, String-Parsing wie folgt durchzuführen:

python
# Fragiler Parsing-Ansatz
text = response.content
price_line = [line for line in text.split('\n') if 'Preis:' in line][0]
price_str = price_line.split('$')[1]
price = float(price_str)  # Fragil - was, wenn sich das Format ändert?

Dieses Parsing scheint zu funktionieren. Aber es funktioniert eigentlich nicht. Hier ist der Grund:

Warum dieser Ansatz fehlschlägt:

  1. Das LLM könnte die Antwort beim nächsten Mal anders formatieren ("Preis: 299,99 USD" oder "Verkaufspreis: 299,99 $")

Hier sind Beispiele für verschiedene Ausgaben, die für dieselbe Eingabeaufforderung auftreten können:

# Beispiel 1
"Das Produkt ist UltraWidget Pro, zum Preis von 299,99 $, und es ist verfügbar."
 
# Beispiel 2
"Produkt: UltraWidget Pro
Kosten: 299,99 Dollar
Status: Verfügbar"
 
# Beispiel 3
"UltraWidget Pro - 299,99 $ (auf Lager)"
 
# Beispiel 4
"Ich habe das UltraWidget Pro gefunden. Es kostet 299,99 $ und ist derzeit zum Kauf verfügbar."

Wenn sich die LLM-Antwort ändert, benötigen Sie völlig andere Parsing-Logik. Das macht es schwierig, zuverlässige Anwendungen zu erstellen.

  1. LLM-Antworten sind unvorhersehbar: Dieselbe Eingabeaufforderung kann jedes Mal unterschiedliche Formate erzeugen
  2. String-Parsing ist schwieriger als es aussieht: Sie müssen $, Leerzeichen, Zeilenumbrüche, Kommas und mehr handhaben
  3. Keine Typsicherheit: Sie können nicht sicher sein, ob price ein Float, String oder None ist
  4. Fehlerbehandlung ist schwierig: Wenn das LLM "Preis nicht verfügbar" sagt, stürzt Ihr float()-Aufruf ab
  5. Nicht wartbar: Ändern Sie die Eingabeaufforderung leicht und Sie schreiben den gesamten Parsing-Code neu

Die Kernidee: Python braucht Verträge, keine Prosa

Denken Sie daran, wann Sie in Python mit einem API-Server kommunizieren. Wenn Sie eine bestimmte REST-API aufrufen, erwarten Sie, dass sie eine definierte JSON-Antwort zurückgibt:

python
# Sie erwarten diese Struktur
{
    "product_name": "UltraWidget Pro",
    "price": 299.99,
    "in_stock": true
}

Beim Erstellen von AI-Anwendungen benötigen Sie dasselbe Prinzip. LLM-Ausgabe sollte als Daten mit einer definierten Struktur zurückgegeben werden, nicht jedes Mal als Freitext.

Prosa vs. Vertrag:

  • Prosa: Freiformiger natürlicher Text. Gut für Menschen zum Lesen, aber schwer für Programme zu verarbeiten.
  • Vertrag: Daten mit definierter Struktur und Typen. Ein Versprechen, dass "diese Felder mit diesen Typen existieren werden."

Strukturierte Ausgabe bedeutet, einen Vertrag zu definieren: "LLM, ich brauche genau diese Felder, mit genau diesen Typen, in genau diesem Format."

Hier kommt Pydantic ins Spiel. Pydantic ist Pythons beliebteste Datenvalidierungsbibliothek, und LangChain verwendet sie, um LLM-Ausgaben in strukturierter Form zu empfangen.

Die mentale Modellverschiebung:

  • Vorher: "LLM, erzähl mir von diesem Produkt" → Unvorhersehbaren Text parsen
  • Nachher: "LLM, antworte in einem definierten Format" → Strukturiertes Python-Objekt erhalten

Diese Verschiebung von Prosa zu Verträgen ist grundlegend für den Aufbau zuverlässiger AI-Agents. Wenn ein Agent seine nächste Aktion basierend auf LLM-Antworten entscheiden muss (z. B. kaufen, wenn auf Lager, für Benachrichtigung registrieren, wenn nicht), muss er Antworten in einem definierten Format erhalten.

Manuelles Parsing

Strukturierte Ausgabe

Bricht oft

Typsicher

LLM-Textausgabe

Fragile String-Logik

Typisiertes Python-Objekt

Laufzeitfehler

Zuverlässiger Code

7.2) Ihre erste strukturierte Ausgabe

In 7.1 haben wir gelernt, warum LLMs mit definierter Struktur anstatt mit Freitext antworten sollten. Jetzt sehen wir uns an, wie man dies tatsächlich implementiert.

Die Schlüsselidee: Einfach das LLM zu bitten "bitte antworte in diesem Format" reicht nicht aus. Sie müssen die genaue Datenstruktur im Python-Code definieren und LangChain sie an das LLM übergeben lassen. Diese definierte Datenstruktur wird als Schema bezeichnet.

Was ist ein Schema?

Ein Schema ist ein Blueprint, der die Struktur von Daten definiert. Es spezifiziert:

  • Welche Felder vorhanden sein müssen
  • Welchen Typ jedes Feld haben sollte (String, Zahl, Boolean usw.)
  • Welche Einschränkungen gelten (optional vs. erforderlich, gültige Bereiche usw.)

In Python definieren wir Schemas mit der BaseModel-Klasse von Pydantic. Hier ist das einfachste mögliche Beispiel:

python
from pydantic import BaseModel
 
class ProductInfo(BaseModel):
    product_name: str
    price: float
    in_stock: bool

Dieses Schema sagt: "Ein ProductInfo-Objekt muss genau drei Felder haben: einen product_name (String), einen price (Float) und ein in_stock (Boolean)."

Das Drei-Schritte-Muster: Definieren, Binden, Aufrufen

Die Verwendung strukturierter Ausgabe ist einfach. Merken Sie sich einfach drei Schritte:

  1. Definieren: Erstellen Sie ein Schema mit einer Pydantic-Klasse
  2. Binden: Verbinden Sie das Schema mit dem LLM mit .with_structured_output()
  3. Aufrufen: Rufen Sie .invoke() auf, um ein typisiertes Objekt zu erhalten

Dies ist die Standardvorlage, die Sie für die meisten strukturierten Extraktionsaufgaben verwenden werden:

python
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from pydantic import BaseModel, Field
 
# Schritt 1: Definieren Sie das Schema
class ProductInfo(BaseModel):
    product_name: str = Field(description="Der vollständige Produktname")
    price: float = Field(description="Preis in USD")
    in_stock: bool = Field(description="Ob das Produkt verfügbar ist")
 
# Schritt 2: Binden Sie das Schema an das LLM
llm = ChatOpenAI(model="gpt-4o-mini")
structured_llm = llm.with_structured_output(ProductInfo)
 
# Schritt 3: Rufen Sie auf und erhalten Sie ein typisiertes Objekt
message = HumanMessage(content="""
Extrahieren Sie Produktinformationen aus diesem Text:
"Das UltraWidget Pro kostet 299,99 $ und ist derzeit auf Lager."
""")
 
result = structured_llm.invoke([message])
 
# result ist jetzt ein ProductInfo-Objekt, kein String
print(type(result))  # <class '__main__.ProductInfo'>
print(result.product_name)  # UltraWidget Pro
print(result.price)  # 299.99
print(result.in_stock)  # True

Was ist gerade passiert?

  1. Schema-Definition: Wir haben die Felder und Typen definiert, die wir wollen
  2. Bindung: .with_structured_output(ProductInfo) konfiguriert das LLM zur Verwendung strukturierter Ausgabe
  3. Aufruf & Antwort: Wenn .invoke() aufgerufen wird, übergibt LangChain das JSON-Schema an das LLM, und das LLM antwortet mit JSON, das dieser Struktur entspricht
  4. Automatische Konvertierung: LangChain konvertiert das JSON in ein ProductInfo-Objekt - kein Parsing-Code erforderlich

Kein Parsing. Keine Typkonvertierung. Keine Fehler.

Verwenden Sie dieses 3-Schritte-Muster als Ihre Vorlage. Befolgen Sie es, wann immer Sie strukturierte Ausgabe benötigen.

Feldbeschreibungen: Der Schlüssel zur Steuerung des LLM

Im obigen Schema-Definitionsbeispiel haben wir Field(description="...") verwendet. Diese Beschreibung ist nicht nur Dokumentation. Es sind Anweisungen, die das LLM liest und befolgt.

Bei typischer Pydantic-Verwendung sind Field-Beschreibungen optional:

python
# Reguläres Pydantic - Beschreibung ist Dokumentation für Menschen
class User(BaseModel):
    name: str = Field(description="Name des Benutzers")  # Funktioniert gut ohne sie

Aber bei der Arbeit mit LLMs sind sie essentiell:

python
# Mit LLMs - Beschreibung bestimmt LLM-Verhalten
class CustomerFeedback(BaseModel):
    sentiment: str = Field(
        description="Gesamtstimmung: 'positive', 'negative' oder 'neutral'"
    )

Das LLM liest diese Beschreibung und verwendet sie, um zu entscheiden, wie es antworten soll.

Sehen wir uns dies in Aktion an:

python
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from pydantic import BaseModel, Field
 
class CustomerFeedback(BaseModel):
    sentiment: str = Field(
        description="Gesamtstimmung: 'positive', 'negative' oder 'neutral'"
    )
    main_issue: str = Field(
        description="Die primäre Beschwerde oder das Anliegen, falls vorhanden. Verwenden Sie 'none', wenn keine Probleme erwähnt wurden."
    )
    urgency: str = Field(
        description="Wie dringend ist das Problem: 'low', 'medium' oder 'high'"
    )
 
llm = ChatOpenAI(model="gpt-4o-mini")
structured_llm = llm.with_structured_output(CustomerFeedback)
 
message = HumanMessage(content="""
Analysieren Sie dieses Kundenfeedback:
"Das Produkt funktioniert gut, aber der Versand dauerte 3 Wochen. Ich habe bereits meine Projektfrist verpasst. Bitte antworten Sie sofort."
""")
 
result = structured_llm.invoke([message])
print(result.sentiment)    # negative
print(result.main_issue)   # Slow shipping
print(result.urgency)      # high

Wie Beschreibungen LLM-Entscheidungen formen:

  • sentiment-Beschreibung → LLM lernt, dass die gültigen Werte 'positive', 'negative', 'neutral' sind → Versandproblem verursachte verpasste Frist, also wählt es 'negative'
  • main_issue-Beschreibung → LLM wird angewiesen, "die primäre Beschwerde zu finden" → Identifiziert "langsamer Versand" als das Problem
  • urgency-Beschreibung → LLM lernt, dass Dringlichkeit 'low', 'medium' oder 'high' sein muss → Sieht "Bitte antworten Sie sofort" und wählt 'high'

Was passiert ohne Beschreibungen?

python
sentiment: str  # Keine Beschreibung

Das LLM könnte "negative", "bad", "unsatisfied", "2/5", "disappointed" in unvorhersehbaren Formaten zurückgeben, was es schwierig macht, die Werte in Ihrem Code zu handhaben.

Wichtiger Punkt: Feldbeschreibungen sind Teil Ihres Codes, der das LLM-Verhalten steuert. Schreiben Sie sie klar und spezifisch.

Kategorische Felder: Zulässige Werte spezifizieren

Im obigen Beispiel kann das sentiment-Feld nur drei Werte haben: 'positive', 'negative' oder 'neutral'. Felder, die eines aus einer bestimmten Menge von Werten sein müssen, werden kategorische Felder genannt.

Für kategorische Felder listen Sie alle möglichen Werte in der Beschreibung auf:

python
sentiment: str = Field(
    description="Stimmung: genau 'positive', 'negative' oder 'neutral' (Kleinbuchstaben)"
)

Durch die Angabe von "genau" und "(Kleinbuchstaben)" betonen wir, dass das LLM mit genau einem dieser drei Werte antworten sollte.

Es gibt jedoch keine Garantie, dass das LLM immer mit einem der angegebenen Werte antwortet. Deshalb müssen Sie defensiven Code schreiben.

Fälle, in denen das LLM unerwartete Werte zurückgibt:

python
result.sentiment = "Positive"    # Großgeschrieben
result.sentiment = "NEGATIVE"    # Alles Großbuchstaben
result.sentiment = "good"        # Völlig anderes Wort

Defensiven Code schreiben:

python
allowed = {"positive", "negative", "neutral"}
 
# In Kleinbuchstaben konvertieren und prüfen
sentiment = result.sentiment.lower()
 
if sentiment not in allowed:
    sentiment = "neutral"  # Standardwert für unerwartete Werte verwenden
 
# Jetzt ist sentiment garantiert einer der erlaubten Werte

Wichtige Erkenntnis:

  1. Zulässige Werte in Beschreibung angeben → LLM antwortet wahrscheinlicher korrekt
  2. Im Code validieren → Unerwartete Werte sicher handhaben

Hinweis: Kapitel 18 zeigt stärkere Muster mit Python-Enums zur Durchsetzung.

Vergleich: Manuelles Parsing vs. strukturierte Ausgabe

Vergleichen wir dieselbe Aufgabe mit und ohne strukturierte Ausgabe, um den Unterschied zu sehen:

Manuelles Parsing:

python
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
 
llm = ChatOpenAI(model="gpt-4o-mini")
 
message = HumanMessage(content="""
Extrahieren Sie den Produktnamen, Preis und die Verfügbarkeit aus:
"Das UltraWidget Pro kostet 299,99 $ und ist derzeit auf Lager."
Format: Name | Preis | Verfügbarkeit
""")
 
response = llm.invoke([message])
text = response.content
 
# Manuelles Parsing
parts = text.split('|')
product_name = parts[0].strip()
price_str = parts[1].strip().replace('$', '')
price = float(price_str)
availability = parts[2].strip().lower()
in_stock = 'auf lager' in availability or 'verfügbar' in availability
 
print(f"Name: {product_name}")
print(f"Preis: ${price}")
print(f"Auf Lager: {in_stock}")

Strukturierte Ausgabe:

python
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from pydantic import BaseModel, Field
 
class ProductInfo(BaseModel):
    product_name: str = Field(description="Der vollständige Produktname")
    price: float = Field(description="Preis in USD")
    in_stock: bool = Field(description="Ob das Produkt verfügbar ist")
 
llm = ChatOpenAI(model="gpt-4o-mini")
structured_llm = llm.with_structured_output(ProductInfo)
 
message = HumanMessage(content="""
Extrahieren Sie Produktinformationen aus:
"Das UltraWidget Pro kostet 299,99 $ und ist derzeit auf Lager."
""")
 
result = structured_llm.invoke([message])
 
print(f"Name: {result.product_name}")
print(f"Preis: ${result.price}")
print(f"Auf Lager: {result.in_stock}")

Hauptunterschiede:

  1. Keine Parsing-Logik: Die strukturierte Version hat null Parsing-Code
  2. Typsicherheit: result.price ist garantiert ein Float
  3. Einfacherer Code: Kein Regex, kein String-Splitting, keine manuelle Typkonvertierung
  4. Validierung: Pydantic stellt sicher, dass alle erforderlichen Felder vorhanden sind
  5. Wartbarkeit: Das Ändern des Schemas ist einfacher als das Aktualisieren der Parsing-Logik

7.3) Überlegungen zum Schema-Design

Jetzt, da Sie wissen, wie man strukturierte Ausgabe verwendet, lernen wir, wie man gute Schemas entwirft. Dieser Abschnitt behandelt praktische Designprinzipien zur Unterscheidung erforderlicher von optionalen Feldern.

Erforderliche Felder

Standardmäßig sind alle Felder in einem Pydantic-Modell erforderlich. Das bedeutet, dass das LLM für jedes erforderliche Feld einen Wert aus der Eingabeaufforderung des Benutzers extrahieren oder ableiten und ihn in der Antwort bereitstellen muss.

python
from pydantic import BaseModel
 
class ProductInfo(BaseModel):
    product_name: str
    price: float
    in_stock: bool

Wenn Sie dieses Schema verwenden, wird das LLM versuchen, Werte für alle drei Felder (product_name, price, in_stock) im Eingabetext zu finden.

Aber was passiert, wenn der Eingabeaufforderung Informationen für ein erforderliches Feld fehlen?

Wir könnten folgendes Verhalten erwarten:

  1. Das LLM kann die Informationen in der Eingabeaufforderung nicht finden
  2. Das LLM lässt dieses Feld in seiner Antwort weg
  3. LangChain kann keine gültige ProductInfo-Instanz erstellen
  4. ValidationError wird ausgelöst

Dies geschieht jedoch nicht immer.

Der Grund ist, dass verschiedene LLMs fehlende Informationen unterschiedlich handhaben können.

Einige LLMs (wie OpenAI-Modelle) neigen dazu, Werte zu generieren, selbst wenn die erforderlichen Informationen nicht in der Eingabeaufforderung vorhanden sind. In diesem Fall tritt kein ValidationError auf, aber dies kann größere Probleme verursachen, da Ihre Python-Anwendung erfundene Informationen verarbeiten könnte, als wären sie echt.

Wir werden uns ansehen, wie man dieses Problem in Abschnitt 7.4: Wenn Dinge schiefgehen löst.

Seien Sie sich vorerst nur bewusst, dass nicht alle LLMs fehlende Informationen auf die gleiche Weise handhaben.

Optionale Felder

Sie benötigen möglicherweise Felder, die legitim vorhanden oder abwesend sein können, selbst in normalen Fällen. Zum Beispiel kann eine Liefernotiz (delivery_note) vom Kunden bereitgestellt werden oder auch nicht, selbst für eine gültige Bestellung.

Wann Optional verwendet werden sollte:

  • Die Daten selbst existieren möglicherweise nicht (z. B. wenn anonyme Bewertungen erlaubt sind, haben anonyme Bewertungen keinen Bewertername)
  • Sie möchten, dass das LLM fehlende Informationen explizit anzeigt, anstatt einen Wert zu erfinden

Um ein Feld optional zu machen, verwenden Sie Pythons Optional-Typ aus dem typing-Modul:

python
from typing import Optional
 
class ProductReview(BaseModel):
    rating: int
    review_text: str
    reviewer_name: Optional[str] = None  # Anonyme Bewertungen haben keinen Bewertername

Hinweis: Python 3.10+ Benutzer können str | None anstelle von Optional[str] verwenden.

Wenn ein Feld Optional ist:

  • Das LLM kann es in der Antwort weglassen, wenn die Informationen nicht in der Eingabeaufforderung gefunden werden
  • Weggelassene Felder werden auf den Standardwert (None) gesetzt

Hier ist ein vollständiges Beispiel:

python
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from pydantic import BaseModel, Field
from typing import Optional
 
class ProductInfo(BaseModel):
    product_name: str
    price: float
    in_stock: bool
    discount_percentage: Optional[float] = None
    warranty_years: Optional[int] = None
 
llm = ChatOpenAI(model="gpt-4o-mini")
structured_llm = llm.with_structured_output(ProductInfo)
 
message = HumanMessage(content="""
Extrahieren Sie Produktinformationen: "Das UltraWidget Pro kostet 299,99 $ und ist auf Lager."
""")
 
result = structured_llm.invoke([message])
print(result.product_name)  # UltraWidget Pro
print(result.price)  # 299.99
print(result.in_stock)  # True
print(result.discount_percentage)  # None (nicht erwähnt)
print(result.warranty_years)  # None (nicht erwähnt)

Checkliste für Schema-Design

Bevor Sie Ihr Schema finalisieren, fragen Sie sich:

Feldauswahl:

  • Sind erforderliche Felder wirklich essentiell? (Was passiert, wenn dieses Feld in der Eingabeaufforderung fehlt?)
  • Können optionale Felder legitim abwesend sein, selbst in normalen Fällen?

Feldspezifikation:

  • Hat jedes Feld eine klare Beschreibung?
  • Sind kategorische Felder explizit eingeschränkt? (z. B. "muss genau 'A', 'B' oder 'C' sein")

7.4) Wenn Dinge schiefgehen

Zwei Probleme können bei der Verwendung strukturierter Ausgabe auftreten:

  1. LLM lässt erforderliche Feldwerte weg → ValidationError tritt auf
  2. LLM erfindet fehlende Informationen → Kein ValidationError, aber Ihr Code verarbeitet falsche Daten

Dieser Abschnitt behandelt, wie man mit jedem umgeht.

Validierungsfehler verstehen

Wenn der Eingabeaufforderung des Benutzers Informationen für im Schema definierte erforderliche Felder fehlen, kann das LLM keine Werte für diese Felder bereitstellen. Das Python-Programm löst dann einen ValidationError aus:

python
from langchain_anthropic import ChatAnthropic
from langchain_core.messages import HumanMessage
from pydantic import BaseModel, ValidationError
 
class ProductInfo(BaseModel):
    product_name: str
    price: float
    in_stock: bool
 
llm = ChatAnthropic(model='claude-sonnet-4-5')
structured_llm = llm.with_structured_output(ProductInfo)
 
# Eingabe fehlt erforderliche Informationen
message = HumanMessage(content="""
Extrahieren Sie Produktinformationen aus: "Das Widget ist großartig! Sehr empfehlenswert."
""")
 
try:
    result = structured_llm.invoke([message])
    print(result)
except ValidationError as e:
    print("ValidationError ist aufgetreten")

Hinweis: Wenn der Eingabeaufforderung Informationen für erforderliche Felder fehlen, können einige LLMs Werte erfinden und sie in der Antwort bereitstellen. In diesem Fall tritt kein ValidationError auf, aber ein größeres Problem entsteht. Wir werden dies im nächsten Abschnitt behandeln.

Wenn der Eingabeaufforderung Informationen für erforderliche Felder fehlen und ValidationError auftritt, ist dies tatsächlich hilfreich für Ihre Python-Anwendung. Die Anwendung kann erkennen, dass ein Problem aufgetreten ist, und den Fehler auf kontrollierte Weise behandeln. Fehlerwiederherstellungsstrategien werden in Kapitel 14 (Fehlerwiederherstellung auf Agent-Ebene) und Kapitel 17 (Wiederholungslogik mit Zustandsverwaltung) behandelt.

Das größere Problem: LLM erfindet fehlende Informationen

Wie wir in Abschnitt 7.3 besprochen haben, zeigen einige LLMs gefährlicheres Verhalten: Sie erfinden Werte und stellen sie in Antworten bereit, wenn Informationen in der Eingabeaufforderung fehlen.

Wie das Problem auftritt:

  1. Eingabeaufforderung fehlt erforderliche Informationen
  2. LLM generiert trotzdem plausibel aussehende Werte
  3. ValidationError tritt NICHT auf
  4. Ihre Python-Anwendung verarbeitet erfundene Daten, als wären sie echt

Beispiel:

python
class ProductInfo(BaseModel):
    product_name: str
    price: float
    in_stock: bool
 
message = HumanMessage(content="""
Extrahieren Sie Produktinformationen aus: "Das Widget ist großartig!"
""")
 
# Wenn einige LLMs Werte für product_name, price und in_stock erfinden
result = structured_llm.invoke([message])
# Kein Fehler ausgelöst!
print(result.product_name)  # "widget" (aus Text extrahiert)
print(result.price)  # 0.0 (erfunden!)
print(result.in_stock)  # False (erfunden!)
 
# Problem: Sie können nicht erkennen, welche Werte echt vs. erfunden sind

Dies ist schlimmer als ein ValidationError, weil:

  • Ihre Python-Anwendung mit schlechten Daten weiterläuft
  • Sie nicht wissen, welche Felder echt vs. erfunden sind
  • Nachgelagerte Logik kann falsche Entscheidungen basierend auf gefälschten Daten treffen

Lösung: Verwenden Sie optionale Felder mit Validierung

Die Lösung besteht darin, alle erforderlichen Felder als Optional zu definieren und dann einen Validator zu verwenden, um zu prüfen, dass alle erforderlichen Felder Werte haben.

python
from typing import Optional
from pydantic import BaseModel, model_validator, ValidationError
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
 
class ProductInfo(BaseModel):
    # Diese sind tatsächlich erforderlich, aber als Optional deklariert
    # Echte Validierung erfolgt im Validator unten
    product_name: Optional[str] = None
    price: Optional[float] = None
    in_stock: Optional[bool] = None
    
    @model_validator(mode='after')
    def check_required_fields(self):
        """Validiert, dass alle wesentlichen Felder vorhanden sind"""
        if self.product_name is None or self.price is None or self.in_stock is None:
            raise ValueError("Alle Felder (product_name, price, in_stock) müssen bereitgestellt werden")
        return self
 
llm = ChatOpenAI(model="gpt-4o-mini")
structured_llm = llm.with_structured_output(ProductInfo)
 
# Test mit unvollständigen Daten
message = HumanMessage(content="""
Extrahieren Sie Produktinformationen aus: "Das Widget ist großartig!"
""")
 
try:
    result = structured_llm.invoke([message])
    # Wenn wir hier ankommen, sind alle Felder garantiert vorhanden
    print(f"Produkt: {result.product_name}")
    print(f"Preis: ${result.price}")
except ValidationError as e:
    # Erforderliche Felder fehlen - Extraktion fehlgeschlagen
    print(f"Unvollständige Extraktion: {e}")

Was passiert hier:

  • @model_validator ist Pydantics Decorator, der benutzerdefinierte Validierungslogik hinzufügt
  • mode='after' bedeutet, dass die Validierung ausgeführt wird, nachdem alle Felder geparst wurden
  • Wenn ein Feld None ist, lösen wir ValueError aus, um unvollständige Daten zu signalisieren
  • Pydantic verpackt diesen ValueError automatisch in einen ValidationError

Warum das funktioniert:

Wenn der Eingabeaufforderung Informationen für Felder fehlen:

  • Das LLM erfindet keine Werte und lässt diese Felder in der Antwort weg
  • In diesem Fall werden diese Felder zu None
  • Wenn diese Felder tatsächlich erforderlich sind, löst der Pydantic-Validator ValueError aus
  • Pydantic verpackt es als ValidationError

Auf diese Weise erhält Ihre Python-Anwendung einen expliziten Fehler zur Behandlung, anstatt erfundener Daten.

Wichtige Erkenntnis: Wenn der Eingabeaufforderung Informationen für erforderliche Felder fehlen, ist das Erhalten eines ValidationError völlig normal und erwartet. Die echte Gefahr sind erfundene Daten. Verwenden Sie optionale Felder mit Validatoren, um zu verhindern, dass das LLM fehlende Informationen erfindet, während Sie explizit erkennen, wenn erforderliche Felder fehlen.


Kapitelzusammenfassung:

In diesem Kapitel haben Sie gelernt, wie Sie LLM-Ausgabe in zuverlässige Python-Objekte umwandeln:

  • Warum es wichtig ist: Freitext-Parsing ist fragil; schemabasierte Ausgabe bietet Typsicherheit
  • Wie man es verwendet: Definieren Sie Schemas mit Pydantic BaseModel → Binden mit .with_structured_output()
  • Designprinzipien: Wählen Sie erforderliche vs. optionale Felder, leiten Sie das LLM mit Feldbeschreibungen
  • Probleme behandeln: ValidationError ist normal; echte Gefahr sind erfundene Daten