Ограничение 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 к таблицам сравнения)

llm-chart

  1. Определите схему для данных, которые вы хотите

Используйте 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]
  1. Попросите 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
  1. Валидация и нормализация

Всегда валидируйте перед использованием в продакшене.

from pydantic import TypeAdapter

adapter = TypeAdapter(list[LLMFeatures])
models = adapter.validate_json(raw_json)  # -> list[LLMFeatures]
  1. Построение таблицы сравнения (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)
  1. (Опционально) Быстрая визуализация

Простые диаграммы помогают вам быстро оценить различия между моделями.

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, панель оценки или исследовательского помощника на базе ИИ, структурированные выводы делают интеграцию плавной, надежной и готовой к продакшену.

Полезные ссылки

Подписаться

Получайте новые материалы про системы, инфраструктуру и AI engineering.