章节04 / 14
  1. 01先把 AI 应用当成系统,而不是聊天框
  2. 02打通第一个多 Provider 模型请求
  3. 03把 Prompt 写成任务协议
  4. 04结构化输出不是格式化,而是业务门禁
  5. 05把等待时间拆成事件:流式响应实战
  6. 06Tool Calling:模型提出动作,应用执行动作
  7. 07有副作用的工具要先设计刹车
  8. 08RAG 的第一性问题:答案从哪里来
  9. 09让 RAG 回答经得起追问
  10. 10长上下文、会话状态与记忆压缩
  11. 11Agent 工作流要像状态机一样可恢复
  12. 12评测工程:用样例集防止应用退化
  13. 13可观测性与安全:线上问题要能被看见
  14. 14上线不是结束:路由、灰度、回滚和治理复盘
本文目录12
  1. 结构化输出的误区:别把“格式正确”等同于“业务合规”
  2. 业务门禁的三层架构:Schema、业务规则与事实核验
  3. 第一层网关:用 Zod 声明强类型契约
  4. 强锁底层 API:启用 Provider 级别的约束机制
  5. 1. OpenAI Structured Outputs 的原理与配置
  6. 2. Google Gemini 的结构化输出机制
  7. 3. Vercel AI SDK 的统一封装
  8. 第二与第三层门禁:运行时业务规则与事实核验拦截器
  9. 故障自愈:设计容错重试与优雅降级方案
  10. 实战验收:构建并测试课程大纲生成门禁系统
  11. 1. 准备你的测试脚本
  12. 2. 自测与验收标准
04

结构化输出不是格式化,而是业务门禁

本指南将带你打破“JSON 格式正确等于数据合规”的误区。你将学习如何结合 JSON Schema、Zod 与大模型厂商的内置 Structured Outputs 机制,构建一个包含 Schema 校验、业务规则拦截和事实核验的三层门禁系统,并实现高可用的错误恢复与降级流程。

前置基础
  • 理解 TypeScript 基础语法与异步编程
  • 了解大模型 API 的调用流程与 System Prompt 机制
学习结果
  • 掌握用 Zod 定义强类型 Schema 并推导 TypeScript 类型的方法
  • 学会配置 OpenAI 和 Gemini 的强约束结构化输出参数
  • 掌握利用 Vercel AI SDK 实现多层级业务门禁的拦截与校验逻辑
  • 实现一套包含重试、提示词修正和备用降级的数据自愈流程

在大模型应用开发中,许多人将“结构化输出”简单理解为“让模型返回 JSON 字符串,然后用 JSON.parse 解析”。在这种粗糙的模式下,一旦大模型输出的 JSON 缺胳膊少腿,或者字段值不符合预期,系统就会瞬间崩溃。

真正的结构化输出,在工程上应当被视为业务门禁(Guardrails)。这意味着我们不仅要确保数据的物理结构(JSON 语法)合法,还要确保数据的逻辑内容(业务规则、事实真伪)完全符合系统的运行边界。本指南将通过 Zod、JSON Schema 以及主流 API 提供商的底层约束技术,带你构建一个工业级的、具备自愈能力的三层业务门禁系统。


结构化输出的误区:别把“格式正确”等同于“业务合规”

本节操作锚点:围绕“结构化输出的误区:别把“格式正确”等同于“业务合”记录步骤、样例、诊断、风险、检查清单和验收结果。

在构建 AI 驱动的课程生成器或自动化报告工具时,开发者常常会遇到以下三种典型失败场景:

  1. 物理结构破碎:模型返回的 JSON 在中途被截断,或者 key 没有加双引号。此时 JSON.parse 报错,服务直接挂掉。
  2. 物理结构正确,但业务逻辑荒谬:例如,模型生成了一份课程大纲 JSON,结构完全合法,但里面的 chapterCount(章节数)是 -5,或者 lessons 数组为空。这会导致前端页面渲染空白或报指针异常。
  3. 物理与业务正确,但事实幻觉横行:模型生成了一份“React 基础课程”,大纲格式完美,章节数也对,但在“新特性”中赫然写着“React 19 引入了革命性的 useBackToTheFuture Hook”。这属于严重的内容幻觉,不应被写入数据库。

如果模型返回的 JSON 结构完全符合 Schema,但内容违反了核心业务规则(例如课程大纲的节数小于 1),我们必须直接拦截并抛出自定义业务异常,而不要让错误流入下游系统,因为格式正确并不等于逻辑合规。


业务门禁的三层架构:Schema、业务规则与事实核验

本节操作锚点:围绕“业务门禁的三层架构:Schema、业务规则与事实”记录步骤、样例、诊断、风险、检查清单和验收结果。

为了降低上述问题,我们需要在模型和数据库/前端之间建立一道三层纵深防御体系:

text
┌────────────────────────────────────────────────────────┐
│                     LLM 原始响应                       │
└──────────────────────────┬─────────────────────────────┘
                           │
                           ▼
┌────────────────────────────────────────────────────────┐
│ 第一层:Schema 强校验 (Zod / JSON Schema)              │
│ ─ 确保数据类型正确、非空、属性完整                     │
└──────────────────────────┬─────────────────────────────┘
                           │ 成功
                           ▼
┌────────────────────────────────────────────────────────┐
│ 第二层:业务规则拦截器 (Imperative Business Rules)     │
│ ─ 确保数值区间合理、逻辑自洽、关联关系成立             │
└──────────────────────────┬─────────────────────────────┘
                           │ 成功
                           ▼
┌────────────────────────────────────────────────────────┐
│ 第三层:事实核验/安全过滤 (Content Verification)       │
│ ─ 过滤幻觉、注入攻击、违规词、政治敏感内容             │
└──────────────────────────┬─────────────────────────────┘
                           │ 成功
                           ▼
┌────────────────────────────────────────────────────────┐
│                   安全存入数据库/交付前端              │
└────────────────────────────────────────────────────────┘
  • 第一层(Schema 强校验):在网络协议层或 API 调用层,利用 Zod 或 JSON Schema 锁死返回类型。这一层主要解决“是不是合法的 JSON”以及“字段全不全”的问题。
  • 第二层(业务规则拦截器):在应用层通过编写命令式代码(Imperative Code),对已经成功解析的对象进行深度逻辑校验(例如校验结束时间是否大于开始时间)。
  • 第三层(事实核验与安全过滤):对文本内容进行特定的分词、正则匹配或小模型判别,拦截潜在的提示词注入和纯幻觉概念。

第一层网关:用 Zod 声明强类型契约

本节操作锚点:围绕“第一层网关:用Zod声明强类型契约”记录步骤、样例、诊断、风险、检查清单和验收结果。

Zod 是 TypeScript 生态中进行运行时校验与类型推导的行业标准。根据 Zod 官方文档(Zod documentation)的核心设计,它支持声明式的 Schema 定义,并在校验失败时提供极度详尽的错误路径与信息。

让我们先定义一个“课程大纲生成器”的契约。它要求模型输出课程的标题、难度级别、章节列表,并且每个章节必须包含知识点标签。

typescript
import { z } from 'zod';

// 1. 定义课程难度枚举
export const CourseLevelSchema = z.enum(['beginner', 'intermediate', 'advanced']);

// 2. 定义单个章节的 Schema
export const ChapterSchema = z.object({
  title: z.string().min(3, "章节标题不能少于3个字符").max(100, "章节标题不能超过100个字符"),
  summary: z.string().min(10, "章节简介不能少于10个字符"),
  durationMinutes: z.number().int().positive("课时长度必须是正整数"),
  keyPoints: z.array(z.string()).min(1, "每个章节至少包含一个核心知识点")
});

// 3. 定义完整大纲的 Schema
export const CourseOutlineSchema = z.object({
  title: z.string().min(5, "课程标题不能少于5个字符"),
  subTitle: z.string().optional(),
  level: CourseLevelSchema,
  targetAudience: z.string(),
  chapters: z.array(ChapterSchema).min(3, "一份合格的课程大纲至少需要3个章节")
});

// 4. 从 Zod Schema 自动推导 TypeScript 类型
export type CourseOutline = z.infer<typeof CourseOutlineSchema>;

通过这段代码,我们不仅拥有了运行时的“验证过滤器”,还自动获得了 TypeScript 编译期的强类型提示。任何不符合该结构的 JSON 对象在进入 Zod 时,都会被当场捕获并抛出 ZodError


强锁底层 API:启用 Provider 级别的约束机制

本节操作锚点:围绕“强锁底层API:启用Provider级别的约束机”记录步骤、样例、诊断、风险、检查清单和验收结果。

有了 Zod Schema,我们不应该只在收到响应后才去校验,而是应该将这个 Schema 传递给大模型厂商,让模型在**解码生成(Decoding)**的阶段就受到限制。

1. OpenAI Structured Outputs 的原理与配置

根据 OpenAI 发布的 Structured model outputs 指南,启用 strict: true 模式时,OpenAI 会在首次请求时预先编译 JSON Schema,这虽然会带来第一次调用的额外延迟(Latency Overhead),但能确保后续所有响应 100% 符合定义的 Schema 结构。因此,我们在生产环境初始化客户端时,应当提前进行一次热身调用(Warm-up call),避免首个用户的请求因编译 Schema 而超时。

2. Google Gemini 的结构化输出机制

根据 Google AI 的 Gemini API structured output 文档,Gemini 支持通过 responseSchema 参数传入标准 JSON Schema。在调用 Gemini 时,模型生成的 Token 会被严格限制在 Schema 定义的语法树分支中,从而绝不会产生非法的 JSON 字符。

3. Vercel AI SDK 的统一封装

为了避免手写特定厂商的 API payload,我们应当采用 Vercel AI SDK 进行上层抽象。根据 Vercel 的 AI SDK generating structured data 文档,使用 generateObject 并传入 Zod schema 时,SDK 会在底层自动适配不同服务商(如 OpenAI 的 JSON mode/Structured Outputs,或 Gemini 的 Response Schema)。因此,如果项目需要支持多模型切换,我们应当统一使用 Vercel AI SDK 作为抽象层,而不是手写特定厂商的 API payload,这样可以极大降低多模型适配的维护成本。

以下是使用 Vercel AI SDK 调用 OpenAI 和 Gemini 的统一代码示例:

typescript
import { generateObject } from 'ai';
import { openai } from '@ai-sdk/openai'; // 需要安装 @ai-sdk/openai
import { CourseOutlineSchema } from './schemas';

async function generateCourse(topic: string) {
  try {
    const { object } = await generateObject({
      // 强制约束底层模型
      model: openai('gpt-4o-2024-08-06', { 
        structuredOutputs: true // 开启 OpenAI Structured Outputs 强约束
      }),
      schema: CourseOutlineSchema,
      schemaName: 'CourseOutline',
      schemaDescription: '一份结构化、符合教学逻辑的专业课程大纲',
      prompt: `请针对主题 "${topic}" 生成一份详尽的课程大纲。`,
    });

    return object; // 此时 object 的类型已经被自动推导为 CourseOutline
  } catch (error) {
    console.error("API 调用或 Schema 校验失败:", error);
    throw error;
  }
}

除非遇到高并发下的极端延迟或 API 彻底不可用,否则不要轻易跳过大模型的 Structured Outputs 强约束模式,因为关闭强约束会导致输出结果退化到非确定性的纯文本状态,极大增加解析失败的概率。


第二与第三层门禁:运行时业务规则与事实核验拦截器

本节操作锚点:围绕“第二与第三层门禁:运行时业务规则与事实核验拦截器”记录步骤、样例、诊断、风险、检查清单和验收结果。

虽然 Schema 锁死了物理类型,但模型依然可能在数字和事实上“胡说八道”。例如,模型生成了 5 个章节,总课时居然达到了 1000 分钟,或者在“前端开发”课程里大书特书“后端 Java Spring 框架”。我们需要在数据落地前,追加第二层与第三层的门禁拦截器。

typescript
import { CourseOutline } from './schemas';

// 模拟一个包含行业术语或禁用词的本地库(可对接向量数据库或 Redis)
const BLOCKED_TERMS = ['useBackToTheFuture', 'quantumCSS', 'cyberHTML'];
const VALID_TECH_TERMS = ['React', 'TypeScript', 'Next.js', 'Tailwind', 'Zod'];

class BusinessGatekeeper {
  /**
   * 第二层:业务规则拦截(计算校验、逻辑完整性)
   */
  static validateBusinessRules(outline: CourseOutline): void {
    // 规则 1:总课程时长不能过长或过短
    const totalDuration = outline.chapters.reduce((sum, ch) => sum + ch.durationMinutes, 0);
    if (totalDuration < 30) {
      throw new Error(`[业务门禁拦截] 总课程时长 (${totalDuration}分钟) 过短,无法构成完整课程。`);
    }
    if (totalDuration > 600) {
      throw new Error(`[业务门禁拦截] 总课程时长 (${totalDuration}分钟) 超过 10 小时限制,请缩减课时。`);
    }

    // 规则 2:章节之间的时间分布不能极度不均
    const durations = outline.chapters.map(ch => ch.durationMinutes);
    const maxDuration = Math.max(...durations);
    const minDuration = Math.min(...durations);
    if (maxDuration > minDuration * 5) {
      throw new Error(`[业务门禁拦截] 章节时长分布极度不均 (最大:${maxDuration}分, 最小:${minDuration}分),教学逻辑不合理。`);
    }
  }

  /**
   * 第三层:事实核验与敏感词拦截(幻觉过滤)
   */
  static verifyFactsAndSecurity(outline: CourseOutline): void {
    const textToScan = JSON.stringify(outline);

    // 1. 拦截已知的幻觉/非法词汇
    for (const term of BLOCKED_TERMS) {
      if (textToScan.includes(term)) {
        throw new Error(`[事实核验拦截] 检测到虚假/幻觉概念或禁用词: "${term}"`);
      }
    }

    // 2. 行业对齐校验(针对特定主题的粗糙语义校准)
    if (outline.title.includes('React')) {
      const hasValidTerm = VALID_TECH_TERMS.some(term => textToScan.includes(term));
      if (!hasValidTerm) {
        throw new Error(`[事实核验拦截] 生成的 React 课程中未包含任何主流关联技术栈关键字。`);
      }
    }
  }
}

通过这一层,你成功将“大模型的自由发挥”约束在“企业级软件运行规范”的围墙之内。


故障自愈:设计容错重试与优雅降级方案

本节操作锚点:围绕“故障自愈:设计容错重试与优雅降级方案”记录步骤、样例、诊断、风险、检查清单和验收结果。

在生产环境中,任何门禁的拦截都意味着当前输出是不合格的。对于不合格的输出,我们不能直接对用户抛出红屏报错,而应该尝试自动修复优雅降级

我们设计如下容错生命周期:

  1. 首轮请求:启用 Strict Mode 的 generateObject
  2. 如果 Zod 或底层 API 报错:捕获错误,并将错误堆栈拼入 System Prompt,向模型发起“退回重写”请求(修正重试)。
  3. 如果重试依然失败:执行降级策略,返回一个本地预先准备好的、静态的通用模板(或者提示用户手动微调)。
typescript
import { generateObject } from 'ai';
import { openai } from '@ai-sdk/openai';
import { CourseOutlineSchema, CourseOutline } from './schemas';
import { BusinessGatekeeper } from './gatekeeper';

const FALLBACK_OUTLINE: CourseOutline = {
  title: "Web 开发核心基础(系统默认模板)",
  level: "beginner",
  targetAudience: "初学 Web 开发的零基础学员",
  chapters: [
    { title: "HTML5 语义化标签实战", summary: "掌握现代网页的基础骨架搭建。", durationMinutes: 45, keyPoints: ["Semantic Tags", "SEO Basics"] },
    { title: "CSS3 现代布局与响应式", summary: "深入 Flexbox 与 CSS Grid 布局。", durationMinutes: 60, keyPoints: ["Flexbox", "Grid Layout"] },
    { title: "JavaScript 异步编程精要", summary: "理解 Promise 与 async/await 的底层原理。", durationMinutes: 60, keyPoints: ["Promises", "Event Loop"] }
  ]
};

export async function safeGenerateCourse(topic: string, maxRetries = 2): Promise<CourseOutline> {
  let currentPrompt = `请针对主题 "${topic}" 生成一份详尽的课程大纲。`;
  let attempts = 0;

  while (attempts < maxRetries) {
    try {
      attempts++;
      console.log(`正在进行第 ${attempts} 次大纲生成尝试...`);

      const { object } = await generateObject({
        model: openai('gpt-4o-2024-08-06', { structuredOutputs: true }),
        schema: CourseOutlineSchema,
        prompt: currentPrompt,
      });

      // 依次穿过第二层和第三层门禁
      BusinessGatekeeper.validateBusinessRules(object);
      BusinessGatekeeper.verifyFactsAndSecurity(object);

      console.log("生成的大脑数据顺利穿过所有业务门禁!");
      return object;

    } catch (error: any) {
      console.warn(`第 ${attempts} 次尝试失败。错误原因: ${error.message}`);
      
      if (attempts >= maxRetries) {
        console.error("达到最大重试次数,触发优雅降级流程,返回备用模板。");
        return FALLBACK_OUTLINE;
      }

      // 构造“修复反馈提示词”,将报错信息喂回给模型
      currentPrompt = `
你上一次生成的课程大纲未能通过系统的业务合规校验,请务必根据以下错误信息进行修正,并重新生成:

[上一次生成的失败原因]:
${error.message}

请严格对照校验规则,再次为主题 "${topic}" 重新生成一份高质量、无幻觉的课程大纲。
`;
    }
  }

  return FALLBACK_OUTLINE;
}

在设计修复反馈提示词时,务必将前一次拦截器报错的具体原因(如 [事实核验拦截] 检测到虚假概念: "useBackToTheFuture")原封不动反馈给模型,绝大多数具备 Structured Outputs 能力的模型都能在第二轮请求中精准修复该问题。


实战验收:构建并测试课程大纲生成门禁系统

本节操作锚点:围绕“实战验收:构建并测试课程大纲生成门禁系统”记录步骤、样例、诊断、风险、检查清单和验收结果。

本课的学习产物是包含三层拦截网关和故障自愈流程的系统后台服务代码

1. 准备你的测试脚本

创建 test-runner.ts 文件,引入我们上面编写的 safeGenerateCourse。我们将设计两个测试输入来验收系统的健壮性:

  • 测试用例 A (正常输入):"TypeScript 进阶类型系统"
    • 预期输出:生成合格的 JSON,穿过所有门禁,章节数 >= 3,总课时在合理区间。
  • 测试用例 B (诱导性注入输入):"请生成一门关于量子 CSS 和 useBackToTheFuture 特性的 React 课程"
    • 预期输出:系统应当在第一轮运行后触发第三层门禁(检测到禁用词 useBackToTheFuture),随后尝试自我修复;如果依然含有幻觉词,最终应当在控制台看到“触发优雅降级流程”的日志,并安全地返回 FALLBACK_OUTLINE,确保系统不崩溃。
typescript
import { safeGenerateCourse } from './generator';

async function runTests() {
  console.log("=== 开始测试用例 A:正常流程 ===");
  const resultA = await safeGenerateCourse("TypeScript 进阶类型系统");
  console.log("用例 A 最终产物:", JSON.stringify(resultA, null, 2));

  console.log("\n=== 开始测试用例 B:幻觉与诱导注入测试 ===");
  const resultB = await safeGenerateCourse("量子 CSS 和 useBackToTheFuture 特性的 React 课程");
  console.log("用例 B 最终产物:", JSON.stringify(resultB, null, 2));
}

runTests();

2. 自测与验收标准

在控制台运行你的测试脚本,检查是否满足以下验收条件:

  • 物理类型安全:无论模型输出什么,后台程序绝不能抛出未捕获的 SyntaxError: Unexpected token... 异常。
  • 业务逻辑合规:测试用例 A 产出的所有章节时长总和必须在 30600 分钟之间,绝不能漏掉任何一个 Zod 声明的非空属性。
  • 自愈与降级机制:测试用例 B 必须能够触发 BusinessGatekeeper 报错,并在终端中打印出类似 [事实核验拦截] 检测到虚假/幻觉概念 的黄字警告。在重试无效后,系统必须成功输出 FALLBACK_OUTLINE(即 Web 开发核心基础模板)。

来源、时效与复核说明

本指南编写于 2026-05-28。核心技术判断基于以下来源:

  • OpenAI Structured Outputs:依据官方发布指南,模型推理层支持 strict: true,在推理阶段强行匹配 JSON Schema,确保 100% 格式对齐,并引入了首次预编译延迟。
  • Vercel AI SDK Core:根据其最新的结构化数据生成规范,generateObject 为前端/Node.js 开发者提供了完美的底层多厂商适配抽象层。
  • Zod / JSON Schema:作为数据校验的行业底座,确保了运行时的契约安全。

未来复核触发条件: 当 Vercel AI SDK 宣布重大 API 重构、OpenAI 推出全新格式约束参数或 Zod 发布破坏性重大更新(如 v4 正式版)时,需要重新复核上述代码的兼容性。

结构化输出不是格式化,而是业务门禁:把判断写成可复查证据

本课最怕只留下结论,所以修订时要把输入、步骤、样例和验收放在同一张纸上。本课交付物是 一套 answer schema、Zod 校验、错误恢复和降级流程,它要服务于 AI 应用 的下一步,而不是只证明你读过这一节。

如果你现在还没有真实输入,先用一个最小样例完成 结构化输出不是格式化,而是业务门禁,因为没有输入就无法判断产物是否可用。 如果本课产物会影响后续交付,应该写明适用条件和不适用条件,否则下一课会把不确定性误当成基础。 如果检查结果只剩一句“看起来可以”,必须补一条反例或失败样例,只有能解释失败原因时,这个判断才站得住。

常见故障不是“做错了”,而是没有把错误留下来:缺少截图、日志、参数、原片或对照样例。围绕 结构化输出不是格式化,而是业务门禁 做检查时,至少保留步骤、样例、风险、修复和验收五项。

OpenAI 的《Structured model outputs》说明:支撑 schema 约束输出、结构化结果解析、业务门禁和失败重试。;这意味着 结构化输出不是格式化,而是业务门 不能只写经验结论,要把来源变成检查动作。Google AI 的《Gemini API structured output》提醒:支撑 Gemini 结构化输出、JSON schema、枚举约束和解析失败边界。;因此本课方案必须写清边界。JSON Schema 的《JSON Schema official documentation》提供的证据是:支撑 schema 驱动输出、参数校验、合同式接口和结构化数据边界。;所以当前结论按 2026-05-28 的来源状态使用。

复核触发条件要写具体:如果官方 API/SDK、软件工具版本、版权政策、安全合规要求、行业规范、平台发布规则或关键来源页面发生变化,需要重新检查 结构化输出不是格式化,而是业务门 的步骤、样例、风险和验收清单。

练习验收:把 结构化输出不是格式化,而是业务门禁 应用到一个自己的任务,交付一份包含输入、步骤、样例、失败诊断、来源证据和复测结果的记录。缺少其中任何一项,都先标记为 needs_review。