構造化出力によるLLMの制約:Ollama、Qwen3、PythonおよびGo

Ollamaから構造化された出力を取得する方法

目次

大規模言語モデル(LLM) は強力ですが、本番環境では自由な形式のパラグラフ(段落)を返すことは稀です。 代わりに、アプリに投入できる予測可能なデータ:属性、事実、または構造化されたオブジェクトを求めます。 それが LLM 構造化出力 です。

スキーマの強制は、無効な JSON になる悪いログิต(確率分布)の頻度を減らしますが、リトライの嵐を避けるために温度(temperature)やペナルティも依然として重要です。エージェントと format 制約を組み合わせる場合は、Qwen と Gemma のエージェント推論パラメータ を参照してください。

以前、Ollama は構造化出力サポートを導入しました (発表)。これにより、モデルの応答をJSON スキーマに一致させることが可能になりました。 これにより、LLM 機能のカタログ作成、モデルのベンチマーク、システム統合の自動化などのタスクにおいて、一貫したデータ抽出パイプラインが実現します。

ducks in a row

この記事では、以下の内容をカバーします:

  • 構造化出力とは何か、そしてそれがなぜ重要なのか
  • LLM から構造化出力を取得する簡単な方法
  • Ollama の新機能がどのように動作するか
  • LLM 機能を抽出する例:

構造化出力とは何か?

通常、LLM は自由なテキストを生成します:

「モデル X は連鎖思考(chain-of-thought)による推論をサポートし、20万トークンのコンテキストウィンドウを持ち、英語、中国語、スペイン語を扱います。」

これは人間にとって読みやすいですが、パース(解析)が困難です。

一方、構造化出力では、厳格なスキーマを要求します:

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

この JSON は検証、データベースへの保存、または UI への投入が容易です。


LLM から構造化出力を取得する簡単な方法

LLM はスキーマの概念を理解することができ、特定のスキーマに従って JSON 形式で出力を返すよう指示することができます。 Alibaba の Qwen3 モデルは推論と構造化された応答に最適化されています。明示的にJSON で応答するよう指示することができます。

例 1: Python で ollama を使用し、スキーマ付きの JSON を要求する Qwen3

import json
import ollama

prompt = """
あなたは構造化データ抽出アシスタントです。
JSON のみを返してください。
テキスト: "Elon Musk is 53 and lives in Austin."
スキーマ: { "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("Error parsing JSON:", e)

出力:

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

Pydantic でスキーマ検証を強制する

不正な出力を避けるために、PythonPydantic スキーマに対して検証を行うことができます。

from pydantic import BaseModel

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

# 'output' が Qwen3 から得られた JSON 文字列であると仮定
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 has strong multilingual support (English, Chinese, French, Spanish, Arabic).
It allows reasoning steps (chain-of-thought).
The context window is 128K tokens.'
"""

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 のブログ + 経験に基づく)

  • スキーマを定義し、JSON スキーマを自動生成するために**Pydantic(Python)またはZod(JavaScript)**を使用します。これにより手動エラーを避けます。
  • プロンプトに**「JSON のみで応答」または「注釈や余分なテキストを含めない」**といった指示を必ず含めます。
  • ランダム性を最小化し、スキーマへの準拠を最大化するためにtemperature = 0(または非常に低い値)を使用します。Ollama は決定性を推奨しています。
  • JSON のパースまたはスキーマ検証が失敗した場合は、検証し、必要に応じてフォールバック(例:リトライまたはクリーンアップ)を行います。
  • シンプルなスキーマから始め、徐々に拡張します。最初は複雑にしすぎないよう注意します。
  • 有用だが制約のあるエラー指示を含めます:例えば、モデルが必須フィールドを埋められない場合、スキーマが許可しているなら省略するのではなく null で応答します。

Go 例 1: LLM 機能の抽出

以下は、Qwen3 に LLM の機能に関する構造化出力を要求するシンプルな Go プログラムです。

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 := `
  Analyze the following description and return the model’s features in JSON only.
  Description:
  "Qwen3 has strong multilingual support (English, Chinese, French, Spanish, Arabic).
  It allows reasoning steps (chain-of-thought).
  The context window is 128K tokens."
  `

	// Define the JSON schema for structured output
	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"},
	}

	// Convert schema to JSON
	formatJSON, err := json.Marshal(formatSchema)
	if err != nil {
		log.Fatal("Failed to marshal format schema:", 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 {
		// Accumulate content as it streams
		rawResponse += response.Response

		// Only parse when the response is complete
		if response.Done {
			if err := json.Unmarshal([]byte(rawResponse), &features); err != nil {
				return fmt.Errorf("JSON parse error: %v", err)
			}
		}
		return nil
	})
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Parsed struct: %+v\n", features)
}

この例の Go プログラムをコンパイルして実行するには - ollama-struct というフォルダにこの main.go ファイルがあると仮定します、 このフォルダ内で以下を実行する必要があります:

# initialise module
go mod init ollama-struct
# pull all the dependencise
go mod tidy
# build & execute
go build -o ollama-struct main.go
./ollama-struct

出力例

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

Go 例 2: 複数のモデルを比較する

比較用のモデルリストを抽出するためにこれを拡張できます。

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

	prompt = `
	Extract features from the following model descriptions and return as JSON:

	1. PaLM 2: This model has limited reasoning capabilities and focuses on basic language understanding. It supports a context window of 8,000 tokens. It primarily supports English language only.
	2. LLaMA 2: This model has moderate reasoning abilities and can handle some logical tasks. It can process up to 4,000 tokens in its context. It supports English, Spanish, and Italian languages.
	3. Codex: This model has strong reasoning capabilities specifically for programming and code analysis. It has a context window of 16,000 tokens. It supports English, Python, JavaScript, and Java languages.

	Return a JSON object with a "models" array containing all models.
	`

	// Define the JSON schema for model comparison
	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"},
	}

	// Convert schema to JSON
	comparisonFormatJSON, err := json.Marshal(comparisonSchema)
	if err != nil {
		log.Fatal("Failed to marshal comparison schema:", 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 {
		// Accumulate content as it streams
		comparisonResponse += response.Response

		// Only parse when the response is complete
		if response.Done {
			if err := json.Unmarshal([]byte(comparisonResponse), &comp); err != nil {
				return fmt.Errorf("JSON parse error: %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 開発者向けのベストプラクティス

  • 最大限のスキーマ準拠のために temperature を 0 に設定します。
  • json.Unmarshal で検証し、パースが失敗した場合はフォールバックします。
  • スキーマをシンプルに保つ — 深くネストされた再帰的な JSON 構造は問題を引き起こす可能性があります。
  • 欠落データが予想される場合は、オプションのフィールドを許可します(Go の構造体タグで omitempty を使用)。
  • モデルが稀に無効な JSON を出力する場合、リトライを追加します。

完全な例 - LLM 仕様を使ってチャートを描画する(ステップバイステップ:構造化 JSON から比較テーブルまで)

llm-chart

  1. 欲しいデータのスキーマを定義する

Ollama 用の JSON スキーマを生成し、かつモデルの応答を検証するために Pydantic を使用します。

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 = """
Extract features for each model. Return JSON only matching the schema.
1) Qwen3 supports chain-of-thought; 128K context; English, Chinese, French, Spanish, Arabic.
2) Llama 3.1 supports chain-of-thought; 128K context; English.
3) GPT-4 Turbo supports chain-of-thought; 128K context; English, Japanese.
"""

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

# Reorder columns for readability
df = df[["name", "supports_thinking", "max_context_tokens", "languages_count", "languages"]]

# Save as CSV for further use
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("Max Context Window by Model (tokens)")
plt.xlabel("Model")
plt.ylabel("Max Context Tokens")
plt.xticks(rotation=20, ha="right")
plt.tight_layout()
plt.savefig("max_context_window.png")

TL;DR

Ollama の新しい構造化出力サポートにより、LLM をチャットボットとしてだけでなく、データ抽出エンジンとして扱うことができます。

上記の例は、推論サポート、コンテキストウィンドウのサイズ、サポートされている言語などのLLM 機能に関する構造化メタデータを自動的に抽出する方法を示しました — これらは以前であれば脆いパース処理が必要だったタスクです。

LLM モデルカタログ評価ダッシュボード、またはAI 搭載のリサーチアシスタントを構築しているかどうかに関わらず、構造化出力は統合をスムーズで、信頼でき、本番環境対応にします。

有用なリンク

購読する

システム、インフラ、AIエンジニアリングの新記事をお届けします。