JSON-Extraktion aus E-Mails: Warum Schemavalidität keine Wahrheit garantiert und wie Sie Automationen schützen
Warum die syntaktische Konformität mit einem JSON-Schema keineswegs die inhaltliche Richtigkeit extrahierter Daten garantiert und wie Sie eine zweistufige Validierungs-Pipeline auf Anwendungsebene aufbauen, bevor Daten an interne Produktivsysteme weitergeleitet werden.
Inhalt

Der Modus „Structured Outputs“ in modernen LLMs löst eine grundlegende ingenieurtechnische Herausforderung: Er garantiert, dass die Antworten des Modells strikt einem JSON-Schema-Vertrag oder einem Pydantic-Modell entsprechen. Ein valides Schema sichert jedoch lediglich die syntaktische Struktur des Dokuments ab – nicht den faktischen Wahrheitsgehalt seiner Inhalte.
Die offizielle Google Gemini API documentation on Structured Outputs warnt ausdrücklich davor, dass Entwickler Werte selbst bei syntaktisch korrekter Ausgabe auf Anwendungsebene validieren und semantische Diskrepanzen abfangen müssen. Ein Schema schützt Ihren Parser zwar zuverlässig vor JSONDecodeError-Exceptions, bleibt jedoch machtlos gegenüber Halluzinationen, fehlerhaften Schlussfolgerungen und übersehenen modalen Einschränkungen.
Im Folgenden wird ein durchgängiges Szenario für die Verarbeitung eingehender Support-E-Mails analysiert: von der Übergabe des Rohtextes an die API über die anwendungsseitige semantische Validierung bis hin zum sicheren Routing der einzelnen Felder.
1. Eingabedaten: Eine mehrdeutige Support-E-Mail
Betrachten wir eine synthetische, illustrative Support-E-Mail, die bewusst mit typischen Mehrdeutigkeiten natürlicher Sprache konstruiert wurde: bedingte Fristen und unbestätigte Annahmen.
email_text = """От: alex.m@partner-example.com
Тема: Проблема с выгрузкой отчетов и планы по релизу
Привет! У нас со вчерашнего дня сбоит генерация PDF в биллинге.
Мы хотели бы закрыть этот вопрос как можно быстрее, в идеале к концу недели,
но если ваш релиз 2.4 задерживается, то крайний срок сдвигается на следующий вторник.
Кстати, затронут ли европейский кластер платежей eu-central-1, мы пока точно не знаем — коллеги еще перепроверяют трейсы."""
Dieser Text enthält zwei kritische Nuancen:
- Bedingte Frist: Das Fälligkeitsdatum hängt von einem externen Faktor ab (einer Verzögerung von Release 2.4) und ist relativ formuliert („следующий вторник“ / nächster Dienstag), anstatt ein absolutes Kalenderdatum zu nennen.
- Unbestätigter Fakt: Der Cluster
eu-central-1wird lediglich als offene Frage und zu überprüfende Hypothese anhand von Traces erwähnt. Der String ist ohne interne Zeilenumbrüche formatiert, was einen exakten Substring-Abgleich ermöglicht.
2. Pydantic-Schema und Aufruf über die Interactions API
Definieren wir den Extraktionsvertrag mit Pydantic und rufen das Modell über die offizielle Schnittstelle des Google GenAI SDK auf.
from typing import Optional
from google import genai
from pydantic import BaseModel, Field
class TicketExtraction(BaseModel):
issue_summary: str = Field(
description="Краткая суть проблемы."
)
deadline_iso: Optional[str] = Field(
default=None,
description="Однозначно зафиксированный срок в формате YYYY-MM-DD. Если дата не определена точно или зависит от условий, вернуть null."
)
deadline_context: Optional[str] = Field(
default=None,
description="Оговорки, условия или контекст, сопровождающие срок."
)
affected_cluster: Optional[str] = Field(
default=None,
description="Точно подтвержденный затронутый кластер или сервис. Если факт не подтвержден или выражено сомнение, вернуть null."
)
raw_quote: Optional[str] = Field(
default=None,
description="Точная цитата из текста, обосновывающая извлеченные факты."
)
client = genai.Client()
prompt = f"""Извлеки параметры инцидента из письма.
Строго следуй правилу: если автор выражает сомнение или значение зависит от условий,
записывай в соответствующее поле null, а контекст выноси в deadline_context.
Письмо:
{email_text}"""
interaction = client.interactions.create(
model="gemini-3.8-flash",
input=prompt,
response_format={
"type": "text",
"mime_type": "application/json",
"schema": TicketExtraction.model_json_schema(),
},
)
live_extraction = TicketExtraction.model_validate_json(interaction.output_text)
3. Illustrativer Fehlerfall: Syntaktischer Erfolg bei semantischem Fehlschlag
Der folgende Ausschnitt illustriert einen hypothetischen, typischen logischen Fehlerfall und stellt keine garantierte Ausgabe eines bestimmten API-Aufrufs dar. Er veranschaulicht, wie ein Modell formal valides JSON zurückgeben kann, dabei jedoch die semantischen Vorgaben des Prompts ignoriert.
{
"issue_summary": "Сбой генерации PDF в биллинге",
"deadline_iso": "следующий вторник",
"deadline_context": "в идеале к концу недели, но если релиз 2.4 задерживается, то следующий вторник",
"affected_cluster": "eu-central-1",
"raw_quote": "затронут ли европейский кластер платежей eu-central-1, мы пока точно не знаем"
}
Aus Sicht des JSON-Schema-Validators und der Pydantic-Bibliothek ist dieses Objekt makellos: Alle Schlüssel sind vorhanden und die Typen stimmen überein. Für nachgelagerte Automatisierungsprozesse birgt es jedoch gravierende Fehler:
- Warum String-Typen beliebigen Text zulassen:
Das Feld
Optional[str]wird im generierten Schema zu einemanyOf-Konstrukt bzw. einer Typ-Union (stringundnull). Für den Schema-Validator ist der Wert"следующий вторник"ein vollkommen valider String. Textuelle Anweisungen innerhalb vondescriptionsetzen auf Parser-Ebene keinerlei Beschränkungen durch, sodass jeder nicht-leere String die Prüfung erfolgreich durchläuft. - Verwandlung einer Hypothese in eine gesicherte Tatsache:
Das Modell hat den Bezeichner
eu-central-1extrahiert, dabei jedoch den fragenden, spekulativen Charakter der Aussage ignoriert. - Die Unzuverlässigkeit von Prompt-basierten
null-Werten: Selbst strikte Anweisungen im Prompt, bei Unsicherheitennullzurückzugeben, unterliegen häufig dem Drang des Modells zur Entitätsextraktion, sobald ein Entitätsname explizit im Quelltext auftaucht.
4. Grenzen naiver Prüfungen: Zitate, Stoppwörter und Infrastruktur-Register
Entwickler versuchen häufig, die semantische Unzuverlässigkeit von LLMs durch Heuristiken zu kompensieren. Hier sind die Gründe, warum grundlegende Techniken keine verlässlichen Garantien bieten:
- Prüfung auf Vorhandensein des Zitats (
raw_quote in email_text): In unserem Beispiel ist die Phrase"затронут ли европейский кластер платежей eu-central-1, мы пока точно не знаем"ein exakter, zusammenhängender Substring der E-Mail, weshalb diese PrüfungTruezurückgibt. Das rein physische Vorkommen im Text beweist jedoch lediglich, dass das Zitat nicht halluziniert wurde. Semantisch belegt es genau das Gegenteil: Der Autor ist sich unsicher, ob tatsächlich ein Ausfall vorliegt. - Heuristischer Abgleich von Stoppwörtern:
Die Suche nach Signalwörtern (wie etwa Partikeln wie
"ли"oder Phrasen wie"возможно","не знаем") ist fehleranfällig. Kurze Signalwörter führen leicht zu False Positives innerhalb anderer Wörter oder in Sätzen mit völlig anderer Modalität. - Validierung gegen eine Infrastruktur-Allowlist (Allowed Clusters):
Der Abgleich von
eu-central-1mit einem internen Server-Register bestätigt lediglich, dass ein solcher Cluster tatsächlich existiert (Entitätsidentifikation). Er beweist keineswegs, dass auf diesem Knoten ein tatsächlicher Störfall oder Ausfall vorliegt (Fehlerereignis). Ohne externe, vertrauenswürdige Nachweise (wie Monitoringsignale oder bestätigte Alerts) bleibt jede Erwähnung eines Clusters in einer eingehenden E-Mail eine unbestätigte Hypothese.
5. Anwendungsseitige Validierungs-Pipeline und sicheres Routing
Um Fehler in internen Downstream-Systemen zu verhindern, müssen Anwendungen eine zweite Validierungsebene implementieren. Diese Schicht prüft Datumsformate unabhängig, filtert fragwürdige Entitäten heraus und leitet unvollständige oder mehrdeutige Daten an einen menschlichen Prüfer weiter.
Die Hilfsfunktion date.fromisoformat() in Python garantiert für sich genommen nicht die strikte Einhaltung des Formats YYYY-MM-DD (moderne Python-Versionen akzeptieren erweiterte ISO-Strings). Aus diesem Grund müssen Formatprüfungen einen strikten regulären Ausdruck mit anschließendem Kalender-Parsing kombinieren.
Um diesen Validator in Aktion zu demonstrieren, nutzen wir ein separates demo_extraction-Objekt, das aus unserem hypothetischen, fehlerhaften JSON initialisiert wurde.
import re
from datetime import date
from typing import Any, Optional
from pydantic import BaseModel, Field
class ValidationResult(BaseModel):
is_safe_for_automation: bool
confirmed_deadline: Optional[date] = None
confirmed_cluster: Optional[str] = None
review_reasons: list[str] = Field(default_factory=list)
downstream_payload: dict[str, Any] = Field(default_factory=dict)
def validate_and_route_ticket(
extracted: TicketExtraction,
source_text: str
) -> ValidationResult:
reasons: list[str] = []
if extracted.raw_quote and extracted.raw_quote not in source_text:
reasons.append("Указанная цитата отсутствует в исходном письме.")
parsed_date: Optional[date] = None
if extracted.deadline_iso:
if not re.fullmatch(r"\d{4}-\d{2}-\d{2}", extracted.deadline_iso):
reasons.append(
f"Значение deadline_iso не соответствует строгому формату YYYY-MM-DD: '{extracted.deadline_iso}'."
)
else:
try:
parsed_date = date.fromisoformat(extracted.deadline_iso)
except ValueError:
reasons.append(
f"Значение deadline_iso содержит недопустимую календарную дату: '{extracted.deadline_iso}'."
)
parsed_date = None
if parsed_date and extracted.deadline_context:
reasons.append(
f"Срок {parsed_date.isoformat()} сопровождается оговорками ('{extracted.deadline_context}') и требует ручного согласования."
)
parsed_date = None
validated_cluster: Optional[str] = None
if extracted.affected_cluster:
reasons.append(
f"Кластер '{extracted.affected_cluster}' упомянут в письме как предположение; без внешнего подтверждения из мониторинга требуется проверка оператором."
)
needs_manual_review = len(reasons) > 0 or parsed_date is None or validated_cluster is None
safe_payload = {
"summary": extracted.issue_summary,
"deadline": parsed_date.isoformat() if parsed_date else None,
"cluster": validated_cluster,
"routing_status": "MANUAL_REVIEW" if needs_manual_review else "AUTO_APPROVED",
}
return ValidationResult(
is_safe_for_automation=not needs_manual_review,
confirmed_deadline=parsed_date,
confirmed_cluster=validated_cluster,
review_reasons=reasons,
downstream_payload=safe_payload,
)
hypothetical_bad_json = """{
"issue_summary": "Сбой генерации PDF в биллинге",
"deadline_iso": "следующий вторник",
"deadline_context": "в идеале к концу недели, но если релиз 2.4 задерживается, то следующий вторник",
"affected_cluster": "eu-central-1",
"raw_quote": "затронут ли европейский кластер платежей eu-central-1, мы пока точно не знаем"
}"""
demo_extraction = TicketExtraction.model_validate_json(hypothetical_bad_json)
decision = validate_and_route_ticket(demo_extraction, email_text)
6. Endzustand: Manuelle Prüfung und Feldisolation
Nach Ausführung der anwendungsseitigen Validierung geht das decision-Objekt in einen strikt kontrollierten Zustand über:
{
"is_safe_for_automation": false,
"confirmed_deadline": null,
"confirmed_cluster": null,
"review_reasons": [
"Значение deadline_iso не соответствует строгому формату YYYY-MM-DD: 'следующий вторник'.",
"Кластер 'eu-central-1' упомянут в письме как предположение; без внешнего подтверждения из мониторинга требуется проверка оператором."
],
"downstream_payload": {
"summary": "Сбой генерации PDF в биллинге",
"deadline": null,
"cluster": null,
"routing_status": "MANUAL_REVIEW"
}
}
Feld-Triage nach Vertrauensstufe
| Feld | Routing-Status | Begründung |
|---|---|---|
summary | Ticket-Entwurf | Das Problem mit der PDF-Generierung im Billing ist explizit benannt. Dieses Feld kann bedenkenlos als vorläufige Ticketbeschreibung im Issue-Tracker übernommen werden. |
cluster | Blockiert (null) | Die Erwähnung des Clusters ist eine unbestätigte Vermutung des Verfassers. Eine automatische Zuweisung an die Infrastruktur-Rufbereitschaft wird blockiert, um Fehlalarme zu vermeiden. |
deadline | Blockiert (null) | Die unstrukturierte Formulierung ist kein Kalenderdatum und der Zeitrahmen ist an Bedingungen geknüpft. Eine automatische Bindung an ein striktes SLA ist gesperrt. |
routing_status | Operator-Warteschlange | Der Status MANUAL_REVIEW signalisiert, dass automatische Trigger ausgesetzt sind und das Ticket zusammen mit der detaillierten Liste der Prüfgründe an einen First-Level-Bearbeiter geleitet wird. |
Vor jeder SLA-Berechnung, Prioritätseskalation oder Weiterleitung an spezialisierte Engineering-Teams ist eine menschliche Verifikation zwingend erforderlich. Die Anwendung kann zwar einen Ticket-Entwurf mit der gemeldeten Problemzusammenfassung erstellen, kritische Incident-Attribute bleiben jedoch unzugewiesen, bis sie von einem Operator explizit bestätigt werden.
Zusammenfassung
Structured Outputs gewährleisten Formatstabilität auf Schemaebene und schützen nachgelagerten Code vor unerwarteten Syntaxfehlern. Die Einhaltung eines Schemakontrakts ist jedoch nicht mit inhaltlicher Wahrheit gleichzusetzen. Eine sichere Integration von LLMs in Geschäftsprozesse erfordert eine zweistufige Architektur: syntaktische Durchsetzung an der API-Grenze, strikte Regex- und Typvalidierung im Anwendungscode sowie die obligatorische Isolierung unsicherer Felder in Prüfwarteschlangen mit Human-in-the-Loop-Verifikation.