Limitando LLMs com Saída Estruturada: Ollama, Qwen3 & Python ou Go

Alguns modos 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 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.

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

patos em linha

Neste post, vamos abordar:

  • O que é saída estruturada e por que importa
  • Uma maneira simples de obter saída estruturada de LLMs
  • Como o novo recurso de 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 pensamento em cadeia, tem uma janela de contexto de 200K e fala inglês, chinês e espanhol.”

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

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

{
  "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

Às vezes, os LLMs entendem o que é o esquema e podemos pedir ao LLM para retornar a saída em JSON usando um esquema específico. O modelo Qwen3 da Alibaba está 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 = """
Você é um extrator de dados estruturados.
Retorne apenas JSON.
Texto: "Elon Musk tem 53 anos e mora em Austin."
Esquema: { "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("Erro ao analisar JSON:", e)

Saída:

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

Forçando 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 de Qwen3
data = Person.model_validate_json(output)
print(data.name, data.age, data.city)

Isso garante que a saída corresponda à estrutura esperada.


Saída Estruturada de Ollama

Agora, Ollama permite que você passe um esquema no parâmetro format. O modelo é então restringido a responder apenas em JSON que corresponda ao esquema (docs).

Em Python, você normalmente define seu esquema com Pydantic e deixa Ollama usá-lo 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:

“Qwen3 tem forte suporte multilíngue (Inglês, Chinês, Francês, Espanhol, Árabe). Ele permite etapas de raciocínio (pensamento em cadeia). 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 as características do modelo em JSON apenas.
Descrição do modelo:
'Qwen3 tem forte suporte multilíngue (Inglês, Chinês, Francês, Espanhol, Árabe).
Ele permite etapas de raciocínio (pensamento em cadeia).
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

Alimente descrições de múltiplos modelos e extraia em forma estruturada:

from typing import List

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

prompt = """
Extraia as características de cada modelo em JSON.

1. Llama 3.1 suporta raciocínio. Janela de contexto é de 128K. Idiomas: Inglês apenas.
2. GPT-4 Turbo suporta raciocínio. Janela de contexto é de 128K. Idiomas: Inglês, Japonês.
3. Qwen3 suporta raciocínio. Janela de contexto é de 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 benchmarking, visualização ou filtragem de modelos com base em suas características.


Exemplo 4: Detectar Falhas Automaticamente

Você até pode permitir valores null quando um campo estiver 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 se alguma informação estiver desconhecida.


Benefícios, Cuidados e Boas Práticas

Usar saída estruturada por meio do Ollama (ou qualquer sistema que suporte) oferece muitas vantagens — mas também tem alguns cuidados.

Benefícios

  • Garantias mais fortes: O modelo é pedido para corresponder a um esquema JSON em vez de texto livre.
  • Análise mais fácil: Você pode diretamente json.loads ou validar com Pydantic / Zod, em vez de expressões regulares ou heurísticas.
  • Evolução baseada em esquema: Você pode versionar seu esquema, adicionar campos (com valores padrão) e manter compatibilidade para trás.
  • Interoperabilidade: Sistemas downstream esperam dados estruturados.
  • Determinismo (melhor com temperatura baixa): Quando a temperatura é baixa (ex: 0), o modelo é mais propenso a seguir estritamente o esquema. Os documentos do Ollama recomendam isso.

Cuidados e Armadilhas

  • Desconcordância de esquema: O modelo ainda pode se desviar — por exemplo, omitir uma propriedade necessá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 token: O próprio esquema adiciona custo de token ao prompt ou chamada de API.

Boas Práticas e Dicas (extraídas do 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 “responda apenas em JSON” ou “não inclua comentários ou texto extra” em seu prompt.
  • Use temperature = 0 (ou muito baixo) para minimizar aleatoriedade e maximizar a adesão ao esquema. Ollama recomenda determinismo.
  • Valide e potencialmente recupere (ex: retenção ou limpeza) quando a análise de JSON falhar ou a validação do esquema falhar.
  • Comece com um esquema mais simples, depois estenda gradualmente. Não complice inicialmente.
  • Inclua instruções de erro úteis, mas restritas: por exemplo, se o modelo não puder preencher um campo necessário, responda com null em vez de omiti-lo (se o esquema permitir).

Exemplo em Go 1: Extrair Recursos de LLM

Aqui está um programa simples Go que pede ao Qwen3 para 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 as características do modelo em JSON apenas.
  Descrição:
  "Qwen3 tem forte suporte multilíngue (Inglês, Chinês, Francês, Espanhol, Árabe).
  Ele permite etapas de raciocínio (pensamento em cadeia).
  A janela de contexto é de 128K tokens."
  `

	// Defina 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"},
	}

	// Converta o esquema para JSON
	formatJSON, err := json.Marshal(formatSchema)
	if err != nil {
		log.Fatal("Falha ao serializar o 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 {
		// Acumule conteúdo conforme o streaming
		rawResponse += response.Response

		// Apenas analise 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 — suponha que tenhamos este arquivo main.go em uma pasta ollama-struct, Precisamos executar dentro desta pasta:

# inicialize o módulo
go mod init ollama-struct
# obtenha todas as dependências
go mod tidy
# compile e execute
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: Comparar 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 as características das seguintes descrições de modelos e retorne como JSON:

	1. PaLM 2: Este modelo tem capacidades de raciocínio limitadas e se concentra em compreensão básica da linguagem. Ele suporta uma janela de contexto de 8.000 tokens. Ele suporta principalmente o idioma Inglês.
	2. LLaMA 2: Este modelo tem capacidades 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 uma "array de modelos" contendo todos os modelos.
	`

	// Defina 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"},
	}

	// Converta o esquema para JSON
	comparisonFormatJSON, err := json.Marshal(comparisonSchema)
	if err != nil {
		log.Fatal("Falha ao serializar o 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 {
		// Acumule conteúdo conforme o streaming
		comparisonResponse += response.Response

		// Apenas analise 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]

De passagem, qwen3:4b nestes exemplos funciona bem, da mesma forma que qwen3:8b.

Boas Práticas para Desenvolvedores em Go

  • Defina a temperatura como 0 para adesão máxima ao esquema.
  • Valide com json.Unmarshal e recupere se a análise falhar.
  • Mantenha os esquemas simples — estruturas JSON profundas ou recursivas podem causar problemas.
  • Permita campos opcionais (use omitempty nas tags de struct em Go) se esperar dados ausentes.
  • Adicione tentativas 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 tanto (a) gerar um esquema JSON para Ollama quanto (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

Envie o esquema em format= e reduza a temperatura para determinismo.

from ollama import chat

prompt = """
Extraia as características de cada modelo. Retorne apenas JSON que corresponda ao esquema.
1) Qwen3 suporta pensamento em cadeia; 128K contexto; Inglês, Chinês, Francês, Espanhol, Árabe.
2) Llama 3.1 suporta pensamento em cadeia; 128K contexto; Inglês.
3) GPT-4 Turbo suporta pensamento em cadeia; 128K contexto; 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 & normalize

Sempre valide antes de usar em produção.

from pydantic import TypeAdapter

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

Transforme seus objetos validados em um DataFrame que você possa 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))

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

# Salve como CSV para uso posterior
df.to_csv("llm_feature_comparison.csv", index=False)
  1. (Opcional) Visualizações rápidas

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

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 a pensamento, tamanho da janela de contexto e idiomas suportados — tarefas que, de outra forma, exigiriam análise frágil.

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