# LLM과 Reduce

> client.llm으로 언어 모델에서 검증된 JSON을 받고, client.reduce로 여러 결과 중 가장 좋은 것을 고르거나, 투표하거나, 이어 붙이거나, 병합합니다.

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

<strong>`client.llm`</strong>은 언어 모델에 **구조화된 출력**을 요청합니다. 시스템 프롬프트, 입력, JSON Schema를 보내면 스키마에 맞는 객체를 돌려받습니다. <strong>`client.reduce`</strong>는 Reduce 팬인 단계를 단독으로 실행합니다. 여러 결과 중 가장 좋은 것을 고르고, 개수를 세고, 투표하고, 텍스트를 이어 붙이거나 JSON을 병합합니다. 워크플로에서 [최적 선택](https://nodaro.ai/docs/nodes/automate/choose-best) 노드가 하는 일입니다. 둘 다 모델 등급에 따라 크레딧이 듭니다.

## 메서드
| 메서드 | 기능 |
| --- | --- |
| [`llm.structured(input)`](#llmstructuredinput) | 요청 한 번으로 언어 모델에서 검증된 객체 받기 |
| [`llm.structuredJob(input)`](#llmstructuredjobinput) | 같은 호출을 폴링하는 작업으로 실행 |
| [`reduce.run(input)`](#reduceruninput) | 여러 입력을 고르기, 세기, 투표, 이어 붙이기, 병합으로 하나로 줄이기 |

## client.llm
구조화된 출력입니다. 내 시스템 프롬프트와 JSON Schema를 넣으면 검증된 객체가 나옵니다. 플랫폼이 모델 경로를 고르고, JSON 출력을 강제하고, 내 스키마로 검증하며, 포기하기 전에 유효하지 않은 답을 모델에 다시 보냅니다. 모델 등급에 따라 `llm-structured`로 청구됩니다.

### llm.structured(input)
모델에 요청하고 답을 기다립니다(`POST /v1/llm/structured`). 호출 한 번에 몇 분이 걸릴 수 있으며, 이는 클라이언트의 기본 타임아웃 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이며, 최대 64KB, 깊이 20단계까지입니다." },
schemaName: { type: 'string', description: "모델이 보는 스키마 이름이며, 최대 64자입니다." },
llmModel: { type: 'string', description: "모델 ID입니다. 기본 모델을 쓰려면 생략하세요." },
reasoningEffort: { type: 'string', description: "모델에 따라 none, low, medium, high, xhigh, max 중 하나입니다. xhigh와 max는 한 등급 위로 청구되지만, 프리미엄 모델은 premium 그대로입니다. Claude 모델에서는 어떤 추론 강도든 제작사 자체 API에서 실행되며, 한 단계 더 위로 청구됩니다. 최대 premium-direct까지입니다." },
maxRetries: { type: 'number', default: '2', description: "호출이 실패하기 전에 유효하지 않은 답을 모델에 다시 보내는 횟수이며, 0~3입니다." },
origin: { type: 'string', description: "작업에 저장되는 내 앱의 이름입니다. client.jobs.list({ origin })로 내 실행을 찾을 수 있습니다." },
advancedMode: { type: 'boolean', description: "Gemini와 Claude 모델용입니다. 개발사 자체 API에서 실행하며, temperature와 maxTokens가 완전히 적용됩니다. 한 단계 위로 청구됩니다. 프리미엄 모델은 premium-direct 가격으로 올라갑니다." },
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: "동영상을 다시 분석하는 대신 초안의 바탕으로 쓸, 내가 완료한 동영상 분석 작업입니다. 두 번째 분석 비용을 아낄 수 있습니다. 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)
여러 입력을 하나로 줄입니다. 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? }`, 기본값은 빈 줄 | 모든 입력을 이어 붙인 텍스트 하나 |
| `first-non-empty` | 없음 | 비어 있지 않은 첫 입력 |
| `count` | 없음 | 입력의 개수 |
| `vote` | `{ caseSensitive? }`, 기본값은 `false` | 가장 많이 나온 입력. 동률이면 먼저 나온 입력입니다. |
| `merge-json` | `{ strategy? }`: `"deep"`(기본값) 또는 `"shallow"` | JSON 입력을 병합한 객체 하나 |

```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`가 발생합니다.

## 자주 묻는 질문

### 언어 모델에서 내 스키마에 맞는 JSON을 받으려면 어떻게 하나요?

시스템 프롬프트, 입력, JSON Schema 객체를 넣어 client.llm.structured를 호출하세요. 플랫폼이 모델이 그 형태로 답하도록 강제하고, 답을 검증하고, 유효하지 않은 답은 다시 시도한 다음, 파싱된 객체를 반환합니다.

### structured 대신 structuredJob은 언제 사용하나요?

오래 걸리는 호출에는 structuredJob을 사용하세요. structured는 요청 한 번으로 답을 기다리며, 이는 기본 타임아웃 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뿐입니다.
