Skip to main content

構造化出力と Function Calling

ファインチューニング済みのモデルは、本来決まった形を返すように学習します。triage なら { category, urgency, summary, nextAction }redaction なら { redactedText, redactedCount, tags }。ただし学習途中のチェックポイントでは出力がブレます。余計な前置きが入る、キーが抜ける、ときにそもそもパースできない文字列が返ってくる。「データセットだけで形を保つ」前提は脆いのです。 infer({ responseFormat }) を使うと、その形をハード制約にできます。モデルはデコード時点で「渡された JSON Schema に合致する文字列しか出せない」状態に縛られるので、JSON.parse は常に成功し、結果のオブジェクトは必ず指定したキーを持ちます。これにより中間チェックは「サンプルを見て目で確認」から「型付きフィールドを抽出して条件分岐する」に変わります。 このレシピでは出番の多い 3 つのフィールド (JSON Schema を強制する responseFormat、関数呼び出し用の tools、そして JSON Schema では表現できない制約用の structuredOutputs) を扱います。

パターン

responseFormat: { type: "json_schema", json_schema: { name, schema, strict: true } } が OpenAI 互換のシェイプです(strict はトップレベルではなく json_schema の中)。スキーマは推論バックエンドにそのまま転送され、デコード時にレスポンス全体がそれを満たすよう制約されます。strict: true を付けると、properties に宣言されていないキーは拒否され、required が強制されます。 infer({ stream: false }) は OpenAI chat-completions 形式の単一 JSON ボディを返すので、data.choices[0].message.contentJSON.parse する標準的な経路でそのまま読めます。

Early Stopping に繋ぐ

型付きフィールドが取れれば、それで分岐できます。Early stopping レシピと組み合わせて:
スキーマが「category が必ず存在する文字列であること」を保証してくれるので、あとは「自分のラベル集合にとって正しいか」を判定するだけです。urgency も同じ発想で、step 30 のチェックポイントが既に "high" ばかり返している場合、モデルは崩れていて以降の学習は無駄です。

Function Calling

外部ツールを呼ぶ必要があるとき (注文の状態を引く、天気を取る、社内 API を叩く) は、toolstoolChoiceinfer に渡します。応答は free-form なテキストではなく tool_calls を含む形になり、あなたのコードがそのツールを実行し、続けたければ tool ロールのメッセージを足して再度 infer を呼びます。
Function Calling を使うには推論エンドポイント側で auto-tool-extraction が有効になっている必要があります。設定が無いと 400 tool_calling_not_configured で返ります。これはエンドポイント設定を直すサインで、リトライしても通りません。toolChoice: "required"toolChoice: { type: "function", function: { name } } は guided-decoding 経路を通るので parser は不要、parser が要るのは "auto" だけです。

responseFormat で表現できない制約

responseFormat で 9 割は足ります。残りの 1 割は structuredOutputs(vLLM のスーパーセット)で扱います: 正規表現マッチ、固定 choice、独自 EBNF グラマーなどが乗ります。json / regex / choice / grammar / json_object のうちちょうど 1 つだけを設定する規約で、型システムがその不変条件を強制するので 2 つ同時には設定できません。 よくあるのは「固定の文字列集合のいずれかしか出さない」というケースで、余計な前置きを許したくない分類タスクのプロンプトに有用です:
他の形式も同じパターンです。チケット ID 形式の強制なら regex: "^[A-Z]{3}-\\d{4}$"、自前の EBNF なら grammar: "..."。フィールド名は snake_case (json_object, disable_any_whitespace, whitespace_pattern) で、vLLM の wire format に合わせてあるためそのままワーカーまで通ります。

注意点

  • strict: true が事実上の正解。 付けないとスキーマは「ヒント」扱いで、モデルがブレる余地が残ります。付けると properties に無いキーを拒否し、required を強制します。
  • パースするなら stream: false stream を有効にすると SSE デルタを自分で組み立ててから JSON にパースする必要があります。本レシピのように 1 チェックポイントに 1 推論なら、単一 JSON ボディの方が短く、レイテンシー差は意味を持ちません。
  • try / catch で囲む。 callback 内での throw は trainer.wait() を即座に reject します。SSE 再接続ループには乗せません。再接続ループはトランスポート障害のみを扱います (SDK § ライフサイクルコールバック)。致命的でない処理は callback 内で catch し、状態変更には他のレシピと同様に AbortController を使います。
  • スキーマは検証なしで素通り。 SDK は渡された JSON Schema を検証しません。スキーマ自体のバリデーションは推論バックエンド側で行われ、不正な箇所があれば 4xx で返ります。