Ollama構造化出力の二重検証設計

CHATGPT / AIChatGPTなどの生成AIを活用して運営・記事制作・更新を行っています。制作方針

Ollama v0.32.0のローカルAPIを基準に、生成制約とアプリ側型検証を分離し、schema複雑度・temperature・streaming・model差を再現可能に比較する設計を解説します。

生成制約と型検証の二段ゲートを通し、失敗だけを上限付き再試行へ戻す設計図

先に結論

Ollamaの構造化出力をproductionへ組み込むなら、modelへJSON Schemaを渡す生成制約と、受信後に同じschemaでparse・型検証するアプリ境界を二重化します。前者は失敗を減らす仕組み、後者は不正な値を後続へ流さない仕組みであり、置き換え関係ではありません。

評価では「JSONとしてparseできた」と「schemaに適合した」を別指標にし、model、tag、Ollama version、schema、prompt、temperature、stream、試行数を固定します。schema複雑度、temperature、streaming、modelを同時に変えると原因を識別できません。本記事は測定設計までを示す机上調査で、成功率やlatencyの実測値は掲載しません。

確認日時: 2026年8月3日 19:15(Asia/Tokyo)
対象version: Ollama v0.32.0、commit f1a0ffd(2026年7月11日公開)を基準。APIとstructured outputs docsは2026年8月3日に再監査
検証区分: 公式documentation/repository releaseに基づく机上調査・検証計画です。Ollama、Pydantic、Zodを実行しておらず、model別のschema適合率、latency、memory、retry効果は未測定です。
適用範囲: local /api/chat。Ollama Cloud、OpenAI互換endpoint、vision、tool calling、外部公開networkは対象外

確認済みのAPI契約と推論を分ける

2026年8月3日のOllama公式資料から確認できる事実は次のとおりです。

  • /api/chatformat”json”またはJSON Schemaを受け取る
  • REST APIのstreamは既定でtrueであり、streaming responseはpartial contentを返す
  • structured outputsの公式例はstream: falseでschemaを渡している
  • PythonではPydanticのmodel_json_schema()model_validate_json()、JavaScriptではZodのz.toJSONSchema()とparseを組み合わせる例がある
  • 再現性を高める出発点として低いtemperatureが案内されている
  • Ollama Cloudは構造化出力に未対応と明記されている

ここから「productionでは二重検証し、streamingは連結後に検証する」とするのは、本記事の設計判断です。公式streaming docsがpartial fieldを蓄積する必要を説明しているため、完成前のchunk単体を最終documentとしてschema検証しない方針を導きます。ただし、特定modelで何chunkに分かれるか、連結後に何%適合するかは実測していません。

また、v0.32.0を検証基準に固定しますが、「structured outputsがv0.32.0で初めて追加された」とは扱いません。releaseは再現用のruntime境界、現行docsはAPI契約の確認元として役割を分けます。

二段ゲートの実装境界

推奨する流れは次の6段階です。

  1. schemaを型定義から1回生成し、requestとvalidatorで同じartifactを使う
  2. model digest/tag、prompt、options、stream条件とともにrequestを記録する
  3. HTTP statusとAPI response全体のJSON parseを確認する
  4. message.contentをJSONとしてparseする
  5. JSON Schema/Pydantic/Zodで型と制約を検証する
  6. business ruleを別に検証し、合格した値だけを保存・実行へ渡す

Gate 1:生成時に形を制約する

Ollamaへ渡すschemaは、アプリが本当に必要とするpropertyだけから始めます。深い入れ子、巨大なenum、複数の分岐を一度に足すと、どの要素が失敗原因か追いにくくなります。promptにも期待するfieldの意味を明記します。公式docsはschema文字列をpromptにも渡してmodelをgroundする方法を案内していますが、同じ巨大schemaを無制限に重複させるとprompt tokenと保守箇所が増えるため、その影響は測定対象にします。

{
  "type": "object",
  "properties": {
    "summary": {"type": "string"},
    "keywords": {
      "type": "array",
      "items": {"type": "string"}
    }
  },
  "required": ["summary", "keywords"]
}

Gate 2:アプリ境界で同じ契約を検証する

受信側は「HTTP 200」「outer JSON」「content JSON」「schema」「business rule」を別々に判定します。たとえば要約の文字数上限やkeywordの許可listは、JSONとして正しいだけでは保証されません。model出力をSQL、shell、HTML、URL fetchへ使う場合は、schema適合後にもそのsink固有のescape、allowlist、権限分離が必要です。

公式例のPydantic/Zod経路を使う利点は、requestのschemaとアプリの型を同じ定義から作れることです。手書きschemaと手書きvalidatorを別々に更新すると、property追加時に片方だけ古くなるdriftが起きます。schema hashまたはversionを結果recordへ持たせ、どの契約で判定したか追跡します。

再現可能な比較条件

測定前に固定する値と、1回に変更する変数を分けます。

区分記録する項目理由
runtimeOllama version、commit/build、OS、CPU/GPU、memoryupdateやbackend差を混ぜない
model正確な名前、tagまたはdigest、量子化、context同じ表示名でも実体差を残す
inputprompt本文、schema bytes/hash、system message契約と入力の変更を検出する
generationtemperature、seed対応の有無、context、生成上限sampling差を分離する
transportendpoint、stream、timeoutchunk処理とtimeoutを分離する
repetitionwarm-up方針、試行回数、実行順初回loadと繰り返しを混ぜない

同一modelを比較するときも、modelがmemoryにload済みかでload_durationが変わり得ます。API responseにはtotal_durationload_durationprompt_eval_countprompt_eval_durationeval_counteval_durationが定義されています。これらは観測に使えますが、単位と欠損を確認し、schema適合率と速度を同じ1指標へまとめません。

変数ごとの検証計画

Schema複雑度

複雑度は段階的に増やします。

  1. requiredなstring 1件
  2. stringとarray
  3. nested object
  4. enumや長さなどの制約
  5. optional/nullable field

各段階で同じinput setを使い、前段が安定してから次へ進みます。記録するのはHTTP成功、outer parse、content parse、schema適合、business rule適合の件数です。「失敗率」だけでは、接続失敗と型不一致を区別できません。

Temperature

公式docsのtipに合わせ、まず0をbaselineにします。その後に1値ずつ上げ、内容の多様性とschema適合を別々に観測します。temperatureを変えながらmodelも交換すると因果が分からないため、同じmodel artifactとprompt順を固定します。temperature 0でも完全な決定性は仮定しません。

Streaming

非streamingをcontract testのbaselineにし、streamingは別suiteにします。streamingでは各chunkのouter JSONを検査し、message.contentの断片を順序どおり連結し、doneを確認した後に1回だけcontent JSON/schema検証を行います。途中の文字列をparse失敗として数えると、正常なpartial JSONを誤って不合格にします。

接続切断、timeout、done未到達は「schema不適合」ではなくtransport failureです。途中まで受け取った値を保存や後続処理へ渡さず、request IDと受信済みbyte数など秘密を含まない診断情報だけを残します。

Model差

同じschemaでもmodelごとの追従は実測が必要です。比較では各modelの正確なtag/digest、量子化、context、prompt templateを固定し、同じtest caseを同じ回数だけ実行します。model Aで成功したためmodel Bも成功すると一般化せず、applicationが許容する最小適合率とfailure budgetを先に決めます。

記録形式と判定指標

1試行につき次のfieldを持つJSON Linesなどへ記録すると、段階別に集計できます。実際のpromptや生成本文に機密情報がある場合は保存せず、hashとtest case IDへ置き換えます。

timestamp, run_id, ollama_version, model_digest, schema_hash,
temperature, stream, http_ok, outer_json_ok, content_json_ok,
schema_ok, business_rule_ok, failure_class,
total_duration, load_duration, prompt_eval_count, eval_count

主要指標は次のように分けます。

  • content JSON率: HTTP成功のうちmessage.contentをparseできた割合
  • schema適合率: content JSONのうち型・制約を通った割合
  • end-to-end合格率: 全試行のうちbusiness ruleまで通った割合
  • latency: loadを含む/除く、非streaming/streamingを分けた分布
  • retry増幅: 1つのlogical requestあたりの実API call数

平均値だけでなく、用途に応じてp50/p95とfailure class件数を確認します。ただし本記事には実測sampleがなく、数値目標やmodel順位は提示しません。

Retry、fallback、停止条件

retryは失敗を隠す仕組みではありません。少なくとも次を分けます。

  • connection failure/timeout:短いbackoffで上限付きretry候補
  • HTTP 4xx:request、model名、schemaを直すまで原則retryしない
  • content JSON不正:同条件でのretry回数を制限し、raw responseを診断対象にする
  • schema不適合:違反fieldを記録し、schema簡略化またはmodel変更を明示的に判断する
  • business rule不適合:modelへ丸投げせず、後続処理を停止する

fallbackでschemaを緩める場合は、同じ成功指標へ混ぜません。strict schemaとfallback schemaには別versionを付けます。fallback後の値を「strictに成功」と数えると、運用品質を過大評価します。

Trade-offと適用外

選択利点Cost/注意点
小さいschema切り分けやすくpromptも短いbusiness ruleをアプリ側で補う必要がある
厳しいschemadownstreamの形を揃えやすいmodel差や複雑な制約で不適合が増える可能性
非streaming完成documentを検証しやすいfirst tokenの表示を待つ
streamingUIへ早く表示できる連結、切断、done判定のstate管理が増える
retry一時的失敗を吸収できるlatency、計算量、重複処理が増える
model fallback可用性を上げられる品質、license、memory、schema適合を再検証する必要がある

本設計は、JSONをtrusted commandへ変換する安全性、model出力の事実性、prompt injection対策、個人情報処理を自動的には解決しません。またCloudは公式に未対応で、OpenAI互換APIはresponse_format経路になるため、local native APIの結果をそのまま一般化しません。

入門記事

初めて試す場合は、OllamaでJSON Schema出力を作る入門で、非streamingの小さなschemaを1回通し、外側JSONとcontent JSONを分けて確認してください。

参考資料