Бонусы за приглашения

Как работают бонусы за приглашения

Поделитесь ссылкой. Когда друг зарегистрируется по ней и пополнит баланс, вы получите указанный бонус за его последующие пополнения.

Извлечение JSON из писем: почему валидная схема не гарантирует правду и как защитить автоматизацию

Почему синтаксическая корректность JSON Schema не гарантирует фактическую точность извлеченных данных и как выстроить двухэтапную проверку перед передачей значений во внутренние системы.

Содержание
Извлечение JSON из писем: почему валидная схема не гарантирует правду и как защитить автоматизацию

Режим Structured Outputs в современных LLM решает фундаментальную инженерную задачу: гарантирует, что ответ модели строго соответствует контракту JSON Schema или модели Pydantic. Однако валидная схема гарантирует только синтаксическую форму документа, но не истинность его содержимого.

В официальной документации Google Gemini API по Structured Outputs зафиксировано явное предостережение: хотя выходные данные синтаксически корректны, разработчик обязан валидировать значения на стороне приложения и обрабатывать смысловые расхождения. Схема защищает парсер от исключений JSONDecodeError, но бессильна перед галлюцинациями, ложными выводами и игнорированием модальных оговорок.

Ниже разобран сквозной сценарий обработки входящего письма службы поддержки: от передачи сырого текста в API до прикладной семантической проверки и безопасной маршрутизации полей.


1. Входные данные: неоднозначное письмо службы поддержки

Возьмем синтетическое иллюстративное письмо, в котором намеренно заложены типичные проблемы естественной речи: условные сроки и неподтвержденные предположения.

email_text = """От: alex.m@partner-example.com
Тема: Проблема с выгрузкой отчетов и планы по релизу

Привет! У нас со вчерашнего дня сбоит генерация PDF в биллинге.
Мы хотели бы закрыть этот вопрос как можно быстрее, в идеале к концу недели,
но если ваш релиз 2.4 задерживается, то крайний срок сдвигается на следующий вторник.
Кстати, затронут ли европейский кластер платежей eu-central-1, мы пока точно не знаем — коллеги еще перепроверяют трейсы."""

В этом тексте присутствуют два критических нюанса:

  1. Условный срок: дедлайн зависит от внешнего фактора (задержки релиза 2.4) и сформулирован относительно («следующий вторник»), а не абсолютной календарной датой.
  2. Неподтвержденный факт: кластер eu-central-1 упомянут как вопрос и гипотеза, требующая проверки по трейсам. Строка оформлена без переноса внутри фразы, что позволяет выполнять точную проверку подстрок.

2. Схема Pydantic и вызов через Interactions API

Опишем контракт извлечения данных с помощью Pydantic и вызовем модель через официальный интерфейс Google GenAI SDK.

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. Иллюстративный сбой: синтаксический успех при семантической ошибке

Приведенный ниже фрагмент представляет собой гипотетический пример типичного логического сбоя, а не гарантированный ответ конкретного запуска API. Он демонстрирует ситуацию, когда модель возвращает формально валидный JSON, игнорируя смысловые ограничения промпта.

{
  "issue_summary": "Сбой генерации PDF в биллинге",
  "deadline_iso": "следующий вторник",
  "deadline_context": "в идеале к концу недели, но если релиз 2.4 задерживается, то следующий вторник",
  "affected_cluster": "eu-central-1",
  "raw_quote": "затронут ли европейский кластер платежей eu-central-1, мы пока точно не знаем"
}

С точки зрения валидатора JSON Schema и библиотеки Pydantic этот объект безупречен: все ключи присутствуют, типы согласованы. Однако для downstream-автоматизации здесь содержатся критические ошибки:

  1. Почему строковый тип пропускает произвольный текст? Поле Optional[str] в сгенерированной схеме превращается в конструкцию с anyOf или объединением типов (string и null). С точки зрения валидатора схемы значение "следующий вторник" — это абсолютно корректная строка. Текстовые инструкции в description не создают ограничений на уровне парсера, поэтому любая непустая строка успешно проходит проверку.
  2. Превращение гипотезы в утверждение: Модель извлекла идентификатор eu-central-1, проигнорировав вопросительный и предположительный характер высказывания.
  3. Ненадежность значения null по инструкции: Даже строгие инструкции заполнять поле значением null при неуверенности часто уступают паттерну выделения сущностей, если название кластера явно встретилось в тексте.

4. Ограничения наивных проверок: цитаты, стоп-слова и реестры инфраструктуры

Разработчики часто пытаются компенсировать семантическую ненадежность LLM эвристиками. Рассмотрим, почему базовые приемы не дают гарантий:

  1. Проверка вхождения цитаты (raw_quote in email_text): В нашем примере фраза "затронут ли европейский кластер платежей eu-central-1, мы пока точно не знаем" является точной непрерывной подстрокой письма, поэтому проверка вернет True. Однако сам факт физического присутствия фразы доказывает лишь отсутствие галлюцинации текста, но прямо подтверждает обратное по смыслу: автор не уверен в наличии сбоя.
  2. Эвристический поиск стоп-слов: Попытки искать слова-маркеры (например, частицу "ли", "возможно", "не знаем") нестабильны. Короткие маркеры вроде "ли" легко дают ложные срабатывания внутри других слов или предложений с совершенно иной модальностью.
  3. Проверка по белому списку инфраструктуры (Allowed Clusters): Наличие eu-central-1 в справочнике серверов компании подтверждает лишь то, что такой кластер действительно существует (идентификация сущности). Оно никак не доказывает факт аварии или инцидента на этом узле (событие сбоя). Без внешних доверенных свидетельств (сигналов мониторинга, подтвержденных алертов) любое упоминание кластера из входящего письма остается непроверенной гипотезой.

5. Прикладной конвейер валидации и безопасная маршрутизация

Чтобы исключить сбои во внутренних системах, на стороне приложения создается второй контур валидации. Он изолированно проверяет синтаксический формат дат, отсекает сомнительные сущности и направляет неполные данные человеку.

Вспомогательная функция date.fromisoformat() в Python не гарантирует соблюдение исключительно формата YYYY-MM-DD (в современных версиях языка она принимает расширенные ISO-строки). Поэтому проверка формы должна сочетать строгое регулярное выражение и последующий календарный парсинг.

Для демонстрации работы валидатора воспользуемся отдельным объектом demo_extraction, инициализированным из гипотетического некорректного JSON.

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. Итоговое состояние: ручной разбор и разделение полей

После выполнения прикладной проверки объект decision переходит в строго контролируемое состояние:

{
  "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"
  }
}

Разделение полей по уровню доверия

ПолеСтатус передачиОбоснование
summaryЧерновик тикетаСуть сбоя генерации PDF зафиксирована прямо. Поле безопасно использовать для создания предварительного описания задачи в трекере.
clusterЗаблокировано (null)Упоминание кластера является неподтвержденным предположением автора. Любое автоматическое перенаправление на дежурную группу инфраструктуры запрещено во избежание ложной тревоги.
deadlineЗаблокировано (null)Неструктурированная фраза не является календарной датой, а сам срок условен. Автоматическая привязка к жесткому SLA запрещена.
routing_statusОчередь оператораСтатус MANUAL_REVIEW сигнализирует, что автоматические триггеры остановлены, а тикет передан сотруднику первой линии с перечнем причин сомнения.

Перед любым расчетом SLA, эскалацией приоритета или маршрутизацией тикета в профильные инженерные команды обязательно требуется верификация человеком. Приложение может сформировать черновой тикет с темой проблемы, но критические атрибуты инцидента остаются пустыми до явного подтверждения оператором.

Резюме

Structured Outputs обеспечивают контрактную стабильность формата, избавляя код от непредвиденных структурных ошибок. Однако соответствие контракту схемы не означает достоверности данных. Безопасная интеграция моделей в бизнес-процессы требует двухуровневой архитектуры: синтаксический контроль на стороне API, строгая проверка типов и регулярных выражений в коде приложения, а также обязательное сохранение сомнительных полей в ручном режиме обработки.

Готовы оптимизировать LLM workflow?

Подключите единый API, управляйте ключами и контролируйте расходы на AI-модели в BetterToken.

Начать бесплатно