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 до таблиц сравнения)
- Определите схему для данных, которые вы хотите
Используйте 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]
- Попросите 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
- Проверьте и нормализуйте
Всегда проверяйте перед использованием в продакшене.
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))
# Переупорядочьте столбцы для удобства чтения
df = df[["name", "supports_thinking", "max_context_tokens", "languages_count", "languages"]]
# Сохраните как CSV для дальнейшего использования
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("Максимальное окно контекста по моделям (токены)")
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-помощник для исследований, структурированные выходные данные делают интеграцию плавной, надежной и готовой к продакшену.
Полезные ссылки
- https://ollama.com/blog/structured-outputs
- Ollama cheatsheet
- Python Cheatsheet
- AWS SAM + AWS SQS + Python PowerTools
- Golang Cheatsheet
- Сравнение Go ORMs для PostgreSQL: GORM vs Ent vs Bun vs sqlc
- Переранжирование текстовых документов с Ollama и моделью Qwen3 Embedding - на Go
- Производительность AWS lambda: JavaScript vs Python vs Golang