# LLM と Reduce

> client.llm で言語モデルから検証済みの JSON を取得し、client.reduce で複数の結果からベストを選んだり、多数決を取ったり、結合や統合をしたりします。

Source: https://nodaro.ai/ja/docs/developers/sdk/llm-and-reduce

**`client.llm`** は、言語モデルに**構造化出力**を依頼します。システムプロンプトと入力、JSON Schema を送ると、そのスキーマに一致するオブジェクトが返ります。**`client.reduce`** は、Reduce のファンインの処理を単体で実行します。複数の結果からベストを選ぶ、件数を数える、多数決を取る、テキストを結合する、JSON をマージするといった、ワークフロー内の[**ベストを選択**（Choose Best）](https://nodaro.ai/docs/nodes/automate/choose-best)ノードが行う処理です。どちらも、モデルのティアに応じてクレジットがかかります。

## メソッド
| メソッド | 内容 |
| --- | --- |
| [`llm.structured(input)`](#llmstructuredinput) | 言語モデルから、検証済みのオブジェクトを 1 回のリクエストで取得します |
| [`llm.structuredJob(input)`](#llmstructuredjobinput) | 同じ呼び出しを、ポーリングするジョブとして実行します |
| [`reduce.run(input)`](#reduceruninput) | 多数の入力を、選択、カウント、多数決、結合、マージのいずれかで 1 つにまとめます |

## client.llm
構造化出力：システムプロンプトと JSON Schema を渡すと、検証済みのオブジェクトが返ります。プラットフォームがモデルのレーンを選び、JSON 出力を強制し、指定したスキーマに沿って検証し、無効な答えは、あきらめる前にモデルへ差し戻します。課金は `llm-structured` として、モデルのティアごとに行われます。

### llm.structured(input)
モデルに問い合わせ、答えを待ちます（`POST /v1/llm/structured`）。1 回の呼び出しに数分かかることがあり、クライアントのデフォルトの 60 秒のタイムアウトより長くなる場合があります。大きめの `timeoutMs` を指定してクライアントを作成するか、`structuredJob()` を使ってください。

```ts
structured<T>(input: LlmStructuredInput): Promise<{
jobId: string
output: T
usage: { inputTokens: number; outputTokens: number }
}>
```

<TypeTable
type={{
system: { type: 'string', required: true, description: "システムプロンプトです。最大 100,000 文字です。" },
input: { type: 'string', required: true, description: "ユーザーメッセージです。1〜100,000 文字です。" },
jsonSchema: { type: 'Record<string, unknown>', required: true, description: "type が object の JSON Schema です。最大 64 KB、深さ 20 段階までです。" },
schemaName: { type: 'string', description: "モデルに見せるスキーマの名前です。最大 64 文字です。" },
llmModel: { type: 'string', description: "モデル ID です。省略するとデフォルトが使われます。" },
reasoningEffort: { type: 'string', description: "none、low、medium、high、xhigh、max のいずれかで、モデルによって異なります。xhigh と max は、1 段階上のティアで課金されます。" },
maxRetries: { type: 'number', default: '2', description: "呼び出しが失敗するまでに、無効な答えをモデルへ差し戻す回数です。0〜3 です。" },
origin: { type: 'string', description: "あなたのアプリの名前で、ジョブに保存されます。client.jobs.list({ origin }) で、自分の実行を検索できます。" },
advancedMode: { type: 'boolean', description: "Gemini モデル専用です。開発元自身の API 上で実行し、temperature と maxTokens が完全に有効になります。1 段階上のティアで課金されます。" },
temperature: { type: 'number', description: "サンプリングの温度です。" },
maxTokens: { type: 'number', description: "答えの最大の長さで、トークン数です。" },
}}
/>

```ts
type Plan = { title: string; scenes: string[] }

const { output } = await client.llm.structured<Plan>({
system: "You write production plans for short films.",
input: "A rainy chase through Rome, 60 seconds.",
jsonSchema: {
type: "object",
properties: {
title: { type: "string" },
scenes: { type: "array", items: { type: "string" } },
},
required: ["title", "scenes"],
},
schemaName: "production_plan",
})
console.log(output.title, output.scenes.length)
```

### llm.structuredJob(input)
同じ呼び出しを、ジョブとして実行します（`POST /v1/llm/structured/jobs`）。`jobId` がすぐに返るので、[`client.jobs.getStatus()`](https://nodaro.ai/docs/developers/sdk/jobs-and-executions) でポーリングします。ジョブは、動画から下書きを作ることもできます。その場合、プラットフォームはまず動画を分析し、その分析結果を入力に加えます。

```ts
structuredJob(input: LlmStructuredJobInput): Promise<{ jobId: string }>
```

<TypeTable
type={{
'...': { type: 'LlmStructuredInput', description: "structured() のすべてのフィールドです。" },
label: { type: 'string', description: "実行のタイトルです。最大 120 文字で、実行の一覧に表示されます。" },
videoUrl: { type: 'string', description: "下書きのもとにする動画です。あなた自身が所有する別のジョブとして、先に分析されます。" },
videoAnalysis: { type: '{ llmModel?: string; selectionMode?: "choose" | "combine" }', description: "その分析のオプションです。" },
analysisJobId: { type: 'string', description: "下書きのもとにする、完了済みの自分の動画分析ジョブです。動画をもう一度分析する代わりに使い、2 回目の分析料金がかかりません。videoAnalysis と組み合わせることはできません。" },
}}
/>

```ts
const { jobId } = await client.llm.structuredJob({
system: "You write production plans.",
input: "A rainy chase through Rome.",
jsonSchema: { type: "object", properties: { title: { type: "string" } }, required: ["title"] },
origin: "my-app",
label: "Rome chase",
})

// later, even from another session
const { data } = await client.jobs.getStatus(jobId)
if (data.status === "completed") {
console.log((data.output_data as { output: { title: string } }).output.title)
}
const { data: runs } = await client.jobs.list({ type: "llm-structured", origin: "my-app" })
```

ジョブの実行中、`output_data` には `stage` が入り、値は `analyzing`（動画からの下書きの場合）または `drafting` です。完了すると、`output_data` には `output`、`inputTokens`、`outputTokens` が入り、動画からの下書きの場合はさらに `analysisJobId` と `analysisCredits` も入ります。

- `analysisJobId` が指すジョブが自分のものではない、存在しない、または完了した動画分析ではない場合、422 で失敗します。コードは `analysis_not_found`、`not_analysis`、`analysis_failed`、`analysis_not_ready`、`invalid_analysis` のいずれかです。
- このルートがないプラットフォームでは、`NotFoundError` がスローされます。
- 言語モデルの呼び出しを Nodaro Cloud に送るセルフホスティング環境では、`503 provider_unavailable` が返ります。これは一時的なエラーではなく、そのインスタンスでは利用できないものとして扱ってください。

## client.reduce
### reduce.run(input)
多数の入力を 1 つに集約します。MCP の `reduce` ツールや、[ベストを選択](https://nodaro.ai/docs/nodes/automate/choose-best)ノードと同じ処理です。

```ts
run(input: ReduceInput): Promise<ReduceResult>
```

<TypeTable
type={{
strategyId: { type: '"pick-best-llm" | "concat" | "first-non-empty" | "count" | "vote" | "merge-json"', required: true, description: "入力をどう集約するかです。" },
inputs: { type: 'string[]', required: true, description: "最大 1,000 件の入力で、テキストまたは URL です。" },
strategyConfig: { type: 'Record<string, unknown>', default: '{}', description: "戦略の設定です。下の表を参照してください。" },
workflowId: { type: 'string', description: "この実行を一覧表示する対象のワークフローで、その実行履歴に表示されます。" },
}}
/>

| 戦略 | `strategyConfig` | 返す内容 |
| --- | --- | --- |
| `pick-best-llm` | `{ criteria, inputKind?, llmModel? }`。`inputKind` は `"text"` または `"image-url"` です。`llmModel` は判定に使うモデルを選び、そのモデルのクレジットのティアが適用されます。 | 言語モデルが最も優れていると判断した入力で、そのインデックスと判断理由を含みます |
| `concat` | `{ separator? }`。デフォルトは空行です。 | すべての入力を 1 つのテキストに結合したもの |
| `first-non-empty` | なし | 空でない最初の入力 |
| `count` | なし | 入力の件数 |
| `vote` | `{ caseSensitive? }`。デフォルトは `false` です。 | 最も頻度の高い入力です。同数の場合は最初のものが選ばれます。 |
| `merge-json` | `{ strategy? }`：`"deep"`（デフォルト）または `"shallow"` | JSON の入力を 1 つのオブジェクトにマージしたもの |

```ts
const result = await client.reduce.run({
strategyId: "pick-best-llm",
strategyConfig: { criteria: "The sharpest image with no artifacts", inputKind: "image-url" },
inputs: [url1, url2, url3, url4, url5],
})
console.log(result.output)             // the chosen URL
console.log(result.meta.selectedIndex) // 0 to 4
console.log(result.meta.reasoning)     // why the model chose it
```

結果は `{ jobId, output, meta }` です。`output` は、選ばれた値または結合された値で、文字列です。`meta.summary` は常に設定されます。`pick-best-llm` と `vote` は `meta.selectedIndex` を設定し、`pick-best-llm` はさらに `meta.reasoning` も設定します。

```ts
// Majority vote
const winner = await client.reduce.run({ strategyId: "vote", inputs: ["red", "blue", "red"] })

// Deep-merge JSON fragments
const merged = await client.reduce.run({
strategyId: "merge-json",
inputs: [JSON.stringify({ a: 1, nested: { x: 1 } }), JSON.stringify({ b: 2, nested: { y: 2 } })],
})
JSON.parse(merged.output) // { a: 1, b: 2, nested: { x: 1, y: 2 } }
```

すべての入力が空か空白だけの場合、呼び出しは失敗し、ステータスが 400 で `code` が `no_valid_inputs` の `NodaroError` になります。クレジットは、ほかの生成と同じように確保されるため、残高が足りない場合は `InsufficientCreditsError` がスローされます。

## Frequently asked questions

### スキーマに一致する JSON を、言語モデルから取得するには、どうすればよいですか？

システムプロンプトと入力、JSON Schema オブジェクトを指定して client.llm.structured を呼び出します。プラットフォームはモデルにその形式で答えるよう強制し、検証し、無効な答えはモデルに差し戻したうえで、パースされたオブジェクトを返します。

### structured の代わりに structuredJob を使うべきなのは、どんなときですか？

時間のかかる呼び出しには structuredJob を使います。structured は 1 回のリクエストで答えを待つため、数分かかることがあり、デフォルトの 60 秒のタイムアウトより長くなります。structuredJob は jobId をすぐに返します。

### 生成した複数の画像から、最も良いものを選ぶには、どうすればよいですか？

strategyId に pick-best-llm を、criteria に判断基準を、inputKind に image-url を指定して client.reduce.run を呼び出し、画像の URL を inputs として渡します。結果には、選ばれた URL、そのインデックス、モデルの判断理由が示されます。

### 言語モデルを必要としない Reduce の戦略は、どれですか？

concat、first-non-empty、count、vote、merge-json は、モデルなしで実行されます。言語モデルに判断を依頼するのは pick-best-llm だけです。
