構造化された出力でLLMを制約する:Ollama、Qwen3およびPythonまたはGo

Ollamaから構造化された出力を得るいくつかの方法

目次

大規模言語モデル(LLM) は強力ですが、実運用では自由な形式の段落はほとんど使いません。 代わりに、予測可能なデータ:属性、事実、またはアプリにフィードできる構造化されたオブジェクトを望みます。 それはLLM構造化出力です。

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

ducks in a row

この投稿では以下をカバーします:

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

構造化出力とは?

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

“モデルXは、思考の連鎖をサポートし、200Kのコンテキストウィンドウを持ち、英語、中国語、スペイン語を話します。”

これは読みやすいですが、解析が難しいです。

代わりに、構造化出力では厳密なスキーマを要求します:

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

このJSONは、検証、データベースへの保存、UIへのフィードが簡単です。


LLMから構造化出力を簡単に得る方法

LLMがスキーマを理解している場合、特定のスキーマを使用してJSON形式で出力を要求できます。 アリババのQwen3モデルは、推論と構造化された応答に最適化されています。明示的にJSON形式で応答するように指示できます。

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

import json
import ollama

prompt = """
あなたは構造化されたデータ抽出者です。
JSONのみを返してください。
テキスト: "Elon Muskは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": "Elon Musk", "age": 53, "city": "Austin"}

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

不完全な出力を避けるために、PythonPydanticスキーマを使用して検証できます。

from pydantic import BaseModel

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

# Qwen3からのJSON文字列が`output`であると仮定
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の機能を抽出

以下は、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 := `
  以下の記述を分析し、モデルの機能を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("JSONスキーマのマーシャリングに失敗:", 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プログラムをコンパイルして実行するには、ollama-structというフォルダにmain.goファイルがあると仮定します。 このフォルダ内で以下を実行してください:

# モジュールを初期化
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をサポートします。

	"models"配列を含むJSONオブジェクトを返してください。
	`

	// モデル比較のための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構造は問題を引き起こす可能性があります。
  • オプションフィールドを許可(Go構造体タグでomitemptyを使用)して、欠損データを期待する場合。
  • 再試行を追加して、モデルがたまに無効なJSONを出力する場合。

完全な例—LLM仕様から比較表へのチャートの描画(ステップバイステップ:構造化JSONから比較表へ)

llm-chart

  1. 抽出したいデータのスキーマを定義

Pydanticを使用して、(a) Ollama用のJSONスキーマを生成し、(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]
  1. 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リスト
  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))

# 読みやすさのために列を再順序付け
df = df[["name", "supports_thinking", "max_context_tokens", "languages_count", "languages"]]

# さらに使用するためにCSVとして保存
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("モデルごとの最大コンテキストウィンドウ(トークン)")
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駆動型研究アシスタントを構築している場合、構造化出力は統合をスムーズで、信頼性が高く、本番環境に適したものです。

有用なリンク