Ограничение LLM с помощью структурированного вывода: Ollama, Qwen3 и Python или Go
Несколько способов получения структурированного вывода из Ollama
Большие языковые модели (LLM) являются мощным инструментом, однако в производственных условиях мы редко хотим получать свободные текстовые абзацы. Вместо этого нам нужны предсказуемые данные: атрибуты, факты или структурированные объекты, которые можно использовать в приложении. Это и есть Структурированный вывод LLM.
Принудительное соблюдение схемы снижает вероятность того, что некорректные логиты превратятся в невалидный JSON, однако температура и штрафы по-прежнему важны для предотвращения лавинообразных повторных попыток; см. параметры агентного вывода для Qwen и Gemma при использовании ограничений format вместе с агентами.
Некоторое время назад Ollama представил поддержку структурированного вывода (объявление), что позволило ограничить ответы модели в соответствии со схемой JSON. Это открывает возможности для создания согласованных конвейеров извлечения данных для таких задач, как каталогизация функций LLM, бенчмаркинг моделей или автоматизация интеграции систем.

В этой статье мы рассмотрим:
- Что такое структурированный вывод и почему он важен
- Простой способ получения структурированного вывода от LLM
- Как работает новая функция Ollama
- Примеры извлечения возможностей LLM:
Что такое структурированный вывод?
Обычно LLM генерируют свободный текст:
«Модель X поддерживает рассуждения с использованием метода цепочки мыслей (chain-of-thought), имеет окно контекста в 200K токенов и понимает английский, китайский и испанский языки».
Это читаемо, но сложно для парсинга.
Вместо этого, при использовании структурированного вывода, мы запрашиваем строгую схему:
{
"name": "Model X",
"supports_thinking": true,
"max_context_tokens": 200000,
"languages": ["English", "Chinese", "Spanish"]
}
Этот JSON легко валидировать, хранить в базе данных или передавать в пользовательский интерфейс.
Простой способ получения структурированного вывода от LLM
LLM иногда понимают, что такое схема, и мы можем попросить LLM вернуть вывод в формате JSON, используя конкретную схему. Модель Qwen3 от Alibaba оптимизирована для рассуждений и структурированных ответов. Вы можете явно указать ей ответить в формате JSON.
Пример 1: Использование Qwen3 с ollama на Python, запрос JSON со схемой
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)
Вывод:
{"name": "Elon Musk", "age": 53, "city": "Austin"}
Принудительная валидация схемы с помощью Pydantic
Чтобы избежать некорректного вывода, вы можете валидировать данные по схеме Pydantic в 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)
Это гарантирует, что вывод соответствует ожидаемой структуре.
Структурированный вывод Ollama
Теперь Ollama позволяет передавать схему в параметре format. Модель затем ограничивается в ответах только JSON, соответствующим схеме (документация).
В Python вы обычно определяете свою схему с помощью Pydantic и позволяете Ollama использовать её как JSON-схему.
Пример 2: Извлечение метаданных функций LLM
Предположим, у вас есть фрагмент текста, описывающий возможности LLM:
«Qwen3 обладает мощной поддержкой многоязычности (английский, китайский, французский, испанский, арабский). Он поддерживает шаги рассуждения (цепочка мыслей). Окно контекста составляет 128K токенов».
Вы хотите получить структурированные данные:
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)
Возможный вывод:
{
"name": "Qwen3",
"supports_thinking": true,
"max_context_tokens": 128000,
"languages": ["English", "Chinese", "French", "Spanish", "Arabic"]
}
Пример 3: Сравнение нескольких моделей
Введите описания нескольких моделей и извлеките их в структурированную форму:
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)
Вывод:
{
"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"]
}
]
}
Это делает тривиальным бенчмаркинг, визуализацию или фильтрацию моделей по их характеристикам.
Пример 4: Автоматическое обнаружение пробелов
Вы даже можете разрешить значения null, когда поле отсутствует:
from typing import Optional
class FlexibleLLMFeatures(BaseModel):
name: str
supports_thinking: Optional[bool]
max_context_tokens: Optional[int]
languages: Optional[List[str]]
Это гарантирует, что ваша схема останется валидной, даже если некоторые данные неизвестны.
Преимущества, подводные камни и лучшие практики
Использование структурированного вывода через Ollama (или любую другую поддерживающую систему) предлагает множество преимуществ — но также имеет некоторые нюансы.
Преимущества
- Более сильные гарантии: Модели предписывается соответствовать схеме JSON, а не генерировать свободный текст.
- Более легкий парсинг: Вы можете напрямую использовать
json.loadsили валидацию с помощью Pydantic / Zod, вместо регулярных выражений или эвристик. - Эволюция на основе схемы: Вы можете версионировать свою схему, добавлять поля (со значениями по умолчанию) и поддерживать обратную совместимость.
- Интероперабельность: Последующие системы ожидают структурированные данные.
- Детерминизм (лучше при низкой температуре): При низкой температуре (например, 0) модель с большей вероятностью строго придерживается схемы. В документации Ollama это рекомендуется.
Подводные камни и ловушки
- Несовпадение схем: Модель все равно может отклониться — например, пропустить обязательное свойство, изменить порядок ключей или включить дополнительные поля. Вам нужна валидация.
- Сложные схемы: Очень глубокие или рекурсивные JSON-схемы могут запутать модель или привести к сбоям.
- Двусмысленность в промпте: Если ваш промпт неточен, модель может неправильно угадать поля или единицы измерения.
- Несогласованность между моделями: Некоторые модели могут лучше или хуже соблюдать структурированные ограничения.
- Лимиты токенов: Сама схема добавляет стоимость токенов к промпту или вызову API.
Лучшие практики и советы (на основе блога Ollama и опыта)
- Используйте Pydantic (Python) или Zod (JavaScript) для определения ваших схем и автоматической генерации JSON-схем. Это избегает ручных ошибок.
- Всегда включайте инструкции, такие как «ответьте только в JSON» или «не включайте комментарии или дополнительный текст» в ваш промпт.
- Используйте temperature = 0 (или очень низкое значение), чтобы минимизировать случайность и максимизировать соблюдение схемы. Ollama рекомендует детерминизм.
- Валидируйте и потенциально используйте резервный вариант (например, повторную попытку или очистку) при сбоях парсинга JSON или валидации схемы.
- Начните с более простой схемы, затем постепенно расширяйте её. Не усложняйте изначально.
- Включайте полезные, но ограниченные инструкции по обработке ошибок: например, если модель не может заполнить обязательное поле, ответьте
null, а не опускайте его (если ваша схема позволяет).
Пример на Go 1: Извлечение функций LLM
Вот простая программа на Go, которая просит Qwen3 предоставить структурированный вывод о функциях 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)
}
Чтобы скомпилировать и запустить этот пример программы на Go, предположим, что у нас есть файл main.go в папке ollama-struct,
Нам нужно выполнить внутри этой папки:
# 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
Пример вывода
Parsed struct: {Name:Qwen3 SupportsThinking:true MaxContextTokens:128000 Languages:[English Chinese French Spanish Arabic]}
Пример на Go 2: Сравнение нескольких моделей
Вы можете расширить это для извлечения списка моделей для сравнения.
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)
}
Пример вывода
PaLM 2: Context=8000, Languages=[English]
LLaMA 2: Context=4000, Languages=[English Spanish Italian]
Codex: Context=16000, Languages=[English Python JavaScript Java]
Кстати, qwen3:4b на этих примерах работает хорошо, так же как и qwen3:8b.
Лучшие практики для разработчиков на Go
- Установите температуру равной 0 для максимального соблюдения схемы.
- Валидируйте с помощью
json.Unmarshalи используйте резервный вариант при сбоях парсинга. - Держите схемы простыми — глубоко вложенные или рекурсивные структуры JSON могут вызвать проблемы.
- Разрешайте опциональные поля (используйте
omitemptyв тегах структур Go), если вы ожидаете отсутствующие данные. - Добавьте повторные попытки, если модель иногда выдает невалидный JSON.
Полный пример — Построение диаграммы со спецификациями LLM (Пошагово: от структурированного JSON к таблицам сравнения)

- Определите схему для данных, которые вы хотите
Используйте Pydantic, чтобы вы могли (а) генерировать JSON-схему для Ollama и (б) валидировать ответ модели.
from pydantic import BaseModel
from typing import List, Optional
class LLMFeatures(BaseModel):
name: str
supports_thinking: bool
max_context_tokens: int
languages: List[str]
- Попросите Ollama вернуть только JSON в этой форме
Передайте схему в format= и снизьте температуру для детерминизма.
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
- Валидация и нормализация
Всегда валидируйте перед использованием в продакшене.
from pydantic import TypeAdapter
adapter = TypeAdapter(list[LLMFeatures])
models = adapter.validate_json(raw_json) # -> list[LLMFeatures]
- Построение таблицы сравнения (pandas)
Преобразуйте ваши валидированные объекты в DataFrame, который можно сортировать/фильтровать и экспортировать.
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)
- (Опционально) Быстрая визуализация
Простые диаграммы помогают вам быстро оценить различия между моделями.
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 (Краткое содержание)
С новой поддержкой структурированного вывода в Ollama, вы можете рассматривать LLM не просто как чат-ботов, а как движки извлечения данных.
Приведенные выше примеры показали, как автоматически извлекать структурированные метаданные о функциях LLM, таких как поддержка рассуждений, размер окна контекста и поддерживаемые языки — задачи, которые в противном случае требовали бы ненадежного парсинга.
Будете ли вы создавать каталог моделей LLM, панель оценки или исследовательского помощника на базе ИИ, структурированные выводы делают интеграцию плавной, надежной и готовой к продакшену.
Полезные ссылки
- https://ollama.com/blog/structured-outputs
- Шпаргалка по Ollama
- Шпаргалка по Python
- AWS SAM + AWS SQS + Python PowerTools
- Шпаргалка по Golang
- Сравнение ORM для Go в PostgreSQL: GORM vs Ent vs Bun vs sqlc
- Переупорядочение текстовых документов с Ollama и моделью эмбеддинга Qwen3 - на Go
- Производительность AWS lambda: JavaScript vs Python vs Golang