← Back to 文字

结构化输出与 JSON Schema

本文来自《AI 应用开发课程》月份 1 课程文档,已整理为网站文章版本。

学习目标

学完本节后,你应当能够:

  • 理解为什么 AI 应用不能只依赖自由文本输出。
  • 用 Schema 思维约束模型返回结构。
  • 实现月份 1 的最小结构化输出流程。
  • 知道模型输出之后程序还需要做什么校验。

前置知识

  • 已能发出普通聊天请求
  • 已理解 JSON 和 Pydantic

1. 为什么要结构化输出

自由文本适合阅读,但不适合程序继续处理。你后面会经常需要模型返回:

  • 标题
  • 摘要
  • 优先级
  • 操作步骤
  • 工具参数

如果模型只返回一段散文,程序很难稳定提取。

2. 最简单的结构化目标

例如让模型返回:

{
  "summary": "一句话总结",
  "priority": "high",
  "action_items": ["事项1", "事项2"]
}

3. 结构化输出不是“相信模型”,而是“三层约束”

第一层:Prompt 约束

明确告诉模型必须按 JSON 返回。

第二层:Schema 约束

用明确字段结构告诉模型应该返回什么。

第三层:程序校验

即使模型返回了 JSON,也要用 Pydantic 或解析器校验。

4. 最小示例

先定义 Pydantic 模型:

from pydantic import BaseModel


class TaskAnalysis(BaseModel):
    summary: str
    priority: str
    action_items: list[str]

再提示模型:

你是研发助手。请将结果严格输出为 JSON,字段必须包含:
- summary: 字符串
- priority: 字符串,只能是 low / medium / high
- action_items: 字符串数组

5. 程序侧解析

import json

from pydantic import ValidationError


def parse_task_analysis(raw_text: str) -> TaskAnalysis:
    data = json.loads(raw_text)
    return TaskAnalysis.model_validate(data)

6. 为什么这里仍然可能失败

因为模型可能:

  • 返回了非 JSON 文本
  • 多输出解释性语句
  • 字段缺失
  • 字段类型错误

所以结构化输出的关键不是“让模型尽量对”,而是“当模型不对时程序也能清楚处理”。

7. Schema 的思维方式

当你设计结构化输出时,先回答 4 个问题:

  1. 我后续程序真正需要哪些字段?
  2. 哪些字段是必填?
  3. 哪些字段有枚举范围?
  4. 哪些字段以后可能扩展?

月份 1 不需要追求非常复杂的 Schema,但一定要把“程序真正需要的字段”先想清楚。

8. 实操任务

  1. 定义一个 TaskAnalysis 模型
  2. 用模型分析一段需求描述
  3. 让模型返回 JSON
  4. 用 Pydantic 验证结果
  5. 故意让模型漏掉字段,观察程序报错

9. 自测题

  1. 为什么结构化输出比自由文本更适合工程系统?
  2. 为什么不能只做 Prompt 约束而不做程序校验?
  3. 什么叫“面向后续程序消费”设计字段?

10. 作业与验收

作业:

  • prompt_lab 中实现 structured_generate() 的最小版本。

验收标准:

  • 能成功返回并解析结构化结果
  • 失败时有清晰报错
  • 你能解释模型约束和程序校验分别负责什么

11. 常见错误

  • 字段设计过多,导致模型不稳定
  • 输出结构和后续程序需求不一致
  • 解析失败后继续当成功结果使用

12. 本章与前文关系

上一章解决了“输入如何组织”,这一章解决“输出如何落地”。这是月份 1 的第一条真正工程分水岭:

  • 在普通聊天视角里,模型返回一段文本就够了
  • 在应用开发视角里,模型输出往往要继续被程序消费

因此,本章的核心不是“怎么让模型说得更好”,而是“怎么让结果更可处理、更可验证”。

13. 本章在研发助手项目中的位置

研发助手项目里的很多任务,本质都更适合结构化输出,而不是自由文本:

  • 抽取待办项
  • 生成 commit message 草稿
  • 总结错误日志时拆出原因、影响、建议动作
  • 为后续工具调用准备参数

所以本章不是附加技巧,而是项目可用性的核心部分。

14. 为什么结构化输出是月份 1 的硬能力

因为你后面会持续面对这个问题:

模型生成的内容,到底是给人看的,还是给程序继续处理的?

如果答案是“给程序继续处理”,那你就必须:

  • 定义结构
  • 控制输出
  • 校验结果
  • 处理失败

这就是结构化输出的完整闭环。

15. 错误示例 vs 正确示例

错误示例:只告诉模型“请输出 JSON”,却不定义字段

请把结果输出成 JSON。

这类 Prompt 太模糊,模型可能:

  • 字段名不一致
  • 多写解释文字
  • 结构层级不稳定

正确示例:输出目标和字段边界明确

你是研发助手。请将结果严格输出为 JSON,字段必须包含:
- summary: 字符串
- priority: 只能是 low / medium / high
- action_items: 字符串数组
不要输出任何 JSON 之外的解释文字。

这仍然不保证 100% 成功,但会显著提高结构稳定性。

16. 完整文件级示例:structured_output_demo.py

"""演示结构化输出的最小闭环。

包含:
1. 定义输出模型
2. 调用模型
3. 解析 JSON
4. 用 Pydantic 校验
"""

from __future__ import annotations

import json
from typing import Any

from pydantic import BaseModel, ValidationError


class TaskAnalysis(BaseModel):
    """研发任务分析结果。"""

    summary: str
    priority: str
    action_items: list[str]


def build_structured_prompt(task_text: str) -> str:
    """构造最小结构化输出提示词。"""

    return f"""
你是研发助手。
请分析下面的任务描述,并严格输出 JSON。

输出字段要求:
- summary: 字符串,一句话总结任务
- priority: 只能是 low / medium / high
- action_items: 字符串数组,列出 2 到 5 个待办项

任务描述:
{task_text}
""".strip()


def parse_task_analysis(raw_text: str) -> TaskAnalysis:
    """先解析 JSON,再做 Pydantic 校验。"""

    data: dict[str, Any] = json.loads(raw_text)
    return TaskAnalysis.model_validate(data)


def safe_parse_task_analysis(raw_text: str) -> TaskAnalysis | None:
    """提供一个最小 fallback 入口。

    月份 1 阶段先返回 None,后面你可以继续扩成日志记录、重试或修复逻辑。
    """

    try:
        return parse_task_analysis(raw_text)
    except json.JSONDecodeError:
        print("模型输出不是合法 JSON")
        return None
    except ValidationError as error:
        print(f"结构校验失败: {error}")
        return None

17. 逐段解释这份完整示例

为什么输出模型先定义在程序侧

因为程序需要先知道“自己真正想要什么结构”,再去约束模型。如果程序自己都没有明确定义字段,就不可能稳定得到结果。

为什么解析分成 parsesafe_parse

这是为了分离:

  • 纯解析逻辑
  • 带错误处理的应用逻辑

这种分层在后面写 service 层时会很常见。

为什么 fallback 先只返回 None

因为月份 1 目标是先让你看清失败类型,而不是一开始就把所有恢复机制做得很重。

18. 结构化输出为什么会失败

结构化输出失败通常不是单一原因,而是至少有三类失败源:

第一类:Prompt 不清楚

模型根本不知道你想要哪些字段。

第二类:模型输出偏离

模型多写了解释、少了字段、或者把类型写错。

第三类:程序校验不完整

你只做了 json.loads,却没有进一步校验字段结构。

所以课程强调“结构化输出 = Prompt + Schema + 校验”,三者缺一不可。

19. 一个更接近工程的增强版思路

你可以继续做下面两步增强:

  1. priority 建立更严格的枚举约束
  2. 在结构化失败时,把原始输出写入日志,方便后续复盘

例如:

from typing import Literal


Priority = Literal["low", "medium", "high"]

这能让结果更清晰,也更贴近后续系统使用。

20. 调试与排错:本章最值得刻意练习的失败案例

案例一:模型输出前后包了说明文字

这会导致 json.loads 直接失败。

案例二:字段缺失

例如少了 action_items,这会在 Pydantic 校验时暴露。

案例三:类型错误

例如 action_items 变成一整个字符串,而不是字符串数组。

这些失败不是坏事,恰恰是你建立稳定结构化输出思维的入口。

21. 本章完成后你应该具备的能力

完成本章后,你应当做到:

  1. 能定义一个程序真正需要的输出结构。
  2. 能为该结构写出约束 Prompt。
  3. 能完成 json.loads + Pydantic 校验闭环。
  4. 能解释结构化输出失败时应该看哪一层。

22. 如果你卡在这里,先回看哪几章

23. 从本章过渡到下一章的桥接说明

接下来进入 04-流式输出与成本统计.md

到这里你已经掌握:

  • 普通文本输出
  • 结构化输出

下一步要处理的是“输出是如何到达用户的”以及“每次调用要记录什么”。这会把模型调用从“能用”进一步推向“可观察、可调试、可持续开发”。

Fin