Saídas Estruturadas

Visão geral

Saídas estruturadas são respostas de LLMs que devem obedecer a uma estrutura legível por máquina — mais comumente JSON — em vez de texto livre. Na prática, “saídas estruturadas” normalmente significa três técnicas complementares:

  1. Esquemas JSON (JSON Schemas) para especificar qual formato a saída deve ter (campos, tipos, valores permitidos).
  2. Decodificação restrita (constrained decoding) para influenciar como o modelo gera tokens, de modo que permaneça dentro desse formato.
  3. Validação de saída (output validation) para verificar — e, se necessário, reparar — saídas antes que sistemas downstream as consumam.

Saídas estruturadas são uma camada central de confiabilidade para sistemas modernos de IA generativa, especialmente em pipelines de recuperação e ferramental como Chamada de Funções / Uso de Ferramentas, RAG e RAG Agêntico. Elas reduzem a fragilidade de parsing de prompts, previnem muitas classes de erros de formatação e facilitam testar, monitorar e evoluir aplicações alimentadas por LLMs.

Por que saídas estruturadas importam (especialmente em recuperação e ferramental)

LLMs são excelentes em linguagem natural, mas pouco confiáveis como “geradores de strings” para formatos semelhantes a código. Sem estrutura, você frequentemente acaba escrevendo regexes frágeis ou heurísticas de prompt para extrair:

  • uma consulta de busca para um recuperador
  • um nome de ferramenta + argumentos
  • citações vinculadas a fontes recuperadas (Fundamentação & Citações)
  • rótulos de classificação
  • filtros de banco de dados
  • formulários de UI

Saídas estruturadas ajudam porque elas:

  • Transformam respostas de LLM em dados tipados, não texto que você precisa fazer parsing.
  • Melhoram a segurança ao permitir listas de permissão (por exemplo, enums) e rejeitar campos inesperados.
  • Habilitam automação: roteiam para ferramentas, disparam recuperação, constroem UIs, executam fluxos de trabalho.
  • Dão suporte à avaliação: você pode medir “taxa de JSON válido” e “taxa de conformidade com o esquema” como métricas objetivas.

Em pipelines de recuperação — como Busca Híbrida (Esparsa + Densa) seguida de Reordenação (Reranking) — saídas estruturadas facilitam orquestrar etapas como reescrita de consulta, geração de filtros e formatação de citações.

A ideia central: especificar, restringir, validar

Pense em saídas estruturadas como um contrato:

  • Especificação: “A saída deve corresponder a este esquema.”
  • Geração: “Gere apenas saídas que poderiam corresponder a este esquema.”
  • Verificação: “Verifique a saída e trate falhas de forma robusta.”

Em sistemas maduros, você usa as três. Confiar apenas em prompts (“Retorne JSON válido”) raramente é suficiente em produção.

Esquema JSON: definindo o contrato

O que é Esquema JSON

Esquema JSON (JSON Schema) é uma forma padronizada de descrever a estrutura de documentos JSON: propriedades obrigatórias, tipos, valores permitidos, objetos aninhados, arrays, padrões de string e muito mais. Ele é amplamente suportado em diversas linguagens e é um encaixe natural para saídas estruturadas de LLMs.

Um esquema responde perguntas como:

  • Quais chaves são permitidas?
  • Chaves extras são permitidas?
  • Quais tipos os valores devem ter?
  • Quais campos são obrigatórios?
  • Existem valores enumerados?
  • Quão profunda/aninhada é a estrutura?

Exemplo 1: Um esquema para um plano de consulta de recuperação

Em muitos sistemas de RAG, você quer que o modelo produza um plano de busca estruturado: texto da consulta mais filtros de metadados (intervalo de tempo, tipos de documento, área de produto etc.).

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "query": { "type": "string", "minLength": 1 },
    "filters": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "product": { "type": "string" },
        "doc_type": { "type": "string", "enum": ["policy", "runbook", "spec", "faq"] },
        "updated_after": { "type": "string", "format": "date-time" }
      }
    },
    "top_k": { "type": "integer", "minimum": 1, "maximum": 50 }
  },
  "required": ["query", "top_k"]
}

Principais escolhas de design:

  • additionalProperties: false impede o modelo de inventar campos que o código downstream não espera.
  • enum cria uma lista de permissão, o que é útil para roteamento e segurança.
  • limites (minimum, maximum) evitam requisições patológicas.

Exemplo 2: Citações estruturadas para respostas fundamentadas

Para Fundamentação & Citações, você pode querer que o modelo retorne uma resposta final mais citações legíveis por máquina que referenciem trechos recuperados por ID:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "answer": { "type": "string" },
    "citations": {
      "type": "array",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "chunk_id": { "type": "string" },
          "quote": { "type": "string" }
        },
        "required": ["chunk_id"]
      }
    }
  },
  "required": ["answer", "citations"]
}

Essa estrutura torna simples:

  • renderizar citações em uma UI,
  • verificar se cada chunk_id citado existe,
  • detectar “alucinação de citação” (um trecho citado que nunca foi recuperado).

Recursos comuns de esquema que importam para LLMs

  • required: força a presença de campos críticos.
  • additionalProperties: false: reduz chaves inesperadas.
  • enum: restringe a rótulos/ferramentas permitidos.
  • oneOf / anyOf: representa variantes (use com cuidado — pode aumentar a confusão do modelo).
  • restrições de string (pattern, minLength) e restrições numéricas (minimum, maximum).
  • objetos/arrays aninhados: poderosos, mas aninhamento profundo aumenta as taxas de falha.

Dica prática: mantenha esquemas tão pequenos e planos quanto possível enquanto atende aos requisitos. Esquemas complexos demais aumentam saídas inválidas e dificultam o debugging.

Decodificação restrita: tornando saídas inválidas mais difíceis de gerar

O que é decodificação restrita

Decodificação restrita (constrained decoding) modifica o processo de geração de tokens para que o modelo só possa emitir tokens que mantenham a saída parcial consistente com uma restrição — frequentemente uma gramática JSON derivada do seu esquema.

Em alto nível, a decodificação comum amostra tokens de uma distribuição:

[ p(x_t \mid x_{<t}) ]

A decodificação restrita aplica uma máscara sobre tokens que violariam restrições, resultando em:

[ p'(x_t \mid x_{<t}) \propto p(x_t \mid x_{<t}) \cdot \mathbb{1}[\text{token is allowed}] ]

Onde o indicador é 1 apenas se adicionar o token mantém a saída válida com respeito a uma gramática/máquina de estados.

Por que apenas prompts são insuficientes

Um prompt como “Produza JSON válido” é uma instrução suave. O modelo ainda pode:

  • esquecer uma chave de fechamento,
  • produzir comentários ao final,
  • usar aspas simples em vez de aspas duplas,
  • emitir NaN ou Infinity (JSON inválido),
  • desencontrar colchetes em estruturas aninhadas,
  • violar enums ou campos obrigatórios.

A decodificação restrita transforma muitos desses casos em gerações impossíveis.

Mecanismos comuns de restrição

  1. Modo somente JSON / gramática JSON

    • Garante que a saída seja JSON sintaticamente válido.
    • Não garante conformidade com o esquema (por exemplo, campos obrigatórios).
  2. Decodificação guiada por esquema

    • Usa o esquema para restringir chaves, tipos e às vezes até formatos de valor.
    • Normalmente implementada compilando fragmentos do esquema em uma gramática ou parser incremental.
  3. Geração guiada por regex ou padrão

    • Útil para saídas mais simples (IDs, rótulos curtos).
    • Pode ser frágil com tokenizadores (regex opera em caracteres; a decodificação opera em tokens).
  4. Restrições de chamada de ferramenta/função

    • Muitos sistemas tratam chamadas de ferramentas como objetos estruturados internamente, restringindo o modelo a escolher um nome de ferramenta e emitir argumentos JSON compatíveis com um esquema declarado. Veja Chamada de Funções / Uso de Ferramentas.

Exemplo conceitual: restrição incremental de JSON

Abaixo está um pseudocódigo ilustrando a ideia (não vinculado a uma biblioteca específica):

state = JsonParserState()  # tracks whether we're in a string, expecting a key, etc.

output_tokens = []
for step in range(max_tokens):
    logits = model.next_logits(output_tokens)

    allowed = state.allowed_next_tokens(tokenizer.vocab)
    masked_logits = mask(logits, allowed)

    token = sample(masked_logits, temperature=0.2)
    output_tokens.append(token)

    state = state.consume(tokenizer.decode([token]))

    if state.is_complete_json_document():
        break

json_text = tokenizer.decode(output_tokens)

Uma versão guiada por esquema substitui JsonParserState por uma máquina de estados mais rica que também rastreia:

  • quais chaves de objeto são permitidas em seguida,
  • se chaves obrigatórias já foram emitidas,
  • se o valor atual deve ser string/número/array/etc.

Limitações e trade-offs

  • Desalinhamento do tokenizador: restrições frequentemente operam em caracteres; LLMs emitem tokens. Algumas strings podem ser difíceis de restringir perfeitamente.
  • Desempenho: mascarar tokens e manter estado do parser adiciona overhead, especialmente para esquemas complexos.
  • Restrição excessiva: se a restrição for rígida demais ou incompatível com a distribuição aprendida do modelo, as saídas podem ficar pouco naturais ou o modelo pode “lutar” contra as restrições (por exemplo, produzindo JSON estranho, porém válido).
  • Validade semântica não é garantida: decodificação restrita pode garantir estrutura, não veracidade.

Na prática, é melhor enxergar a decodificação restrita como uma garantia de sintaxe e formato, não uma garantia de correção.

Validação de saída: confie, mas verifique

Mesmo com decodificação restrita, validação é essencial. A validação detecta:

  • saídas parciais/truncadas (interrupções de rede, limites máximos de tokens),
  • casos de borda do esquema não cobertos por restrições de decodificação,
  • restrições semânticas (IDs devem existir; intervalos de datas devem ser válidos),
  • dados estruturados “válidos, porém errados”.

Duas camadas: validação sintática e semântica

  1. Validação sintática

    • É JSON válido?
    • Corresponde ao Esquema JSON (tipos, campos obrigatórios, enums)?
  2. Validação semântica

    • chunk_id referencia um trecho recuperado?
    • top_k é apropriado dado o tamanho do índice?
    • Filtros são permitidos para este usuário/tenant?
    • A consulta atende a regras de segurança/política?

Exemplo: validando uma saída do modelo em Python

Usando jsonschema:

import json
from jsonschema import validate, Draft202012Validator

schema = {
    "type": "object",
    "additionalProperties": False,
    "properties": {
        "query": {"type": "string", "minLength": 1},
        "top_k": {"type": "integer", "minimum": 1, "maximum": 50},
    },
    "required": ["query", "top_k"]
}

def parse_and_validate(text: str) -> dict:
    data = json.loads(text)  # raises on invalid JSON
    Draft202012Validator(schema).validate(data)  # raises on schema mismatch
    return data

Usar um modelo tipado (por exemplo, abordagem no estilo Pydantic) fornece uma camada adicional de ergonomia:

from pydantic import BaseModel, Field

class SearchPlan(BaseModel):
    query: str = Field(min_length=1)
    top_k: int = Field(ge=1, le=50)

plan = SearchPlan.model_validate_json(llm_text)

Modelos tipados frequentemente são mais fáceis de manter do que dicionários de esquema “crus”, ao mesmo tempo em que fornecem validação forte.

O que fazer quando a validação falha

Sistemas robustos implementam caminhos explícitos de tratamento de falha:

  • Tentar novamente com um prompt de “reparo”: forneça o JSON inválido e o erro de validação e peça ao modelo para retornar um objeto JSON corrigido apenas.
  • Fallback para um esquema mais simples: se um esquema complexo falha com frequência, peça primeiro um subconjunto mínimo e depois expanda em uma segunda etapa.
  • Aceitação parcial: se campos não críticos falharem, descarte-os e continue (cuidado: isso pode ocultar problemas).
  • Escalonar: registre a falha, retorne um erro para o usuário ou roteie para um humano/modelo alternativo.

Um padrão comum de reparo:

You returned JSON that failed validation.

Validation error:
- 'top_k' must be <= 50

Return a corrected JSON object that matches this schema exactly:
{ ...schema... }

Return JSON only.

Evitando corrupção “silenciosa”

Não “conserte” saídas com hacks ad-hoc de string (por exemplo, regex substituindo aspas) a menos que você também revalide. Um loop seguro é:

  1. fazer parse de JSON
  2. validar o esquema
  3. aplicar verificações semânticas
  4. só então executar ferramentas / recuperação

Aplicações práticas em sistemas de IA generativa

Invocação e orquestração de ferramentas

Saídas estruturadas são a espinha dorsal do uso de ferramentas: o modelo deve selecionar uma ferramenta e produzir argumentos com os tipos corretos. Esse padrão é central para Chamada de Funções / Uso de Ferramentas e muitas arquiteturas de RAG Agêntico, nas quais o agente emite repetidamente “ações” estruturadas (buscar, obter, reordenar, resumir).

Um esquema típico de “chamada de ferramenta”:

{
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "tool": { "type": "string", "enum": ["vector_search", "web_search", "lookup_doc"] },
    "args": { "type": "object" }
  },
  "required": ["tool", "args"]
}

Então cada ferramenta pode ter seu próprio esquema de args, ou você pode modelar variantes com oneOf (com cautela).

Reescrita e filtragem de consulta de recuperação

Antes de chamar Busca Vetorial & Embeddings ou Busca Híbrida (Esparsa + Densa), você pode pedir ao modelo para gerar:

  • uma consulta reescrita para busca semântica,
  • filtros estruturados a partir do pedido do usuário,
  • um top_k com base na ambiguidade.

Isso reduz lógica de prompt na sua aplicação e torna a etapa de recuperação mais testável.

Respostas fundamentadas com citações rastreáveis

Em vez de pedir citações em prosa (“[1] [2]”), exija uma lista estruturada que referencie IDs de trechos recuperados. Isso torna possível validar a fundamentação: IDs citados devem existir no contexto recuperado. Essa é uma técnica-chave em Fundamentação & Citações.

Extração de dados (IE) de documentos

Em pipelines de ingestão (veja Ingestão & Segmentação), saídas estruturadas podem extrair campos como:

Aqui, o design do esquema é crucial: tarefas de extração frequentemente se beneficiam de:

  • campos opcionais (ausência é válida),
  • tipos normalizados (datas, moeda),
  • escores de confiança (com interpretação cuidadosa).

Boas práticas de design de esquema para LLMs

Mantenha-o mínimo e determinístico

  • Prefira chaves fixas a dicionários dinâmicos.
  • Prefira enums a rótulos livres.
  • Evite estruturas profundamente aninhadas, a menos que seja necessário.
  • Desabilite propriedades adicionais, a menos que você realmente precise delas.

Separe campos “finais” de “explicações”

Se você quiser que o modelo forneça racional, você pode:

  • solicitar um campo explanation separado (e decidir se ele é armazenado/exibido), ou
  • manter a saída estruturada estritamente para consumo por máquina e gerar explicações separadamente.

Um padrão comum é:

  • Etapa 1: produzir plano estruturado (JSON)
  • Etapa 2: executar ferramentas/recuperação
  • Etapa 3: produzir resposta voltada ao usuário (texto), opcionalmente com citações estruturadas

Essa separação reduz vazamento acidental de campos internos e simplifica a validação.

Versione seus esquemas

Em produção, esquemas evoluem. Adicione um campo schema_version à saída para oferecer compatibilidade retroativa e rollouts mais seguros.

{ "schema_version": "1.2", "query": "...", "top_k": 10 }

Tenha cuidado com `oneOf` e polimorfismo

Esquemas variantes (oneOf) podem ser poderosos, mas aumentam a ambiguidade. Se você precisar usá-los, adicione um campo discriminante:

{
  "type": "object",
  "properties": {
    "kind": { "type": "string", "enum": ["by_id", "by_query"] }
  },
  "required": ["kind"],
  "allOf": [
    {
      "if": { "properties": { "kind": { "const": "by_id" } } },
      "then": { "required": ["doc_id"] }
    }
  ]
}

(Construtos exatos do Esquema JSON variam por draft; teste com seu validador.)

Modos comuns de falha e como mitigá-los

1) “JSON válido, significado errado”

Exemplo: top_k: 50 sempre, independentemente da necessidade.

Mitigação:

  • validação semântica,
  • calibrar com exemplos,
  • limitar intervalos e aplicar heurísticas do lado do servidor.

2) Truncamento / objetos incompletos

Mitigação:

  • impor condições de parada (objeto JSON completo),
  • definir limites adequados de tokens,
  • validar e tentar novamente.

3) Referências alucinadas (IDs, citações, nomes de ferramenta)

Mitigação:

  • enums para nomes de ferramenta,
  • validar IDs contra conjuntos conhecidos,
  • vincular IDs de citação ao contexto recuperado.

4) Esquemas rígidos demais causando muitas tentativas

Mitigação:

  • começar com um esquema mínimo e adicionar complexidade incrementalmente,
  • relaxar campos opcionais,
  • evitar padrões complexos de regex a menos que necessário.

5) Problemas de segurança (injeção de prompt em args de ferramenta)

Saídas estruturadas ajudam, mas não resolvem a segurança de ferramentas por si só.

Mitigação:

  • listas de permissão do lado do servidor,
  • checagens de autorização em cada chamada de ferramenta,
  • tratar args de ferramenta como entrada não confiável (validar, sanitizar),
  • registrar e monitorar.

Testes e monitoramento de saídas estruturadas

Saídas estruturadas fornecem sinais mensuráveis de confiabilidade:

  • Taxa de sucesso de parse de JSON
  • Taxa de sucesso de validação do esquema
  • Taxa de retry / taxa de reparo
  • Deriva de distribuição em campos-chave (por exemplo, top_k subindo gradualmente)
  • Taxas de validade semântica (por exemplo, % de citações que correspondem a trechos recuperados)

Use conjuntos de teste offline e testes de regressão. Se você empregar cache (veja Cache), lembre-se de que mudanças no esquema podem invalidar resultados em cache.

Juntando tudo: um loop robusto de saída estruturada

Um padrão prático de produção se parece com:

  1. Pedir saída estruturada (com esquema / contrato tipado).
  2. Decodificar com restrições quando disponível (guiada por JSON ou por esquema).
  3. Fazer parse de JSON de forma estrita.
  4. Validar contra o Esquema JSON (ou modelo tipado).
  5. Checagens semânticas (IDs, permissões, restrições).
  6. Reparar/tentar novamente com erros de validação explícitos se necessário.
  7. Executar etapas downstream (recuperação/ferramentas).
  8. Registrar o objeto estruturado + resultados de validação para monitoramento.

Isso transforma um gerador probabilístico de texto em um componente que se comporta muito mais como um serviço de software convencional.

Resumo

Saídas estruturadas são um alicerce de confiabilidade para sistemas modernos de LLMs — especialmente aqueles que recuperam informações e chamam ferramentas. Esquemas JSON definem o contrato, a decodificação restrita reduz erros de formatação e de formato no momento da geração, e a validação de saída garante que sistemas downstream consumam apenas dados bem formados e autorizados. Usadas em conjunto, essas técnicas tornam aplicações com LLMs mais testáveis, mais seguras e mais fáceis de integrar a pipelines reais de software em RAG, Chamada de Funções / Uso de Ferramentas e fluxos de trabalho agênticos.