O time de contratos de uma empresa de logística pediu ao modelo: liste os dados principais deste arquivo. E a resposta voltou como um parágrafo simpático.
O contrato é com a Transportadora Aurora. Valor total, doze mil e quatrocentos dólares. Começa em janeiro, renovação automática, doze meses. Uma expressão regular tirava o total. E funcionou por semanas.
Até que um dia o modelo escreveu o valor por extenso. Doze mil e quatrocentos dólares, escrito com letras.
E na data de início, só janeiro.
O sistema gravou o total vazio, e a data de início vazia. Ninguém percebeu até o financeiro conciliar o mês seguinte.
E o parser também deixava passar coisas que só pareciam certas. Um contrato terminando num ano e começando no seguinte entrou, porque o texto tinha as duas datas, e a expressão regular pegou as duas. Nesta aula, você vai fazer a refatoração que acaba com isso, com segurança, medindo antes e depois.
A ideia
A solução tem três movimentos.
Defina um schema para o que esta etapa entrega.
Peça ao modelo que responda nesse schema, usando o recurso de saída estruturada da API. Não pedindo no prompt.
E valide o resultado no código, antes de seguir.
Antes e depois
Antes, o formato é uma convenção que ninguém escreveu. O modelo escreve texto, uma expressão regular adivinha, e o que ela achar vai para o sistema. Inclusive nada.
Depois, o formato é um contrato, conferido nas duas pontas. A mesma chamada de extração, agora com um schema junto.
A API devolve um objeto que segue o schema. Tipos certos, campos obrigatórios presentes.
O código confere as regras que um schema não expressa, como datas que fazem sentido juntas.
E só dado válido chega ao destino. As falhas vão para revisão, com o motivo junto.
Aqui está o mesmo contrato, agora como objeto. O CNPJ, só com dígitos, porque o schema pediu isso. O total como inteiro, em centavos. Datas num formato fixo. Renovação automática, verdadeiro ou falso.
Nada de doze mil por extenso. E se o arquivo não tem data de fim, esse campo volta vazio, de forma explícita. Não como uma frase tipo prazo indeterminado que o parser antigo nunca esperava.
Por que funciona
Com texto livre, o formato é uma promessa implícita. O modelo costuma escrever de um jeito, e o seu código conta com isso. Com saída estruturada, o formato vira parte da chamada. A API restringe o que o modelo pode gerar, token a token, então a resposta sempre fecha em dado válido, com os campos obrigatórios.
O schema também melhora a extração. Nomes de campo, tipos e descrições claros funcionam como instrução. Total em centavos, inteiro, diz muito mais que o valor total. O modelo não precisa decidir se escreve dólares, centavos ou o número por extenso.
E o erro muda de lugar. Em vez de um campo vazio que aparece semanas depois, a falha aparece na hora, na etapa que falhou. Quem revisa recebe o objeto e o motivo. Não um parágrafo para reler.
Um desafio
Uma pergunta para você. O seu schema tem um campo de multa obrigatório, inteiro. Mas metade dos contratos da pasta nem tem cláusula de multa. O eval mostra a multa preenchida nesses casos mesmo assim, quase sempre zero, ou um número copiado de outro campo. O que você faz?
Torne o campo opcional e valide no código o que voltar preenchido. Um campo obrigatório num documento que às vezes não tem o dado empurra o modelo a inventar.
Mandar o prompt usar zero quando não há multa faz o zero significar duas coisas. Multa zero e multa ausente. A próxima etapa não consegue diferenciar. E tirar o campo joga fora a metade dos contratos em que o jurídico precisa dele. Ausência se modela com opcional, não apagando o campo.
Passo a passo seguro
Agora o passo a passo, numa ordem que mantém você seguro.
O passo zero é medir. Monte um eval com documentos reais, com o valor esperado de cada campo. Rode a versão atual e anote o acerto por campo e a taxa de campo vazio. Um campo que já volta vazio 20% das vezes, porque a expressão regular falhou, é o primeiro a ganhar.
Depois, liste o que a próxima etapa usa de verdade. Se o sistema só guarda o CNPJ, o total, as datas e a renovação, esses são os seus campos. Resumos, adjetivos e principais obrigações saem.
Escreva o schema com tipos fortes. Inteiro para centavos, data de verdade, lista fechada. E uma descrição curta em cada campo. Essa descrição é o prompt do campo.
E decida com honestidade o que é opcional. Não deixe o modelo inventar data de fim para um contrato sem fim.
Aí troque a chamada pela versão estruturada e apague o parser antigo. Enquanto os dois existirem, alguém vai só ajustar a expressão regular mais uma vez.
Coloque validação de negócio no código. Itens que somam o total. Data de fim depois da de início. Dígito verificador do CNPJ.
Mande as falhas para revisão, com o objeto e o motivo, em vez de gravar dado pela metade.
E meça de novo. O acerto por campo deve se manter ou subir. Campo vazio por erro de parse deve cair a zero. Erro de leitura continua existindo, como o modelo ler doze mil e quatrocentos como mil duzentos e quarenta. Mas agora ele aparece na validação, não semanas depois.
Riscos
Alguns riscos para ficar de olho. Um schema que obriga a inventar. Acabamos de ver com a multa.
Um schema grande demais. Dezenas de campos aninhados, e a qualidade cai. O modelo espalha a atenção e começa a preencher no chute, normalmente nos campos do meio. Se o documento tem duas partes, como a capa e os anexos, divida em duas chamadas, cada uma com o que a próxima etapa usa.
Confiar só no schema. Dado válido ainda pode estar errado. Um total de um centavo é um inteiro perfeitamente válido, e um contrato absurdo. Some os itens no código e mande para revisão quando os números não baterem.
E algumas regras o modo estruturado do provedor não aceita, como certos padrões. Tudo bem. Peça um texto simples e valide os catorze dígitos no código, logo depois da chamada.
E o erro mais comum. Trocar a expressão regular pela saída estruturada e deixar o total com tipo texto. O modelo continua livre para devolver o valor com símbolo de moeda, como número simples ou por extenso. Você só mudou o parser para um campo com nome bonito. É o tipo forte que fecha a porta.
Além da extração
Essa refatoração serve além de arquivos. Imagine dois agentes dividindo um bloco de texto, uma espécie de rascunho do processo. Um escreve: cliente ok, pode seguir. O outro entende isso como aprovação de crédito.
A mesma cura. O rascunho vira um objeto, com um campo como status de crédito, de uma lista fechada de valores, validado antes de o próximo agente ler. A prosa do meio vira contrato, e o segundo agente para de interpretar ok.
Fechamento
Então, a ideia para levar. O parser que adivinha não é um bug para consertar mais uma vez. É um contrato que ninguém escreveu. Meça, escreva o contrato como um schema com tipos fortes, valide o que o schema não expressa e apague o parser de vez.
Na próxima aula, a gente olha a mesma extração por outro ângulo: o modelo fazendo conta e regra que o código acertaria sempre. LLM doing deterministic work. Até lá.
15Rf
RefatoraçãoOrganizando dados
Trocar Texto Livre por Saída Estruturada
Replace Free Text with Structured Output
Trocar Texto Livre por Saída Estruturada (Replace Free Text with Structured Output) é uma refatoração de IA para sistemas com LLM, do grupo Organizando dados. Definir um schema para a saída da etapa e validar antes de seguir.
Um passo entrega texto solto para o próximo, que precisa adivinhar onde está cada dado. É o smell Passagem em Texto Livre, e também o Rascunho Compartilhado quando vários agentes escrevem prosa no mesmo lugar.
O time de contratos de uma empresa de logística pedia ao modelo: “Liste os dados principais deste PDF”. A resposta vinha assim:
O contrato é com a Transportadora Aurora Ltda (CNPJ 12.345.678/0001-90).Valor total: R$ 12.400,00. Início em 03/01/2025, renovação automática.Prazo: 12 meses.
Um regex extraía o total. Funcionou por semanas. No dia em que o modelo escreveu “doze mil e quatrocentos reais” e “início: janeiro de 2025”, o ERP gravou total = None e start_date = None. Ninguém notou até o financeiro conciliar no mês seguinte.
O parser também aceitava o que parecia certo. Um contrato com fim em 2024 e início em 2025 passou, porque o texto tinha as duas datas e o regex pegou as duas. A incoerência só apareceu quando o jurídico tentou rescindir.
Solução
Defina um schema para a saída do passo, peça ao modelo que responda nesse schema usando o recurso de saída estruturada da API e valide o resultado antes de seguir.
Antes e depois
-summary = llm("Liste os dados principais do contrato.", pdf_text)-match = re.search(r"Total:\s*R\$\s*([\d\.,]+)", summary)-total = parse_brl(match.group(1)) if match else None+class Contract(BaseModel):+ supplier_cnpj: str = Field(pattern=r"^\d{14}$")+ total_cents: int+ start_date: date+ end_date: date | None+ auto_renewal: bool++contract = llm_parse("Extraia os dados do contrato.", pdf_text, schema=Contract)+if contract.end_date and contract.end_date < contract.start_date:+ raise InvalidExtraction("fim antes do início")+erp.save(contract)
Antes: o formato é uma convenção que ninguém escreveu.
flowchart LR
A(["1. Extrair dados"]):::llm --> T["2. Texto livre"]:::tool
T --> R["3. Regex"]:::tool
R --> E[("4. ERP")]:::store
Depois: o formato é um contrato, checado nas duas pontas.
flowchart LR
A(["1. Extrair dados"]):::llm --> T["2. Objeto no schema"]:::tool
T --> R["3. Validação de negócio"]:::tool
R --> E[("4. ERP")]:::store
R -->|"falhou"| Q((("Revisão"))):::human
A mesma chamada de extração, agora com um schema anexado.
A API devolve um objeto que obedece ao schema: tipos certos, campos obrigatórios presentes.
O código checa regras que o schema não expressa, como datas coerentes.
Só dado válido chega ao destino. O que falha vai para revisão, com o motivo.
O mesmo contrato, dois retornos
Com texto livre, o passo seguinte ainda precisa adivinhar. Com o schema, o objeto já chega pronto:
Não existe “R$ 12.400,00” nem “doze mil”. O CNPJ veio só com dígitos, porque o schema pediu isso. Se o PDF não tiver data de fim, end_date é null, e não uma frase como “prazo indeterminado” que o parser antigo não previa.
Por que funciona
Com texto livre, o formato é uma promessa implícita: o modelo costuma escrever de um jeito, e o código conta com isso. Com saída estruturada, o formato vira parte da chamada. A API restringe o que o modelo pode gerar (constrained decoding: o próximo token só é válido se continuar um JSON que ainda pode fechar no schema). O JSON é sempre válido e sempre tem os campos que o schema exige.
O schema também melhora a extração. Nomes de campo claros, tipos e descrições funcionam como instrução: total_cents: int diz mais que “o valor total”. O modelo não precisa decidir se escreve real, centavos ou “doze mil”.
O erro muda de lugar. Em vez de um None que aparece no ERP semanas depois, a falha aparece na hora, no passo que falhou. Uma data invertida não chega a gravar. Um CNPJ com pontuação não passa no pattern. Quem revisa recebe o objeto e o motivo, não um parágrafo para reler.
O padrão de destino é Saída Estruturada. Esta refatoração é o caminho até ele, a partir de um parser que já existe e já quebra.
Passo a passo seguro
Meça. Monte um eval com documentos reais e os valores esperados por campo. Rode a versão atual e guarde a acurácia por campo e a taxa de campos vazios. Um campo que já vem vazio 20% das vezes (porque o regex falha) é o primeiro a se beneficiar.
Liste o que o passo seguinte usa de verdade. Se o ERP só grava CNPJ, total, datas e renovação, esses são os campos. Trechos de resumo, adjetivos e “principais obrigações” saem.
Escreva o schema com tipos fortes (inteiro em centavos, date, enum) e uma descrição curta em cada campo. A descrição é o prompt do campo.
Decida o que é opcional. Campo que pode faltar no documento vira opcional e explícito. Não deixe o modelo inventar um end_date para contrato por prazo indeterminado.
Troque a chamada pela versão com saída estruturada e apague o parser antigo. Enquanto os dois existirem, alguém vai consertar o regex “só mais uma vez”.
Acrescente validação de negócio em código: soma de itens contra total, data de fim depois da de início, dígito verificador de CNPJ.
Mande o que falhar para revisão com o objeto e o motivo, em vez de gravar parcial.
Meça de novo. A acurácia por campo deve se manter ou subir, e campos vazios por erro de parsing devem chegar a zero. Erros de leitura (o PDF diz 12.400 e o modelo extrai 1.240) continuam existindo: eles agora aparecem na validação, não no regex.
Riscos
Schema que força invenção. Um campo obrigatório que não existe no documento leva o modelo a inventar um valor. Use opcional, ou um valor explícito como null com um campo end_date_reason.
Schema grande demais. Dezenas de campos aninhados pioram a qualidade: o modelo espalha atenção e começa a preencher com palpite. Divida em duas chamadas se o documento tiver duas partes (capas e anexos, por exemplo).
Confiar só no schema. Um JSON válido pode ter um valor errado. total_cents: 1 é um inteiro legal e um contrato absurdo. A validação de negócio continua necessária.
Recursos de schema não suportados. Algumas restrições (certos formatos, limites numéricos, regex elaborada) podem não ser aceitas pelo modo estruturado do provedor. Valide essas regras em código, depois que o objeto chegou.
Erro comum: string que “parece” estruturada
Trocar o regex por llm_parse e deixar total: str não resolve. O modelo continua livre para devolver "R$ 12.400,00", "12400" ou "doze mil". Você só moveu o parser para um campo com nome bonito. Tipo forte é o que fecha a porta.
Desafio
Este schema vai gerar dados ruins. Encontre dois problemas.
total e date como texto voltam ao problema de formato (“R$ 1.200,00”, “12/03/26”). discount obrigatório força o modelo a inventar um desconto quando a nota não tem.
class Invoice(BaseModel): total_cents: int = Field(description="Valor total da nota em centavos") issue_date: date customer_cnpj: str = Field(description="Só os 14 dígitos") discount_cents: int | None = Field(description="Nulo se a nota não tem desconto")
Teste seu entendimento
Checagem da aula
Responda as 6 perguntas para concluir a aula. Só assistir não conclui: a checagem é o que conta para a trilha e para o certificado.
Perguntas rápidas
O que é Trocar Texto Livre por Saída Estruturada?
Trocar Texto Livre por Saída Estruturada (Replace Free Text with Structured Output) é uma refatoração de IA para sistemas com LLM, do grupo Organizando dados. Definir um schema para a saída da etapa e validar antes de seguir.
Que problema Trocar Texto Livre por Saída Estruturada resolve?