Restringindo LLMs com Saída Estruturada: Ollama, Qwen3 e Python ou Go

Algumas formas de obter saída estruturada do Ollama

Conteúdo da página

Modelos de Linguagem de Grande Escala (LLMs) são poderosos, mas em produção raramente queremos parágrafos livres. Em vez disso, queremos dados previsíveis: atributos, fatos ou objetos estruturados que você pode alimentar em um aplicativo. Isso é Saída Estruturada de LLM.

A imposição de esquema reduz a frequência com que logits ruins se transformam em JSON inválido, mas temperatura e penalidades ainda são importantes para tempestades de repetição; veja parâmetros de inferência agêntica para Qwen e Gemma quando você combina restrições de format com agentes.

Algum tempo atrás, Ollama introduziu suporte a saída estruturada (anúncio), tornando possível restringir as respostas de um modelo para que correspondam a um esquema JSON. Isso desbloqueia pipelines de extração de dados consistentes para tarefas como catalogar recursos de LLMs, benchmark de modelos ou automatizar integração de sistemas.

patos alinhados

Neste post, cobriremos:

  • O que é saída estruturada e por que isso é importante
  • Uma maneira simples de obter saída estruturada de LLMs
  • Como o novo recurso da Ollama funciona
  • Exemplos de extração de capacidades de LLM:

O que é Saída Estruturada?

Normalmente, LLMs geram texto livre:

“O Modelo X suporta raciocínio com chain-of-thought (raciocínio passo a passo), tem uma janela de contexto de 200K e fala inglês, chinês e espanhol.”

Isso é legível, mas difícil de analisar (parsear).

Em vez disso, com saída estruturada, pedimos um esquema rigoroso:

{
  "name": "Modelo X",
  "supports_thinking": true,
  "max_context_tokens": 200000,
  "languages": ["Inglês", "Chinês", "Espanhol"]
}

Este JSON é fácil de validar, armazenar em um banco de dados ou alimentar em uma interface do usuário.


Maneira simples de obter Saída Estruturada de LLM

Os LLMs às vezes entendem qual é o esquema e podemos pedir ao LLM para retornar a saída em JSON usando um esquema particular. O modelo Qwen3 da Alibaba é otimizado para raciocínio e respostas estruturadas. Você pode instruí-lo explicitamente a responder em JSON.

Exemplo 1: Usando Qwen3 com ollama em Python, solicitando JSON com esquema

import json
import ollama

prompt = """
Você é um extrator de dados estruturados.
Retorne apenas JSON.
Texto: "Elon Musk tem 53 anos e vive em Austin."
Esquema: { "name": string, "age": int, "city": string }
"""

response = ollama.chat(model="qwen3", messages=[{"role": "user", "content": prompt}])
output = response['message']['content']

# Parsear JSON
try:
    data = json.loads(output)
    print(data)
except Exception as e:
    print("Erro ao analisar JSON:", e)

Saída:

{"name": "Elon Musk", "age": 53, "city": "Austin"}

Impondo Validação de Esquema com Pydantic

Para evitar saídas malformadas, você pode validar contra um esquema Pydantic em Python.

from pydantic import BaseModel

class Person(BaseModel):
    name: str
    age: int
    city: str

# Suponha que 'output' seja a string JSON do Qwen3
data = Person.model_validate_json(output)
print(data.name, data.age, data.city)

Isso garante que a saída esteja em conformidade com a estrutura esperada.


Saída Estruturada do Ollama

O Ollama agora permite que você passe um esquema no parâmetro format. O modelo então é restrito a responder apenas em JSON que esteja em conformidade com o esquema (documentação).

Em Python, você tipicamente define seu esquema com Pydantic e permite que o Ollama use isso como um esquema JSON.


Exemplo 2: Extrair Metadados de Recursos de LLM

Suponha que você tenha um trecho de texto descrevendo as habilidades de um LLM:

“O Qwen3 tem forte suporte multilíngue (Inglês, Chinês, Francês, Espanhol, Árabe). Ele permite etapas de raciocínio (chain-of-thought). A janela de contexto é de 128K tokens.”

Você quer dados estruturados:

from pydantic import BaseModel
from typing import List
from ollama import chat

class LLMFeatures(BaseModel):
    name: str
    supports_thinking: bool
    max_context_tokens: int
    languages: List[str]

prompt = """
Analise a seguinte descrição e retorne os recursos do modelo apenas em JSON.
Descrição do modelo:
'O Qwen3 tem forte suporte multilíngue (Inglês, Chinês, Francês, Espanhol, Árabe).
Ele permite etapas de raciocínio (chain-of-thought).
A janela de contexto é de 128K tokens.'
"""

resp = chat(
    model="qwen3",
    messages=[{"role": "user", "content": prompt}],
    format=LLMFeatures.model_json_schema(),
    options={"temperature": 0},
)

print(resp.message.content)

Saída possível:

{
  "name": "Qwen3",
  "supports_thinking": true,
  "max_context_tokens": 128000,
  "languages": ["Inglês", "Chinês", "Francês", "Espanhol", "Árabe"]
}

Exemplo 3: Comparar Múltiplos Modelos

Forneça descrições de vários modelos e extraia-os em formato estruturado:

from typing import List

class ModelComparison(BaseModel):
    models: List[LLMFeatures]

prompt = """
Extraia os recursos de cada modelo para JSON.

1. Llama 3.1 suporta raciocínio. A janela de contexto é 128K. Idiomas: Apenas Inglês.
2. GPT-4 Turbo suporta raciocínio. A janela de contexto é 128K. Idiomas: Inglês, Japonês.
3. Qwen3 suporta raciocínio. A janela de contexto é 128K. Idiomas: Inglês, Chinês, Francês, Espanhol, Árabe.
"""

resp = chat(
    model="qwen3",
    messages=[{"role": "user", "content": prompt}],
    format=ModelComparison.model_json_schema(),
    options={"temperature": 0},
)

print(resp.message.content)

Saída:

{
  "models": [
    {
      "name": "Llama 3.1",
      "supports_thinking": true,
      "max_context_tokens": 128000,
      "languages": ["Inglês"]
    },
    {
      "name": "GPT-4 Turbo",
      "supports_thinking": true,
      "max_context_tokens": 128000,
      "languages": ["Inglês", "Japonês"]
    },
    {
      "name": "Qwen3",
      "supports_thinking": true,
      "max_context_tokens": 128000,
      "languages": ["Inglês", "Chinês", "Francês", "Espanhol", "Árabe"]
    }
  ]
}

Isso torna trivial realizar benchmark, visualizar ou filtrar modelos por seus recursos.


Exemplo 4: Detectar Lacunas Automaticamente

Você pode até permitir valores null quando um campo estiver faltando:

from typing import Optional

class FlexibleLLMFeatures(BaseModel):
    name: str
    supports_thinking: Optional[bool]
    max_context_tokens: Optional[int]
    languages: Optional[List[str]]

Isso garante que seu esquema permaneça válido mesmo que algumas informações sejam desconhecidas.


Benefícios, Cuidados e Melhores Práticas

O uso de saída estruturada através do Ollama (ou qualquer sistema que a suporte) oferece muitas vantagens — mas também tem alguns cuidados.

Benefícios

  • Garantias mais fortes: O modelo é pedido para estar em conformidade com um esquema JSON em vez de texto livre.
  • Análise mais fácil: Você pode json.loads diretamente ou validar com Pydantic / Zod, em vez de regex ou heurísticas.
  • Evolução baseada em esquema: Você pode versionar seu esquema, adicionar campos (com valores padrão) e manter compatibilidade com versões anteriores.
  • Interoperabilidade: Sistemas downstream esperam dados estruturados.
  • Determinismo (melhor com baixa temperatura): Quando a temperatura é baixa (ex., 0), o modelo é mais propenso a aderir rigidamente ao esquema. A documentação do Ollama recomenda isso.

Cuidados e Armadilhas

  • Incompatibilidade de esquema: O modelo ainda pode desviar — por exemplo, perder uma propriedade obrigatória, reordenar chaves ou incluir campos extras. Você precisa de validação.
  • Esquemas complexos: Esquemas JSON muito profundos ou recursivos podem confundir o modelo ou levar a falhas.
  • Ambiguidade no prompt: Se seu prompt for vago, o modelo pode adivinhar campos ou unidades incorretamente.
  • Inconsistência entre modelos: Alguns modelos podem ser melhores ou piores em honrar restrições estruturadas.
  • Limites de tokens: O esquema em si adiciona custo de token ao prompt ou à chamada de API.

Melhores Práticas e Dicas (baseadas no blog do Ollama + experiência)

  • Use Pydantic (Python) ou Zod (JavaScript) para definir seus esquemas e gerar automaticamente esquemas JSON. Isso evita erros manuais.
  • Sempre inclua instruções como “responder apenas em JSON” ou “não incluir comentários ou texto extra” em seu prompt.
  • Use temperatura = 0 (ou muito baixa) para minimizar a aleatoriedade e maximizar a adesão ao esquema. O Ollama recomenda determinismo.
  • Valide e potencialmente faça fallback (ex., repetição ou limpeza) quando a análise JSON falhar ou a validação do esquema falhar.
  • Comece com um esquema mais simples, depois estenda gradualmente. Não complique demais inicialmente.
  • Inclua instruções de erro úteis, mas restritas: por exemplo, se o modelo não puder preencher um campo obrigatório, responda com null em vez de omiti-lo (se seu esquema permitir).

Exemplo em Go 1: Extraindo Recursos de LLM

Aqui está um programa simples em Go que pede ao Qwen3 uma saída estruturada sobre os recursos de um LLM.

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log"

	"github.com/ollama/ollama/api"
)

type LLMFeatures struct {
	Name             string   `json:"name"`
	SupportsThinking bool     `json:"supports_thinking"`
	MaxContextTokens int      `json:"max_context_tokens"`
	Languages        []string `json:"languages"`
}

func main() {
	client, err := api.ClientFromEnvironment()
	if err != nil {
		log.Fatal(err)
	}

	prompt := `
  Analise a seguinte descrição e retorne os recursos do modelo apenas em JSON.
  Descrição:
  "O Qwen3 tem forte suporte multilíngue (Inglês, Chinês, Francês, Espanhol, Árabe).
  Ele permite etapas de raciocínio (chain-of-thought).
  A janela de contexto é de 128K tokens."
  `

	// Definir o esquema JSON para saída estruturada
	formatSchema := map[string]any{
		"type": "object",
		"properties": map[string]any{
			"name": map[string]string{
				"type": "string",
			},
			"supports_thinking": map[string]string{
				"type": "boolean",
			},
			"max_context_tokens": map[string]string{
				"type": "integer",
			},
			"languages": map[string]any{
				"type": "array",
				"items": map[string]string{
					"type": "string",
				},
			},
		},
		"required": []string{"name", "supports_thinking", "max_context_tokens", "languages"},
	}

	// Converter esquema para JSON
	formatJSON, err := json.Marshal(formatSchema)
	if err != nil {
		log.Fatal("Falha ao marshalar esquema de formato:", err)
	}

	req := &api.GenerateRequest{
		Model:   "qwen3:8b",
		Prompt:  prompt,
		Format:  formatJSON,
		Options: map[string]any{"temperature": 0},
	}

	var features LLMFeatures
	var rawResponse string
	err = client.Generate(context.Background(), req, func(response api.GenerateResponse) error {
		// Acumular conteúdo conforme faz streaming
		rawResponse += response.Response

		// Analisar apenas quando a resposta estiver completa
		if response.Done {
			if err := json.Unmarshal([]byte(rawResponse), &features); err != nil {
				return fmt.Errorf("erro de análise JSON: %v", err)
			}
		}
		return nil
	})
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Estrutura analisada: %+v\n", features)
}

Para compilar e executar este exemplo de programa Go - vamos supor que tenhamos este arquivo main.go em uma pasta ollama-struct, Precisamos executar dentro desta pasta:

# inicializar módulo
go mod init ollama-struct
# puxar todas as dependências
go mod tidy
# compilar & executar
go build -o ollama-struct main.go
./ollama-struct

Saída de Exemplo

Estrutura analisada: {Name:Qwen3 SupportsThinking:true MaxContextTokens:128000 Languages:[Inglês Chinês Francês Espanhol Árabe]}

Exemplo em Go 2: Comparando Múltiplos Modelos

Você pode estender isso para extrair uma lista de modelos para comparação.

  type ModelComparison struct {
		Models []LLMFeatures `json:"models"`
	}

	prompt = `
Extraia recursos das seguintes descrições de modelos e retorne como JSON:

1. PaLM 2: Este modelo tem capacidades de raciocínio limitadas e se concentra na compreensão básica de linguagem. Ele suporta uma janela de contexto de 8.000 tokens. Ele suporta principalmente apenas o idioma inglês.
2. LLaMA 2: Este modelo tem habilidades de raciocínio moderadas e pode lidar com algumas tarefas lógicas. Ele pode processar até 4.000 tokens em seu contexto. Ele suporta os idiomas inglês, espanhol e italiano.
3. Codex: Este modelo tem fortes capacidades de raciocínio especificamente para programação e análise de código. Ele tem uma janela de contexto de 16.000 tokens. Ele suporta os idiomas inglês, Python, JavaScript e Java.

Retorne um objeto JSON com um array "models" contendo todos os modelos.
	`

	// Definir o esquema JSON para comparação de modelos
	comparisonSchema := map[string]any{
		"type": "object",
		"properties": map[string]any{
			"models": map[string]any{
				"type": "array",
				"items": map[string]any{
					"type": "object",
					"properties": map[string]any{
						"name": map[string]string{
							"type": "string",
						},
						"supports_thinking": map[string]string{
							"type": "boolean",
						},
						"max_context_tokens": map[string]string{
							"type": "integer",
						},
						"languages": map[string]any{
							"type": "array",
							"items": map[string]string{
								"type": "string",
							},
						},
					},
					"required": []string{"name", "supports_thinking", "max_context_tokens", "languages"},
				},
			},
		},
		"required": []string{"models"},
	}

	// Converter esquema para JSON
	comparisonFormatJSON, err := json.Marshal(comparisonSchema)
	if err != nil {
		log.Fatal("Falha ao marshalar esquema de comparação:", err)
	}

	req = &api.GenerateRequest{
		Model:   "qwen3:8b",
		Prompt:  prompt,
		Format:  comparisonFormatJSON,
		Options: map[string]any{"temperature": 0},
	}

	var comp ModelComparison
	var comparisonResponse string
	err = client.Generate(context.Background(), req, func(response api.GenerateResponse) error {
		// Acumular conteúdo conforme faz streaming
		comparisonResponse += response.Response

		// Analisar apenas quando a resposta estiver completa
		if response.Done {
			if err := json.Unmarshal([]byte(comparisonResponse), &comp); err != nil {
				return fmt.Errorf("erro de análise JSON: %v", err)
			}
		}
		return nil
	})
	if err != nil {
		log.Fatal(err)
	}

	for _, m := range comp.Models {
		fmt.Printf("%s: Contexto=%d, Idiomas=%v\n", m.Name, m.MaxContextTokens, m.Languages)
	}

Saída de Exemplo

PaLM 2: Contexto=8000, Idiomas=[Inglês]
LLaMA 2: Contexto=4000, Idiomas=[Inglês Espanhol Italiano]
Codex: Contexto=16000, Idiomas=[Inglês Python JavaScript Java]

A propósito, o qwen3:4b funciona bem nestes exemplos, assim como o qwen3:8b.

Melhores Práticas para Desenvolvedores Go

  • Defina temperatura como 0 para máxima adesão ao esquema.
  • Valide com json.Unmarshal e faça fallback se a análise falhar.
  • Mantenha esquemas simples — estruturas JSON profundamente aninhadas ou recursivas podem causar problemas.
  • Permita campos opcionais (use omitempty nas tags de estrutura Go) se você esperar dados ausentes.
  • Adicione repetições se o modelo ocasionalmente emitir JSON inválido.

Exemplo Completo - Desenhando um Gráfico com Especificações de LLM (Passo a passo: de JSON estruturado a tabelas de comparação)

llm-chart

  1. Defina um esquema para os dados que você deseja

Use Pydantic para que você possa (a) gerar um JSON Schema para o Ollama e (b) validar a resposta do modelo.

from pydantic import BaseModel
from typing import List, Optional

class LLMFeatures(BaseModel):
    name: str
    supports_thinking: bool
    max_context_tokens: int
    languages: List[str]
  1. Peça ao Ollama para retornar apenas JSON nessa forma

Passar o esquema em format= e reduzir a temperatura para determinismo.

from ollama import chat

prompt = """
Extraia recursos para cada modelo. Retorne apenas JSON correspondente ao esquema.
1) Qwen3 suporta chain-of-thought; contexto 128K; Inglês, Chinês, Francês, Espanhol, Árabe.
2) Llama 3.1 suporta chain-of-thought; contexto 128K; Inglês.
3) GPT-4 Turbo suporta chain-of-thought; contexto 128K; Inglês, Japonês.
"""

resp = chat(
    model="qwen3",
    messages=[{"role": "user", "content": prompt}],
    format={"type": "array", "items": LLMFeatures.model_json_schema()},
    options={"temperature": 0}
)

raw_json = resp.message.content  # Lista JSON de LLMFeatures
  1. Valide e normalize

Sempre valide antes de usar em produção.

from pydantic import TypeAdapter

adapter = TypeAdapter(list[LLMFeatures])
models = adapter.validate_json(raw_json)  # -> list[LLMFeatures]
  1. Construa uma tabela de comparação (pandas)

Transforme seus objetos validados em um DataFrame que você pode classificar/filtrar e exportar.

import pandas as pd

df = pd.DataFrame([m.model_dump() for m in models])
df["languages_count"] = df["languages"].apply(len)
df["languages"] = df["languages"].apply(lambda xs: ", ".join(xs))

# Reordenar colunas para legibilidade
df = df[["name", "supports_thinking", "max_context_tokens", "languages_count", "languages"]]

# Salvar como CSV para uso posterior
df.to_csv("llm_feature_comparison.csv", index=False)
  1. (Opcional) Visuais rápidos

Gráficos simples ajudam você a ver as diferenças entre modelos rapidamente.

import matplotlib.pyplot as plt

plt.figure()
plt.bar(df["name"], df["max_context_tokens"])
plt.title("Janela de Contexto Máxima por Modelo (tokens)")
plt.xlabel("Modelo")
plt.ylabel("Tokens de Contexto Máximo")
plt.xticks(rotation=20, ha="right")
plt.tight_layout()
plt.savefig("max_context_window.png")

TL;DR

Com o novo suporte a saída estruturada do Ollama, você pode tratar LLMs não apenas como chatbots, mas como motores de extração de dados.

Os exemplos acima mostraram como extrair automaticamente metadados estruturados sobre recursos de LLM como suporte de raciocínio, tamanho da janela de contexto e idiomas suportados — tarefas que, de outra forma, exigiriam uma análise frágil.

Seja construindo um catálogo de modelos LLM, um painel de avaliação ou um assistente de pesquisa alimentado por IA, as saídas estruturadas tornam a integração suave, confiável e pronta para produção.

Assinar

Receba novos artigos sobre sistemas, infraestrutura e engenharia de IA.