Extração de JSON de E-mails: Por Que a Validade do Schema Não Garante a Verdade e Como Proteger a Automação
Por que a conformidade sintática com o JSON Schema não garante a precisão factual dos dados extraídos e como construir um pipeline de validação em duas etapas na aplicação antes de rotear valores para sistemas internos de produção.
Conteúdo

O modo de Saídas Estruturadas (Structured Outputs) em LLMs modernas resolve um problema fundamental de engenharia: ele garante que as respostas do modelo sigam rigorosamente um contrato de JSON Schema ou um modelo Pydantic. No entanto, um schema válido garante apenas o formato sintático do documento, e não a veracidade factual do seu conteúdo.
A documentação oficial da Google Gemini API sobre Structured Outputs adverte explicitamente que, mesmo quando a saída é sintaticamente correta, os desenvolvedores precisam validar os valores no lado da aplicação e tratar discrepâncias semânticas. Um schema protege o seu parser contra exceções do tipo JSONDecodeError, mas é ineficaz diante de alucinações, inferências equivocadas e qualificadores modais ignorados.
Abaixo, apresentamos um cenário de ponta a ponta cobrindo o processamento de e-mails recebidos pelo suporte ao cliente: desde o envio do texto bruto para a API até a validação semântica no nível da aplicação e o roteamento seguro de campos.
1. Dados de Entrada: Um E-mail de Suporte Ambíguo
Considere um e-mail de suporte sintético e ilustrativo, projetado intencionalmente com ambiguidades típicas da linguagem natural: prazos condicionais e suposições não verificadas.
email_text = """От: alex.m@partner-example.com
Тема: Проблема с выгрузкой отчетов и планы по релизу
Привет! У нас со вчерашнего дня сбоит генерация PDF в биллинге.
Мы хотели бы закрыть этот вопрос как можно быстрее, в идеале к концу недели,
но если ваш релиз 2.4 задерживается, то крайний срок сдвигается на следующий вторник.
Кстати, затронут ли европейский кластер платежей eu-central-1, мы пока точно не знаем — коллеги еще перепроверяют трейсы."""
Este texto contém duas nuances críticas:
- Prazo condicional: O prazo final depende de um fator externo (um atraso no release 2.4) e é formulado de maneira relativa (“следующий вторник”, ou próxima terça-feira), em vez de uma data de calendário absoluta.
- Fato não confirmado: O cluster
eu-central-1é mencionado apenas como uma pergunta aberta e uma hipótese sujeita à verificação de traces. A string é formatada sem quebras de linha internas, permitindo a correspondência exata de substrings.
2. Schema Pydantic e Invocação via Interactions API
Vamos definir o contrato de extração usando o Pydantic e invocar o modelo por meio da interface oficial do SDK Google GenAI.
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. Falha Ilustrativa: Sucesso Sintático com Falha Semântica
O trecho abaixo ilustra uma falha lógica hipotética e típica, e não a saída garantida de uma execução específica da API. Ele demonstra como um modelo pode retornar um JSON formalmente válido, ignorando as restrições semânticas definidas no prompt.
{
"issue_summary": "Сбой генерации PDF в биллинге",
"deadline_iso": "следующий вторник",
"deadline_context": "в идеале к концу недели, но если релиз 2.4 задерживается, то следующий вторник",
"affected_cluster": "eu-central-1",
"raw_quote": "затронут ли европейский кластер платежей eu-central-1, мы пока точно не знаем"
}
Do ponto de vista de um validador de JSON Schema e da biblioteca Pydantic, esse objeto é impecável: todas as chaves estão presentes e os tipos coincidem. No entanto, para a automação downstream, ele contém erros críticos:
- Por que tipos string permitem texto arbitrário:
O campo
Optional[str]no schema gerado se transforma em uma estruturaanyOfou união de tipos (stringenull). Para o validador do schema, o valor"следующий вторник"é uma string perfeitamente válida. Instruções textuais dentro dedescriptionnão impõem restrições no nível do parser, permitindo que qualquer string não vazia passe pela validação. - Transformação de uma hipótese em fato estabelecido:
O modelo extraiu o identificador
eu-central-1, ignorando o tom interrogativo e especulativo da afirmação. - A falta de confiabilidade de valores
nullguiados por instruções: Mesmo instruções rigorosas no prompt para retornarnullem caso de incerteza costumam ceder à tendência de extração de entidades quando o nome de uma entidade aparece explicitamente no texto de origem.
4. Limitações de Verificações Ingênuas: Citações, Stop Words e Registros de Infraestrutura
Desenvolvedores frequentemente tentam compensar a falta de confiabilidade semântica dos LLMs utilizando heurísticas. Veja por que essas técnicas básicas falham em fornecer garantias:
- Verificação de presença da citação (
raw_quote in email_text): Em nosso exemplo, a frase"затронут ли европейский кластер платежей eu-central-1, мы пока точно не знаем"é uma substring exata e contínua do e-mail, de modo que essa verificação é avaliada comoTrue. Contudo, a presença literal no texto apenas comprova que a citação não foi alucinada; semanticamente, ela prova exatamente o oposto: o autor não tem certeza se uma falha realmente ocorreu. - Correspondência heurística de stop words:
Buscar por palavras marcadoras (como partículas do tipo
"ли"ou termos como"возможно","не знаем") é uma abordagem frágil. Marcadores curtos geram facilmente falsos positivos dentro de outras palavras ou em frases com sentidos modais completamente diferentes. - Validação contra uma lista de permissões de infraestrutura (Allowed Clusters):
Verificar
eu-central-1contra um catálogo interno de servidores apenas confirma que esse cluster existe (identificação de entidade). Isso não comprova que uma pane ou incidente de fato aconteceu naquele nó (evento de falha). Sem evidências externas confiáveis (como sinais de monitoramento ou alertas confirmados), qualquer menção a um cluster em um e-mail recebido permanece sendo uma hipótese não verificada.
5. Pipeline de Validação na Aplicação e Roteamento Seguro
Para evitar falhas em sistemas downstream internos, as aplicações precisam implementar uma segunda camada de validação. Essa camada verifica de forma independente os formatos de data, filtra entidades questionáveis e encaminha dados incompletos ou ambíguos para um revisor humano.
A função auxiliar date.fromisoformat() do Python não garante, por si só, conformidade estrita apenas com o formato YYYY-MM-DD (versões modernas do Python aceitam strings ISO estendidas). Portanto, as verificações de formato devem combinar uma expressão regular rigorosa com uma posterior análise de calendário (parsing).
Para demonstrar esse validador em ação, utilizamos um objeto demo_extraction separado, inicializado a partir do nosso JSON inválido hipotético.
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. Estado Final: Revisão Manual e Isolamento de Campos
Após a execução da validação na aplicação, o objeto decision transita para um estado rigorosamente controlado:
{
"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"
}
}
Triagem de Campos por Nível de Confiança
| Campo | Status de Roteamento | Justificativa |
|---|---|---|
summary | Rascunho de Ticket | O problema na geração de PDFs do faturamento é declarado explicitamente. É seguro preencher este campo como uma descrição preliminar de ticket em um rastreador de chamados. |
cluster | Bloqueado (null) | A menção ao cluster é uma suposição não confirmada pelo autor. O encaminhamento automático para a escala de plantão da infraestrutura é bloqueado para evitar alarmes falsos. |
deadline | Bloqueado (null) | A frase não estruturada não é uma data de calendário, e o próprio prazo é condicional. A vinculação automática a um SLA rígido fica bloqueada. |
routing_status | Fila do Operador | O status MANUAL_REVIEW indica que os gatilhos automáticos foram suspensos, direcionando o ticket para um operador de nível 1 acompanhado da lista detalhada dos motivos de revisão. |
A verificação humana é obrigatória antes de qualquer cálculo de SLA, escalonamento de prioridade ou direcionamento para equipes de engenharia especializadas. A aplicação pode gerar um rascunho de ticket contendo o resumo do problema relatado, mas os atributos críticos do incidente permanecem não atribuídos até que sejam explicitamente confirmados por um operador.
Resumo
O recurso de Structured Outputs garante a estabilidade de formato no nível do schema, protegendo o código downstream contra erros inesperados de sintaxe. No entanto, a conformidade com o contrato do schema não equivale à veracidade dos dados. Integrar LLMs de forma segura a processos de negócios exige uma arquitetura em dois níveis: imposição sintática na fronteira da API, validação rigorosa de tipos e expressões regulares no código da aplicação e quarentena obrigatória de campos incertos em filas de revisão humana (human-in-the-loop).