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

Algumas maneiras de obter saídas estruturadas 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 de texto livre. Em vez disso, queremos dados previsíveis: atributos, fatos ou objetos estruturados que você possa alimentar em um aplicativo. Isso é Saída Estruturada de LLM.

Há algum tempo, o 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 desbloqueia pipelines de extração de dados consistentes para tarefas como catalogar recursos de LLM, realizar benchmark de modelos ou automatizar a integração de sistemas.

patos em fila

Neste post, cobriremos:

  • O que é saída estruturada e por que é importante
  • Uma maneira simples de obter saída estruturada de LLMs
  • Como a nova funcionalidade do 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, 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 uma interface de usuário (UI).


Maneira Simples de Obter Saída Estruturada de LLM

Os LLMs às vezes entendem o que é o esquema e podemos pedir ao LLM que retorne 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 mora em Austin."
Esquema: { "name": string, "age": int, "city": string }
"""

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

# Analisar 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"}

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

# 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 passar um esquema no parâmetro format. O modelo é então restringido a responder apenas em JSON que esteja em conformidade com o esquema (docs).

Em Python, você geralmente define seu esquema com Pydantic e deixa o Ollama usar 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). 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 descrição a seguir 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).
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

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

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. Janela de contexto de 128K. Idiomas: Apenas Inglês.
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 realizar benchmark, visualizar ou filtrar modelos por seus recursos.


Exemplo 4: Detectar Lacunas Automaticamente

Você pode até 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 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 o suporte) oferece muitas vantagens, mas também tem alguns cuidados.

Benefícios

  • Garantias mais fortes: O modelo é solicitado a estar em conformidade com 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 regex ou heurísticas.
  • Evolução baseada em esquema: Você pode versionar seu esquema, adicionar campos (com valores padrão) e manter a compatibilidade com versões anteriores.
  • Interoperabilidade: Sistemas downstream esperam dados estruturados.
  • Determinismo (melhor com temperatura baixa): Quando a temperatura é baixa (ex: 0), o modelo tem maior probabilidade de 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 próprio esquema 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” no seu prompt.
  • Use temperature = 0 (ou muito baixo) para minimizar a aleatoriedade e maximizar a adesão ao esquema. O Ollama recomenda determinismo.
  • Valide e potencialmente faça fallback (ex: retentar ou limpar) quando a análise JSON falhar ou a validação do esquema falhar.
  • Comece com um esquema mais simples e, em seguida, estenda gradualmente. Não o complique 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 Go 1: Extraindo Recursos de LLM

Aqui está um programa simples 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 descrição a seguir 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).
  Permite etapas de raciocínio (chain-of-thought).
  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 marshar 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 o conteúdo conforme ele é transmitido
		rawResponse += response.Response

		// Analise 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("Struct analisado: %+v\n", features)
}

Para compilar e executar este exemplo de programa Go - vamos assumir 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 e executar
go build -o ollama-struct main.go
./ollama-struct

Exemplo de Saída

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

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 = `
	Extraia recursos das seguintes descrições de modelo e retorne como JSON:

	1. PaLM 2: Este modelo tem capacidades de raciocínio limitadas e foca na compreensão básica de linguagem. Suporta uma janela de contexto de 8.000 tokens. Suporta principalmente apenas a língua inglesa.
	2. LLaMA 2: Este modelo tem habilidades moderadas de raciocínio e pode lidar com algumas tarefas lógicas. Pode processar até 4.000 tokens em seu contexto. Suporta 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. Tem uma janela de contexto de 16.000 tokens. Suporta inglês, Python, JavaScript e Java.

	Retorne um objeto JSON com um array "models" 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 marshar 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 o conteúdo conforme ele é transmitido
		comparisonResponse += response.Response

		// Analise 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)
	}

Exemplo de Saída

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 nesses exemplos funciona bem, assim como o qwen3:8b.

Melhores Práticas para Desenvolvedores Go

  • Defina a temperatura em 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 struct Go) se você espera dados ausentes.
  • Adicione re-tentativas 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 para tabelas de comparação)

llm-chart

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

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

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. Validar & normalizar

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))

# Reordene 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 a visualizar as diferenças entre os modelos rapidamente.

import matplotlib.pyplot as plt

plt.figure()
plt.bar(df["name"], df["max_context_tokens"])
plt.title("Janela de Contexto Máximo 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 raciocínio, 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 alimentado por IA, as saídas estruturadas tornam a integração suave, confiável e pronta para produção.