Por Que Structured Output Importa para LLMs Locais
E aí, dev! Vamos lá: se sua aplicação lida com dados sensíveis — anotações domésticas, prontuários, logs internos — rodar o modelo localmente é quase obrigatório. Mas rodar o modelo é só o primeiro passo. Na prática, a resposta do LLM vai alimentar outro componente: um scheduler, um writer de banco, um modelo em cloud. Texto livre quebra esse pipeline. Você precisa de estrutura previsível.
Structured Output resolve exatamente isso: você define o formato esperado (um schema) antes, o runtime de serving restringe a geração a esse schema, e o modelo te devolve um objeto Python que seu código consegue parsear e validar sem gambiarra. Neste guia, usamos Gemma 4 (4B) como LLM local, Ollama como runtime e Pydantic para definir e validar o schema.
O padrão funciona — mas tem uma pegadinha que derruba quase todo mundo na primeira implementação. (Se quiser ver a análise original, o artigo do Towards Data Science cobre o tema em detalhe.)
![]()
Mão na Massa: Um Caso de Casa Inteligente
O Cenário
O usuário pergunta: "Devo ligar a lava-louças agora ou depois?" Antes de responder, o app precisa extrair informações de dispositivos, restrições de horário e tarifas de energia das anotações privadas da casa. Um LLM local sanitiza essas notas em um objeto estruturado, que depois vai para um LLM em cloud para raciocínio.
Passo 1 — Defina o Schema com Pydantic
from pydantic import BaseModel
from typing import List
class DeviceToSchedule(BaseModel):
name: str
runtime_minutes: int
earliest_start: str
latest_end: str
class SchedulingContext(BaseModel):
current_time: str
device_in_question: str
controller_capacity_kw: float
electricity_prices: dict[str, float]
devices_to_schedule: List[DeviceToSchedule]
Passo 2 — Instale o Ollama e Baixe o Modelo
# macOS / Linux
curl -fsSL https://ollama.com/install.sh | sh
# Baixa a variante compacta 4B
ollama pull gemma4:4b
# Client Python + Pydantic
pip install ollama pydantic
Passo 3 — Conecte Pydantic ao Ollama
import ollama
from pydantic import BaseModel
response = ollama.chat(
model="gemma4:4b",
messages=[{"role": "user", "content": prompt}],
format=SchedulingContext.model_json_schema(), # força o schema
)
# Parse do JSON de volta para um objeto Python validado
context = SchedulingContext.model_validate_json(response["message"]["content"])
Dois pontos-chave: model_json_schema() converte o modelo Pydantic em JSON Schema que o Ollama usa para restringir a geração. model_validate_json() parseia a resposta de volta em um objeto tipado — prontinho para o próximo passo do pipeline.

A Pegadinha: Estrutura Válida ≠ Conteúdo Correto
Roda o call one-shot e inspeciona o resultado. Na superfície, tudo certo: JSON válido, schema correto, Pydantic parseia sem erro. Mas olha só — o aspirador robô aparece em devices_to_schedule, mesmo com as anotações dizendo claramente que ele terminou às 16:10 e não precisa rodar mais hoje.
Essa é a distinção crítica:
Structured output força o formato da resposta. Não garante, por si só, que o conteúdo esteja correto.
Por Que o Modelo Erra
Em um único call, o Gemma 4B precisa simultaneamente:
- Determinar quais dispositivos ainda precisam de agendamento
- Extrair os fatos relevantes desses dispositivos
- Mapear os fatos para os campos corretos do schema
- Montar o objeto aninhado final
É decisão demais coordenada para um modelo local pequeno. Quanto mais complexo o schema, maior a pressão para coordenar decisões numa única geração — e maior a taxa de erro.
A Solução: Decompor a Tarefa
Quebra o trabalho em duas etapas focadas.
Passo 1 — Determinar escopo (schema pequeno, tarefa estreita):
class SchedulingScope(BaseModel):
devices_to_schedule: List[str]
scope = SchedulingScope.model_validate_json(
ollama.chat(
model="gemma4:4b",
messages=[{"role": "user", "content": scope_prompt}],
format=SchedulingScope.model_json_schema(),
)["message"]["content"]
)
Resultado: ["dishwasher", "ev_charger", "washing_machine"] — aspirador robô corretamente excluído.
Passo 2 — Extrair fatos desses dispositivos selecionados para o SchedulingContext completo, alimentando o output do passo 1 no prompt.
Comparação
| Abordagem | Estrutura Válida | Conteúdo Correto |
|---|---|---|
| Call one-shot | ✅ | ❌ (aspirador incluído) |
| Decomposição em 2 passos | ✅ | ✅ (3 dispositivos batem com as notas) |
Armadilhas Frequentes
- Nunca pule a validação. Sempre chame
model_validate_json()— até modelo com schema forçado pode emitir output malformado em edge cases. - Cuidado com profundidade do schema. Modelos aninhados amplificam o problema de coordenação. Achate quando possível.
- Tamanho do modelo importa. Modelos 4B toleram schemas rasos; aninhamento profundo exige modelos maiores ou mais decomposição.

Considerações Finais
LLM local + structured output é um padrão limpo para workflows com dados sensíveis: define o schema com Pydantic, restringe a geração via argumento format do Ollama, parseia a resposta de volta num objeto tipado. A implementação é realmente simples.
Mas a lição que separa protótipo de produção é essa: estrutura válida não garante conteúdo correto. Quando um modelo pequeno trava com schema complexo, decomposição — separar determinação de escopo da extração de fatos — é a solução prática.
Próximos passos pra explorar:
- Testar
outlinesou grammars dollama.cpppara restrições mais rígidas em tempo de geração - Comparar schema-following entre tamanhos de modelo (4B vs. 12B vs. 27B)
- Adicionar uma camada de validação que cruze fatos extraídos com o texto-fonte
Pra times que rodam workflows confiáveis em escala, a mesma mentalidade de confiabilidade se aplica — veja como o Temporal sustenta operações cloud confiáveis na Netflix e como a Meta migrou ingestão em escala de petabytes sem perda de dados.