OllamaのStructured Outputsは、/api/chatのformatにJSON Schemaを渡すと、応答をそのSchemaに沿ったJSONに強制する機能です。自然文から壊れにくいComfyUI設定JSONを作るには、Schemaをprompt・seed・width・height・presetなど数項目に絞り、受信後に値域と意味を再検証し、workflowのノードIDには触れさせないMapping層を置きます。
情報確認日:2026年8月22日(日本時間)
結論:Schemaで「形」を保証し、値域と意味はこちらで検証し、ノードには自分で差す
責任範囲
- 「JSONらしい文字列」と「Schemaで構造が保証されたJSON」の違いを説明する
- ComfyUI用の最小Schema(5項目)を設計し、
formatに渡す現行の書き方を示す - 型が合っていても必要な値域・意味の再検証をJavaScriptで示す
- LLMの出力をノードIDに変換するMapping層と、Preset方式を提案する
- 自然文20件で「parseは通るが検証で落ちる」件数を記録する手順を載せる
- Tool Callingの実行フロー、Ollamaの基本操作、RAGは扱わない
基本の連携はOllamaとComfyUIを連携する方法、LLMに「呼ぶかどうか」を判断させる構成はOllamaのTool CallingでComfyUIを実行するで扱っています。この記事は「必ずJSONを作らせる」場面のSchema設計に絞ります。
なぜSchemaが必要か
「JSONで答えて」とpromptに書くだけでも、多くの場合はJSONらしい文字列が返りますが、構造の保証ではありません。前後に説明文が付く、キー名が揺れる、数値が文字列になるといった崩れは頻度が低くても起き、アプリはその「たまに」で落ちます。
| 観点 | promptで「JSONで」と頼む | formatにSchemaを渡す |
|---|---|---|
| 構造 | モデルの善意に依存 | Schemaに沿った形に制約される |
| キー名 | 揺れる(Prompt/prompt_text) |
Schemaのpropertiesに固定 |
| 型 | "1024"と1024が混在し得る |
integerなら数値 |
| 値域・意味 | 保証なし | 保証なし。別に検証が要る |
表の最後の行が要点です。Schemaが守るのは「形」までで、「promptが空文字でないか」「存在しないpresetを指していないか」は守りません。形の保証と意味の検証を分けて考えます。
最小Schemaをどう設計するか
Schemaは「workflowの中で差し替える値」だけにします。ComfyUIのworkflow JSONをまるごと表すSchemaは作りません。5項目あれば大半の用途は足ります。
{
"type": "object",
"required": ["prompt", "preset"],
"properties": {
"prompt": { "type": "string", "description": "English positive prompt" },
"negative": { "type": "string", "description": "English negative prompt. Empty string if none" },
"preset": { "type": "string", "enum": ["portrait", "landscape", "square"] },
"seed": { "type": "integer", "description": "-1 for random" },
"style": { "type": "string", "enum": ["photo", "illustration", "watercolor"] }
}
}
width・heightを数値で持たせずpresetの列挙にしています。数値だと「1023」や「8192」が返る余地が残りますが、列挙なら選択肢の外が構造上ありません。styleも同様に、checkpoint名を直接返させず、アプリ側で対応表を引く名前だけを返させます。
列挙にできるものは列挙にする:自由値を返させるのはpromptとseedだけに留めると、次の章の再検証が短く済みます。
Schema付きでどう依頼するか
公式ドキュメント(2026年8月時点)では、/api/chatのformatパラメータにJSON Schemaオブジェクトを渡します。文字列"json"を渡す簡易モードもありますが、構造を固定したいならSchemaを渡します。推奨事項として、temperatureを下げる(例:0)、Schemaをpromptにも文字列で入れる、の2点が挙げられています。
// 確認日: 2026-08-22 / Ollama Structured Outputs の format パラメータに基づく
const SCHEMA = { /* 上のSchema */ };
async function toSettings(userText) {
const res = await fetch("http://127.0.0.1:11434/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: "qwen3",
stream: false,
format: SCHEMA,
options: { temperature: 0 },
messages: [
{ role: "system", content: "Convert the request into image settings. Return JSON matching this schema: " + JSON.stringify(SCHEMA) },
{ role: "user", content: userText },
],
}),
});
if (!res.ok) throw new Error(`ollama ${res.status}`);
const { message } = await res.json();
return JSON.parse(message.content); // 構造はSchemaで保証。意味はまだ未検証
}
message.contentは文字列なのでJSON.parseが必要です。例外は稀ですがゼロではないため、try-catchで「parse失敗」として記録してください。JavaScriptではZod、PythonではPydanticでSchemaを定義し、同じ定義を検証にも使う方法が公式で案内されています。
受信後に何を再検証するか
parseが通った時点で保証されているのは「キーと型」だけです。次の検証を必ず挟みます。
const PRESETS = { portrait: [832, 1216], landscape: [1216, 832], square: [1024, 1024] };
const STYLES = { photo: "base.safetensors", illustration: "illust.safetensors", watercolor: "base.safetensors" };
const BANNED = [/\bnsfw\b/i, /\bnude\b/i]; // 用途に応じて管理
function validateSettings(s) {
const errors = [];
if (typeof s.prompt !== "string" || !s.prompt.trim()) errors.push("prompt empty");
if (s.prompt && s.prompt.length > 400) errors.push("prompt too long");
if (BANNED.some((re) => re.test(s.prompt ?? ""))) errors.push("banned word");
if (!(s.preset in PRESETS)) errors.push(`preset ${s.preset}`);
if (s.style !== undefined && !(s.style in STYLES)) errors.push(`style ${s.style}`);
const seed = Number.isInteger(s.seed) ? s.seed : -1;
if (seed < -1 || seed > 2 ** 32 - 1) errors.push("seed range");
return { ok: errors.length === 0, errors, value: { ...s, seed, negative: s.negative ?? "" } };
}
型が合っていても落とすべき例は、空文字のprompt、負の大きなseed、enumにないはずの値(制約が効かないモデルで起こり得る)、禁止語を含むpromptです。検証を省くと、これらがそのままworkflowに入ります。
自然文20件で検証する
設計の良し悪しは、自分の用途に近い日本語の依頼を20件用意し、「parseが通ったか」「検証が通ったか」「落ちた理由」を1件ずつ記録すると分かります。結果は環境とモデルで変わるため、この記事では表を空欄のまま載せます。
| No. | 依頼文 | parse | 検証 | 落ちた理由 | 対処(Schema/prompt/検証) |
|---|---|---|---|---|---|
| 1 | 「夕焼けの海辺を写真っぽく、縦長で」 | ||||
| 2 | 「さっきと同じ構図で色違い」(文脈なし) | ||||
| 3 | 「8Kで超高解像度に」 | ||||
| … | |||||
| 20 |
見るべきは「parse成功なのに検証で落ちた」件数です。これが多ければSchemaの形は正しく、意味の指示(systemメッセージやdescription)が足りないことを示します。逆にparse自体が落ちるなら、モデルの対応状況かSchemaの複雑さを疑います。
Workflowへどう安全に差し替えるか
LLMにノードIDを扱わせないために、検証済みの設定をworkflow JSONに書き込む層を1か所に分離します。ノードIDを知っているのはこの層だけです。
// ノードIDは自分のworkflow(Export (API))に合わせる
const NODE = { positive: "6", negative: "7", latent: "5", sampler: "3", checkpoint: "4" };
function applyToWorkflow(base, v) {
const wf = structuredClone(base);
wf[NODE.positive].inputs.text = v.prompt;
wf[NODE.negative].inputs.text = v.negative;
[wf[NODE.latent].inputs.width, wf[NODE.latent].inputs.height] = PRESETS[v.preset];
wf[NODE.sampler].inputs.seed = v.seed >= 0 ? v.seed : Math.floor(Math.random() * 2 ** 32);
if (v.style) wf[NODE.checkpoint].inputs.ckpt_name = STYLES[v.style];
return wf; // これを POST /prompt の body.prompt に入れる
}
この層があると、workflowを作り直してノードIDが変わっても、直すのはNODEの対応表だけで、LLM側のSchemaとpromptは変わりません。LLMにノードIDを返させる設計では、workflowを変えるたびにSchemaとモデルの挙動を再確認することになります。
書き込む先を限定する:Mapping層が触るのはtext・width・height・seed・ckpt_nameのような値だけにし、ノードの追加・削除・接続の変更はしません。ckpt_nameも対応表にある名前しか入らないため、存在しないファイル名がworkflowに入る経路はありません。
Schemaを大きくしすぎないためにどうするか
「samplerも」「stepsも」「LoRAの強度も」と足していくと、検証が追いつかなくなり、出力も不安定になります。代わりにPreset方式を使います。
Workflow全構造をSchemaにする
- ノード・接続・全パラメータを表す巨大なSchema
- モデルが存在しないノードや無効な接続を作り得る
- 検証がworkflowの再実装に近づく
Preset方式(推奨)
- 「誰が見ても意味が分かる名前」の列挙だけ返させる
- 名前→実値の対応表はアプリが持ち、検証は
in判定で済む - 新しい組み合わせが欲しければPresetを1つ足す
目安として、自由値が3つ、列挙が3つを超えたら、別Schemaに分けるかPresetに畳めないかを考えてください。数値は仮置きです。スタイル規約をRAGで参照させる方法や、n8nで同じ変換を組む方法は別記事で扱います。
よくある質問
format: "json"とSchema指定はどう違いますか?
"json"は「有効なJSONであること」だけを求める簡易モードで、キー名や型は固定されません。アプリで使うならSchemaオブジェクトを渡し、その上で再検証してください。
Schemaにenumを書いても外れた値が返ることはありますか?
構造の制約はモデルとバージョンに依存します。公式は制約を強制すると説明していますが、アプリ側の検証を省く根拠にはしないでください。検証があれば、外れた値が返ってもworkflowには入りません。
日本語のpromptをそのままComfyUIに入れてよいですか?
多くのcheckpointは英語promptを前提に学習されています。Schemaのdescriptionで「English」と指定し、モデルに翻訳させる形にすると、ComfyUI側の変更なしで済みます。
まとめ
Structured OutputsはformatにSchemaを渡して「形」を保証する機能であり、値域と意味は保証しません。Schemaはprompt・negative・preset・seed・styleの5項目に絞り、列挙にできるものは列挙にし、受信後に空文字・値域・禁止語・対応表の有無を検証します。
検証済みの値をworkflowに書き込むMapping層を1か所に置けば、ノードIDはLLMから完全に隠せます。まず自然文20件で「parse成功・検証失敗」の件数を記録し、Schemaと指示のどちらを直すべきかを決めてください。