Gustavo Ramos
Voltar para o blog
Aprofundado

Obtendo Saída Estruturada Confiável de LLMs Com Zod (O Padrão Que Move Este Blog)

Publicado em2 de julho de 20265 min de leitura
aillmvercel-ai-sdkzodstructured-output

Texto livre é ótimo quando um humano lê a saída. Vira um passivo no momento em que um programa precisa consumi-la — um modelo instruído a "retornar JSON" vai, com frequência razoável, embrulhar isso num bloco de código markdown, adicionar uma frase amigável antes, ou produzir um nome de campo sutilmente diferente do que seu código espera. O generateObject do Vercel AI SDK existe especificamente para fechar essa lacuna: em vez de pedir texto e torcer para que ele seja interpretável, você dá ao modelo um schema e recebe de volta um valor que já é garantidamente compatível com ele.

Este post não é teórico — ele descreve o padrão real que o próprio gerador diário de posts deste blog usa para produzir o conteúdo que você está lendo agora, tirando a chamada ao modelo em si (este post específico foi escrito manualmente em vez de gerado por esse pipeline, mas a lógica de validação abaixo é real e vive neste repositório).

O schema é o contrato, não uma sugestão

import { z } from "zod";

const generatedPostSchema = z.object({
  title: z.object({ en: z.string().min(1), pt: z.string().min(1) }),
  excerpt: z.object({
    en: z.string().min(20).max(280),
    pt: z.string().min(20).max(280),
  }),
  tags: z.array(z.string().min(1)).min(3).max(6),
});

O generateObject pega esse schema, converte para um formato que o provedor de modelo entende e — o ponto crítico — valida a resposta do modelo contra ele antes de retornar. Se a saída do modelo não bater (tipos errados, campos faltando, uma string longa demais), você recebe um erro lançado em vez de um objeto malformado entrando silenciosamente no seu banco de dados ou no seu sistema de arquivos. Essa única propriedade é o que torna a geração estruturada utilizável num pipeline que roda sem supervisão: o modo de falha muda de "algo silenciosamente errado mais adiante" para "uma exceção que você consegue capturar."

Validação além do que o sistema de tipos consegue expressar

O .superRefine() do Zod permite validar regras que um schema simples não consegue expressar — não só "isso é uma string," mas "essa string satisfaz restrições específicas do domínio." O gerador real deste blog usa exatamente isso para impor regras de conteúdo que um tipo TypeScript sozinho não tem como checar:

function validateMdxBody(body: string, ctx: z.RefinementCtx, field: string) {
  const wordCount = body.trim().split(/\s+/).filter(Boolean).length;
  if (wordCount < 500) {
    ctx.addIssue(`${field}: needs at least 500 words, got ${wordCount}`);
  }
  if (/^#{1}(?!#)\s/m.test(body)) {
    ctx.addIssue(`${field}: must not contain an H1 heading`);
  }
}

const schema = z.object({
  bodyEn: z.string().superRefine((b, ctx) => validateMdxBody(b, ctx, "bodyEn")),
  bodyPt: z.string().superRefine((b, ctx) => validateMdxBody(b, ctx, "bodyPt")),
});

Usar superRefine em vez de encadear vários .refine() separados importa por um motivo específico e prático: o superRefine consegue reportar todas as regras violadas numa única passada, em vez de parar na primeira falha. Isso importa bastante para o próximo passo.

Transformando falhas de validação numa tentativa melhor

Um erro de validação não é só um motivo para desistir — é informação que você pode devolver ao modelo. Quando a primeira tentativa falha, alimente a lista específica e detalhada de erros de volta num prompt de acompanhamento, em vez de tentar de novo às cegas:

async function generateWithRetry(prompt: string) {
  let lastIssues: string | undefined;

  for (let attempt = 1; attempt <= 2; attempt++) {
    try {
      const { object } = await generateObject({
        model: "anthropic/claude-sonnet-4-5",
        schema,
        prompt: lastIssues ? `${prompt}\n\nFix these issues: ${lastIssues}` : prompt,
      });
      return object;
    } catch (error) {
      lastIssues = error instanceof Error ? error.message : String(error);
    }
  }
  throw new Error(`Failed validation twice. Last error: ${lastIssues}`);
}

Essa é a diferença entre uma integração frágil e uma resiliente. Uma única nova tentativa com a falha de validação exata incluída no prompt corrige a grande maioria dos problemas da primeira tentativa — modelos geralmente são bons em corrigir um problema específico e nomeado, muito melhores do que em adivinhar qual era uma restrição não declarada.

Falhar alto, não suave, quando é um portão de qualidade

Um detalhe que vale destacar explicitamente: depois de duas tentativas falhas, esse padrão lança um erro em vez de recorrer a algum valor padrão. Isso é deliberado, e é o oposto do padrão de falha suave que você gostaria numa renderização de página ao vivo (onde uma chamada de API falha deveria degradar graciosamente em vez de quebrar a página de um visitante). Um pipeline de geração de conteúdo que roda sem supervisão e escreve num site público é um tipo diferente de sistema — publicar algo que falhou na validação, em silêncio, é pior do que não publicar nada naquele dia. Saída estruturada com Zod não só torna o caminho feliz type-safe; torna o caminho de falha algo que você projeta de propósito, em vez de descobrir por acidente.

Gerado por Claude Sonnet 5 (seeded manually via Claude Code) · 2 de julho de 2026 · verificado em build antes da publicação