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

Algumas maneiras de obter saída estruturada do Ollama

Conteúdo da página

Grandes Modelos de Linguagem (LLMs) são poderosos, mas em produção raramente queremos parágrafos em formato livre. 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 aplicação de esquemas reduz com que frequência logits ruins se transformam em JSON inválido, mas a temperatura e as penalidades ainda importam para tempestades de tentativas; veja parâmetros de inferência agêntica para Qwen e Gemma quando você combina restrições de format com agentes. A camada de serviço ainda precisa de lógica de análise, normalização e validação — veja validação de saída estruturada de LLM em Python que funciona — porque o Ollama e os clientes podem não concordar sobre as strings exatas, mesmo quando format está definido.

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

patos em fila

Nesta postagem, cobriremos:

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

O que é Saída Estruturada?

Normalmente, LLMs geram texto livre:

“O Modelo X suporta raciocínio com cadeia de pensamento, tem uma janela de contexto de 200K e fala inglês, chinês e espanhol.”

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

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

{
  "name": "Model X",
  "supports_thinking": true,
  "max_context_tokens": 200000,
  "languages": ["English", "Chinese", "Spanish"]
}

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


Maneira simples de obter Saída Estruturada de LLM

Às vezes, os LLMs entendem o que 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 para responder em JSON.

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

import json
import ollama

prompt = """
You are a structured data extractor.
Return JSON only.
Text: "Elon Musk is 53 and lives in Austin."
Schema: { "name": string, "age": int, "city": string }
"""

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

# Parse JSON
try:
    data = json.loads(output)
    print(data)
except Exception as e:
    print("Error parsing JSON:", e)

Saída:

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

Aplicando 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

# Suppose 'output' is the JSON string from 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 restringido para responder apenas em JSON que esteja em conformidade com o esquema (documentação).

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


Exemplo 2: Extrair Metadados de Recursos de LLM

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

“O Qwen3 tem forte suporte multilíngue (inglês, chinês, francês, espanhol, árabe). Ele permite etapas de raciocínio (cadeia de pensamento). 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 = """
Analyze the following description and return the model’s features in JSON only.
Model description:
'Qwen3 has strong multilingual support (English, Chinese, French, Spanish, Arabic).
It allows reasoning steps (chain-of-thought).
The context window is 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": ["English", "Chinese", "French", "Spanish", "Arabic"]
}

Exemplo 3: Comparar Múltiplos Modelos

Alimente descrições de vários modelos e extraia-os para uma forma estruturada:

from typing import List

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

prompt = """
Extract features of each model into JSON.

1. Llama 3.1 supports reasoning. Context window is 128K. Languages: English only.
2. GPT-4 Turbo supports reasoning. Context window is 128K. Languages: English, Japanese.
3. Qwen3 supports reasoning. Context window is 128K. Languages: English, Chinese, French, Spanish, Arabic.
"""

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": ["English"]
    },
    {
      "name": "GPT-4 Turbo",
      "supports_thinking": true,
      "max_context_tokens": 128000,
      "languages": ["English", "Japanese"]
    },
    {
      "name": "Qwen3",
      "supports_thinking": true,
      "max_context_tokens": 128000,
      "languages": ["English", "Chinese", "French", "Spanish", "Arabic"]
    }
  ]
}

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


Exemplo 4: Detectar Lacunas Automaticamente

Você pode até permitir valores null quando um campo está ausente:

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, Ressalvas e Boas Práticas

Usar saída estruturada através do Ollama (ou qualquer sistema que a suporte) oferece muitas vantagens — mas também tem algumas ressalvas.

Benefícios

  • Garantias mais fortes: O modelo é solicitado a corresponder a um esquema JSON em vez de texto em formato livre.
  • Análise (parsing) mais fácil: Você pode usar diretamente json.loads 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 padrões) e manter compatibilidade reversa.
  • 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.

Ressalvas e Armadilhas

  • Incompatibilidade de esquema: O modelo ainda pode desviar — ex.: 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 é vago, o modelo pode chutar campos ou unidades incorretamente.
  • Inconsistência entre modelos: Alguns modelos podem ser melhores ou piores em respeitar restrições estruturadas.
  • Limites de tokens: O próprio esquema adiciona custo de tokens ao prompt ou chamada de API.

Boas 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.
  • Inclua sempre instruções como “responder apenas em JSON” ou “não incluir comentários ou texto extra” no seu prompt.
  • Use temperature = 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.: tentar novamente ou limpar) quando a análise de JSON falhar ou a validação de esquema falhar.
  • Comece com um esquema mais simples, então estenda gradualmente. Não complique demais inicialmente.
  • Inclua instruções de erro úteis, porém restritas: ex.: se o modelo não puder preencher um campo obrigatório, responda com null em vez de omitir (se seu esquema permitir).

Exemplo 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 := `
  Analyze the following description and return the model’s features in JSON only.
  Description:
  "Qwen3 has strong multilingual support (English, Chinese, French, Spanish, Arabic).
  It allows reasoning steps (chain-of-thought).
  The context window is 128K tokens."
  `

	// Define the JSON schema for structured output
	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"},
	}

	// Convert schema to JSON
	formatJSON, err := json.Marshal(formatSchema)
	if err != nil {
		log.Fatal("Failed to marshal format schema:", 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 {
		// Accumulate content as it streams
		rawResponse += response.Response

		// Only parse when the response is complete
		if response.Done {
			if err := json.Unmarshal([]byte(rawResponse), &features); err != nil {
				return fmt.Errorf("JSON parse error: %v", err)
			}
		}
		return nil
	})
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Parsed struct: %+v\n", features)
}

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

# initialise module
go mod init ollama-struct
# pull all the dependencise
go mod tidy
# build & execute
go build -o ollama-struct main.go
./ollama-struct

Exemplo de Saída

Parsed struct: {Name:Qwen3 SupportsThinking:true MaxContextTokens:128000 Languages:[English Chinese French Spanish Arabic]}

Exemplo 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 = `
	Extract features from the following model descriptions and return as JSON:

	1. PaLM 2: This model has limited reasoning capabilities and focuses on basic language understanding. It supports a context window of 8,000 tokens. It primarily supports English language only.
	2. LLaMA 2: This model has moderate reasoning abilities and can handle some logical tasks. It can process up to 4,000 tokens in its context. It supports English, Spanish, and Italian languages.
	3. Codex: This model has strong reasoning capabilities specifically for programming and code analysis. It has a context window of 16,000 tokens. It supports English, Python, JavaScript, and Java languages.

	Return a JSON object with a "models" array containing all models.
	`

	// Define the JSON schema for model comparison
	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"},
	}

	// Convert schema to JSON
	comparisonFormatJSON, err := json.Marshal(comparisonSchema)
	if err != nil {
		log.Fatal("Failed to marshal comparison schema:", 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 {
		// Accumulate content as it streams
		comparisonResponse += response.Response

		// Only parse when the response is complete
		if response.Done {
			if err := json.Unmarshal([]byte(comparisonResponse), &comp); err != nil {
				return fmt.Errorf("JSON parse error: %v", err)
			}
		}
		return nil
	})
	if err != nil {
		log.Fatal(err)
	}

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

Exemplo de Saída

PaLM 2: Context=8000, Languages=[English]
LLaMA 2: Context=4000, Languages=[English Spanish Italian]
Codex: Context=16000, Languages=[English Python JavaScript Java]

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

Boas Práticas para Desenvolvedores Go

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

Exemplo completo - Desenhar um Gráfico com Especificações de LLM (Passo a passo: de JSON estruturado para 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 Esquema JSON 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

Passe o esquema em format= e diminua a temperatura para determinismo.

from ollama import chat

prompt = """
Extract features for each model. Return JSON only matching the schema.
1) Qwen3 supports chain-of-thought; 128K context; English, Chinese, French, Spanish, Arabic.
2) Llama 3.1 supports chain-of-thought; 128K context; English.
3) GPT-4 Turbo supports chain-of-thought; 128K context; English, Japanese.
"""

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  # JSON list of 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 ordenar/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))

# Reorder columns for readability
df = df[["name", "supports_thinking", "max_context_tokens", "languages_count", "languages"]]

# Save as CSV for further use
df.to_csv("llm_feature_comparison.csv", index=False)
  1. (Opcional) Visualizações rápidas

Gráficos simples ajudam a examinar visualmente as diferenças entre modelos rapidamente.

import matplotlib.pyplot as plt

plt.figure()
plt.bar(df["name"], df["max_context_tokens"])
plt.title("Max Context Window by Model (tokens)")
plt.xlabel("Model")
plt.ylabel("Max Context Tokens")
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 LLMs como suporte a raciocínio, tamanho da janela de contexto e idiomas suportados — tarefas que de outra forma exigiriam análise (parsing) frágil.

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

Subscrever

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