Conteúdo da trilha
8 minNarração em inglês, legenda em portuguêsPodcast (RSS)
Capítulos
  1. 0:00Abertura
  2. 0:58A ideia
  3. 1:48Como funciona
  4. 2:23Um caso real
  5. 3:27Um desafio
  6. 4:11Quando não usar
  7. 5:16Em produção
  8. 6:53O código
  9. 7:41Fechamento
Transcrição

Abertura

A Bianca automatizou a leitura de notas de fornecedores. Toda empresa no Brasil é identificada por um CNPJ, catorze dígitos com dois dígitos verificadores no fim.

O prompt dela pedia: devolva o CNPJ do emissor, a data de emissão e o valor total, como dados estruturados. Na primeira semana, funcionou em quase tudo.

Aí começaram os casos estranhos. Uma resposta abriu com uma frase simpática: claro, aqui estão os dados. Outra devolveu o total como texto, com o símbolo da moeda, quando o sistema esperava um número. Uma terceira deu outro nome ao campo.

Cada caso virou mais um remendo no código. O parser ficou maior que a regra de negócio, e ainda falhava algumas vezes por dia. Nesta aula, você vai ver como fazer o modelo entregar dados que o seu código usa sem adivinhar. E por que isso é só metade do trabalho.

A ideia

A correção começa com uma mudança de cabeça. Pare de pedir um formato no texto do prompt e passe a declarar o formato como parte da chamada.

As APIs atuais aceitam um schema, uma descrição dos campos, dos tipos e dos valores permitidos. E restringem a geração para que a saída siga esse formato. É a diferença entre entregar uma folha em branco para alguém e entregar um formulário com campos rotulados.

Mas aqui está a parte que muita gente perde. O schema garante a forma, não a verdade.

Um CNPJ pode ter catorze dígitos e mesmo assim ser inválido. Então a segunda metade do padrão é conferir as regras de negócio no código. Dígitos verificadores, data no passado, valor positivo. Forma garantida pela API, conteúdo conferido por você.

Como funciona

Vamos seguir uma nota. A entrada é o texto que precisa virar dado.

A chamada leva o schema junto. O modelo só consegue gerar saídas naquele formato.

O SDK transforma a resposta num objeto tipado. Sem expressão regular, sem caçar chaves.

Depois, o seu código confere as regras que o schema não consegue expressar.

Dado válido vai direto para o sistema contábil.

E dado com a forma certa, mas conteúdo suspeito, vai para uma pessoa revisar.

Um caso real

Aqui está uma nota real de uma padaria. Um CNPJ, uma data de emissão em agosto, um total de mil duzentos e trinta e quatro dólares, e dois itens, pão francês e café.

Com devolva os dados no prompt, uma resposta realista tinha três falhas de forma ao mesmo tempo. Texto em volta dos dados, o nome errado do campo e o total como texto com símbolo de moeda. Foram três correções no parser. Na semana seguinte, apareceu uma quarta.

Com um schema declarado, a API só consegue emitir quatro campos. O CNPJ do emissor, a data de emissão, o total como número e a lista de itens. O SDK devolve um objeto.

E aí o código da Bianca ainda tem trabalho. O CNPJ precisa passar na regra dos dígitos verificadores.

Os itens precisam somar o total.

E a data de emissão não pode estar no futuro. Se alguma falhar, a nota vai para revisão, com a lista do que falhou.

Um desafio

Uma pergunta para você. O modelo devolveu um objeto exatamente no schema. O CNPJ tem catorze dígitos. Mas os dígitos verificadores não batem. O que o padrão manda fazer?

Valide os dígitos verificadores no código e mande para revisão humana quando falharem. Forma não é verdade. Um valor inválido de catorze dígitos passa num schema de texto e estraga o cadastro do emissor.

E pedir de novo, várias vezes, até passar? Um retry no mesmo documento costuma devolver a mesma leitura errada. A fila de revisão existe exatamente para quando a fonte está ilegível, ou o modelo leu errado.

Quando não usar

Saída estruturada não serve para tudo. Se a saída é texto para uma pessoa ler, como uma resposta de chat, forçar um schema só engessa a escrita. Ninguém vai interpretar aquilo.

Se é um texto livre curto que o código só exibe, mesma coisa.

E se o seu schema seria um campo chamado resposta, do tipo texto, ele não garante nada. Só acrescenta cerimônia.

Agora, o erro mais comum. Toda nota tem um emissor. Mas nem todo contrato de serviço tem multa. Se o campo de multa é obrigatório, o modelo inventa algo como dez por cento padrão, só para não quebrar o schema.

Então marque como opcional o que pode mesmo faltar. E meça. No seu eval, inclua documentos em que o campo não existe, e conte quantas vezes o modelo devolve vazio, e quantas vezes inventa. E prefira uma lista fechada de valores a texto aberto. Campo de texto aberto é como super urgente vai parar na sua coluna de prioridade.

Em produção

Com schema nativo, a saída quase sempre é válida. Então válida deixa de ser métrica. Ela esconde erro de conteúdo.

Avalie campo a campo, contra valores rotulados. E normalize antes. A nota diz mil duzentos e trinta e quatro dólares com vírgula, e o rótulo guarda o número. Compare texto com texto, e o eval pune a formatação enquanto um CNPJ errado com a pontuação certa passa.

Meça a taxa de invenção em documentos em que o campo realmente não existe.

E acompanhe o que vai para revisão humana, e quanto disso era erro de verdade. Em produção, uma regra de validação que de repente dispara muito costuma ser um fornecedor que mudou o layout.

Versione o schema como você versiona o prompt, e registre os dois em todo trace. Um campo obrigatório novo empurra invenção em documentos antigos. Um campo removido quebra o código que ainda lê o nome velho.

E um ponto de segurança. Saída estruturada elimina uma classe de risco. O seu código para de interpretar texto livre que poderia conter qualquer coisa. Mas os valores dos campos ainda vêm de um modelo que leu um documento de fora. Trate como entrada não confiável. Um campo de descrição copiado numa página web pode levar um script. Um campo colado numa consulta ao banco é injeção. Escape antes de exibir, use parâmetros nas consultas e nunca execute um campo como código.

O código

Na página, a mesma extração vem em quatro versões. Todas começam igual: uma chamada, com o schema junto, que devolve um objeto tipado.

Depois, três checagens no código. Os dígitos verificadores, a data e a soma. O que falha vai para revisão. O resto é gravado.

O que muda entre as versões é o provedor e o sistema de tipos. Em Python, uma classe define o schema e o tipo de uma vez. Em TypeScript, um objeto Zod faz o mesmo, e o compilador conhece cada campo. Tem uma versão no SDK da OpenAI com exatamente o mesmo schema. E o LangChain envolve os dois provedores, então as checagens de negócio não mudam quando o modelo muda.

Fechamento

Então, a ideia para levar. Não peça um formato ao modelo. Declare, e deixe a API segurar a forma. Depois confira o conteúdo no código, porque um formulário perfeito ainda pode ser preenchido errado.

Na próxima aula, a gente usa essa mesma ideia na direção contrária: o modelo preenchendo um formulário que pede para o seu código fazer algo. Ferramentas e function calling. Até lá.

PadrãoComposição

Saída Estruturada

Structured Output

Saída Estruturada (Structured Output) é um padrão de projeto de IA para apps com LLM e agentes, do grupo Composição. Exigir que o modelo responda em um schema e validar a resposta.

Elemento
07/70
Nível
Iniciante
Leitura
9 min
Atualizado
Nesta página 18
  1. 01Intenção
  2. 02Problema
  3. 03Solução
  4. 04Estrutura
  5. 05Pseudocódigo
  6. 06Cenários reais
  7. 07Aplicabilidade
  8. 08Quando não usar
  9. 09Como implementar
  10. 10Variações
  11. 11Custo, latência e falhas
  12. 12Como avaliar
  13. 13Em produção
  14. 14Segurança
  15. 15Prós e contras
  16. 16Código
  17. 17Suporte por framework
  18. 18Teste seu entendimento
  19. ··Perguntas rápidas

Intenção

Exigir que o modelo responda num schema definido e validar a resposta no código antes de usar. Serve para qualquer saída que outro programa vai ler: extração, classificação, roteamento, chamada de API, dados que vão para o banco.

Em uma frase
Um formulário com campos marcados em vez de uma folha em branco.

Problema

A Bianca automatizou a leitura de notas fiscais de fornecedores. O prompt pedia: “devolva um JSON com CNPJ do emitente, data de emissão e valor total”. Na primeira semana, deu certo em quase tudo.

Aí começaram os casos estranhos. Uma nota veio com o JSON embrulhado em três crases e a palavra json. Outra trouxe o valor como "R$ 1.234,56", em texto, quando o sistema esperava número. Uma terceira chamou o campo de cnpj_emissor em vez de cnpj_emitente. E numa delas o modelo respondeu “Claro! Aqui está o JSON:” antes das chaves.

Cada caso virou um try, um replace e uma expressão regular no código. O parser cresceu mais que a lógica de negócio, e mesmo assim falhava algumas vezes por dia.

A pergunta que este padrão responde: como fazer o modelo entregar dados que o seu código pode usar sem adivinhar?

Solução

Pare de pedir formato no texto do prompt e passe a declarar o formato como parte da chamada. As APIs atuais aceitam um schema JSON (ou uma classe Pydantic, ou um objeto Zod) e restringem a geração para que a saída siga esse schema: nomes de campo, tipos e valores permitidos.

O schema garante a forma, não a verdade. Um CNPJ pode ter 14 dígitos e mesmo assim estar errado. Por isso a segunda metade do padrão é validar regras de negócio no código: dígito verificador, data no passado, valor positivo. Forma garantida pela API, conteúdo conferido por você.

A refatoração que chega aqui é Trocar Texto Livre por Saída Estruturada. Use-a quando o parser de JSON já virou o produto.

Estrutura

flowchart LR
  IN(["1. Texto da nota"]):::io --> M(["2. Modelo com schema"]):::llm
  M --> P["3. Parse tipado"]:::tool
  P --> V{"4. Regras de negócio OK?"}
  V -->|"sim"| DB[("5. Sistema contábil")]:::store
  V -->|"não"| Q((("6. Fila de revisão"))):::human
  1. A entrada é o texto que precisa virar dado.
  2. A chamada leva o schema junto. O modelo só consegue gerar saídas nesse formato.
  3. O SDK transforma a resposta num objeto tipado, sem expressão regular.
  4. O código confere as regras que o schema não expressa.
  5. Dado válido segue direto para o sistema.
  6. Dado com forma certa e conteúdo suspeito vai para uma pessoa revisar.

Exemplo trabalhado

A Bianca recebe esta nota (trecho):

EMITENTE: Padaria São Jorge Ltda
CNPJ: 11.222.333/0001-81
Data de emissão: 14/08/2026
Total: R$ 1.234,56
Itens: Pão francês 800un  R$ 800,00 | Café 20kg  R$ 434,56

Com “devolva um JSON” no prompt, uma resposta realista era um parágrafo (“Claro! Segue o JSON:”), três crases, o campo cnpj_emissor no lugar de issuer_cnpj, e o total como a string "R$ 1.234,56".

Três falhas de forma numa resposta: texto em volta, nome de campo errado, número como string com R$. O parser da Bianca precisava das três correções, e na semana seguinte aparecia uma quarta.

Com schema nativo, a API só consegue emitir os campos issuer_cnpj, issue_date, total, items. O SDK entrega um objeto. Aí o código da Bianca ainda tem trabalho: o CNPJ 11222333000181 precisa passar no dígito verificador, a soma dos itens precisa bater com o total, a data não pode ser futura.

O CNPJ da Padaria São Jorge neste exemplo é ilustrativo. Em produção, use o validador de dígitos; 14 dígitos iguais (111…) já são inválidos pela regra.

Checagem +20 XP de primeiraO modelo devolveu um objeto no schema, com issuer_cnpj de 14 dígitos. O dígito verificador não fecha. O que o padrão manda fazer?

Pseudocódigo

schema Invoice {
    issuer_cnpj: string        # 14 dígitos
    issue_date: date
    total: float
    items: list of { description: string, amount: float }
}

function read_invoice(text):
    # 2 e 3. forma garantida pela API
    invoice = llm(EXTRACT_PROMPT, text) -> Invoice

    # 4. conteúdo conferido no código
    errors = []
    if not valid_cnpj(invoice.issuer_cnpj): errors.add("cnpj")
    if invoice.issue_date > today(): errors.add("data futura")
    if abs(sum(invoice.items.amount) - invoice.total) > 0.01: errors.add("soma")

    if errors: return human.review(invoice, errors)   # 6
    return accounting.save(invoice)                    # 5

Cenários reais

  • Extração de NF-e e contratos. Cada documento vira um objeto com campos fiscais. O schema acaba com o parser frágil, e a validação de CNPJ e de soma pega o que o modelo leu errado.
  • Triagem automática de tickets. Fila, prioridade e produto vêm como enums. O código nunca recebe uma prioridade “urgentíssima” que não existe na lista.
  • Triagem de currículos com controle de viés. Cada critério de avaliação vira um campo com nota e justificativa. A estrutura permite auditar a decisão campo por campo e checar viés por grupo.

Aplicabilidade

  • Outro programa vai ler a saída do modelo.
  • A resposta tem campos que você precisa acessar por nome.
  • Os valores possíveis de um campo são uma lista fechada.
  • Você vai gravar o resultado num banco ou mandar para uma API.
  • Etapas de uma cadeia passam dados entre si.

Quando não usar

  • A saída é texto para uma pessoa ler, como uma resposta de chat. Forçar schema ali só atrapalha a escrita.
  • O formato é um texto curto e livre, e o código só exibe o que veio.
  • O schema seria tão aberto (um campo resposta: string) que não garante nada. Aí ele só adiciona cerimônia.

Erro comum: campo obrigatório que a fonte não tem

Toda NF-e tem emitente. Nem todo contrato de prestação tem multa. Se penalty é obrigatório, o modelo inventa “10% padrão” para não quebrar o schema. No eval, isso aparece como taxa de invenção: documentos sem o campo deveriam devolver null.

Marque como opcional o que pode faltar. Use enum para prioridade, produto, tipo de nota. string aberto é o schema largo que deixa "urgentíssima" passar.

Saída estruturada também não é para a resposta de chat que uma pessoa lê. Forçar JSON ali deixa o texto engessado e não ajuda o código, porque ninguém vai parsear.

Checagem +20 XP de primeiraO schema exige penalty: string. Num contrato sem cláusula de multa, o modelo devolve '10% do valor'. Como você descobre, e como conserta?

Como implementar

  1. Escreva o schema antes do prompt. Use os tipos mais estreitos possíveis: enum no lugar de string, número no lugar de texto.
  2. Marque como opcional o que pode realmente faltar no documento. Campo obrigatório que não existe na fonte leva o modelo a inventar.
  3. Use o recurso nativo de saída estruturada do SDK (messages.parse com Pydantic ou Zod).
  4. Mantenha no prompt só a explicação de cada campo, com exemplos de casos difíceis. O formato já está no schema.
  5. Escreva no código as validações de negócio que o schema não cobre.
  6. Decida o que acontece com dados inválidos: reparo automático, revisão humana ou descarte com log.
  7. Monte um eval com valores esperados por campo.

Não avalie “JSON válido”. Com schema nativo isso quase sempre passa e esconde o erro de conteúdo. Avalie issuer_cnpj, total, issue_date contra o rótulo, depois da normalização (só dígitos no CNPJ, data em ISO).

Variações

  • Saída estruturada nativa. A API restringe a geração ao schema. É a opção padrão hoje.
  • Ferramenta como schema. Você define uma ferramenta com strict: true e o modelo “chama” essa ferramenta com os dados. Útil quando a mesma chamada também usa outras ferramentas.
  • Validar e reparar. Sem suporte nativo, você valida a saída e, se falhar, manda o erro de volta para o modelo corrigir. Tem página própria em Validação e Reparo de Saída.

Versione o schema como versiona o prompt. Um campo novo obrigatório empurra invenção nos documentos antigos do eval. Um campo removido quebra o código que ainda lê o nome velho. Guarde schema_version no span junto com a versão do prompt, para um pico de “cnpj inválido” apontar para o deploy certo.

Quando o provedor ainda não restringe a geração, não finja que pediu JSON no texto. Valide, e se falhar, entre no reparo com a mensagem de erro do parser. Isso é outro padrão, e o teto de tentativas dele é o mesmo teto de qualquer laço.

Custo, latência e falhas

Aspecto O que acontece Como aparece no trace
Chamadas Nenhuma a mais; o schema vai na mesma chamada mesmo span de antes, com o schema anexado
Latência A primeira chamada com um schema novo pode demorar mais duração maior só na primeira vez
Custo O schema conta como entrada; prompts de formato ficam menores tokens de entrada parecidos com antes
Falha: campo inventado Campo obrigatório que não existe na fonte vem preenchido valor plausível sem trecho correspondente na entrada
Falha: forma certa, conteúdo errado CNPJ com 14 dígitos inválidos, data trocada validação de negócio barrando no span seguinte
Falha: schema largo demais string aceita qualquer coisa valores fora do esperado chegando ao banco

Como avaliar

Avalie campo por campo. “O JSON era válido” deixou de ser métrica: com saída estruturada nativa, ele quase sempre é.

  • Acerto por campo: o valor bate com o rótulo? Normalize antes de comparar (datas, maiúsculas).
  • Taxa de invenção: em documentos onde o campo não existe, o modelo devolve null ou inventa?
  • Taxa de barrados pela validação: quanto vai para revisão humana, e quantos desses eram de fato erros.

Um caso de eval:

O rótulo precisa do valor canônico, não do texto da nota. R$ 1.234,56 no PDF vira 1234.56 no expected. Se você comparar string com string, o eval pune formatação e deixa passar CNPJ errado com a pontuação certa.

Inclua documentos ruins de propósito: escaneados tortos, campos faltando, dois valores parecidos na mesma página. O caso fácil não ensina o schema. O caso com dois totais (o da capa e o da última página) ensina se o modelo chuta o maior.

Separe no eval os documentos onde o campo existe dos onde ele não existe. Acurácia geral mistura acerto em campo presente com invenção em campo ausente. São dois erros diferentes, com dois consertos: prompt e exemplos para o primeiro, opcionalidade para o segundo.

{ "input": "notas/fornecedor-118.txt", "expected": { "issuer_cnpj": "11222333000181", "issue_date": "2026-08-14", "total": 1234.56 } }

Em produção

  • Log da versão do schema junto com a versão do prompt em cada span.
  • Métrica de quantos documentos falham em cada regra de validação. Uma regra que dispara de repente costuma indicar um fornecedor com layout novo.
  • Tamanho da fila de revisão humana e tempo até alguém olhar.
  • Amostra periódica de documentos aprovados automaticamente, para achar erros que passaram em todas as regras.

Segurança

Saída estruturada reduz uma classe de risco: o código para de interpretar texto livre que pode conter qualquer coisa. Mas o conteúdo dos campos ainda vem do modelo, que leu um documento externo. Trate os valores como entrada não confiável: escape antes de exibir, use parâmetros em consultas SQL e nunca execute um campo como código.

O OWASP Top 10 for LLM Applications trata isso como handling inseguro de saída: o modelo não é uma fonte confiável, mesmo quando a forma está certa. Um campo description copiado para HTML é XSS. Um campo query interpolado em SQL é injeção. Schema não sanitiza.

Prós e contras

Prós Contras
Acaba com parser frágil e expressões regulares O schema vira mais um contrato para versionar
O tipo da saída é conhecido pelo código e pelo compilador Forma certa pode passar a falsa sensação de dado certo
Enums impedem valores fora da lista Campos obrigatórios mal pensados induzem invenção
Facilita evals por campo Não serve para texto livre destinado a pessoas

Código

As abas extraem a mesma nota com schema e validam CNPJ e soma no código. Python e TypeScript usam o SDK da Anthropic. A aba OpenAI é a mesma chamada em outro provedor. LangChain esconde o envelope para os dois.

Python. Use quando o backend é Python. Pydantic define o schema e o tipo ao mesmo tempo.

from datetime import date

import anthropic
from pydantic import BaseModel

client = anthropic.Anthropic()


class Item(BaseModel):
    description: str
    amount: float


class Invoice(BaseModel):
    issuer_cnpj: str
    issue_date: date
    total: float
    items: list[Item]


def valid_cnpj(cnpj: str) -> bool:
    digits = [int(c) for c in cnpj if c.isdigit()]
    if len(digits) != 14 or len(set(digits)) == 1:
        return False
    for size in (12, 13):
        weights = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2][-size:]
        check = sum(d * w for d, w in zip(digits, weights)) % 11
        if digits[size] != (0 if check < 2 else 11 - check):
            return False
    return True


def read_invoice(text: str) -> Invoice | list[str]:
    # 2 e 3. forma garantida pela API
    invoice = client.messages.parse(
        model="claude-opus-5-5",
        max_tokens=4096,
        system="Extraia os dados da nota fiscal. CNPJ só com dígitos. Valores em reais, como número.",
        messages=[{"role": "user", "content": text}],
        output_format=Invoice,
    ).parsed_output

    # 4. conteúdo conferido no código
    errors = []
    if not valid_cnpj(invoice.issuer_cnpj):
        errors.append("cnpj")
    if invoice.issue_date > date.today():
        errors.append("data futura")
    if abs(sum(i.amount for i in invoice.items) - invoice.total) > 0.01:
        errors.append("soma")
    return errors or invoice

O que você ganhou: objeto tipado, sem parser. O que você perdeu: nada que você tinha antes; a validação de negócio continua sendo sua.

TypeScript. Use quando o backend é Node. Zod define o schema e o tipo.

import Anthropic from "@anthropic-ai/sdk";
import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod";
import { z } from "zod";

const client = new Anthropic();

const Invoice = z.object({
  issuer_cnpj: z.string(),
  issue_date: z.string(), // AAAA-MM-DD
  total: z.number(),
  items: z.array(z.object({ description: z.string(), amount: z.number() })),
});
type Invoice = z.infer<typeof Invoice>;

function validCnpj(cnpj: string): boolean {
  const d = cnpj.replace(/\D/g, "").split("").map(Number);
  if (d.length !== 14 || new Set(d).size === 1) return false;
  for (const size of [12, 13]) {
    const weights = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2].slice(-size);
    const check = weights.reduce((acc, w, i) => acc + w * d[i], 0) % 11;
    if (d[size] !== (check < 2 ? 0 : 11 - check)) return false;
  }
  return true;
}

export async function readInvoice(text: string): Promise<Invoice | string[]> {
  // 2 e 3. forma garantida pela API
  const { parsed_output: invoice } = await client.messages.parse({
    model: "claude-opus-5-5",
    max_tokens: 4096,
    system: "Extraia os dados da nota fiscal. CNPJ só com dígitos. Valores em reais, como número. Data em AAAA-MM-DD.",
    messages: [{ role: "user", content: text }],
    output_config: { format: zodOutputFormat(Invoice) },
  });
  if (!invoice) return ["schema"];

  // 4. conteúdo conferido no código
  const errors: string[] = [];
  if (!validCnpj(invoice.issuer_cnpj)) errors.push("cnpj");
  if (new Date(invoice.issue_date) > new Date()) errors.push("data futura");
  const sum = invoice.items.reduce((acc, i) => acc + i.amount, 0);
  if (Math.abs(sum - invoice.total) > 0.01) errors.push("soma");
  return errors.length ? errors : invoice;
}

O que você ganhou: o compilador conhece cada campo. O que você perdeu: datas chegam como texto e precisam de conversão.

Python + OpenAI. Use quando o backend é Python e o contrato do provedor é com a OpenAI.

from datetime import date

from openai import OpenAI

client = OpenAI()  # lê OPENAI_API_KEY do ambiente


def read_invoice(text: str) -> Invoice | list[str]:
    # 2 e 3. forma garantida pela API. Invoice é a classe da aba Python.
    invoice = client.chat.completions.parse(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content": "Extraia os dados da nota fiscal. CNPJ só com dígitos. Valores em reais, como número."},
            {"role": "user", "content": text},
        ],
        response_format=Invoice,
    ).choices[0].message.parsed
    if invoice is None:
        return ["schema"]

    # 4. conteúdo conferido no código
    errors = []
    if not valid_cnpj(invoice.issuer_cnpj):
        errors.append("cnpj")
    if invoice.issue_date > date.today():
        errors.append("data futura")
    if abs(sum(i.amount for i in invoice.items) - invoice.total) > 0.01:
        errors.append("soma")
    return errors or invoice

O que você ganhou: o mesmo schema Pydantic, noutro SDK. O que você perdeu: o nome choices[0].message é da OpenAI; não deixe ele vazar para o módulo contábil.

Python + LangChain. Use quando a extração precisa da mesma função em mais de um provedor.

from datetime import date

from langchain.chat_models import init_chat_model

MODEL = "openai:gpt-4o-mini"  # ou "anthropic:claude-opus-5-5"
reader = init_chat_model(MODEL, temperature=0).with_structured_output(Invoice)


def read_invoice(text: str) -> Invoice | list[str]:
    # 2 e 3. o schema é o mesmo objeto Pydantic da aba Python
    invoice = reader.invoke([
        ("system", "Extraia os dados da nota fiscal. CNPJ só com dígitos. Valores em reais, como número."),
        ("human", text),
    ])

    # 4. conteúdo conferido no código
    errors = []
    if not valid_cnpj(invoice.issuer_cnpj):
        errors.append("cnpj")
    if invoice.issue_date > date.today():
        errors.append("data futura")
    if abs(sum(i.amount for i in invoice.items) - invoice.total) > 0.01:
        errors.append("soma")
    return errors or invoice

O que você ganhou: a validação de negócio não muda quando o prefixo do modelo muda. O que você perdeu: uma dependência a mais para uma chamada que o SDK já fazia.

Suporte por framework

Conferido nas documentações da OpenAI e do LangChain 1.x em outubro de 2026.

Biblioteca Neste padrão Continua seu
SDK da Anthropic messages.parse com Pydantic ou Zod CNPJ, data e soma
SDK da OpenAI chat.completions.parse com o mesmo Pydantic As mesmas regras
LangChain with_structured_output As mesmas regras, sem o envelope do provedor
Instructor ou Pydantic AI O schema na chamada, se o time já usa um desses A validação que o schema não expressa

Teste seu entendimento

Checagem +20 XP de primeiraO bot de suporte responde pessoas em linguagem natural. Um colega quer schema com campo answer: string para 'garantir o formato'. Vale a pena?

Checagem +20 XP de primeiraA triagem precisa de prioridade p1, p2 ou p3. O schema declara priority: string. O modelo devolve 'urgentíssima'. Qual é o conserto?

Checagem +20 XP de primeiraO time grava a nota extraída direto no banco. Um item veio com description contendo um script HTML. O schema passou. De quem é a falha?

Checagem +20 XP de primeiraA primeira chamada com um schema novo demora mais que as seguintes. Um colega quer abandonar saída estruturada por causa dessa latência. O que você responde?

Perguntas rápidas

O que é Saída Estruturada?

Saída Estruturada (Structured Output) é um padrão de projeto de IA para apps com LLM e agentes, do grupo Composição. Exigir que o modelo responda em um schema e validar a resposta.

Quais smells o padrão Saída Estruturada ajuda a curar?

Passagem em Texto Livre e Rascunho Compartilhado.

Onde Saída Estruturada aparece na prática?

Nos cenários reais Extração de NF-e e contratos, Triagem automática de tickets e Triagem de currículos com controle de viés.

Para ir além

Relações

Refatorações que chegam aqui 1
Buscar