LLM и структурированный вывод: Ollama, Qwen3 & Python или Go

Несколько способов получения структурированного вывода из Ollama

Содержимое страницы

Большие языковые модели (LLM) мощные, но в производстве мы редко хотим свободноформатных абзацев. Вместо этого нам нужны предсказуемые данные: атрибуты, факты или структурированные объекты, которые можно передать в приложение. Это Структурированный вывод LLM.

Недавно Ollama представила поддержку структурированного вывода (объявление), что делает возможным ограничение ответов модели в соответствии с JSON-схемой. Это открывает возможности для создания последовательных конвейеров извлечения данных для задач, таких как каталогизация функций LLM, тестирование моделей или автоматизация системной интеграции.

утки в ряд

В этом посте мы рассмотрим:

  • Что такое структурированный вывод и почему это важно
  • Простой способ получения структурированного вывода от LLM
  • Как работает новая функция Ollama
  • Примеры извлечения возможностей LLM:

Что такое структурированный вывод?

Обычно LLM генерируют свободный текст:

«Модель X поддерживает рассуждение с цепочкой мыслей, имеет контекстное окно на 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 = """
Вы — извлекатель структурированных данных.
Возвращайте только JSON.
Текст: "Илон Маск 53 года и живет в Остине."
Схема: { "name": string, "age": int, "city": string }
"""

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

# Парсинг JSON
try:
    data = json.loads(output)
    print(data)
except Exception as e:
    print("Ошибка парсинга JSON:", e)

Вывод:

{"name": "Илон Маск", "age": 53, "city": "Остин"}

Принудительная валидация схемы с Pydantic

Чтобы избежать некорректных выходных данных, вы можете валидировать их по схеме Pydantic в Python.

from pydantic import BaseModel

class Person(BaseModel):
    name: str
    age: int
    city: str

# Предположим, что 'output' — это строка JSON от 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 = """
Проанализируйте следующее описание и верните функции модели в формате JSON только.
Описание модели:
'Qwen3 имеет сильную многозначную поддержку (английский, китайский, французский, испанский, арабский).
Он позволяет шаги рассуждения (цепочка мыслей).
Контекстное окно — 128K токенов.'
"""

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 = """
Извлеките функции каждой модели в формате JSON.

1. Llama 3.1 поддерживает рассуждение. Контекстное окно — 128K. Языки: только английский.
2. GPT-4 Turbo поддерживает рассуждение. Контекстное окно — 128K. Языки: английский, японский.
3. Qwen3 поддерживает рассуждение. Контекстное окно — 128K. Языки: английский, китайский, французский, испанский, арабский.
"""

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 := `
  Проанализируйте следующее описание и верните характеристики модели в формате JSON только.
  Описание:
  "Qwen3 имеет сильную многозначную поддержку (английский, китайский, французский, испанский, арабский).
  Он позволяет шаги рассуждения (цепочка мыслей).
  Окно контекста составляет 128K токенов."
  `

	// Определите схему JSON для структурированного вывода
	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"},
	}

	// Преобразуйте схему в JSON
	formatJSON, err := json.Marshal(formatSchema)
	if err != nil {
		log.Fatal("Не удалось преобразовать схему формата:", 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 {
		// Накапливайте содержимое по мере потоковой передачи
		rawResponse += response.Response

		// Разбирайте только когда ответ завершен
		if response.Done {
			if err := json.Unmarshal([]byte(rawResponse), &features); err != nil {
				return fmt.Errorf("Ошибка разбора JSON: %v", err)
			}
		}
		return nil
	})
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Разобранная структура: %+v\n", features)
}

Чтобы скомпилировать и запустить этот пример программы на Go - предположим, что у нас есть файл main.go в папке ollama-struct, Нам нужно выполнить внутри этой папки:

# инициализировать модуль
go mod init ollama-struct
# загрузить все зависимости
go mod tidy
# собрать и запустить
go build -o ollama-struct main.go
./ollama-struct

Пример вывода

Разобранная структура: {Name:Qwen3 SupportsThinking:true MaxContextTokens:128000 Languages:[English Chinese French Spanish Arabic]}

Пример на Go 2: Сравнение нескольких моделей

Вы можете расширить это для извлечения списка моделей для сравнения.

  type ModelComparison struct {
		Models []LLMFeatures `json:"models"`
	}

	prompt = `
	Извлеките характеристики из следующих описаний моделей и верните в формате JSON:

	1. PaLM 2: Эта модель имеет ограниченные возможности рассуждения и сосредоточена на базовом понимании языка. Она поддерживает окно контекста из 8,000 токенов. Она в основном поддерживает только английский язык.
	2. LLaMA 2: Эта модель имеет умеренные возможности рассуждения и может выполнять некоторые логические задачи. Она может обрабатывать до 4,000 токенов в своем контексте. Она поддерживает английский, испанский и итальянский языки.
	3. Codex: Эта модель имеет сильные возможности рассуждения, специально для программирования и анализа кода. У нее есть окно контекста из 16,000 токенов. Она поддерживает английский, Python, JavaScript и Java языки.

	Верните объект JSON с массивом "models", содержащим все модели.
	`

	// Определите схему JSON для сравнения моделей
	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"},
	}

	// Преобразуйте схему в JSON
	comparisonFormatJSON, err := json.Marshal(comparisonSchema)
	if err != nil {
		log.Fatal("Не удалось преобразовать схему сравнения:", 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 {
		// Накапливайте содержимое по мере потоковой передачи
		comparisonResponse += response.Response

		// Разбирайте только когда ответ завершен
		if response.Done {
			if err := json.Unmarshal([]byte(comparisonResponse), &comp); err != nil {
				return fmt.Errorf("Ошибка разбора JSON: %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, чтобы можно было как (a) сгенерировать схему JSON для Ollama, так и (b) проверить ответ модели.

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 = """
Извлеките характеристики для каждой модели. Верните только JSON, соответствующий схеме.
1) Qwen3 поддерживает цепочку мыслей; 128K контекст; английский, китайский, французский, испанский, арабский.
2) Llama 3.1 поддерживает цепочку мыслей; 128K контекст; английский.
3) GPT-4 Turbo поддерживает цепочку мыслей; 128K контекст; английский, японский.
"""

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 список 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))

# Переупорядочьте столбцы для удобства чтения
df = df[["name", "supports_thinking", "max_context_tokens", "languages_count", "languages"]]

# Сохраните как CSV для дальнейшего использования
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("Максимальное окно контекста по моделям (токены)")
plt.xlabel("Модель")
plt.ylabel("Максимальное количество токенов контекста")
plt.xticks(rotation=20, ha="right")
plt.tight_layout()
plt.savefig("max_context_window.png")

TL;DR

С новой поддержкой структурированного вывода в Ollama вы можете рассматривать LLM не только как чат-ботов, но и как движки извлечения данных.

Приведенные выше примеры показали, как автоматически извлекать структурированные метаданные о характеристиках LLM, таких как поддержка мышления, размер окна контекста и поддерживаемые языки — задачи, которые в противном случае требовали бы хрупкого разбора.

Будь то создание каталога моделей LLM, панель оценки или AI-помощник для исследований, структурированные выходные данные делают интеграцию плавной, надежной и готовой к продакшену.

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