Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

Sidebar Navigation

← 人工智能 / Artificial Intelligence

智能体工程 / Agent Engineering

1. Agent 工程体系全景 / Agent Engineering System Overview

2. Function Calling - 让 LLM 具备行动能力 / Function Calling for Giving LLMs the Ability to Act

3. Agent 框架演进 - 从裸 SDK 到 LangGraph / The Evolution of Agent Frameworks from Raw SDKs to LangGraph

4. RAG 基础 - 让 Agent 拥有"知识" / Retrieval-Augmented Generation Fundamentals for Agent Knowledge

5. 记忆管理 - Agent 的大脑 / Memory Management as the Brain of an Agent

6. Agent 工作流 - 从单步到复杂的执行编排 / Agent Workflows from Single Steps to Complex Orchestration

7. 多 Agent 系统 - 多个 Agent 协作 / Multi-Agent Systems and Agent Collaboration

8. RAG 进阶 - 企业级知识库实战 / Advanced RAG for Enterprise Knowledge Bases

9. 真实 Agent 应用场景 / Real-World AI Agent Applications

10. Structured Output - 让 LLM 输出可控的结构化数据 / Structured Output for Controllable, Machine-Readable LLM Responses

11. Tools Design Best Practices - AI Agent 工具设计最佳实践 / Tools Design Best Practices for AI Agents

12. Agent 架构模式 - 从单 Agent 到多 Agent 的工程范式 / Agent Architecture Patterns

13. Agent Modes — 编程 Agent 的交互模式设计 / Designing Interaction Modes for Coding Agents

14. Agent Workflow 编排:从循环到持久化执行的演进

15. Context Engineering - 从 Prompt 设计到上下文编排 / Context Engineering: From Prompt Design to Context Orchestration

16. Agent 缓存工程:从 KV Cache、Prompt Cache 到语义缓存 / Agent Caching Engineering

17. Harness Engineering, Skills, and Loop Engineering — 从信任模型到验证系统 / From Trusting Models to Verifying Systems

18. MCP 协议 - AI 工具的"USB 接口" / Model Context Protocol for AI Tool Integration

19. Agent 评估与测试 — 如何衡量一个"不可预测"的系统 / Agent Evaluation and Testing — How to Measure an "Unpredictable" System

20. 安全沙箱 - Agent 的安全边界 / Secure Sandboxes as Agent Safety Boundaries

21. 权限与门卫 - Agent 的安全控制中枢 / Permissions and Policy Gates for Agent Control

22. API Key 管理与安全 - Agent 的密钥生命周期的管理 / API Key Lifecycle Management and Security for Agents

23. 提示词注入防护 - Agent 的防御前沿 / Prompt Injection Defense for AI Agents

24. 可观测性与调试 - Agent 运行的透明度保障 / Observability and Debugging for Transparent Agent Operations

25. 模型路由 - 让正确的模型做正确的事 / Model Routing for Matching Models to Tasks

26. OpenClaw 设计深度分析 - 为什么它让人觉得"活"了 / OpenClaw Design Analysis and the Illusion of Liveliness

27. Claude Code 泄露源码深度分析 - 512,000 行代码揭示的生产级 Agent 架构 / Claude Code Source Analysis and Production Agent Architecture

28. LobeChat 设计深度分析 - 全栈 Agent Chat 应用工程实践 / LobeChat Design Analysis and Full-Stack Agent Chat Engineering

29. 编程 Agent 全面对比:从 Claude Code 到 Pi 的设计哲学 / Coding Agents Comparison: Design Philosophies from Claude Code to Pi

30. 领域 Agent 的确定性工具编译与延迟执行——从自然语言规格到单次 CAE 提交

31. Agent 工程学习指南 / An AI Agent Engineering Learning Guide

本页目录

Structured Output - 让 LLM 输出可控的结构化数据 / Structured Output for Controllable, Machine-Readable LLM Responses ​

📅 创建时间:2026-07-28 🏷️ 标签:#StructuredOutput #ConstrainedGeneration #JSONSchema #Agent基础 📚 前置知识:[[01-function-calling]]


📋 本章目标 ​

  • 理解 Structured Output 在 HTTP 层面的真正含义
  • 区分 JSON Mode 与 JSON Schema 的本质差异
  • 掌握 API 级约束生成 vs 后处理重试两种方案
  • 理解 Zod/Pydantic → JSON Schema → LLM 的完整流水线
  • 能够用 OpenAI 和 Anthropic SDK 实现结构化输出
  • 掌握实体抽取、分类、结构化数据生成等实战模式
  • 理解 Structured Output 的局限性与适用边界

第0部分:先搞清数据到底是怎么流动的 ​

在理解 Structured Output 的设计意图之前,有一个更底层的问题必须先回答:数据在网络上到底长什么样? 很多人卡在这里,因为他们只见过 SDK 封装后的 response.choices[0].message.content,不知道这行代码背后 HTTP 请求体和响应体里到底是什么。

0.1 普通聊天:输出是自由文本 ​

你写的代码:

python
response = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "列出三个中国城市"}]
)
print(response.choices[0].message.content)
1
2
3
4
5

实际发出的 HTTP 请求体:

POST /v1/chat/completions

{
  "model": "gpt-4o",
  "messages": [
    {"role": "user", "content": "列出三个中国城市"}
  ]
}
1
2
3
4
5
6
7
8

LLM 返回的响应体——content 是自由文本:

{
  "id": "chatcmpl-xxx",
  "object": "chat.completion",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "以下是三个中国城市:\n1. 北京\n2. 上海\n3. 广州"
      },
      "finish_reason": "stop"
    }
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

你的程序拿到的是一个字符串:"以下是三个中国城市:\n1. 北京\n2. 上海\n3. 广州"。如果你想把这个字符串变成 ["北京", "上海", "广州"],你必须自己写代码去解析它——正则、字符串切割、或者祈祷 LLM 每次输出格式一致。

0.2 Structured Output:输出是符合 Schema 的 JSON ​

现在加上 Structured Output。你写的代码:

python
from pydantic import BaseModel

class CityList(BaseModel):
    cities: list[str]

response = client.beta.chat.completions.parse(
    model="gpt-4o",
    messages=[{"role": "user", "content": "列出三个中国城市"}],
    response_format=CityList   # 告诉 API:输出必须符合这个结构
)

# response.choices[0].message.parsed 已经是 CityList 对象
print(response.choices[0].message.parsed.cities)
# 输出:['北京', '上海', '广州']
1
2
3
4
5
6
7
8
9
10
11
12
13
14

实际发出的 HTTP 请求体——多了一个 response_format 字段:

POST /v1/chat/completions

{
  "model": "gpt-4o",
  "messages": [
    {"role": "user", "content": "列出三个中国城市"}
  ],
  "response_format": {
    "type": "json_schema",
    "json_schema": {
      "name": "CityList",
      "strict": true,
      "schema": {
        "type": "object",
        "properties": {
          "cities": {
            "type": "array",
            "items": {"type": "string"}
          }
        },
        "required": ["cities"],
        "additionalProperties": false
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

LLM 返回的响应体——content 是保证符合 Schema 的 JSON 字符串:

{
  "id": "chatcmpl-xxx",
  "object": "chat.completion",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "{\"cities\":[\"北京\",\"上海\",\"广州\"]}"
      },
      "finish_reason": "stop"
    }
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
┌─────────────────────────────────────────────────────────────┐
│              普通输出 vs 结构化输出:数据流对比               │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  普通聊天:                                                 │
│  你的代码  →  HTTP POST  →  LLM  →  content="自由文本"     │
│            (无 schema)          →  你需要手工解析          │
│                                                             │
│  结构化输出:                                               │
│  你的代码  →  HTTP POST  →  LLM  →  content='{"cities":    │
│            (带 json_schema)        ["北京","上海","广州"]}' │
│                                  →  保证 json.loads() 成功  │
│                                  →  保证字段类型正确         │
│                                  →  保证无多余字段           │
│                                                             │
│  区别不在"输出看起来像 JSON"——普通模式下你也可以让 LLM       │
│  输出 JSON。区别在于 API 层面做了强制约束:不符合 Schema     │
│  的 token 根本不会被生成。                                   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

0.3 所以 "response_format" 到底做了什么? ​

LLM 的本质是 token 生成器:给定前文,预测下一个 token。正常情况下,模型在每个位置会选择概率最高的 token,没有任何外部约束。

当你在 API 请求中加了 response_format,API 服务端(不是 LLM 本身)会做一件事:

┌─────────────────────────────────────────────────────────────┐
│              response_format 的工作机制                      │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  第1步:Schema → 语法约束                                   │
│  API 服务端把你的 JSON Schema 转换成一个有限状态机(FSM)    │
│  或上下文无关文法(CFG),定义了每一步"允许哪些 token"。    │
│                                                             │
│  第2步:逐 token 约束                                       │
│  LLM 在每一步生成下一个 token 时,API 服务端会:            │
│    ① 让 LLM 给出所有 token 的概率分布                      │
│    ② 把 Schema 不允许的 token 的概率设为 0(或 -inf)      │
│    ③ 从允许的 token 中采样                                │
│                                                             │
│  第3步:保证输出                                            │
│  因为每一步都被约束,最终生成的 token 序列在语法上必然       │
│  符合给定的 JSON Schema。整个过程对 LLM 本身是透明的——     │
│  LLM 不知道自己被"限制"了,它只是在受限的候选集中选词。    │
│                                                             │
│  关键认知:这不是"先输出再校验",而是"每一步都限制"。       │
│  这就是 constrained generation(约束生成)的含义。          │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

第1部分:为什么需要 Structured Output? ​

1.1 自由文本输出的困境 ​

当你让 LLM 输出 JSON 但没有用 Structured Output 时,你实际上是在赌博:

python
# 方案A:不用 Structured Output —— 脆弱
response = client.chat.completions.create(
    model="gpt-4o",
    messages=[{
        "role": "system",
        "content": "请只输出 JSON,格式:{\"name\": \"...\", \"age\": ...}"
    }, {
        "role": "user",
        "content": "张三今年25岁"
    }]
)

text = response.choices[0].message.content
# text 可能是:
#   ✅ '{"name": "张三", "age": 25}'
#   ❌ '好的,这是 JSON:\n```json\n{"name": "张三", "age": 25}\n```'
#   ❌ '{"name": "张三", "age": 25, "note": "这是一个年轻人"}'
#   ❌ '{"姓名": "张三", "age": "二十五"}'

# 你需要写一堆防御代码:
import json, re
try:
    # 尝试去掉 markdown code block
    match = re.search(r'```(?:json)?\s*\n?(.*?)\n?```', text, re.DOTALL)
    if match:
        text = match.group(1)
    data = json.loads(text)
    name = data.get("name") or data.get("姓名")  # key 名不确定
    age = int(data["age"])  # 可能类型不对
except (json.JSONDecodeError, KeyError, ValueError, TypeError) as e:
    # 解析失败,你可能需要重试——但重试也不保证成功
    ...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
┌─────────────────────────────────────────────────────────────┐
│              自由文本 + 正则解析:脆弱性的根源               │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  你的代码假设 LLM 会按你的格式输出,但 LLM 可能:           │
│                                                             │
│  ❌ 包裹 markdown code block (```json ... ```)              │
│  ❌ 在前面加解释文字:"好的,这是结果:..."                   │
│  ❌ 修改 key 名称:name → 姓名,age → 年龄                   │
│  ❌ 类型错误:age 输出 "二十五" 而不是 25                    │
│  ❌ 多余字段:自动加上 "confidence": 0.95                    │
│  ❌ 缺失字段:忘了输出某个 required 字段                     │
│  ❌ JSON 语法错误:少了引号、逗号、括号                       │
│                                                             │
│  每多一种可能,你的解析代码就多一个 if/else 分支。           │
│  这不可维护,也不可靠。                                      │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

1.2 Structured Output 的保证 ​

┌─────────────────────────────────────────────────────────────┐
│              Structured Output 给你的保证                    │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ✅ 输出一定是合法的 JSON(json.loads 不会抛异常)          │
│  ✅ 输出一定包含所有 required 字段                           │
│  ✅ 输出一定不包含 additionalProperties 之外的字段           │
│  ✅ 每个字段的类型一定正确(string 就是 string,不会变       │
│     number)                                                 │
│  ✅ 如果定义了 enum,输出一定在枚举值范围内                  │
│  ✅ 没有 markdown 包裹,没有解释文字,纯 JSON                │
│                                                             │
│  这些保证不是"大概率"或"通常"——是 API 层面的强制约束。      │
│  当 strict: true 时,任何不符合 Schema 的 token 序列         │
│  在生成阶段就被禁止了。                                      │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

1.3 为什么这对 Agent 至关重要? ​

Agent 不是和人对话——Agent 的输出要喂给下一个程序模块。想想这些场景:

┌─────────────────────────────────────────────────────────────┐
│              Agent 必须用 Structured Output 的场景           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  场景1:工具参数生成                                        │
│  Agent 需要调用 send_email(to, subject, body)。如果 LLM     │
│  输出的 JSON 里有 "recipient" 而不是 "to",程序就挂了。     │
│  Function Calling 本质就是 Structured Output 的特例——       │
│  tools 定义里的 parameters 就是一个 JSON Schema。           │
│                                                             │
│  场景2:实体抽取 → 数据库写入                                │
│  Agent 从文档中抽取 {name, date, amount},直接写入数据库。   │
│  字段类型不匹配 = 数据库报错。必须保证类型正确。             │
│                                                             │
│  场景3:Agent 间通信                                         │
│  Agent A 调用 Agent B,B 的返回值被 A 的代码解析。           │
│  结构不稳定 = A 的解析逻辑崩溃 = 级联失败。                  │
│                                                             │
│  场景4:前端渲染                                             │
│  LLM 输出 UI 组件的 props JSON,不符合类型 → 白屏。         │
│                                                             │
│  一句话:当 LLM 的输出要进程序逻辑而不是给人看时,           │
│  Structured Output 不是"nice to have",而是"必须"。         │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

第2部分:JSON Mode vs JSON Schema —— 两种保证级别 ​

很多初学者把这两个概念混为一谈,但它们能保证的东西完全不同。

2.1 JSON Mode:只保证"是合法 JSON" ​

JSON Mode(OpenAI 的 response_format: {"type": "json_object"})只做一件事:确保 LLM 的输出是一个合法的 JSON 对象。

python
# JSON Mode:type: "json_object"
response = client.chat.completions.create(
    model="gpt-4o",
    messages=[
        {"role": "system", "content": "输出 JSON"},
        {"role": "user", "content": "张三,25岁,工程师"}
    ],
    response_format={"type": "json_object"}  # 只保证是合法 JSON
)

# 输出一定是合法 JSON,但结构完全不保证:
# 可能1: {"name": "张三", "age": 25, "job": "工程师"}
# 可能2: {"姓名": "张三", "年龄": 25, "职业": "工程师"}
# 可能3: {"person": {"name": "张三"}, "details": {"age": 25}}
# 可能4: {"result": "张三,25岁,工程师", "confidence": 0.9}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

2.2 JSON Schema:保证"字段和类型都按你说的来" ​

python
# JSON Schema:type: "json_schema"
response = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "张三,25岁,工程师"}],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "person_extract",
            "strict": True,
            "schema": {
                "type": "object",
                "properties": {
                    "name": {"type": "string"},
                    "age": {"type": "integer"},
                    "job": {"type": "string"}
                },
                "required": ["name", "age", "job"],
                "additionalProperties": False
            }
        }
    }
)

# 输出保证:
# ✅ 一定是 {"name": "张三", "age": 25, "job": "工程师"}
# ✅ name 一定是 string
# ✅ age 一定是 integer(不会是 "25")
# ✅ 不会多出 confidence、note 等额外字段
# ✅ 不会少 name、age、job 中的任何一个
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
┌─────────────────────────────────────────────────────────────┐
│            JSON Mode vs JSON Schema:一张表说清              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  维度              │ JSON Mode         │ JSON Schema        │
│  ─────────────────┼───────────────────┼───────────────────  │
│  API 参数          │ type:json_object  │ type:json_schema   │
│  保证合法 JSON     │ ✅                │ ✅                 │
│  保证字段名        │ ❌                │ ✅                 │
│  保证字段类型      │ ❌                │ ✅                 │
│  保证必填字段      │ ❌                │ ✅                 │
│  禁止多余字段      │ ❌                │ ✅ (additional      │
│                    │                   │  Properties:false) │
│  支持 enum 约束    │ ❌                │ ✅                 │
│  支持嵌套对象      │ 不保证            │ ✅ 保证             │
│  支持数组元素类型  │ 不保证            │ ✅ 保证             │
│                                                             │
│  一句话:JSON Mode 是"输出是 JSON";                         │
│  JSON Schema 是"输出是我指定的那个 JSON"。                   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

2.3 strict: true 的含义 ​

OpenAI 的 strict: true 是 Structured Output 的一个关键开关。不开 strict 时,模型尽力遵循 Schema,但不保证。开了 strict 后:

  • 底层使用 constrained decoding(约束解码),从 token 采样阶段就限制
  • 100% 保证输出符合 Schema,不会出现"字段类型不对"的情况
  • 代价:Schema 必须满足一些限制条件(见第6部分)
python
# strict: true —— 硬约束
# strict: false / 不设置 —— 软约束(尽力而为)
response_format={
    "type": "json_schema",
    "json_schema": {
        "name": "...",
        "strict": True,  # 开!
        "schema": {...}
    }
}
1
2
3
4
5
6
7
8
9
10

第3部分:两种技术路线 —— 约束生成 vs 后处理重试 ​

实现 Structured Output 有两条技术路线:一条是在 API 层面做约束,一条是在应用层面做重试。理解它们的本质区别很重要。

3.1 路线A:API 级约束生成(推荐) ​

┌─────────────────────────────────────────────────────────────┐
│           路线A:API 级约束生成(Constrained Decoding)       │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌──────────┐    Schema     ┌──────────┐   约束后token      │
│  │ 你的代码  │──────────────→│ API 网关  │─────────────────→│
│  │          │               │          │                   │
│  │ Pydantic │  json_schema  │ 构建 FSM │  只采样合法token  │
│  │ 模型     │               │ 约束采样 │                   │
│  └──────────┘               └──────────┘                   │
│                                    │                        │
│                                    ↓  LLM 原始 token 流     │
│                              ┌──────────┐                   │
│                              │   LLM    │                   │
│                              │  (模型)  │                   │
│                              └──────────┘                   │
│                                                             │
│  特点:                                                     │
│  • 一次调用即成功,不需要重试                                │
│  • 从 token 生成阶段就约束,不是"先生成再检查"              │
│  • 需要模型/API 支持(OpenAI、Anthropic、vLLM、llama.cpp)  │
│  • 可靠性最高                                                │
│                                                             │
│  代表实现:                                                 │
│  • OpenAI: response_format + strict:true                    │
│  • Anthropic: tool_choice + tools (利用 tool use 机制)      │
│  • llama.cpp: GBNF grammar                                  │
│  • vLLM: guided_decoding_backend                            │
│  • Outlines, Guidance, LMQL 等库                            │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

OpenAI 示例:

python
from openai import OpenAI
client = OpenAI()

response = client.beta.chat.completions.parse(
    model="gpt-4o",
    messages=[{"role": "user", "content": "张三,25岁,工程师"}],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "person",
            "strict": True,
            "schema": {
                "type": "object",
                "properties": {
                    "name": {"type": "string"},
                    "age": {"type": "integer"},
                    "job": {"type": "string"}
                },
                "required": ["name", "age", "job"],
                "additionalProperties": False
            }
        }
    }
)

# parsed 属性直接返回 Pydantic 模型(SDK 内部做了 json.loads + 校验)
person = response.choices[0].message.parsed
print(person.name, person.age, person.job)  # 张三 25 工程师
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

Anthropic 示例(通过 tool_use 机制实现结构化输出):

python
import anthropic

client = anthropic.Anthropic()

response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    messages=[{"role": "user", "content": "张三,25岁,工程师"}],
    tools=[{
        "name": "extract_person",
        "description": "提取人员信息",
        "input_schema": {
            "type": "object",
            "properties": {
                "name": {"type": "string"},
                "age": {"type": "integer"},
                "job": {"type": "string"}
            },
            "required": ["name", "age", "job"]
        }
    }],
    tool_choice={"type": "tool", "name": "extract_person"}
)

# Claude 一定会返回一个 tool_use,参数符合 input_schema
tool_use = response.content[1]  # content[0] 可能是 thinking,content[1] 是 tool_use
print(tool_use.input)  # {'name': '张三', 'age': 25, 'job': '工程师'}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

3.2 路线B:后处理重试(兜底方案) ​

当你用的 API/模型不支持原生 Structured Output 时,只能用这种方法:

┌─────────────────────────────────────────────────────────────┐
│              路线B:生成 → 校验 → 重试                      │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌──────────┐    提示词      ┌──────────┐    原始文本       │
│  │ 你的代码  │──────────────→│   LLM    │─────────────────→│
│  │          │               │          │                   │
│  │ prompt里 │               │ 自由生成 │  "这是结果:      │
│  │ 写了     │               │          │   {"name":"张      │
│  │ "输出    │               │          │   三"...}"         │
│  │ JSON"    │               │          │                   │
│  └──────────┘               └──────────┘                   │
│       ↑                          │                          │
│       │                          ↓ 原始文本                  │
│       │                    ┌──────────────┐                 │
│       │                    │ 解析 + 校验   │                 │
│       │                    │              │                 │
│       │                    │ ① 去markdown │                 │
│       │                    │ ② json.loads │                 │
│       │                    │ ③ 字段校验   │                 │
│       │                    │ ④ 类型校验   │                 │
│       │                    └──────┬───────┘                 │
│       │                           │                          │
│       │              ┌────────────┴────────────┐            │
│       │              ↓                         ↓            │
│       │         校验通过                    校验失败         │
│       │         → 返回结果                  → 重试(最多N次)│
│       │                                        │            │
│       └────────────────────────────────────────┘            │
│                                                             │
│  特点:                                                     │
│  • 不需要 API 支持,任何 LLM 都能用                          │
│  • 需要写解析 + 重试逻辑                                     │
│  • 不可靠——重试 N 次后仍可能失败                             │
│  • 浪费 token(每次重试都是额外的 API 调用)                 │
│  • Prompt 工程量大——要靠 prompt 让 LLM 输出稳定格式          │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
python
import json
import re
from openai import OpenAI

client = OpenAI()
MAX_RETRIES = 3

def extract_json_with_retry(prompt: str, schema: dict) -> dict:
    """路线B:生成 → 解析 → 失败则重试"""
    messages = [
        {"role": "system", "content": f"只输出 JSON,严格遵循此 Schema:\n{json.dumps(schema, ensure_ascii=False)}"},
        {"role": "user", "content": prompt}
    ]

    for attempt in range(MAX_RETRIES):
        response = client.chat.completions.create(
            model="gpt-4o",
            messages=messages
        )
        text = response.choices[0].message.content

        try:
            # 尝试去除 markdown code block
            match = re.search(r'```(?:json)?\s*\n?(.*?)\n?```', text, re.DOTALL)
            if match:
                text = match.group(1)

            data = json.loads(text)

            # 手动校验字段
            for field, props in schema.get("properties", {}).items():
                if field in schema.get("required", []) and field not in data:
                    raise ValueError(f"缺少必填字段: {field}")
                if field in data and props.get("type") == "integer":
                    data[field] = int(data[field])

            return data

        except (json.JSONDecodeError, ValueError, KeyError) as e:
            if attempt == MAX_RETRIES - 1:
                raise RuntimeError(f"重试 {MAX_RETRIES} 次后仍失败: {e}")
            # 把错误信息反馈给 LLM
            messages.append({"role": "assistant", "content": text})
            messages.append({"role": "user", "content": f"输出格式有误:{e}。请修正后重新输出纯 JSON。"})

    raise RuntimeError("不可达")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

3.3 两条路线的可靠性对比 ​

┌─────────────────────────────────────────────────────────────┐
│          约束生成 vs 后处理重试:可靠性对比                   │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  维度              │ 约束生成         │ 后处理重试           │
│  ─────────────────┼─────────────────┼───────────────────    │
│  一次成功率        │ 100%(硬保证)   │ 70-95%(看 prompt)  │
│  Token 效率        │ 高(一次调用)   │ 低(可多N次调用)    │
│  复杂 Schema 适用  │ 好               │ 差(越复杂越易出错) │
│  Prompt 工程负担   │ 低               │ 高                   │
│  跨模型通用性      │ 需模型支持       │ 所有模型都可用       │
│  延迟稳定性        │ 稳定             │ 不稳定(重试增加延迟)│
│                                                             │
│  结论:如果你的模型/API 支持约束生成,用它。                 │
│  后处理重试只在"没办法"的时候作为兜底。                      │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

第4部分:Zod/Pydantic → JSON Schema → LLM 的核心流水线 ​

这是现代 Agent 开发中最核心的模式。你不需要手写 JSON Schema——你用代码定义类型,工具链帮你做转换和校验。

4.1 完整流水线 ​

┌─────────────────────────────────────────────────────────────┐
│       Zod/Pydantic → JSON Schema → LLM 完整流水线            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  第1步:用代码定义类型(Type Definition)                    │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  Python (Pydantic):                                  │   │
│  │  class Person(BaseModel):                            │   │
│  │      name: str                                       │   │
│  │      age: int                                        │   │
│  │      email: EmailStr                                 │   │
│  │                                                      │   │
│  │  TypeScript (Zod):                                   │   │
│  │  const Person = z.object({                           │   │
│  │      name: z.string(),                               │   │
│  │      age: z.number().int(),                          │   │
│  │      email: z.string().email()                       │   │
│  │  })                                                  │   │
│  └─────────────────────────────────────────────────────┘   │
│         ↓                                                   │
│  第2步:转换为 JSON Schema(.model_json_schema() / .toJSONSchema())│
│  ┌─────────────────────────────────────────────────────┐   │
│  │  {                                                    │   │
│  │    "type": "object",                                  │   │
│  │    "properties": {                                    │   │
│  │      "name": {"type": "string"},                      │   │
│  │      "age": {"type": "integer"},                      │   │
│  │      "email": {"type": "string", "format": "email"}   │   │
│  │    },                                                 │   │
│  │    "required": ["name", "age", "email"]               │   │
│  │  }                                                    │   │
│  └─────────────────────────────────────────────────────┘   │
│         ↓                                                   │
│  第3步:传给 LLM(作为 response_format 或 tool schema)     │
│         ↓                                                   │
│  第4步:LLM 输出符合 Schema 的 JSON                          │
│         ↓                                                   │
│  第5步:用原始类型校验(Pydantic .model_validate() / Zod .parse())│
│  ┌─────────────────────────────────────────────────────┐   │
│  │  raw_json = response.choices[0].message.content      │   │
│  │  person = Person.model_validate_json(raw_json)        │   │
│  │  # 如果到这里没抛异常,数据一定是对的                  │   │
│  │  # person.name 是 str,person.age 是 int              │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  关键洞察:同一个 Schema 用了两次——                        │
│  • 生成时:约束 LLM 输出                                    │
│  • 解析时:校验 LLM 输出                                    │
│  双重保证 = 类型安全                                        │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

4.2 Python 完整示例:Pydantic + OpenAI ​

python
from pydantic import BaseModel, Field
from typing import Literal
from openai import OpenAI

client = OpenAI()

# ── 第1步:定义类型 ──
class Address(BaseModel):
    """地址信息"""
    province: str = Field(description="省份")
    city: str = Field(description="城市")
    district: str | None = Field(default=None, description="区县,可选")

class Person(BaseModel):
    """人员信息提取结果"""
    name: str = Field(description="姓名")
    age: int = Field(description="年龄", ge=0, le=150)
    gender: Literal["男", "女"] = Field(description="性别")
    occupation: str = Field(description="职业")
    address: Address | None = Field(default=None, description="地址信息,可选")

# ── 第2步:自动转换 → 第3步:传给 LLM ──
response = client.beta.chat.completions.parse(
    model="gpt-4o",
    messages=[{
        "role": "user",
        "content": "我叫李四,28岁,男,软件工程师,住在广东省深圳市南山区"
    }],
    response_format=Person  # Pydantic 模型直接传入!
)

# ── 第4、5步:SDK 自动做了 json.loads + model_validate ──
person = response.choices[0].message.parsed
# person 是 Person 实例,类型完全确定
print(person.name)           # "李四"          (str)
print(person.age)            # 28              (int)
print(person.gender)         # "男"            (Literal["男", "女"])
print(person.occupation)     # "软件工程师"    (str)
print(person.address.city)   # "深圳市"        (str)
print(person.address.province) # "广东省"      (str)

# 如果有 refusals(LLM 因安全策略拒绝回答)
if response.choices[0].message.refusal:
    print(f"LLM 拒绝了请求: {response.choices[0].message.refusal}")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

4.3 TypeScript 完整示例:Zod + OpenAI ​

typescript
import OpenAI from "openai";
import { z } from "zod";
import { zodResponseFormat } from "openai/helpers/zod";

const openai = new OpenAI();

// ── 第1步:定义类型 ──
const AddressSchema = z.object({
  province: z.string().describe("省份"),
  city: z.string().describe("城市"),
  district: z.string().nullable().optional().describe("区县,可选"),
});

const PersonSchema = z.object({
  name: z.string().describe("姓名"),
  age: z.number().int().min(0).max(150).describe("年龄"),
  gender: z.enum(["男", "女"]).describe("性别"),
  occupation: z.string().describe("职业"),
  address: AddressSchema.nullable().optional().describe("地址信息,可选"),
});

// ── 第2步:Zod → JSON Schema(zodResponseFormat 内部处理)→ 第3步:传给 LLM ──
const response = await openai.beta.chat.completions.parse({
  model: "gpt-4o",
  messages: [
    {
      role: "user",
      content: "我叫李四,28岁,男,软件工程师,住在广东省深圳市南山区",
    },
  ],
  response_format: zodResponseFormat(PersonSchema, "person_extract"),
});

// ── 第4、5步:SDK 自动校验 ──
const person = response.choices[0].message.parsed;
// person 的类型是 z.infer<typeof PersonSchema>
console.log(person.name);  // TypeScript 知道这是 string
console.log(person.age);   // TypeScript 知道这是 number
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

4.4 这个流水线为什么强大? ​

┌─────────────────────────────────────────────────────────────┐
│           类型驱动开发:定义一次,处处安全                    │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  传统方式:                                                 │
│  1. 手写 prompt 描述输出格式                                │
│  2. 手写 json.loads() 解析逻辑                              │
│  3. 手写字段校验(if "name" in data: ...)                  │
│  4. 字段类型不确定(data["age"] 是 int 还是 str?)         │
│  → prompt、解析、校验三处不同步,改一处忘两处               │
│                                                             │
│  流水线方式:                                               │
│  1. 定义 Pydantic/Zod 模型(唯一的真相来源)                 │
│  2. 自动生成 JSON Schema → 传给 LLM                        │
│  3. 自动校验 LLM 输出 → 类型安全的对象                      │
│  → prompt、解析、校验全部由同一个类型定义驱动               │
│  → 改类型定义 → 自动同步到 LLM约束和运行时校验              │
│                                                             │
│  这就是"Single Source of Truth"原则在 LLM 应用中的体现。    │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

第5部分:实战模式 ​

5.1 模式1:实体抽取(Entity Extraction) ​

从非结构化文本中提取结构化信息,是最常见的用例。

python
from pydantic import BaseModel, Field
from typing import List, Optional
from datetime import date

class Transaction(BaseModel):
    """金融交易记录"""
    date: date = Field(description="交易日期")
    amount: float = Field(description="交易金额,正数为收入,负数为支出")
    counterparty: str = Field(description="交易对手方")
    category: str = Field(description="交易类别,如:餐饮、交通、购物、工资")
    note: Optional[str] = Field(default=None, description="备注")

class TransactionList(BaseModel):
    transactions: List[Transaction]

# 输入是一段自然语言
text = """
2024年3月15日,工资收入15000元。
3月16日,在海底捞吃饭花了320元。
3月17日,坐地铁花了8元,在淘宝买了一件衬衫花了199元。
"""

response = client.beta.chat.completions.parse(
    model="gpt-4o",
    messages=[{
        "role": "system",
        "content": "从文本中提取所有交易记录。金额正数为收入,负数为支出。"
    }, {
        "role": "user",
        "content": text
    }],
    response_format=TransactionList,
)

result = response.choices[0].message.parsed
for t in result.transactions:
    print(f"{t.date} | {t.category:4s} | {t.counterparty:6s} | {t.amount:>8.2f}")
# 输出:
# 2024-03-15 | 工资 | (收入)  | 15000.00
# 2024-03-16 | 餐饮 | 海底捞  |  -320.00
# 2024-03-17 | 交通 | 地铁    |    -8.00
# 2024-03-17 | 购物 | 淘宝    |  -199.00
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

5.2 模式2:分类与路由(Classification & Routing) ​

根据用户输入判断意图,路由到不同的处理分支。这是多 Agent 系统的核心组件。

python
from pydantic import BaseModel, Field
from typing import Literal

class IntentClassification(BaseModel):
    """用户意图分类"""
    intent: Literal[
        "查询天气",
        "预订机票",
        "股票查询",
        "闲聊",
        "投诉"
    ] = Field(description="用户意图类别")
    confidence: float = Field(description="置信度,0-1之间", ge=0, le=1)
    reason: str = Field(description="分类理由")
    key_entities: list[str] = Field(description="关键实体列表")

def classify_user_intent(user_message: str) -> IntentClassification:
    response = client.beta.chat.completions.parse(
        model="gpt-4o",
        messages=[{
            "role": "system",
            "content": "你是一个意图分类器。根据用户消息判断意图类别。"
        }, {
            "role": "user",
            "content": user_message
        }],
        response_format=IntentClassification,
        max_tokens=200,
    )
    return response.choices[0].message.parsed

# 使用示例
result = classify_user_intent("我买的股票茅台今天涨了多少?")
print(f"意图: {result.intent}")       # 股票查询
print(f"置信度: {result.confidence}") # 0.95
print(f"理由: {result.reason}")
print(f"实体: {result.key_entities}")  # ['茅台']

# 根据分类结果路由
if result.intent == "股票查询":
    # 调股票 Agent
    ...
elif result.intent == "查询天气":
    # 调天气 Agent
    ...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

5.3 模式3:结构化合成(Structured Synthesis) ​

给定多个输入,合成一个结构化的摘要或报告。

python
from pydantic import BaseModel
from typing import List

class ProductComparison(BaseModel):
    """产品对比结果"""
    product_name: str
    price_range: str
    pros: List[str]
    cons: List[str]
    best_for: str
    overall_rating: float

class ComparisonReport(BaseModel):
    """完整对比报告"""
    products: List[ProductComparison]
    recommendation: str
    summary: str

response = client.beta.chat.completions.parse(
    model="gpt-4o",
    messages=[{
        "role": "user",
        "content": """
        对比以下三款手机:iPhone 16 Pro、Samsung Galaxy S25、Xiaomi 15 Pro。
        从价格、优缺点、适用人群等方面分析。
        """
    }],
    response_format=ComparisonReport,
)

report = response.choices[0].message.parsed
for p in report.products:
    print(f"\n## {p.product_name} ({p.price_range})")
    print(f"优点: {', '.join(p.pros)}")
    print(f"缺点: {', '.join(p.cons)}")
    print(f"评分: {p.overall_rating}/10")
print(f"\n推荐: {report.recommendation}")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

5.4 模式4:层级分类(Hierarchical Classification) ​

先用粗粒度分类,再在子类别中细分。相比一次性做细粒度分类,层级分类的准确率更高。

python
# 第一级:粗粒度
class CoarseCategory(BaseModel):
    category: Literal["技术问题", "账户问题", "账单问题", "其他"]

# 第二级:细粒度(每个大类有自己的子分类)
class TechSubCategory(BaseModel):
    sub_category: Literal["API", "SDK", "部署", "性能", "Bug"]

class AccountSubCategory(BaseModel):
    sub_category: Literal["注册", "登录", "密码重置", "权限"]

# 分类流程
def hierarchical_classify(user_message: str):
    # 第一级
    coarse = client.beta.chat.completions.parse(
        model="gpt-4o",
        messages=[{"role": "user", "content": user_message}],
        response_format=CoarseCategory,
    ).choices[0].message.parsed

    # 第二级:根据第一级结果选择不同的 Schema
    if coarse.category == "技术问题":
        sub_response_format = TechSubCategory
    elif coarse.category == "账户问题":
        sub_response_format = AccountSubCategory
    else:
        return coarse.category, None

    sub = client.beta.chat.completions.parse(
        model="gpt-4o",
        messages=[{"role": "user", "content": user_message}],
        response_format=sub_response_format,
    ).choices[0].message.parsed

    return coarse.category, sub.sub_category
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

5.5 模式5:Optional 字段与条件输出 ​

用 Optional 和 | None 标记可选字段,让 Schema 更灵活。

python
from pydantic import BaseModel
from typing import Optional, Literal

class Event(BaseModel):
    """日历事件"""
    title: str
    date: str  # ISO date
    time: Optional[str] = None  # 有些事件没有具体时间
    location: Optional[str] = None
    is_all_day: bool = False
    recurrence: Optional[Literal["每天", "每周", "每月", "每年"]] = None

class CalendarExtract(BaseModel):
    events: list[Event]

text = """
下周一上午10点开会,地点在3楼会议室。
周五是妈妈生日(全天)。
每周三下午2点有瑜伽课。
"""

result = client.beta.chat.completions.parse(
    model="gpt-4o",
    messages=[{"role": "user", "content": text}],
    response_format=CalendarExtract,
).choices[0].message.parsed

for e in result.events:
    flags = []
    if e.is_all_day: flags.append("全天")
    if e.recurrence: flags.append(e.recurrence)
    print(f"{e.title} | {e.date} {e.time or ''} | {', '.join(flags)}")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

第6部分:局限性与注意事项 ​

6.1 复杂嵌套 Schema 的挑战 ​

┌─────────────────────────────────────────────────────────────┐
│           复杂嵌套 Schema 可能带来的问题                     │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  问题1:模型"迷路"                                          │
│  嵌套层级超过 3-4 层时,模型可能在深层嵌套中丢失上下文      │
│  → 把深层嵌套拆分为多次调用,每次处理一层                   │
│                                                             │
│  问题2:描述冲突                                            │
│  父级和子级的 description 如果语义不一致,模型困惑          │
│  → 确保 description 层级一致,不矛盾                        │
│                                                             │
│  问题3:枚举爆炸                                            │
│  Literal 的类型太多时(如 50+ 个枚举值),模型选择困难      │
│  → 用层级分类代替大枚举                                     │
│                                                             │
│  问题4:数组元素数量不确定                                   │
│  Schema 可以约束数组中每个元素的类型,但不能精确控制数量    │
│  → 在 prompt 中说明期望数量,在代码中校验长度               │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

6.2 Token 开销 ​

每个 Schema 都是一个大的 prompt 前缀。一个中等复杂的 Schema 可能有 500-2000 tokens。这直接计入每次调用的成本。

python
# 小 Schema:约 50 tokens
class Simple(BaseModel):
    name: str
    age: int

# 大 Schema:约 500+ tokens
class Complex(BaseModel):
    """... 长描述 ..."""
    field1: str = Field(description="很长的描述,解释这个字段的用途和格式要求...")
    field2: list[NestedObject] = Field(description="嵌套对象列表...")
    field3: Literal["a", "b", "c", "d", ..., "z"]  # 26个枚举值

# 权衡:Schema 越精确 → token 开销越大 → 成本越高
#      Schema 越简单 → token 开销越小 → 约束越弱
1
2
3
4
5
6
7
8
9
10
11
12
13
14

6.3 strict: true 的限制(OpenAI) ​

开了 strict 后,你的 JSON Schema 必须满足一些约束:

  • 所有对象必须设置 additionalProperties: false
  • 所有字段必须在 required 中声明(或者全部 optional)
  • 不支持某些 JSON Schema 高级特性(如 oneOf/anyOf/allOf 的部分形式)
  • 嵌套深度限制
python
# OpenAI strict 模式支持的 schema 子集:
# ✅ type: object, string, number, integer, boolean, array, null
# ✅ required, properties, additionalProperties: false
# ✅ items (数组元素类型)
# ✅ enum
# ✅ description
# ✅ $ref, $defs (有限支持)
#
# ❌ oneOf, anyOf, allOf, not (部分受限)
# ❌ pattern (正则约束)
# ❌ minLength, maxLength (字符串长度限制)
# ❌ minimum, maximum (数字范围——可以用但可能被忽略)
1
2
3
4
5
6
7
8
9
10
11
12

6.4 Anthropic 的差异 ​

Anthropic 不直接提供 response_format 参数,而是通过 tool_use 机制实现类似效果。差异点:

  • Claude 的 tool_use 是为"调用工具"设计的,不是为"提取结构"设计的——但你可以强制触发 tool_use 来获得结构化输出
  • Claude 的 input_schema 不保证 100% 约束——它更多是"软引导"
  • Claude 可能在 tool_use 之前输出 text 块(thinking),你需要跳过
  • 复杂 Schema 下,Claude 可能把内容放在 text 块里而不是 tool_use 里

6.5 模型支持现状 ​

┌─────────────────────────────────────────────────────────────┐
│           各模型/API 的 Structured Output 支持               │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  OpenAI GPT-4o / GPT-4o-mini                                │
│  ✅ response_format (json_schema + strict)                  │
│  ✅ client.beta.chat.completions.parse (Pydantic/Zod)       │
│                                                             │
│  Anthropic Claude (Sonnet 4, Opus 4)                        │
│  ⚠️ 无原生 response_format                                  │
│  ⚠️ 通过 tool_use + tool_choice 间接实现                    │
│  ⚠️ 不保证 100% 类型约束                                    │
│                                                             │
│  Google Gemini                                               │
│  ✅ response_schema (类似 OpenAI)                           │
│  ✅ Google GenAI SDK 支持 Pydantic                          │
│                                                             │
│  开源模型 (Llama, Qwen, DeepSeek) + 推理框架                 │
│  ✅ llama.cpp: GBNF grammar (强制约束)                      │
│  ✅ vLLM: guided_decoding_backend                           │
│  ✅ Outlines: 基于 FSM 的约束生成                            │
│  ✅ Guidance: 类似模板引擎的约束                             │
│                                                             │
│  关键:不是模型本身支持,而是推理框架/API网关支持。          │
│  同一个 Llama 模型,用 llama.cpp 可以强制约束,             │
│  用 Ollama 默认配置则不行——不是模型的问题,是框架的问题。   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

核心总结 ​

总结1:Structured Output 的本质 ​

Structured Output = 让 LLM 的输出从"自由文本"变成"类型安全的 JSON"

• 不是在 prompt 里说"请输出 JSON"——那只是建议
• 而是在 API/推理层面限制 token 生成——这是强制
• 结果:100% 保证输出的 JSON 符合你定义的 Schema
1
2
3
4
5

总结2:JSON Mode vs JSON Schema ​

模式保证内容适用场景
JSON Mode输出是合法 JSON结构简单,只需保证可解析
JSON Schema字段名、类型、必填都保证生产环境,输出进程序逻辑

总结3:两条技术路线 ​

路线A(推荐):API 级约束生成
  一次调用 → 100% 符合 Schema → 零解析代码

路线B(兜底):后处理重试
  生成 → json.loads → 失败 → 重试 → 再失败 → ...
  只在不支持约束生成的模型/API 上使用
1
2
3
4
5
6

总结4:核心流水线 ​

定义 Pydantic/Zod 类型
  → 自动生成 JSON Schema
    → 传给 LLM(约束生成)
      → LLM 输出符合 Schema 的 JSON
        → 自动校验(Pydantic/Zod)
          → 得到类型安全的对象

同一个类型定义既是"约束规则"又是"校验规则",双重保险。
1
2
3
4
5
6
7
8

总结5:适用场景矩阵 ​

场景推荐方案原因
实体抽取json_schema字段固定,类型确定
意图分类json_schema + enum枚举约束提高准确率
复杂嵌套结构拆分为多步调用避免模型在深层迷路
简单 KV 提取JSON Mode 即可开销小,够用
本地模型GBNF/Outlines/Guidance在推理层加约束
不支持约束的 API后处理重试没得选

章节测试 ​

测试1:概念理解 ​

Structured Output 和"在 prompt 里让 LLM 输出 JSON"的根本区别是什么?

测试2:HTTP 层面 ​

在 OpenAI API 中,使用 Structured Output 时,请求体比普通请求多了哪个字段?它包含什么内容?

测试3:JSON Mode vs JSON Schema ​

以下场景应该用 JSON Mode 还是 JSON Schema?

  • A. 需要保证 age 字段一定是整数
  • B. 只需要输出是合法的 JSON,结构无所谓
  • C. 需要保证输出只有 name 和 email 两个字段,不能多也不能少

测试4:技术路线 ​

API 级约束生成(如 OpenAI strict)和"生成后 json.loads 再重试"的本质区别是什么?

测试5:Pydantic 流水线 ​

在 Pydantic → JSON Schema → LLM → 校验 这个流水线中,同一个 Schema 被用了两次,分别起什么作用?

测试6:Anthropic Claude ​

Anthropic Claude 如何实现类似 Structured Output 的效果?与 OpenAI 的方案有什么本质区别?

测试7:局限性 ​

你正在做一个法律合同解析系统,合同条款有深度嵌套的结构(合同 → 条款 → 子条款 → 条件 → 例外)。用 Structured Output 可能会遇到什么问题?如何解决?


参考答案 ​

测试1答案 ​

答案:

  • "在 prompt 里让 LLM 输出 JSON"只是软约束——LLM 可能不听话,可能包裹 markdown,可能写错字段名
  • Structured Output 是硬约束——在 token 生成阶段就限制,确保输出 100% 符合 Schema,不是"先输出再看对不对"

测试2答案 ​

答案:多了一个 response_format 字段,包含:

  • type: "json_schema"
  • json_schema.name: Schema 的名称
  • json_schema.strict: 是否开启严格模式
  • json_schema.schema: JSON Schema 定义(type, properties, required, additionalProperties 等)

这个字段告诉 API 服务端:在生成 token 时,每一步都只允许符合 Schema 的 token。

测试3答案 ​

答案:

  • A. JSON Schema——需要保证类型(integer)
  • B. JSON Mode——只要合法 JSON 就行
  • C. JSON Schema——需要禁止多余字段(additionalProperties: false)和强制必填(required)

测试4答案 ​

答案:

API 级约束生成(strict):
  每一步 token 生成都被 Schema 限制 → 不合法 token 不会被生成
  → 一次调用必定成功,不需要重试
  → 类型安全在生成阶段就已保证

后处理重试:
  先生成,再检查 → 可能失败 → 重试
  → 每次重试都不保证成功,浪费 token
  → 类型安全依赖你自己的校验代码
1
2
3
4
5
6
7
8
9

本质区别:前者是"生成阶段约束"(prevention),后者是"生成后校验"(detection + correction)。在安全领域,prevention 总是优于 detection。

测试5答案 ​

答案:

第一次:作为 LLM 的约束规则(response_format),告诉 LLM "你只能输出符合这个结构的数据"。作用在生成阶段。

第二次:作为程序的数据校验规则(model_validate_json()),验证 LLM 的输出确实符合预期。作用在解析阶段。

两次使用确保:即使 API 层面的约束出了问题(虽然理论上不会),程序的校验层也能兜底。这就是 defense in depth(纵深防御)。

测试6答案 ​

答案:

Anthropic Claude 不提供原生的 response_format 参数。要实现结构化输出,需要:

  1. 定义一个 tool,把 input_schema 当作目标 Schema
  2. 设置 tool_choice: {"type": "tool", "name": "..."} 强制 Claude 调用该 tool
  3. Claude 返回的 tool_use.input 就是你想要的结构化数据

与 OpenAI 的本质区别:

  • OpenAI 的方案是真正的约束生成(constrained decoding),100% 保证
  • Anthropic 的方案是利用 tool_use 机制间接实现,更多是"引导"而非"硬约束"
  • Claude 可能在 tool_use 之前输出 text 块(thinking),需要额外处理跳过

测试7答案 ​

答案:

可能遇到的问题:

  1. 嵌套过深(>3-4层)时模型丢失上下文,在深层产生不符合 Schema 的输出
  2. 子结构之间存在微妙的语义依赖,Schema 无法表达(如"如果 type='A' 则需要字段 X,否则需要字段 Y")
  3. 大 Schema 的 token 开销(500-2000 tokens)在高频调用时显著增加成本

解决方案:

  • 拆分为多次调用:先提取第一层结构,再逐步深入
  • 对条件依赖使用 anyOf(如果 API 支持)或者分步处理
  • 权衡 Schema 的精确性和 token 开销,字段描述尽量精简

相关笔记 ​

  • [[01-function-calling]] - Function Calling 本质也是 Structured Output
  • [[03-rag-basics]] - RAG 中使用 Structured Output 做证据提取
  • [[04-memory-management]] - 记忆管理中的结构化存储
  • [[05-agent-workflow]] - 工作流中 Structured Output 作为节点间契约

下一步学习 ​

  • [ ] 阅读 03 - RAG 基础
  • [ ] 实践:用 Pydantic + OpenAI Structured Output 做一个简历解析器
  • [ ] 实践:用 Zod + OpenAI SDK 做一个多意图分类器

学习状态:🟡 开始学习

最后更新于:

Pager
上一篇9. 真实 Agent 应用场景 / Real-World AI Agent Applications
下一篇11. Tools Design Best Practices - AI Agent 工具设计最佳实践 / Tools Design Best Practices for AI Agents

持续记录,持续成长

Copyright © Tidenflow