구조화된 출력 개요
구조화된 출력은 모델 응답이 사용자가 제어하는 JSON Schema와 일치하도록 제한합니다. 파싱해야 하는 문자열이나, 추측해야 하는 "대부분 JSON인" 덩어리, 정규식 일치 결과가 아니라 타입이 지정된 객체를 받습니다. 모델은 스키마에 맞는 값을 반환하거나 타입이 지정된 오류를 반환합니다.
outputSchema를 사용해 한 번 연결하면 됩니다. 나머지는 런타임이 처리합니다. 스키마를 JSON Schema로 변환하고, 제공자의 네이티브 구조화된 출력 API에 전달하며, 응답을 검증하고, 스키마 정의에서 TypeScript 타입을 추론합니다.
import { chat } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
import { z } from "zod";
const Person = z.object({ name: z.string(), age: z.number() });
const person = await chat({
adapter: openaiText("gpt-5.5"),
messages: [{ role: "user", content: "John Doe, 30" }],
outputSchema: Person,
});
person.name; // string — fully typed, no cast
person.age; // number
스키마 라이브러리
TanStack AI는 Standard JSON Schema를 구현하는 모든 라이브러리를 지원합니다.
- Zod (v4.2+)
- ArkType
- Valibot (
@valibot/to-json-schema를 통해) - 일반 JSON Schema 객체 (TypeScript 타입 추론이 손실됩니다 — 일회성 추출 참조)
필드 설명, 세부 검증, 열거형은 사용하는 스키마 라이브러리의 문서를 참조합니다. TanStack AI가 스키마를 자동으로 JSON Schema로 변환합니다.
제공자 지원
모든 어댑터는 제공자의 네이티브 API를 통해 구조화된 출력을 처리합니다.
| 제공자 | 구현 |
|---|---|
| OpenAI | response_format과 json_schema |
| Anthropic | 도구 기반 추출 |
| Google Gemini | responseSchema |
| Ollama | 스키마를 사용하는 JSON 모드 |
| OpenRouter / Grok / Groq | response_format과 json_schema |
| Claude Code / Codex | 동일한 하네스 턴의 네이티브 스키마 플래그 (--json-schema / --output-schema) |
| OpenCode / Grok Build / ACP 호환 | 동일 턴 프롬프트 및 파싱 |
제공자별 세부 사항은 자동으로 처리됩니다. 동일한 chat({ outputSchema }) 호출이 모든 제공자에서 작동합니다. 샌드박스에서 코딩 에이전트를 사용하는 경우 하네스 에이전트를 참조합니다.
Anthropic 스키마 복잡도 제한
Anthropic은 구조화된 출력 스키마를 문법으로 컴파일하고, 너무 크거나 복잡하다고 판단한 스키마를 400 오류로 거부합니다. 일반적으로 Schema is too complex for compilation 또는 output_config.format.schema: Invalid schema: The compiled grammar is too large입니다. 이는 다른 모든 provider가 같은 스키마를 허용하더라도 Claude 모델과 OpenRouter를 통해 라우팅된 anthropic/* 모델에 직접 영향을 줍니다.
이러한 오류가 발생하면 스키마를 단순화합니다. 복잡도는 전체 크기보다 문법의 분기를 증가시키는 구성에 더 크게 좌우됩니다. 흔한 원인은 다음과 같습니다.
- 꼭 필요하지 않은
.optional()필드 .catch()/.default()래퍼 (허용되는 입력을 넓힙니다)- 유니언 타입과 깊게 중첩된 객체
- 제약 없는 선택적 문자열 — 가능한 경우 열거형이나 형식으로 제한합니다
Anthropic의 정확한 제한은 시간이 지나면서 변경되며 모두 공개되어 있지는 않으므로, 여기서는 해당 수치를 의도적으로 재현하지 않습니다 — 현재 제한과 축소 전략은 Anthropic의 구조화된 출력 문서를 참조합니다.
어떤 페이지를 읽어야 하나요?
구축하려는 대상에 맞는 여정을 선택합니다. 구조화된 출력 아래의 가이드는 각각 별도의 사용 사례를 다룹니다. 상황에 맞는 가이드를 읽습니다.
| 하려는 작업 | 읽을 문서 |
|---|---|
단일 프롬프트에서 구조화된 객체 하나를 추출하고 서버 측(스크립트, 엔드포인트, CLI) 또는 브라우저에서 final을 통해 사용합니다 | 일회성 추출 |
| 모델이 스트리밍되는 동안 필드를 하나씩 채우는 UI를 구축합니다 (점진적 폼, 실시간 카드, 타자기식 미리보기) | 스트리밍 UI |
| 여러 턴에 걸쳐 사용자가 구조화된 객체를 반복해서 수정하도록 합니다 — 각 턴이 새로운 타입 지정 객체를 생성하고 기록을 계속 렌더링할 수 있습니다 | 멀티턴 채팅 |
| 구조화된 출력을 도구 호출과 결합합니다 (먼저 도구를 실행한 다음 타입 지정 객체를 반환하는 에이전트 루프) | 도구 사용 |
| 샌드박스의 코딩 에이전트에게 파일을 검사하게 한 다음 타입 지정 객체를 반환하도록 합니다 | 하네스 에이전트 |
스트리밍 경로와 멀티턴 경로는 모두 useChat({ outputSchema })를 기반으로 합니다. "도구 사용" 경로는 이 둘 중 하나 위에 추가됩니다. 출시하려는 형태를 설명하는 경로를 선택하고 — 거기서 시작한 다음 다른 내용이 필요할 때 상호 링크를 따라갑니다.
참고: 서버 측 검증은 경로에 따라 달라집니다. 비스트리밍 에이전트 경로(
await chat({ outputSchema }))에서는 엔진이 최종화 단계 내부에서 Standard Schema 검증을 실행하고 실패를onError를 통해 전달합니다 (대기 중인 프로미스가 거부됩니다). 스트리밍 경로(chat({ outputSchema, stream: true }))에서는 Standard Schema _검증_을 의도적으로 소비자에게 위임합니다 — 소비자는structured-output.complete이벤트의value.object필드에서 객체를 읽거나, 원시 텍스트에 직접parseWithStandardSchema를 호출합니다. 클라이언트에서useChat({ outputSchema })에 전달하는 스키마는 TypeScript 추론과 (useChat에서) 클라이언트 측parsePartialJSON기반 점진적 파싱에 사용됩니다 — 타입 지정 객체 보장은 선택한 서버 측 경로에서 제공됩니다.두 경로 모두에서 엔진은 캡처한 객체가 사용자에게 도달하기 전에 정규화합니다. 엄격한 제공자를 지원하기 위해 선택적 필드를
required+ nullable로 확장하므로, 제공자는 값이 없는 선택적 필드에null을 반환합니다. 엔진은 이 확장을 정확히 되돌립니다 —null로 돌아온.optional()필드는 없음으로 다시 읽히며 (T | undefined와 일치), 실제.nullable()필드의null은 보존됩니다. 따라서value.object(스트리밍)와 대기한 결과 (비스트리밍)는 모두 스키마가 설명하는 확장되지 않은 형태를 전달합니다.
미들웨어 통합
이제 chat()에 구성된 미들웨어는 에이전트 루프뿐 아니라 최종 구조화된 출력 제공자 호출도 관찰합니다. 구조화된 출력 어댑터의 청크에는 ctx.phase === 'structuredOutput'이 할당되며, 전체 실행이 끝날 때 onFinish가 정확히 한 번 발생합니다.
경로에 따라 달라짐:
tools와 스키마로 제한된 최종 답변을 하나의 스트리밍 호출에서 네이티브로 결합하는 어댑터는 별도의 최종화 왕복을 수행하지 않습니다. 엔진은 일반chatStream요청에outputSchema를 연결하고 에이전트 루프 최종 턴의 텍스트에서 구조화된 결과를 수집합니다. 이 경로에서는'structuredOutput'미들웨어 단계가 발생하지 않습니다 — 미들웨어는 평소처럼'beforeModel'/'modelStream'을 통해 실행을 관찰하며,onStructuredOutputConfig는 호출되지 않습니다.네이티브 결합 제공자:
- 현대 OpenAI (Chat Completions + Responses)
- Anthropic Claude 4.5+
- Gemini 3.x
- Grok 4.x 패밀리
- OpenRouter, 모든 라우팅된 모델이
OPENROUTER_COMBINED_TOOLS_AND_SCHEMA_MODELS에 있을 때 (참조 OpenRouter 어댑터)네이티브 결합 모드를 지원하지 않는 어댑터 (Anthropic 4.4-, Gemini 2.x, Grok 2/3, Groq, Ollama 및 해당 집합에 포함되지 않는 OpenRouter 모델)는 기존 최종화 경로를 유지하며
'structuredOutput'단계가 이전과 같이 발생합니다.
구조화된 출력 청크 관찰
import { chat } from "@tanstack/ai";
import type { ChatMiddleware } from "@tanstack/ai";
import { span } from "./span";
const tracing: ChatMiddleware = {
name: "tracing",
onChunk(ctx, chunk) {
// Fires for chunks from the agent loop AND the final structured-output call
// chunk.type narrows inside a conditional — use a discriminant check first:
if ("type" in chunk) {
span.addEvent("chunk", { phase: ctx.phase, type: chunk.type });
}
},
};
제공자 호출 전 JSON Schema 변환
스키마를 변경해야 할 때는 onStructuredOutputConfig 훅을 사용합니다.
import type { ChatMiddleware } from "@tanstack/ai";
import { sharedDefs } from "./schema-defs";
const injectDefs: ChatMiddleware = {
name: "inject-defs",
onStructuredOutputConfig(_ctx, config) {
return {
outputSchema: { ...config.outputSchema, $defs: { ...sharedDefs } },
};
},
};
전체 훅 참조는 고급: 미들웨어를 참조합니다.