第 7 章 · 结构化输出
本章目标:
- 掌握
Output.object()/array()/choice()/json()/text()五种输出类型- 学会用 Zod schema 约束生成数据并用
.describe()提升生成质量- 掌握
partialOutputStream与elementStream两种结构化流式消费方式- 理解结构化输出与工具调用组合时的 step 计数问题与错误处理
7.1 为什么需要结构化输出
文本生成固然有用,但实际业务往往需要结构化数据——从文本中提取信息、对数据分类、生成合成数据等。
许多模型都能生成结构化数据(通常称为「JSON modes」或「tools」),但你需要手动提供 schema 并校验生成的数据,因为 LLM 可能产出不正确或不完整的结构化数据。
AI SDK 通过 generateText 和 streamText 的 output 属性标准化了跨 provider 的结构化对象生成。你可以使用 Zod schemas、Valibot 或 JSON schemas 指定期望的数据形状,模型将生成符合该结构的数据。
💡 结构化输出是
generateText/streamText流程的一部分,意味着可以在同一请求里与工具调用组合使用。
本章示例继续使用 Gateway 构造模型(同样适用于自定义 provider):
import { createGateway } from 'ai';
export const gateway = createGateway({
apiKey: process.env.AI_GATEWAY_API_KEY ?? '',
});7.2 用 Output.object() 生成结构化数据
使用 generateText 配合 Output.object() 从 prompt 生成结构化数据。schema 同时用于校验生成的数据,保证类型安全与正确性:
import { generateText, Output } from 'ai';
import { gateway } from './provider';
import { z } from 'zod';
const { output } = await generateText({
model: gateway('openai/gpt-5'),
output: Output.object({
schema: z.object({
recipe: z.object({
name: z.string(),
ingredients: z.array(
z.object({ name: z.string(), amount: z.string() }),
),
steps: z.array(z.string()),
}),
}),
}),
prompt: 'Generate a lasagna recipe.',
});⚠️ 结构化输出生成在 AI SDK 多轮执行模型中算作一步(每次模型调用或工具执行是一步)。与工具组合时,请在
stopWhen配置中预留步数。
访问响应头与响应体
需要访问 provider 完整响应时,可通过 response 属性:
import { generateText, Output } from 'ai';
const result = await generateText({
// ...
output: Output.object({ schema }),
});
console.log(JSON.stringify(result.response.headers, null, 2));
console.log(JSON.stringify(result.response.body, null, 2));7.3 流式结构化输出
返回结构化数据的复杂度更高,模型响应时间对交互场景可能不可接受。配合 streamText 和 output,你可以在模型生成的同时流式接收结构化响应:
import { streamText, Output } from 'ai';
import { gateway } from './provider';
import { z } from 'zod';
const { partialOutputStream } = streamText({
model: gateway('openai/gpt-5'),
output: Output.object({
schema: z.object({
recipe: z.object({
name: z.string(),
ingredients: z.array(
z.object({ name: z.string(), amount: z.string() }),
),
steps: z.array(z.string()),
}),
}),
}),
prompt: 'Generate a lasagna recipe.',
});
// 将 partialOutputStream 作为异步可迭代对象使用
for await (const partialObject of partialOutputStream) {
console.log(partialObject);
}客户端可以用 useObject hook 消费结构化输出(第 17 章详述)。
流中的错误处理
streamText 立即开始流式传输,流式期间的错误会成为流的一部分而不是抛出异常(防止流崩溃)。用 onError 回调处理:
import { streamText, Output } from 'ai';
const result = streamText({
// ...
output: Output.object({ schema }),
onError({ error }) {
console.error(error); // 记录到你的错误追踪服务
},
});7.4 五种输出类型
AI SDK 通过 Output 对象支持多种指定期望结构的策略。
Output.text()
生成纯文本,不强制任何 schema,直接得到字符串。未指定 output 时的默认行为:
import { generateText, Output } from 'ai';
const { output } = await generateText({
// ...
output: Output.text(),
prompt: 'Tell me a joke.',
});
// output 是一个字符串(笑话内容)Output.object()
基于 schema(如 Zod schema)生成结构化对象,输出经过类型校验:
import { generateText, Output } from 'ai';
import { z } from 'zod';
const { output } = await generateText({
// ...
output: Output.object({
schema: z.object({
name: z.string(),
age: z.number().nullable(),
labels: z.array(z.string()),
}),
}),
prompt: 'Generate information for a test user.',
});
// output 是符合上述 schema 的对象⚠️ 通过
streamText流出的部分输出(partial outputs)无法按 schema 校验,因为不完整的数据可能尚不符合期望结构。
Output.array()
指定期望一个类型化对象数组,每个元素符合 element 中定义的 schema:
import { generateText, Output } from 'ai';
import { z } from 'zod';
const { output } = await generateText({
// ...
output: Output.array({
element: z.object({
location: z.string(),
temperature: z.number(),
condition: z.string(),
}),
}),
prompt: 'List the weather for San Francisco and Paris.',
});
// 输出形如:
// [
// { location: 'San Francisco', temperature: 70, condition: 'Sunny' },
// { location: 'Paris', temperature: 65, condition: 'Cloudy' },
// ]用 streamText 流式生成数组时,可用 elementStream 在每个元素完成时即刻收到它:
import { streamText, Output } from 'ai';
import { z } from 'zod';
const { elementStream } = streamText({
// ...
output: Output.array({
element: z.object({
name: z.string(),
class: z.string(),
description: z.string(),
}),
}),
prompt: 'Generate 3 hero descriptions for a fantasy role playing game.',
});
for await (const hero of elementStream) {
console.log(hero); // 每个元素都是完整且已校验的
}💡
elementStream发出的每个元素都是完整并通过元素 schema 校验的;而partialOutputStream流出的是整个部分数组(含未完成的元素),两者语义不同。
Output.choice()
期望模型从一组固定字符串选项中选择时使用,适合分类或固定枚举回答:
import { generateText, Output } from 'ai';
const { output } = await generateText({
// ...
output: Output.choice({
options: ['sunny', 'rainy', 'snowy'],
}),
prompt: 'Is the weather sunny, rainy, or snowy today?',
});
// output 必然是 'sunny'、'rainy' 或 'snowy' 之一可以提供任意字符串选项集合,输出永远是匹配其中一个的单一字符串值。AI SDK 会校验结果匹配你的选项之一,无效则抛出错误。特别适合分类式生成或为 API 兼容性强制合法值。
Output.json()
想生成并解析非结构化 JSON 值、不强制特定 schema 时使用——适合捕获任意对象或灵活结构:
import { generateText, Output } from 'ai';
const { output } = await generateText({
// ...
output: Output.json(),
prompt:
'For each city, return the current temperature and weather condition as a JSON object.',
});
// output 可以是任意合法 JSON,例如:
// {
// "San Francisco": { "temperature": 70, "condition": "Sunny" },
// "Paris": { "temperature": 65, "condition": "Cloudy" }
// }Output.json 只检查响应是合法 JSON,不校验值的结构和类型。需要 schema 校验请改用 .object 或 .array。
7.5 与工具调用组合
结构化输出的一大优势是可以与工具调用组合:
import { generateText, Output, tool, isStepCount } from 'ai';
import { gateway } from './provider';
import { z } from 'zod';
const { output } = await generateText({
model: gateway('openai/gpt-5'),
tools: {
weather: tool({
description: 'Get the weather for a location',
inputSchema: z.object({ location: z.string() }),
execute: async ({ location }) => {
// 获取天气数据
return { temperature: 72, condition: 'sunny' };
},
}),
},
output: Output.object({
schema: z.object({
summary: z.string(),
recommendation: z.string(),
}),
}),
stopWhen: isStepCount(5),
prompt: 'What should I wear in San Francisco today?',
});⚠️ 使用工具 + 结构化输出时,记住生成本身也算一步。配置
stopWhen为工具执行和输出生成预留足够步数。
7.6 属性描述与输出命名
给 schema 属性添加 .describe("...") 可以为模型提供关于各属性用途的提示,提升结构化数据的质量与准确性:
import { generateText, Output } from 'ai';
import { z } from 'zod';
const { output } = await generateText({
model,
output: Output.object({
schema: z.object({
name: z.string().describe('The name of the recipe'),
ingredients: z
.array(
z.object({
name: z.string(),
amount: z
.string()
.describe('The amount of the ingredient (grams or ml)'),
}),
)
.describe('List of ingredients with amounts'),
steps: z.array(z.string()).describe('Step-by-step cooking instructions'),
}),
}),
prompt: 'Generate a lasagna recipe.',
});属性描述在以下情况特别有用:澄清含糊的属性名、指定期望格式或约定、为复杂嵌套结构提供上下文。
还可以为输出指定可选的 name 和 description,部分 provider 会用它给 LLM 额外指引(如通过 tool 或 schema 名):
import { generateText, Output } from 'ai';
import { z } from 'zod';
const { output } = await generateText({
model,
output: Output.object({
name: 'Recipe',
description: 'A recipe for a dish.',
schema: z.object({
name: z.string(),
ingredients: z.array(z.object({ name: z.string(), amount: z.string() })),
steps: z.array(z.string()),
}),
}),
prompt: 'Generate a lasagna recipe.',
});这适用于所有支持结构化生成的输出类型:Output.object({ name, description, schema })、Output.array({ name, description, element })、Output.choice({ name, description, options })、Output.json({ name, description })。
7.7 访问推理过程
如果使用 reasoning 模型,可以通过结果的 reasoning 相关属性访问模型生成对象时的思考过程字符串(如可用):
import { generateText, Output } from 'ai';
import { z } from 'zod';
const result = await generateText({
model, // 必须是 reasoning 模型
output: Output.object({
schema: z.object({
recipe: z.object({
name: z.string(),
ingredients: z.array(
z.object({
name: z.string(),
amount: z.string(),
}),
),
steps: z.array(z.string()),
}),
}),
}),
prompt: 'Generate a lasagna recipe.',
});
console.log(result.reasoningText);7.8 错误处理
generateText 以两种方式报告结构化输出失败:
- 模型响应无法解析或不符合 schema 时,reject 一个
AI_NoObjectGeneratedError - 返回结果没有 output 时,访问
result.output会抛出AI_NoOutputGeneratedError(比如最后一步以tool-calls而非stop结束)。output是 getter,解构同样会触发该访问
NoObjectGeneratedError 保留以下信息帮助排查:text(模型生成的原始文本)、response(响应元数据)、usage(token 用量)、cause(根因,如 JSON 解析错误):
import {
generateText,
NoObjectGeneratedError,
NoOutputGeneratedError,
Output,
} from 'ai';
try {
const result = await generateText({
model,
output: Output.object({ schema }),
prompt,
});
console.log(result.output);
} catch (error) {
if (NoObjectGeneratedError.isInstance(error)) {
console.log('NoObjectGeneratedError');
console.log('Cause:', error.cause);
console.log('Text:', error.text);
console.log('Response:', error.response);
console.log('Usage:', error.usage);
} else if (NoOutputGeneratedError.isInstance(error)) {
console.log('NoOutputGeneratedError');
}
}本章小结
output属性统一了跨 provider 的结构化生成:Output.object/array/choice/json/text五种类型按需选择- schema 既约束生成也做校验;
.describe()提示属性用途可显著提升质量 - 流式结构化输出有两种消费方式:
partialOutputStream给出部分整体对象,elementStream只给完整已校验的数组元素 - 结构化输出算多轮执行中的一步,与工具组合时要在
stopWhen里留足步数 - 失败时区分
NoObjectGeneratedError(解析/校验失败)与NoOutputGeneratedError(没有产出),两者都携带 cause/text/response/usage 便于排查
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. 关于 Output.text() 的说法正确的是?
2. partialOutputStream 与 elementStream 的核心区别是?
3. 同时使用工具与结构化输出时,为什么要注意 stopWhen 配置?
4. 访问 NoObjectGeneratedError 实例上的哪个属性可以获得 JSON 解析失败之类的根因?
🛠️ 动手实践
- 定义一个「会议纪要提取器」schema(标题、参会人列表、行动项数组含负责人和截止日期),用
Output.object()从一段会议记录文本中提取,并用.describe()补充字段说明。 - 用
streamText + Output.array()实现「英雄卡牌批量生成」,分别体验partialOutputStream与elementStream的区别:打印两种流的中间状态对比。 - 故意用一个必然校验失败的 prompt(如让模型输出与 schema 冲突的内容)触发
NoObjectGeneratedError,打印其cause/text/usage并写日志。