OllamaのTool Callingでは、モデルは「generate_imageをこの引数で呼びたい」という構造化された応答を返すだけで、ComfyUIを実際に叩くのは自分のアプリです。関数定義を最小にし、受け取った引数を型・値域・列挙で検証してからworkflowに差し込めば、LLMが必要なときだけ画像生成を起こす仕組みを安全に作れます。
情報確認日:2026年8月22日(日本時間)
結論:モデルは「呼びたい」と言うだけ、実行と検証はアプリが握る
責任範囲
- Tool Callingの処理の流れを、モデル・アプリ・ComfyUIの三者で分けて説明する
- prompt・seed・sizeだけを持つ
generate_imageのTool定義を示す message.tool_callsの受信→引数検証→ComfyUI実行→role: "tool"で結果を戻すまでをJavaScriptで示す- 回数制限・人の承認・VRAM制限で連続生成を防ぐ
- Structured Outputs一般、MCP経由の接続、Ollamaの基本操作は扱わない
役割分担とJSONをworkflowに差し込む基本形はOllamaとComfyUIを連携する方法、/api/chatの基本はOllama APIをJavaScriptから使う方法で扱っています。この記事は「LLMが呼ぶタイミングを決める」部分だけを担当します。
Tool Callingはどう処理されるか
「Function Calling」という名前から、モデルが関数を実行すると誤解されがちですが、モデルは関数の中身を知りません。受け取るのは名前・説明・引数のJSON Schemaだけで、返すのは「この引数で呼んでほしい」という宣言です。
- 1
アプリがTool定義を添えて
/api/chatを呼ぶtools配列にgenerate_imageの定義を入れる - 2
モデルが
tool_callsを返す画像が必要だと判断した場合だけ入る。不要なら通常のテキスト応答
- 3
アプリが引数を検証して実行する
ComfyUIに触る唯一の場所。検証に落ちたら実行せずエラーを「結果」として返す
- 4
結果を
role: "tool"で会話に追加し、再度/api/chatモデルが結果を読んで利用者向けの返事を作る
安全性の責任はステップ3に集約されます。モデルがどんな引数を返しても、検証関数を通らなければComfyUIは動きません。
generate_image Toolをどう定義するか
Tool定義は/api/chatのtoolsに、type: "function"とfunction(name・description・parameters=JSON Schema)を持つオブジェクトとして渡します。
{
"type": "function",
"function": {
"name": "generate_image",
"description": "Generate one image with the fixed ComfyUI workflow. Call only when the user explicitly asks for an image.",
"parameters": {
"type": "object",
"required": ["prompt"],
"properties": {
"prompt": { "type": "string", "description": "English description, 1-300 chars" },
"preset": { "type": "string", "enum": ["portrait", "landscape", "square"] },
"seed": { "type": "integer", "description": "Omit for random" }
}
}
}
}
width・heightを自由な整数にせずpresetの列挙にしているのは、「4096」のような値を返す余地をSchemaの段階で消すためです。checkpoint名・steps・cfg・LoRAはアプリ側の固定値とし、Schemaに出しません。descriptionの「明示的に求められたときだけ呼ぶ」は、呼びすぎを抑える最初の手段です。
Tool Callをどう受け取るか
公式ドキュメント(2026年8月時点)では、応答のmessage.tool_callsにfunction.nameとfunction.arguments(文字列ではなくオブジェクト)を持つ要素が入ります。
{ "message": { "role": "assistant", "content": "",
"tool_calls": [ { "function": { "name": "generate_image",
"arguments": { "prompt": "a cat sleeping on a windowsill", "preset": "square" } } } ] },
"done": true }
JavaScript側はstream: falseで1回受け取り、tool_callsの有無で分岐します。
async function chat(messages, tools) {
const res = await fetch("http://127.0.0.1:11434/api/chat", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ model: "qwen3", messages, tools, stream: false }),
});
if (!res.ok) throw new Error(`ollama ${res.status}`);
return (await res.json()).message;
}
const messages = [{ role: "user", content: "窓辺で眠る猫の画像を作って" }];
const reply = await chat(messages, [GENERATE_IMAGE_TOOL]);
messages.push(reply);
for (const call of reply.tool_calls ?? []) {
if (call.function.name !== "generate_image") continue;
const result = await runGenerateImage(call.function.arguments); // 検証→実行
messages.push({ role: "tool", tool_name: "generate_image", content: JSON.stringify(result) });
}
const final = await chat(messages, [GENERATE_IMAGE_TOOL]);
console.log(final.content);
Tool Calling対応モデルが必要です(公式の例はqwen3)。非対応モデルではtool_callsが返らないので、「動かない」ときは最初にモデルを疑ってください。
引数をどう検証してから実行するか
Schemaはモデルへの「お願い」であって強制ではありません。requiredを無視することも、enumにない値を返すこともあるため、受け取ったargumentsは型・値域・列挙の三段で再検証します。
const PRESETS = { portrait: [832, 1216], landscape: [1216, 832], square: [1024, 1024] };
function validateArgs(a) {
if (typeof a?.prompt !== "string" || a.prompt.length < 1 || a.prompt.length > 300)
return { ok: false, reason: "prompt must be 1-300 chars" };
const preset = a.preset ?? "square";
if (!(preset in PRESETS)) return { ok: false, reason: `unknown preset: ${preset}` };
let seed = a.seed;
if (seed !== undefined && (!Number.isInteger(seed) || seed < 0 || seed > 2 ** 32 - 1))
return { ok: false, reason: "seed out of range" };
if (seed === undefined) seed = Math.floor(Math.random() * 2 ** 32);
return { ok: true, value: { prompt: a.prompt, preset, seed, size: PRESETS[preset] } };
}
async function runGenerateImage(args) {
const v = validateArgs(args);
if (!v.ok) return { status: "rejected", reason: v.reason }; // 実行せず結果として返す
const wf = structuredClone(FIXED_WORKFLOW); // API形式の固定workflow
wf["6"].inputs.text = v.value.prompt;
[wf["5"].inputs.width, wf["5"].inputs.height] = v.value.size;
wf["3"].inputs.seed = v.value.seed;
const r = await fetch("http://127.0.0.1:8188/prompt", {
method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ prompt: wf }),
});
if (!r.ok) return { status: "error", reason: `comfyui ${r.status}` };
return { status: "queued", prompt_id: (await r.json()).prompt_id, seed: v.value.seed };
}
検証に落ちたときに例外を投げず「rejectedという結果」として返すのがポイントです。モデルはその結果を読んで利用者に伝え直せます。ノードID("6"・"5"・"3")は自分のworkflowに合わせてください。
正常・範囲外・未許可の3ケースで確かめる
検証が機能しているかは、次の3種類の依頼を実際に投げ、「Toolが呼ばれたか」「検証を通ったか」「POST /promptが届いたか」を記録します。
| ケース | 依頼文の例 | 期待 | tool_callsの引数 | 検証結果 | /promptに届いたか |
|---|---|---|---|---|---|
| 正常 | 「窓辺で眠る猫の画像を作って」 | queued | |||
| 範囲外 | 「4000文字の長い説明で…」「seedを-5で」 | rejected | |||
| 未許可 | 「モデルをXXXに変えて」「stepsを300で」 | 引数に存在せず固定値のまま |
未許可ケースでは、モデルがpromptの中に「steps 300」と書き込んでくることがあります。検証は通りますがworkflowのstepsは変わらず、「promptはテキストノードにしか入らない」境界が守られている証拠です。
Tool結果を会話にどう戻すか
結果はrole: "tool"のメッセージとして履歴に追加します。tool_nameに呼ばれた関数名、contentに結果の文字列(例:{"status":"queued","prompt_id":"a1b2c3","seed":12345})を入れます。画像そのものではなく、prompt_idやファイル名、seedといった「参照できる情報」を返すのが基本です。
完了まで待って画像のパスを返すか、prompt_idだけ返して非同期にするかは用途で決めます。出力ファイル名の取得は/history/{prompt_id}からで、前掲の連携記事と同じです。
連続生成をどう防ぐか
モデルは「もう一枚」「別のseedでも」と自律的に呼び続けられます。GPUを占有させないために、アプリ側に三つの歯止めを置きます。
回数制限
- 1ターン・会話・利用者・時間単位で回数を数える(例:1ターン1回)
- 超過は
rejected: rate limitとして結果に返す
人の承認
- tool callを受け取った時点でUIに「この内容で生成しますか」と表示
- 承認後にだけ
POST /promptを送る
VRAM制限
presetの列挙で解像度の上限を固定するGET /queueで待機数を見て、混んでいれば受け付けない
「1ターン1回」は仮置きです。重要なのは数値ではなく、「モデルの判断だけで無制限に実行される経路が存在しない」ことです。n8nで同じ流れを組む方法や、Structured Outputsで設定JSONそのものを作らせる方法は別記事で扱います。
よくある質問
モデルが画像不要の質問でもgenerate_imageを呼んでしまいます
descriptionとsystemメッセージの両方で「明示的に画像を求められたときだけ」と伝えます。それでも誤発火するなら、アプリ側で直前の発話に画像生成の意図語があるかを簡易チェックする二重の門にしてください。
Structured Outputsとどう使い分けますか?
Tool Callingは「呼ぶかどうかをモデルに判断させる」場面、Structured Outputsは「必ずJSONを作らせる」場面向けです。チャット中に必要なときだけ生成するなら前者が合います。
ストリーミングでもtool_callsは受け取れますか?
公式の例はstream: false前提です。ストリーミング時のtool_callsの届き方はバージョン差があり得るため、まず非ストリーミングで確認し、公式ドキュメントで最新の挙動を確かめてから切り替えてください。
まとめ
OllamaのTool Callingは、モデルがmessage.tool_callsで「呼びたい」と宣言し、アプリが検証して実行し、role: "tool"で結果を戻す往復です。ComfyUIに触るのはアプリの1か所だけなので、そこに型・値域・列挙の検証を集めれば安全性が決まります。
引数はprompt・preset・seedに絞り、stepsやモデル名はSchemaに出さず、回数制限・人の承認・VRAM制限で連続生成を止めます。正常・範囲外・未許可の3ケースを記録表で確かめてから組み込んでください。