Ograniczanie LLM za pomocą strukturalnego wyjścia: Ollama, Qwen3 & Python lub Go

Kilka sposobów na uzyskanie strukturalnego wyjścia z Ollama

Page content

Duże modele językowe (LLMs)
są potężne, ale w środowisku produkcyjnym rzadko chcemy wolnych paragrafów.
Zamiast tego chcemy przewidywalne dane: atrybuty, fakty lub strukturalne obiekty, które można przekazać do aplikacji.
To Strukturalne wyjście LLM.

Nieco wcześniej Ollama wprowadził obsługę wyjścia strukturalnego (ogłoszenie), co pozwoliło ograniczyć odpowiedzi modelu, aby dopasować się do JSON schema.
To otwiera drogę do spójnych rur przetwarzania danych dla zadań takich jak katalogowanie funkcji LLM, testowanie modeli lub automatyzacja integracji systemów.

kaczki w rzędzie

W tym poście omówimy:

  • Co to jest strukturalne wyjście i dlaczego jest ważne
  • Prosty sposób uzyskania strukturalnego wyjścia z LLM
  • Jak działa nowa funkcja Ollama
  • Przykłady ekstrakcji możliwości LLM:

Co to jest strukturalne wyjście?

Zwykle LLM generują wolny tekst:

„Model X obsługuje rozumowanie z łańcuchem myślenia, ma okno kontekstu o wielkości 200K i mówi po angielsku, chińsku i hiszpańsku.”

To jest czytelne, ale trudne do przetworzenia.

Zamiast tego, z wyjściem strukturalnym prosimy o ściśle określony schemat:

{
  "name": "Model X",
  "supports_thinking": true,
  "max_context_tokens": 200000,
  "languages": ["English", "Chinese", "Spanish"]
}

Ten JSON łatwo zweryfikować, zapisać w bazie danych lub przekazać do interfejsu użytkownika.


Prosty sposób uzyskania strukturalnego wyjścia z LLM

Czasami LLM rozumie, jaki schemat jest potrzebny, i możemy poprosić, aby zwracał dane w formacie JSON zgodnym z określonym schematem. Model Qwen3 z Alibaba jest zoptymalizowany pod kątem rozumowania i strukturalnych odpowiedzi. Można jawnie poinstruować, aby odpowiedzieć w formacie JSON.

Przykład 1: Użycie Qwen3 z ollama w Pythonie, żądanie JSON z schematem

import json
import ollama

prompt = """
Jesteś ekstraktorem danych strukturalnych.
Zwróć tylko JSON.
Tekst: "Elon Musk ma 53 lata i mieszka w Austin."
Schemat: { "name": string, "age": int, "city": string }
"""

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

# Parsowanie JSON
try:
    data = json.loads(output)
    print(data)
except Exception as e:
    print("Błąd parsowania JSON:", e)

Wyjście:

{"name": "Elon Musk", "age": 53, "city": "Austin"}

Wymuszanie walidacji schematu za pomocą Pydantic

Aby uniknąć nieprawidłowych wyjść, można zweryfikować je w oparciu o schemat Pydantic w Pythonie.

from pydantic import BaseModel

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

# Załóżmy, że 'output' to ciąg JSON z Qwen3
data = Person.model_validate_json(output)
print(data.name, data.age, data.city)

To zapewnia, że wyjście odpowiada oczekiwanemu strukturalnemu formatowi.


Strukturalne wyjście z Ollama

Ollama umożliwia teraz przekazanie schematu w parametrze format. Model jest wtedy ograniczony do odpowiedzi tylko w formacie JSON zgodnym z tym schematem (dokumentacja).

W Pythonie zazwyczaj definiuje się schemat za pomocą Pydantic i pozwala Ollama użyć go jako schematu JSON.


Przykład 2: Ekstrakcja metadanych funkcji LLM

Załóżmy, że masz fragment tekstu opisujący możliwości LLM:

„Qwen3 ma silną wsparcie wielojęzyczne (angielski, chiński, francuski, hiszpański, arabski). Pozwala na kroki rozumowania (łańcuch myślenia). Okno kontekstu to 128K tokenów.”

Chcesz dane strukturalne:

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 = """
Przeanalizuj poniższy opis i zwróć funkcje modelu w formacie JSON.
Opis modelu:
'Qwen3 ma silną wsparcie wielojęzyczne (angielski, chiński, francuski, hiszpański, arabski).
Pozwala na kroki rozumowania (łańcuch myślenia).
Okno kontekstu to 128K tokenów.'
"""

resp = chat(
    model="qwen3",
    messages=[{"role": "user", "content": prompt}],
    format=LLMFeatures.model_json_schema(),
    options={"temperature": 0},
)

print(resp.message.content)

Możliwe wyjście:

{
  "name": "Qwen3",
  "supports_thinking": true,
  "max_context_tokens": 128000,
  "languages": ["English", "Chinese", "French", "Spanish", "Arabic"]
}

Przykład 3: Porównanie wielu modeli

Przekazuj opisy wielu modeli i ekstrahuj je w formacie strukturalnym:

from typing import List

class ModelComparison(BaseModel):
    models: List[LLMFeatures]

prompt = """
Wyodrębnij funkcje każdego modelu w formacie JSON.

1. Llama 3.1 obsługuje rozumowanie. Okno kontekstu to 128K. Języki: tylko angielski.
2. GPT-4 Turbo obsługuje rozumowanie. Okno kontekstu to 128K. Języki: angielski, japoński.
3. Qwen3 obsługuje rozumowanie. Okno kontekstu to 128K. Języki: angielski, chiński, francuski, hiszpański, arabski.
"""

resp = chat(
    model="qwen3",
    messages=[{"role": "user", "content": prompt}],
    format=ModelComparison.model_json_schema(),
    options={"temperature": 0},
)

print(resp.message.content)

Wyjście:

{
  "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"]
    }
  ]
}

To sprawia, że łatwiejsze jest testowanie, wizualizowanie lub filtrowanie modeli na podstawie ich funkcji.


Przykład 4: Automatyczne wykrywanie luk

Możesz również pozwolić na wartości null, jeśli pole jest brakujące:

from typing import Optional

class FlexibleLLMFeatures(BaseModel):
    name: str
    supports_thinking: Optional[bool]
    max_context_tokens: Optional[int]
    languages: Optional[List[str]]

To zapewnia, że schemat pozostaje ważny nawet jeśli pewna informacja jest nieznana.


Zalety, ograniczenia i najlepsze praktyki

Użycie strukturalnego wyjścia przez Ollama (lub dowolny system, który to obsługuje) oferuje wiele zalet — ale również ma pewne ograniczenia.

Zalety

  • Silniejsze gwarancje: Modelowi prosi się, aby dopasował się do schematu JSON zamiast wolnego tekstu.
  • Łatwiejsze parsowanie: Można bezpośrednio użyć json.loads lub zweryfikować z Pydantic / Zod, zamiast wyrażeń regularnych lub heurystyk.
  • Ewolucja oparta na schemacie: Można wersjonować schemat, dodawać pola (z domyślnymi wartościami) i utrzymywać kompatybilność wstecz.
  • Interoperacyjność: Systemy dółstronne oczekują danych strukturalnych.
  • Determinizm (lepszy przy niskiej temperaturze): Gdy temperatura jest niska (np. 0), model ma większą szansę na rygorystyczne przestrzeganie schematu. Dokumentacja Ollama zaleca to.

Ograniczenia i pułapki

  • Niezgodność z schematem: Model może nadal odchodzić — np. pomijać wymagane właściwości, zmieniać kolejność kluczy lub dodawać dodatkowe pola. Potrzebna jest walidacja.
  • Złożone schematy: Bardzo głębokie lub rekurencyjne schematy JSON mogą zmylić model lub prowadzić do błędów.
  • Niejasność w instrukcji: Jeśli instrukcja jest niejasna, model może zgadywać pola lub jednostki błędnie.
  • Niespójność między modelami: Niektóre modele mogą lepiej lub gorzej spełniać ograniczenia strukturalne.
  • Ograniczenia tokenów: Sam schemat dodaje koszt tokenów do instrukcji lub wywołania API.

Najlepsze praktyki i wskazówki (wyciągnięte z bloga Ollama + doświadczenia)

  • Używaj Pydantic (Python) lub Zod (JavaScript) do definiowania schematów i automatycznego generowania schematów JSON. To unika błędów ręcznych.
  • Zawsze dodawaj instrukcje takie jak „odpowiedz tylko w formacie JSON” lub „nie dodawaj komentarzy ani dodatkowego tekstu” w swojej instrukcji.
  • Używaj temperature = 0 (lub bardzo niskiej) w celu minimalizacji losowości i maksymalnego przestrzegania schematu. Ollama zaleca determinizm.
  • Waliduj i potencjalnie cofaj (np. ponawiaj lub czyszczenie), gdy parsowanie JSON zawodzi lub walidacja schematu zawodzi.
  • Zacznij od prostszego schematu, a następnie stopniowo rozszerz. Nie zbyt skomplikuj od razu.
  • Dodaj pomocne, ale ograniczone instrukcje błędowe: np. jeśli model nie może wypełnić wymaganego pola, odpowiedz null zamiast go pomijać (jeśli schemat to pozwala).

Przykład 1 w Go: Ekstrakcja funkcji LLM

Oto prosty program w Go, który prosi Qwen3 o strukturalne wyjście dotyczące funkcji 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 := `
  Przeanalizuj poniższy opis i zwróć funkcje modelu w formacie JSON.
  Opis:
  "Qwen3 ma silną wsparcie wielojęzyczne (angielski, chiński, francuski, hiszpański, arabski).
  Pozwala na kroki rozumowania (łańcuch myślenia).
  Okno kontekstu to 128K tokenów."
  `

	// Zdefiniuj schemat JSON dla wyjścia strukturalnego
	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"},
	}

	// Konwertuj schemat na JSON
	formatJSON, err := json.Marshal(formatSchema)
	if err != nil {
		log.Fatal("Nie udało się zakodować formatu schematu:", 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 {
		// Akumuluj zawartość podczas przesyłania
		rawResponse += response.Response

		// Parsuj tylko wtedy, gdy odpowiedź jest kompletna
		if response.Done {
			if err := json.Unmarshal([]byte(rawResponse), &features); err != nil {
				return fmt.Errorf("błąd parsowania JSON: %v", err)
			}
		}
		return nil
	})
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Zaparsowana struktura: %+v\n", features)
}

Aby skompilować i uruchomić ten przykład programu w Go — załóżmy, że mamy plik main.go w folderze ollama-struct, Musimy wykonać polecenia w tym folderze:

# zainicjalizuj moduł
go mod init ollama-struct
# pobierz wszystkie zależności
go mod tidy
# skompiluj i uruchom
go build -o ollama-struct main.go
./ollama-struct

Przykładowe wyjście

Zaparsowana struktura: {Name:Qwen3 SupportsThinking:true MaxContextTokens:128000 Languages:[English Chinese French Spanish Arabic]}

Przykład 2 w Go: Porównanie wielu modeli

Możesz rozszerzyć to, aby wyodrębniać listę modeli do porównania.

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

	prompt = `
	Wyodrębnij funkcje z opisów poniższych modeli i zwróć jako JSON:

	1. PaLM 2: Ten model ma ograniczone możliwości rozumowania i skupia się na podstawowym zrozumieniu języka. Obsługuje okno kontekstu o wielkości 8 000 tokenów. Głównie obsługuje język angielski.
	2. LLaMA 2: Ten model ma umiarkowane możliwości rozumowania i może radzić sobie z pewnymi zadaniami logicznymi. Może przetwarzać do 4 000 tokenów w swoim kontekście. Obsługuje języki angielski, hiszpański i włoski.
	3. Codex: Ten model ma silne możliwości rozumowania szczególnie dla programowania i analizy kodu. Ma okno kontekstu o wielkości 16 000 tokenów. Obsługuje języki angielski, Python, JavaScript i Java.

	Zwróć obiekt JSON z tablicą "models" zawierającą wszystkie modele.
	`

	// Zdefiniuj schemat JSON do porównania modeli
	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"},
	}

	// Konwertuj schemat na JSON
	comparisonFormatJSON, err := json.Marshal(comparisonSchema)
	if err != nil {
		log.Fatal("Nie udało się zakodować schematu porównania:", 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 {
		// Akumuluj zawartość podczas przesyłania
		comparisonResponse += response.Response

		// Parsuj tylko wtedy, gdy odpowiedź jest kompletna
		if response.Done {
			if err := json.Unmarshal([]byte(comparisonResponse), &comp); err != nil {
				return fmt.Errorf("błąd parsowania JSON: %v", err)
			}
		}
		return nil
	})
	if err != nil {
		log.Fatal(err)
	}

	for _, m := range comp.Models {
		fmt.Printf("%s: Kontekst=%d, Języki=%v\n", m.Name, m.MaxContextTokens, m.Languages)
	}

Przykładowe wyjście

PaLM 2: Kontekst=8000, Języki=[English]
LLaMA 2: Kontekst=4000, Języki=[English Spanish Italian]
Codex: Kontekst=16000, Języki=[English Python JavaScript Java]

Przy okazji, qwen3:4b działa dobrze na tych przykładach, tak samo jak qwen3:8b.

Najlepsze praktyki dla programistów w Go

  • Ustaw temperaturę na 0 dla maksymalnego przestrzegania schematu.
  • Waliduj za pomocą json.Unmarshal i zastosuj fallback, jeśli parsowanie zawiedzie.
  • Zachowuj schematy proste — bardzo zagnieżdżone lub rekurencyjne struktury JSON mogą powodować problemy.
  • Zezwól na pola opcjonalne (użyj omitempty w tagach struktury Go), jeśli oczekujesz braku danych.
  • Dodaj ponowne próby, jeśli model czasami emituje nieprawidłowy JSON.

Pełny przykład — Rysowanie wykresu z specyfikacjami LLM (Krok po kroku: od strukturalnego JSON do tabeli porównawczej)

llm-chart

  1. Zdefiniuj schemat dla danych, które chcesz uzyskać

Użyj Pydantic, aby móc zarówno (a) wygenerować schemat JSON dla Ollama, jak i (b) zweryfikować odpowiedź modelu.

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. Poproś Ollama, aby zwracał tylko JSON w tym kształcie

Przekaż schemat w format= i obniż temperaturę dla determinizmu.

from ollama import chat

prompt = """
Wyodrębnij funkcje każdego modelu. Zwróć tylko JSON dopasowany do schematu.
1) Qwen3 obsługuje łańcuch myślenia; 128K kontekst; angielski, chiński, francuski, hiszpański, arabski.
2) Llama 3.1 obsługuje łańcuch myślenia; 128K kontekst; angielski.
3) GPT-4 Turbo obsługuje łańcuch myślenia; 128K kontekst; angielski, japoński.
"""

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 LLMFeatures
  1. Walidacja i normalizacja

Zawsze waliduj przed użyciem w środowisku produkcyjnym.

from pydantic import TypeAdapter

adapter = TypeAdapter(list[LLMFeatures])
models = adapter.validate_json(raw_json)  # -> lista LLMFeatures
  1. Budowanie tabeli porównawczej (pandas)

Przekształć zweryfikowane obiekty w DataFrame, który możesz sortować, filtrować i eksportować.

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

# Przeorganizuj kolumny dla czytelności
df = df[["name", "supports_thinking", "max_context_tokens", "languages_count", "languages"]]

# Zapisz jako CSV do dalszego użytku
df.to_csv("llm_feature_comparison.csv", index=False)
  1. (Opcjonalnie) Szybkie wizualizacje

Proste wykresy pomagają szybko ocenić różnice między modelami.

import matplotlib.pyplot as plt

plt.figure()
plt.bar(df["name"], df["max_context_tokens"])
plt.title("Maksymalne okno kontekstu według modelu (tokeny)")
plt.xlabel("Model")
plt.ylabel("Maksymalne tokeny kontekstu")
plt.xticks(rotation=20, ha="right")
plt.tight_layout()
plt.savefig("max_context_window.png")

TL;DR

Z nową obsługą wyjścia strukturalnego w Ollama możesz traktować LLM nie tylko jako chatboty, ale również jako silniki ekstrakcji danych.

Przykłady powyżej pokazują, jak automatycznie ekstrahować strukturalne metadane dotyczące funkcji LLM, takich jak wsparcie dla rozumowania, rozmiar okna kontekstu i wspierane języki — zadania, które w przeciwnym razie wymagałyby delikatnego parsowania.

Czy budujesz katalog modeli LLM, dashboard oceny, czy badawczego asystenta AI, strukturalne wyjścia sprawiają, że integracja jest płynna, niezawodna i gotowa do produkcji.

Przydatne linki