Wymuszanie struktury wyjściowej dla LLM: Ollama, Qwen3 oraz Python lub Go
Kilka sposobów na uzyskanie sformatowanego wyjścia z Ollama
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.

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.loadslub 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
nullzamiast 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.Unmarshali 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
omitemptyw 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)

- 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]
- 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
- 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]
- 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)
- (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
- https://ollama.com/blog/structured-outputs
- Cheat sheet Ollama
- Cheat sheet Python
- AWS SAM + AWS SQS + Python PowerTools
- Cheat sheet Golang
- Porównanie ORM Go dla PostgreSQL: GORM vs Ent vs Bun vs sqlc
- Reranking dokumentów tekstowych z Ollama i modelem Embedding Qwen3 - w Go
- Wydajność AWS lambda: JavaScript vs Python vs Golang