Skip to content

第 7 章 · 结构化输出

本章目标:

  • 掌握 Output.object() / array() / choice() / json() / text() 五种输出类型
  • 学会用 Zod schema 约束生成数据并用 .describe() 提升生成质量
  • 掌握 partialOutputStreamelementStream 两种结构化流式消费方式
  • 理解结构化输出与工具调用组合时的 step 计数问题与错误处理

7.1 为什么需要结构化输出

文本生成固然有用,但实际业务往往需要结构化数据——从文本中提取信息、对数据分类、生成合成数据等。

许多模型都能生成结构化数据(通常称为「JSON modes」或「tools」),但你需要手动提供 schema 并校验生成的数据,因为 LLM 可能产出不正确或不完整的结构化数据。

AI SDK 通过 generateTextstreamTextoutput 属性标准化了跨 provider 的结构化对象生成。你可以使用 Zod schemas、Valibot 或 JSON schemas 指定期望的数据形状,模型将生成符合该结构的数据。

💡 结构化输出是 generateText/streamText 流程的一部分,意味着可以在同一请求里与工具调用组合使用。

本章示例继续使用 Gateway 构造模型(同样适用于自定义 provider):

ts
import { createGateway } from 'ai';

export const gateway = createGateway({
  apiKey: process.env.AI_GATEWAY_API_KEY ?? '',
});

7.2 用 Output.object() 生成结构化数据

使用 generateText 配合 Output.object() 从 prompt 生成结构化数据。schema 同时用于校验生成的数据,保证类型安全与正确性:

ts
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 属性:

ts
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 流式结构化输出

返回结构化数据的复杂度更高,模型响应时间对交互场景可能不可接受。配合 streamTextoutput,你可以在模型生成的同时流式接收结构化响应:

ts
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 回调处理:

tsx
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 时的默认行为:

ts
import { generateText, Output } from 'ai';

const { output } = await generateText({
  // ...
  output: Output.text(),
  prompt: 'Tell me a joke.',
});
// output 是一个字符串(笑话内容)

Output.object()

基于 schema(如 Zod schema)生成结构化对象,输出经过类型校验:

ts
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:

ts
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 在每个元素完成时即刻收到它:

ts
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()

期望模型从一组固定字符串选项中选择时使用,适合分类或固定枚举回答:

ts
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 时使用——适合捕获任意对象或灵活结构:

ts
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 与工具调用组合

结构化输出的一大优势是可以与工具调用组合:

ts
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("...") 可以为模型提供关于各属性用途的提示,提升结构化数据的质量与准确性:

ts
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.',
});

属性描述在以下情况特别有用:澄清含糊的属性名、指定期望格式或约定、为复杂嵌套结构提供上下文。

还可以为输出指定可选的 namedescription,部分 provider 会用它给 LLM 额外指引(如通过 tool 或 schema 名):

ts
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 相关属性访问模型生成对象时的思考过程字符串(如可用):

ts
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 解析错误):

ts
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 解析失败之类的根因?

🛠️ 动手实践

  1. 定义一个「会议纪要提取器」schema(标题、参会人列表、行动项数组含负责人和截止日期),用 Output.object() 从一段会议记录文本中提取,并用 .describe() 补充字段说明。
  2. streamText + Output.array() 实现「英雄卡牌批量生成」,分别体验 partialOutputStreamelementStream 的区别:打印两种流的中间状态对比。
  3. 故意用一个必然校验失败的 prompt(如让模型输出与 schema 冲突的内容)触发 NoObjectGeneratedError,打印其 cause / text / usage 并写日志。