Zum Hauptinhalt springen
Lektion 10 von 12 · Strukturierte Ausgabe: JSON und Schemata

Erweiterte Anwendungsfälle

Lektion 10 von 12 Aktualisiert am 29. Juli 2026 Entwickler
Auf dieser Seite
  1. Über einfache Beispiele hinaus
  2. Extrahieren einer Rechnung
  3. Analyse von Bewertungen aspektbezogen
  4. Einordnen von Texten in mehrere Kategorien
  5. Faktorisierung des Aufrufs
  6. Kernaussagen

Über einfache Beispiele hinaus

Sie haben Pydantic, Zod und client.chat.parse() gemeistert. Die folgenden drei Fälle gehen über einfache Vier-Felder-Schemata hinaus und führen Sie in Szenarien, die Sie in Unternehmensumgebungen antreffen werden: Buchhaltungsrechnungen, detaillierte Analysen von Kundenbewertungen und Texte, die mehreren Kategorien gleichzeitig zugeordnet werden können. Jeder Fall illustriert eine unterschiedliche Modellierungstechnik.

Extrahieren einer Rechnung

Dokumentenverarbeitung ist das natürliche Terrain für verschachtelte Schemata. Eine Rechnung ist kein flaches Objekt: Sie enthält einen Kopf, einen Fuß mit Gesamtbeträgen und eine variable Anzahl von Positionen, die ihre eigene Struktur aufweisen. Daher modellieren wir LineItem separat und verweisen darauf innerhalb einer Liste. Die Währung verwendet ein Enum, wodurch verhindert wird, dass das Modell je nach Formulierung des Dokuments "euros" oder "€" zurückgibt.

from pydantic import BaseModel, Field
from typing import Optional
from enum import Enum

class Currency(str, Enum):
    eur = "EUR"
    usd = "USD"
    gbp = "GBP"

class LineItem(BaseModel):
    description: str = Field(description="Beschreibung der Position")
    quantity: float = Field(description="Menge")
    unit_price: float = Field(description="Einheitspreis ohne MwSt.")
    vat_rate: float = Field(description="MwSt.-Satz in Prozent (z. B. 20,0)")
    total_ht: float = Field(description="Positionssumme ohne MwSt.")

class InvoiceData(BaseModel):
    invoice_number: str = Field(description="Rechnungsnummer")
    date: str = Field(description="Rechnungsdatum im Format YYYY-MM-DD")
    due_date: Optional[str] = Field(default=None, description="Fälligkeitsdatum")
    vendor_name: str = Field(description="Name des Lieferanten")
    vendor_address: Optional[str] = Field(default=None)
    client_name: str = Field(description="Name des Kunden")
    line_items: list[LineItem] = Field(description="Positionen der Rechnung")
    subtotal_ht: float = Field(description="Zwischensumme ohne MwSt.")
    total_vat: float = Field(description="Gesamt-MwSt.-Betrag")
    total_ttc: float = Field(description="Gesamtbetrag einschließlich MwSt.")
    currency: Currency = Field(description="Währung")
    payment_method: Optional[str] = Field(default=None)

# Extraktion
response = client.chat.parse(
    model="mistral-large-latest",
    messages=[
        {
            "role": "system",
            "content": """Sie sind ein Buchhaltungsexperte. Extrahieren Sie Daten aus der Rechnung.
Seien Sie bei Beträgen präzise. Berechnen Sie Gesamtbeträge, falls erforderlich.
Datumsangaben müssen im Format YYYY-MM-DD vorliegen."""
        },
        {"role": "user", "content": invoice_text}
    ],
    response_format=InvoiceData,
    temperature=0
)

invoice = response.choices[0].message.parsed
print(f"Rechnung {invoice.invoice_number} — {invoice.vendor_name}")
print(f"Gesamt inkl. MwSt.: {invoice.total_ttc} {invoice.currency.value}")
for item in invoice.line_items:
    print(f"  - {item.description}: {item.quantity} x {item.unit_price}€ = {item.total_ht}€ exkl. MwSt.")

Das im Systemprompt durchgesetzte Datumsformat ist kein Detail von untergeordneter Bedeutung: Ohne es könnte ein französisches Dokument 12/03/2026 produzieren, während ein amerikanisches 03/12/2026 liefert, was zwei für Ihre Datenbank ununterscheidbare Strings erzeugt. Die TypeScript-Version folgt derselben Architektur, wobei das Format über .describe() dokumentiert wird.

import { z } from "zod";

const LineItemSchema = z.object({
  description: z.string(),
  quantity: z.number(),
  unitPrice: z.number().describe("Einheitspreis ohne MwSt."),
  vatRate: z.number().describe("MwSt.-Satz in %"),
  totalHt: z.number(),
});

const InvoiceSchema = z.object({
  invoiceNumber: z.string(),
  date: z.string().describe("Format YYYY-MM-DD"),
  dueDate: z.string().nullable(),
  vendorName: z.string(),
  clientName: z.string(),
  lineItems: z.array(LineItemSchema),
  subtotalHt: z.number(),
  totalVat: z.number(),
  totalTtc: z.number(),
  currency: z.enum(["EUR", "USD", "GBP"]),
});

const response = await client.chat.parse({
  model: "mistral-large-latest",
  messages: [
    { role: "system", content: "Extrahieren Sie Rechnungsdaten präzise." },
    { role: "user", content: invoiceText },
  ],
  responseFormat: InvoiceSchema,
  temperature: 0,
});

const invoice = response.choices[0].message.parsed!;
console.log(`Rechnung ${invoice.invoiceNumber} — Gesamt: ${invoice.totalTtc} ${invoice.currency}`);

Analyse von Bewertungen aspektbezogen

Eine echte Kundenbewertung ist selten einheitlich positiv oder negativ. Die untenstehende Hotelbewertung lobt das Zimmer und das Frühstück, verurteilt die Parkplätze und das WLAN und bietet eine differenzierte Einschätzung des Personals: Eine einzige globale Punktzahl würde all diese Informationen verlieren. Die Lösung besteht darin, ein wiederverwendbares AspectSentiment zu modellieren und eine Liste davon anzufordern. Das Feld evidence ist der interessanteste Teil des Schemas: Durch die Anforderung eines Zitats aus dem Text zwingen Sie das Modell, sein Urteil zu begründen, und Sie erhalten die Möglichkeit, dies zu überprüfen.

from pydantic import BaseModel, Field

class AspectSentiment(BaseModel):
    aspect: str = Field(description="Der analysierte Aspekt (Qualität, Preis, Service, Lieferung usw.)")
    sentiment: str = Field(description="positiv, negativ oder neutral")
    score: float = Field(description="Punktzahl zwischen -1,0 (sehr negativ) und 1,0 (sehr positiv)")
    evidence: str = Field(description="Zitat oder Auszug aus dem Text, der die Stimmung begründet")

class ReviewAnalysis(BaseModel):
    overall_sentiment: str = Field(description="Gesamteinschätzung: positiv, negativ, gemischt oder neutral")
    overall_score: float = Field(description="Gesamtpunktzahl zwischen -1,0 und 1,0")
    aspects: list[AspectSentiment] = Field(description="Aspektbezogene Analyse")
    recommendation: bool = Field(description="Empfehlst der Autor das Produkt/die Dienstleistung?")
    key_strengths: list[str] = Field(description="Identifizierte Stärken")
    key_weaknesses: list[str] = Field(description="Identifizierte Schwächen")

review_text = """
Das Hotel ist magnificent; das Zimmer war geräumig und sauber. Das Frühstück ist hervorragend
mit einer breiten Auswahl. Allerdings sind die Parkplätze überzogen teuer (35 €/Nacht) und das WLAN war
während des Aufenthalts instabil. Das Empfangspersonal war sehr willkommend,
aber der Zimmerservice benötigte über eine Stunde, um unsere Bestellung auszuliefern.
"""

response = client.chat.parse(
    model="mistral-large-latest",
    messages=[
        {
            "role": "system",
            "content": "Analysieren Sie diese Kundenbewertung, indem Sie jeden erwähnten Aspekt detailliert beschreiben."
        },
        {"role": "user", "content": review_text}
    ],
    response_format=ReviewAnalysis,
    temperature=0
)

review = response.choices[0].message.parsed
print(f"Gesamteinschätzung: {review.overall_sentiment} ({review.overall_score:+.1f})")
for aspect in review.aspects:
    print(f"  [{aspect.score:+.1f}] {aspect.aspect}: {aspect.sentiment}")

Einordnen von Texten in mehrere Kategorien

Im Gegensatz zur einfachen Klassifizierung, bei der ein einzelnes Label zugewiesen wird, erkennt die Mehrfachlabel-Klassifizierung, dass ein Text mehreren Kategorien angehören kann. Der unten beschriebene Vorfall berührt gleichzeitig Sicherheit, Finanzen, Betrug und Datenschutz: Die Erzwingung eines einzelnen Labels würde die Routing-Logik verarmen lassen.

Zwei Elemente des Schemas verdienen Aufmerksamkeit. reasoning erfordert eine Begründung für jedes Label, was die Qualität der Punktzahlen verbessert. requires_human_review wandelt Modellunsicherheit in ein operationales Signal um: Statt nachträglich zu entdecken, dass eine Klassifizierung zweifelhaft war, eskalieren Sie sie automatisch an einen Menschen, wenn das Vertrauen unter 0,7 fällt.

from pydantic import BaseModel, Field

class LabelScore(BaseModel):
    label: str = Field(description="Kategoriename")
    confidence: float = Field(description="Vertrauenspunktzahl zwischen 0,0 und 1,0")
    reasoning: str = Field(description="Einsatzbegründung in einem Satz")

class MultiLabelClassification(BaseModel):
    primary_label: str = Field(description="Hauptkategorie")
    all_labels: list[LabelScore] = Field(
        description="Alle anwendbaren Kategorien mit ihren Vertrauenspunktzahlen"
    )
    requires_human_review: bool = Field(
        description="True, wenn die Klassifizierung unsicher ist (Vertrauen < 0,7)"
    )

text = """
Unsere mobile Zahlungsanwendung weist eine Sicherheitslücke auf, die den
Zugriff auf Bankdaten ermöglicht. Mehrere Kunden haben unbefugte
Transaktionen gemeldet. Das technische Team untersucht dies derzeit.
"""

response = client.chat.parse(
    model="mistral-large-latest",
    messages=[
        {
            "role": "system",
            "content": """Ordnen Sie diesen Text einer der folgenden Kategorien zu (mehrere möglich):
security, finance, technical, legal, customer_support, compliance, fraud, data_privacy.
Weisen Sie jeder anwendbaren Kategorie eine Vertrauenspunktzahl zu."""
        },
        {"role": "user", "content": text}
    ],
    response_format=MultiLabelClassification,
    temperature=0
)

result = response.choices[0].message.parsed
print(f"Hauptkategorie: {result.primary_label}")
print(f"Menschliche Prüfung erforderlich: {'Ja' if result.requires_human_review else 'Nein'}")
for label in result.all_labels:
    print(f"  [{label.confidence:.0%}] {label.label} — {label.reasoning}")

Faktorisierung des Aufrufs

Diese drei Beispiele teilen dieselben Mechanismen: ein Systemprompt, ein Text, ein Schema und temperature=0. Kopieren Sie den Aufruf für jeden neuen Fall nicht, sondern extrahieren Sie ihn in eine generische Funktion. Die an BaseModel gebundene TypeVar bewahrt die Typisierung: Das zurückgegebene Objekt hat den Typ des als Argument übergebenen Schemas, und Ihre IDE weiß es.

from typing import TypeVar, Type
from pydantic import BaseModel

T = TypeVar("T", bound=BaseModel)

def extract(client, text: str, schema: Type[T], system_prompt: str = "") -> T:
    """Wiederverwendbare strukturierte Extraktion."""
    messages = []
    if system_prompt:
        messages.append({"role": "system", "content": system_prompt})
    messages.append({"role": "user", "content": text})

    response = client.chat.parse(
        model="mistral-large-latest",
        messages=messages,
        response_format=schema,
        temperature=0
    )
    return response.choices[0].message.parsed

# Verwendung
invoice = extract(client, invoice_text, InvoiceData, "Extrahieren Sie Rechnungsdaten.")
review = extract(client, customer_review, ReviewAnalysis, "Analysieren Sie diese Kundenbewertung.")
classification = extract(client, ticket, MultiLabelClassification, "Ordnen Sie diesen Text zu.")

Drei sehr unterschiedliche Anwendungsfälle, die jeweils nur eine Zeile Code zum Aufrufen benötigen: Dies ist der konkrete Nutzen einer guten Schema-Modellierung.

Kernaussagen

  • Verschachtelte Schemata (list[LineItem], list[AspectSentiment]) ermöglichen komplexe Analysen
  • Enums (Currency, Kategorien) schränken mögliche Werte ein
  • Das Feld evidence / reasoning zwingt das Modell, seine Entscheidungen zu begründen
  • Eine generische Wrapper-Funktion reduziert Code-Duplizierung
  • temperature=0 ist für diese Produktionsanwendungsfälle zwingend erforderlich