Wymuszanie struktury wyjściowej dla LLM: Ollama, Qwen3 oraz Python lub Go

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

Page content

Duże modele językowe (LLM) są potężne, ale w środowiskach produkcyjnych rzadko interesują nas swobodne akapity tekstu. Zamiast tego chcemy przewidywalnych danych: atrybutów, faktów lub ustrukturyzowanych obiektów, które można przekazać do aplikacji. Oto ustrukturyzowana wyjścia z LLM.

Wymuszanie schematu zmniejsza częstotliwość, z jaką błędne logitsy zamieniają się w nieprawidłowy JSON, ale temperatura i kary nadal mają znaczenie dla burz powtórzeń; zobacz parametry wnioskowania agentowego dla Qwen i Gemma gdy łączysz ograniczenia format z agentami.

Pewien czas temu Ollama wprowadził wsparcie dla ustrukturyzowanych wyjść (ogłoszenie), co umożliwiło ograniczenie odpowiedzi modelu tak, aby pasowały do schematu JSON. Odblokowuje to spójne potoki ekstrakcji danych do zadań takich jak katalogowanie funkcji LLM, benchmarkowanie modeli lub automatyzacja integracji systemów.

kaczki w rzędzie

W tym poście omówimy:

  • Co to jest ustrukturyzowane wyjście i dlaczego ma to znaczenie
  • Prosty sposób uzyskiwania ustrukturyzowanego wyjścia z LLM
  • Jak nowa funkcja Ollama działa
  • Przykłady ekstrakcji możliwości LLM:

Czym jest ustrukturyzowane wyjście?

Normalnie LLM generuje swobodny tekst:

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

To czytelne, ale trudne do parsowania.

Zamiast tego, przy ustrukturyzowanym wyjściu prosimy o ścisły schemat:

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

Ten JSON jest łatwy do walidacji, przechowywania w bazie danych lub przekazania do interfejsu użytkownika.


Prosty sposób uzyskiwania ustrukturyzowanego wyjścia z LLM

LLM czasem rozumie, czym jest schemat, i możemy poprosić LLM o zwrócenie wyjścia w formacie JSON przy użyciu określonego schematu. Model Qwen3 od Alibaba jest zoptymalizowany pod kątem rozumowania i ustrukturyzowanych odpowiedzi. Możesz wyraźnie polecić mu odpowiedzieć w JSON.

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

import json
import ollama

prompt = """
Jesteś ekstraktorem ustrukturyzowanych danych.
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']

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

Wynik:

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

Wymuszanie walidacji schematu za pomocą Pydantic

Aby uniknąć błędnie sformułowanych wyjść, możesz zwalidować je względem schematu Pydantic w Pythonie.

from pydantic import BaseModel

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

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

Zapewnia to, że wyjście jest zgodne z oczekiwaną strukturą.


Ustrukturyzowane wyjście w Ollama

Ollama pozwala teraz przekazać schemat w parametrze format. Model jest wtedy ograniczony do odpowiadania tylko w JSON zgodnym ze schematem (dokumentacja).

W Pythonie zazwyczaj definiujesz swój schemat przy użyciu Pydantic i pozwalasz Ollama użyć go jako schematu JSON.


Przykład 2: Ekstrakcja metadanych funkcji LLM

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

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

Chcesz ustrukturyzowane dane:

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 = """
Analizuj poniższy opis i zwróć funkcje modelu tylko w formacie JSON.
Opis modelu:
'Qwen3 ma silne wsparcie wielojęzyczne (angielski, chiński, francuski, hiszpański, arabski).
Pozwala na kroki rozumowania (łańcuch myślenia).
Okno kontekstowe wynosi 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żliwy wynik:

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

Przykład 3: Porównanie wielu modeli

Podaj opisy wielu modeli i wyodrębnij je w formie ustrukturyzowanej:

from typing import List

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

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

1. Llama 3.1 obsługuje rozumowanie. Okno kontekstowe to 128K. Języki: tylko angielski.
2. GPT-4 Turbo obsługuje rozumowanie. Okno kontekstowe to 128K. Języki: angielski, japoński.
3. Qwen3 obsługuje rozumowanie. Okno kontekstowe 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)

Wynik:

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

Ułatwia to benchmarkowanie, wizualizację lub filtrowanie modeli według ich funkcji.


Przykład 4: Automatyczne wykrywanie luk

Możesz nawet zezwolić na wartości null, gdy 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]]

Zapewnia to, że Twój schemat pozostaje ważny nawet wtedy, gdy niektóre informacje są nieznane.


Korzyści, pułapki i najlepsze praktyki

Używanie ustrukturyzowanego wyjścia przez Ollama (lub dowolny system to obsługujący) oferuje wiele zalet – ale ma również pewne pułapki.

Korzyści

  • Silniejsze gwarancje: Model jest poproszony o zgodność ze schematem JSON, a nie swobodnym tekstem.
  • Łatwiejsze parsowanie: Możesz bezpośrednio użyć json.loads lub zwalidować za pomocą Pydantic / Zod, zamiast regex lub heurystyk.
  • Ewolucja oparta na schemacie: Możesz wersjonować swój schemat, dodawać pola (z wartościami domyślnymi) i utrzymywać kompatybilność wstecz.
  • Interoperacyjność: Systemy dalszego przetwarzania oczekują ustrukturyzowanych danych.
  • Determinizm (lepszy przy niskiej temperaturze): Gdy temperatura jest niska (np. 0), model częściej rygorystycznie trzyma się schematu. Dokumentacja Ollama to zaleca.

Pułapki i błędy

  • Niezgodność schematu: Model może nadal odbiegać – np. pominąć wymaganą właściwość, zmienić kolejność kluczy lub dołączyć dodatkowe pola. Potrzebujesz walidacji.
  • Skomplikowane schematy: Bardzo głębokie lub rekurencyjne schematy JSON mogą mylić model lub prowadzić do błędów.
  • Niejasność w promptcie: Jeśli Twój prompt jest niejasny, model może błędnie zgadywać pola lub jednostki.
  • Niezgodność między modelami: Niektóre modele mogą lepiej lub gorzej przestrzegać ustrukturyzowanych ograniczeń.
  • Limit tokenów: Sam schemat dodaje koszt tokenów do promptu lub wywołania API.

Najlepsze praktyki i wskazówki (oparte na blogu Ollama + doświadczenie)

  • Używaj Pydantic (Python) lub Zod (JavaScript) do definiowania schematów i automatycznego generowania schematów JSON. Unikaj błędów ręcznych.
  • Zawsze dołączaj instrukcje takie jak „odpowiedz tylko w JSON” lub „nie dołączaj komentarzy ani dodatkowego tekstu” do swojego promptu.
  • Używaj temperature = 0 (lub bardzo niskiej), aby zminimalizować losowość i zmaksymalizować zgodność ze schematem. Ollama zaleca determinizm.
  • Weryfikuj i ewentualnie cofaj (np. ponawiaj lub porządkuj) przy niepowodzeniu parsowania JSON lub walidacji schematu.
  • Zacznij od prostszego schematu, a następnie stopniowo go rozszerzaj. Nie komplikuj początkowo.
  • Dołącz przydatne, ale ograniczone instrukcje błędów: np. jeśli model nie może wypełnić wymaganego pola, odpowiedz null zamiast go pominąć (jeśli Twój schemat to pozwala).

Przykład Go 1: Ekstrakcja funkcji LLM

Oto prosty program Go, który prosi Qwen3 o ustrukturyzowane 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 := `
  Analizuj poniższy opis i zwróć funkcje modelu tylko w formacie JSON.
  Opis:
  "Qwen3 ma silne wsparcie wielojęzyczne (angielski, chiński, francuski, hiszpański, arabski).
  Pozwala na kroki rozumowania (łańcuch myślenia).
  Okno kontekstowe wynosi 128K tokenów."
  `

	// Zdefiniuj schemat JSON dla ustrukturyzowanego wyjścia
	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 do JSON
	formatJSON, err := json.Marshal(formatSchema)
	if err != nil {
		log.Fatal("Nie udało się serializować schematu formatu:", 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ść w miarę strumienia
		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("Sparsowana struktura: %+v\n", features)
}

Aby skompilować i uruchomić ten przykładowy program Go – załóżmy, że mamy plik main.go w folderze ollama-struct, Musimy wykonać wewnątrz tego folderu:

# zainicjuj moduł
go mod init ollama-struct
# pociągnij wszystkie zależności
go mod tidy
# zbuduj i uruchom
go build -o ollama-struct main.go
./ollama-struct

Przykładowy wynik

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

Przykład Go 2: Porównywanie wielu modeli

Możesz rozszerzyć to o ekstrakcję listy modeli do porównania.

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

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

1. PaLM 2: Ten model ma ograniczone możliwości rozumowania i koncentruje się na podstawowym rozumieniu języka. Obsługuje okno kontekstowe 8000 tokenów. Głównie obsługuje tylko język angielski.
2. LLaMA 2: Ten model ma umiarkowane możliwości rozumowania i może radzić sobie z niektórymi zadaniami logicznymi. Może przetwarzać do 4000 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 specyficzne dla programowania i analizy kodu. Ma okno kontekstowe 16000 tokenów. Obsługuje języki: angielski, Python, JavaScript i Java.

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

	// Zdefiniuj schemat JSON dla 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 do JSON
	comparisonFormatJSON, err := json.Marshal(comparisonSchema)
	if err != nil {
		log.Fatal("Nie udało się serializować 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ść w miarę strumienia
		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: Context=%d, Languages=%v\n", m.Name, m.MaxContextTokens, m.Languages)
	}

Przykładowy wynik

PaLM 2: Context=8000, Languages=[English]
LLaMA 2: Context=4000, Languages=[English Spanish Italian]
Codex: Context=16000, Languages=[English Python JavaScript Java]

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

Najlepsze praktyki dla programistów Go

  • Ustaw temperaturę na 0 dla maksymalnej zgodności ze schematem.
  • Weryfikuj za pomocą json.Unmarshal i cofaj się, jeśli parsowanie nie powiedzie się.
  • Trzymaj schematy proste – głęboko zagnieżdżone lub rekurencyjne struktury JSON mogą powodować problemy.
  • Pozwól na opcjonalne pola (używaj omitempty w tagach struktur Go), jeśli oczekujesz brakujących danych.
  • Dodaj ponowne próby, jeśli model okazjonalnie emituje nieprawidłowy JSON.

Pełny przykład – Rysowanie wykresu ze specyfikacjami LLM (Krok po kroku: od ustrukturyzowanego JSON do tabel porównawczych)

llm-chart

  1. Zdefiniuj schemat dla danych, które chcesz

Użyj Pydantic, aby既能 (a) generować schemat JSON dla Ollama, (b) zwalidować 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 o zwrócenie tylko JSON w tej postaci

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

from ollama import chat

prompt = """
Wyodrębnij funkcje dla każdego modelu. Zwróć tylko JSON pasujące do schematu.
1) Qwen3 obsługuje chain-of-thought; kontekst 128K; angielski, chiński, francuski, hiszpański, arabski.
2) Llama 3.1 obsługuje chain-of-thought; kontekst 128K; angielski.
3) GPT-4 Turbo obsługuje chain-of-thought; kontekst 128K; 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  # JSON lista LLMFeatures
  1. Zwaliduj i znormalizuj

Zawsze waliduj przed użyciem w produkcji.

from pydantic import TypeAdapter

adapter = TypeAdapter(list[LLMFeatures])
models = adapter.validate_json(raw_json)  # -> list[LLMFeatures]
  1. Zbuduj tabelę porównawczą (pandas)

Przekonwertuj swoje zwalidowane obiekty na DataFrame, który możesz posortować/przefiltrować i wyeksportować.

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

# Zmień kolejność kolumn dla czytelności
df = df[["name", "supports_thinking", "max_context_tokens", "languages_count", "languages"]]

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

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

import matplotlib.pyplot as plt

plt.figure()
plt.bar(df["name"], df["max_context_tokens"])
plt.title("Maksymalne okno kontekstowe 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

Dzięki nowemu wsparciu dla ustrukturyzowanych wyjść w Ollama, możesz traktować LLM nie tylko jako chatboty, ale jako silniki ekstrakcji danych.

Powyższe przykłady pokazały, jak automatycznie wyodrębniać ustrukturyzowane metadane dotyczące funkcji LLM, takich jak wsparcie dla myślenia, rozmiar okna kontekstowego i obsługiwane języki – zadania, które w przeciwnym razie wymagałyby krucho parsowania.

Niezależnie od tego, czy budujesz katalog modeli LLM, panel ewaluacyjny, czy asystenta badawczego opartego na AI, ustrukturyzowane wyjścia sprawiają, że integracja jest płynna, niezawodna i gotowa do produkcji.

Przydatne linki

Subskrybuj

Otrzymuj nowe wpisy o systemach, infrastrukturze i inżynierii AI.