前端转 AI 100 天 Day 35:结构化输出——让 LLM 稳定返回 JSON

前端转 AI 第 35 天:结构化输出 - 让 LLM 稳定返回 JSON

昨日回顾

Day 34 搞定了 Function Calling,助手能"调用工具"了。

但工具调用的参数是模型生成的------万一模型输出:

json 复制代码
{"city": "北京"       ← 少了右括号

或者:

json 复制代码
好的,参数是:{"city": "北京"}   ← 多了一堆废话

整个流程直接崩。

今天解决这个问题:结构化输出。

Day 35 目标:搞懂 JSON Mode / JSON Schema,让 LLM 100% 稳定返回合法 JSON。


一、为什么需要结构化输出

1. 真实场景:抽取信息

需求: 从用户的一句话里抽取结构化数据。

输入:

arduino 复制代码
"帮我订明天下午3点从北京到上海的机票,两个人"

期望输出:

json 复制代码
{
  "action": "book_flight",
  "from_city": "北京",
  "to_city": "上海",
  "date": "2026-10-14",
  "time": "15:00",
  "passengers": 2
}

你写了一版 prompt:

javascript 复制代码
请从用户输入里提取信息,按以下 JSON 格式返回:
{"action": "...", "from_city": "...", ...}

模型的输出可能是:

情况 1(好):

json 复制代码
{"action": "book_flight", "from_city": "北京", ...}

情况 2(加了废话):

json 复制代码
好的,我提取到以下信息:
{"action": "book_flight", ...}
希望对你有帮助!

情况 3(格式错):

javascript 复制代码
{
  "action": "book_flight",
  "from_city": "北京",
  "to_city": "上海",
  "date": "2026-10-14",
  "time": "15:00",
  "passengers": 2,   ← 多了逗号,JSON 非法
}

情况 4(用单引号):

json 复制代码
{'action': 'book_flight', ...}   ← 不是合法 JSON

情况 5(字段名错):

json 复制代码
{"action": "book_flight", "from": "北京", "to": "上海"}
                            ← 字段名跟你要的不一样

模型有 5% 的概率"发挥创意"------你的下游代码就得写一堆容错。

2. 传统解法:正则 + 重试

Day 9 写过 extract_json------从文本里硬抠 JSON。

优点: 能处理大部分情况 缺点:

  • 治标不治本:还是在"猜"模型的输出
  • 字段无法校验 :模型给了 from 而不是 from_city,正则救不了
  • 重试成本高:每次重试都是钱

3. 正确的解法:让模型"承诺"按格式输出

OpenAI 和主流厂商都提供了"结构化输出"能力:

  • JSON Mode:承诺返回合法 JSON
  • JSON Schema :承诺返回符合给定 Schema 的 JSON
  • Structured Outputs(严格模式):100% 保证符合 Schema

核心思路:把"约束"前移到模型侧,而不是"事后修补"。

4. 前端视角:就像 TypeScript + Zod

前端怎么做类型安全的 API 响应?

传统方式:

ts 复制代码
const res = await fetch("/api/user").then(r => r.json());
// 只能祈祷 res 符合预期
if (typeof res.name === "string" && typeof res.age === "number") {
  // 手动校验
}

Zod 的方式:

ts 复制代码
const UserSchema = z.object({
  name: z.string(),
  age: z.number().min(0),
});

const user = UserSchema.parse(await fetch("/api/user").then(r => r.json()));
// 校验失败直接抛错,不用手写 if

Python 的 Structured Outputs 就是"LLM 版的 Zod":

python 复制代码
class UserInfo(BaseModel):
    name: str
    age: int = Field(ge=0)

response = client.beta.chat.completions.parse(
    response_format=UserInfo,   # ← 关键
    ...
)
user = response.choices[0].message.parsed   # 直接拿到 UserInfo 实例

框架保证 LLM 的输出符合 Pydantic 模型。 你什么都不用校验。


二、三种层次的结构化输出

别急着上手,先搞清楚"结构化输出"的三个层次。

1. 层次一:Prompt 要求(最原始)

只在 prompt 里说"请返回 JSON":

python 复制代码
response = client.chat.completions.create(
    model="deepseek-chat",
    messages=[
        {"role": "system", "content": "请以 JSON 格式返回"},
        {"role": "user", "content": "..."},
    ],
)

成功率: 70-90% 适用: 原型验证

问题: 模型会加 markdown、会用单引号、会少括号。

2. 层次二:JSON Mode(较可靠)

用 response_format={"type": "json_object"}:

python 复制代码
response = client.chat.completions.create(
    model="deepseek-chat",
    messages=[
        {"role": "system", "content": "以 JSON 返回"},
        {"role": "user", "content": "..."},
    ],
    response_format={"type": "json_object"},   # ← 关键
)

成功率: 99% 适用: 大部分场景

保证:

  • ✅ 返回的是合法 JSON
  • ❌ 不保证字段名、类型、结构符合要求

还是会输出:

json 复制代码
{"from": "北京", "to": "上海"}   ← 合法 JSON,但字段名不对

3. 层次三:JSON Schema(最可靠)

用 JSON Schema 严格约束:

python 复制代码
response = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[...],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "flight_info",
            "schema": {
                "type": "object",
                "properties": {
                    "from_city": {"type": "string"},
                    "to_city": {"type": "string"},
                    "passengers": {"type": "integer", "minimum": 1},
                },
                "required": ["from_city", "to_city", "passengers"],
                "additionalProperties": False,   # ← 关键:禁止额外字段
            },
            "strict": True,   # ← 关键:严格模式
        },
    },
)

成功率: 100%(严格模式下) 适用: 生产环境

strict: True 是"魔法开关"------开启后模型必须严格按 Schema 输出。

4. 三个层次对比

层次 保证 成功率 适用
Prompt 要求 无 70-90% 原型
JSON Mode 合法 JSON 99% 一般场景
JSON Schema 符合 Schema 100% 生产

生产环境一律用 JSON Schema + 严格模式。

5. 厂商支持现状

厂商 JSON Mode JSON Schema 严格模式
OpenAI ✅ ✅ ✅(GPT-4o+)
DeepSeek ✅ ✅ ✅(V3+)
通义千问 ✅ ✅ ✅
智谱 GLM ✅ ✅ ✅
Claude ❌ ✅(工具调用方式) ✅

注意:

  • 严格模式是"新功能"------2024 年 8 月 OpenAI 才首发,各厂商陆续跟上
  • 老模型可能不支持------用之前查文档
  • 各厂商 API 略有差异------但都兼容 OpenAI 格式

三、JSON Mode:最简单的结构化

先上手最简单的。

1. 基础用法

创建 day35/json_mode.py:

python 复制代码
"""JSON Mode 基础用法"""
import json
import os
from openai import OpenAI


client = OpenAI(
    api_key=os.getenv("DEEPSEEK_API_KEY"),
    base_url="https://api.deepseek.com/v1",
)


def extract_info(text: str) -> dict:
    response = client.chat.completions.create(
        model="deepseek-chat",
        messages=[
            {
                "role": "system",
                "content": (
                    "从用户输入里提取机票预订信息,"
                    "返回 JSON,字段:from_city, to_city, date, passengers"
                ),
            },
            {"role": "user", "content": text},
        ],
        response_format={"type": "json_object"},   # ← JSON Mode
    )

    content = response.choices[0].message.content
    return json.loads(content)


if __name__ == "__main__":
    text = "帮我订明天下午3点从北京到上海的机票,两个人"
    result = extract_info(text)
    print(json.dumps(result, ensure_ascii=False, indent=2))

输出:

json 复制代码
{
  "from_city": "北京",
  "to_city": "上海",
  "date": "2026-10-14",
  "passengers": 2
}

稳了。 100% 是合法 JSON。

2. JSON Mode 的"坑"

坑 1:某些模型要求 prompt 里出现"json"这个词

OpenAI 有规定:使用 JSON Mode 时,prompt 里必须提"JSON"。

python 复制代码
# ✅ 可以
system = "以 JSON 格式返回"
system = "返回 JSON"

# ❌ 会报错(OpenAI)
system = "提取以下信息..."

解决:

python 复制代码
system = "从用户输入提取信息。以 JSON 格式返回。"
# 加一句就行

坑 2:模型可能返回一个空对象

json 复制代码
{}

JSON Mode 只保证"合法 JSON",不保证"字段存在"。

校验还得自己做。

坑 3:模型可能返回 {"data": {...}} 嵌套

json 复制代码
{
  "result": {
    "from_city": "北京",
    ...
  }
}

它会"多包一层"------JSON Mode 管不着。

所以 JSON Mode 通常要配合"prompt 明确说明"或"Pydantic 二次校验"。

3. JSON Mode 适合什么

适合:

  • 结构简单:字段少、模型不容易搞错
  • 原型验证:先跑通逻辑
  • 兼容性要求高:老模型也支持

不适合:

  • 复杂嵌套:模型容易跑偏
  • 字段强约束:枚举、数字范围等
  • 生产环境:还是要上 JSON Schema

四、JSON Schema:生产级方案

这才是重头戏。

1. Pydantic 自动生成 Schema

Day 11 学的 Pydantic 又派上用场。

创建 day35/structured.py:

python 复制代码
"""结构化输出 - Pydantic 版"""
import json
import os
from typing import Literal
from datetime import date
from pydantic import BaseModel, Field
from openai import OpenAI


client = OpenAI(
    api_key=os.getenv("DEEPSEEK_API_KEY"),
    base_url="https://api.deepseek.com/v1",
)


# ============ 1. 用 Pydantic 定义输出结构 ============

class FlightBooking(BaseModel):
    """机票预订信息"""
    from_city: str = Field(description="出发城市")
    to_city: str = Field(description="到达城市")
    date: str = Field(description="出发日期,格式 YYYY-MM-DD")
    time: str = Field(description="出发时间,格式 HH:MM")
    passengers: int = Field(ge=1, le=10, description="乘客数量")
    class_type: Literal["economy", "business", "first"] = Field(
        default="economy",
        description="舱位等级",
    )


# ============ 2. 调用(严格模式)============

def extract_flight(text: str) -> FlightBooking:
    """从文本提取机票信息,返回 Pydantic 对象"""

    # 用 Pydantic 生成 Schema
    schema = FlightBooking.model_json_schema()

    response = client.chat.completions.create(
        model="deepseek-chat",
        messages=[
            {"role": "system", "content": "从用户输入提取机票预订信息"},
            {"role": "user", "content": text},
        ],
        response_format={
            "type": "json_schema",
            "json_schema": {
                "name": "FlightBooking",
                "schema": schema,
                "strict": True,        # ← 关键
            },
        },
    )

    content = response.choices[0].message.content
    # 自动校验 + 类型转换
    return FlightBooking.model_validate_json(content)


if __name__ == "__main__":
    text = "帮我订明天下午3点从北京到上海的机票,商务舱,两个人"
    result = extract_flight(text)
    print(result)
    print(f"类型: {type(result)}")
    print(f"出发: {result.from_city} → {result.to_city}")
    print(f"人数: {result.passengers}")

输出:

lua 复制代码
from_city='北京' to_city='上海' date='2026-10-14' time='15:00' passengers=2 class_type='business'
类型: <class '__main__.FlightBooking'>
出发: 北京 → 上海
人数: 2

关键收获:

  • result 是 FlightBooking 实例,不是 dict
  • 可以直接 result.from_city,IDE 有补全
  • Pydantic 已经校验过了,类型安全

2. strict: True 到底做了什么

对比不开启:

python 复制代码
response_format={
    "type": "json_schema",
    "json_schema": {
        "name": "FlightBooking",
        "schema": schema,
        # 没有 strict
    },
}

效果对比:

场景 不严格 严格模式
多一个字段 ✅ 会出现 ❌ 保证没有
少一个字段 ⚠️ 可能出现 ❌ 保证不会
类型错(int vs str) ⚠️ 可能出现 ❌ 保证不会
枚举外取值 ⚠️ 可能出现 ❌ 保证不会

严格模式保证:

  1. 所有字段都在 (required)
  2. 类型都对
  3. enum 内取值
  4. 没有额外字段 (additionalProperties: false)

踩坑提醒:

严格模式要求所有字段都 required,且 additionalProperties: false。 有些 SDK 会自动加,有些要手动。

Pydantic 的 model_json_schema() 默认不生成 additionalProperties: false------需要手动处理:

python 复制代码
def to_strict_schema(model_cls):
    """生成符合严格模式的 Schema"""
    schema = model_cls.model_json_schema()

    # 递归设置 additionalProperties: false
    def _process(obj):
        if isinstance(obj, dict):
            if obj.get("type") == "object":
                obj["additionalProperties"] = False
                # 所有字段必填(严格模式要求)
                if "properties" in obj:
                    obj["required"] = list(obj["properties"].keys())
            for v in obj.values():
                _process(v)
        elif isinstance(obj, list):
            for item in obj:
                _process(item)

    _process(schema)
    return schema

这是很多人在严格模式上踩的坑。

3. 嵌套结构

Pydantic 支持嵌套------严格模式也支持。

python 复制代码
class Address(BaseModel):
    city: str
    district: str
    street: str


class Order(BaseModel):
    order_id: str
    customer_name: str
    shipping_address: Address           # 嵌套
    items: list[str]                    # 列表
    total: float = Field(ge=0)


def parse_order(text: str) -> Order:
    response = client.chat.completions.create(
        model="deepseek-chat",
        messages=[{"role": "user", "content": text}],
        response_format={
            "type": "json_schema",
            "json_schema": {
                "name": "Order",
                "schema": to_strict_schema(Order),
                "strict": True,
            },
        },
    )
    return Order.model_validate_json(response.choices[0].message.content)

嵌套、列表、可选字段全支持。

这就是"生产级结构化输出"。


五、OpenAI SDK 的 .parse() 语法糖

OpenAI 官方 SDK 提供了更优雅的接口:

python 复制代码
from openai import OpenAI

client = OpenAI(...)


# 不用手写 response_format
response = client.beta.chat.completions.parse(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": "帮我订明天下午 3 点..."}],
    response_format=FlightBooking,   # ← 直接传 Pydantic 类
)

# 直接拿对象
booking = response.choices[0].message.parsed
print(booking.from_city)

parse() 帮你做了一切:

  • Pydantic → JSON Schema
  • 设置严格模式
  • 解析响应
  • 返回 Pydantic 实例

限制:

  • 只支持 OpenAI / Azure(其他厂商要看 SDK 是否兼容)
  • 需要模型支持严格模式(GPT-4o / 4o-mini)
  • 旧 SDK 版本可能没有

DeepSeek 现在也兼容部分接口,可以试试。如果不行,用"手动 JSON Schema"版本。


六、实战:给 Agent 加结构化输出

把结构化输出整合到我们 Day 34 的 Agent 里。

1. 应用场景

做一个"智能笔记助手":

输入: 用户的一段自然语言 输出: 结构化笔记(标题 / 标签 / 待办事项 / 优先级)

2. 定义模型

src/ai_assistant/schemas/note.py:

python 复制代码
"""笔记的结构化模型"""
from pydantic import BaseModel, Field
from typing import Literal


class TodoItem(BaseModel):
    """待办事项"""
    content: str = Field(min_length=1, max_length=200, description="事项内容")
    priority: Literal["high", "medium", "low"] = Field(
        default="medium",
        description="优先级",
    )
    due_date: str | None = Field(
        default=None,
        description="截止日期,格式 YYYY-MM-DD,没有就不填",
    )


class Note(BaseModel):
    """结构化笔记"""
    title: str = Field(min_length=1, max_length=100, description="笔记标题")
    summary: str = Field(max_length=500, description="一句话摘要")
    tags: list[str] = Field(
        default_factory=list,
        max_length=5,
        description="标签列表,最多 5 个",
    )
    todos: list[TodoItem] = Field(
        default_factory=list,
        description="从内容里抽取的待办事项",
    )
    category: Literal["work", "life", "study", "idea", "other"] = Field(
        default="other",
        description="笔记分类",
    )

3. 实现抽取函数

src/ai_assistant/services/note_extractor.py:

python 复制代码
"""笔记抽取服务"""
from openai import AsyncOpenAI

from ..schemas.note import Note
from ..observability.logging import get_struct_logger


logger = get_struct_logger(__name__)


SYSTEM_PROMPT = """你是一个智能笔记助手。从用户输入里提取结构化信息。

要求:
- title:简洁的标题
- summary:一句话摘要
- tags:3-5 个相关的标签
- todos:如果用户提到要做的事,提取为待办事项
- category:根据内容分类

如果用户没提到待办事项,todos 返回空数组。"""


def _strict_schema(model_cls) -> dict:
    """生成严格模式 Schema"""
    schema = model_cls.model_json_schema()

    def _process(obj):
        if isinstance(obj, dict):
            if obj.get("type") == "object":
                obj["additionalProperties"] = False
                if "properties" in obj:
                    obj["required"] = list(obj["properties"].keys())
            for v in obj.values():
                _process(v)
        elif isinstance(obj, list):
            for item in obj:
                _process(item)

    _process(schema)
    return schema


class NoteExtractor:
    """笔记抽取器"""

    def __init__(self, client: AsyncOpenAI, model: str):
        self.client = client
        self.model = model
        self.schema = _strict_schema(Note)

    async def extract(self, text: str) -> Note:
        """从文本抽取结构化笔记"""
        response = await self.client.chat.completions.create(
            model=self.model,
            messages=[
                {"role": "system", "content": SYSTEM_PROMPT},
                {"role": "user", "content": text},
            ],
            response_format={
                "type": "json_schema",
                "json_schema": {
                    "name": "Note",
                    "schema": self.schema,
                    "strict": True,
                },
            },
        )

        content = response.choices[0].message.content
        logger.info("抽取笔记完成", content_length=len(content))

        # Pydantic 二次校验
        return Note.model_validate_json(content)

4. 加 API 接口

api/routes.py:

python 复制代码
from ..services.note_extractor import NoteExtractor
from ..schemas.note import Note


@router.post("/notes/extract", response_model=Note)
async def extract_note(
    req: NoteRequest,
    client = Depends(get_llm_client),
):
    """从文本抽取结构化笔记"""
    extractor = NoteExtractor(client, model="deepseek-chat")
    try:
        note = await extractor.extract(req.text)
        return note
    except Exception as e:
        logger.exception("抽取失败")
        raise LLMError(f"抽取失败: {e}")

schemas/note.py 加请求模型:

python 复制代码
class NoteRequest(BaseModel):
    text: str = Field(min_length=1, max_length=5000, description="要抽取的文本")

5. 测试

bash 复制代码
curl -X POST http://localhost:8000/api/notes/extract \
  -H "Content-Type: application/json" \
  -H "X-API-Key: test-key" \
  -d '{
    "text": "下周一前要完成产品设计文档,还要跟张三对一下接口。顺便把会议室订了,最好是下午。"
  }'

返回:

json 复制代码
{
  "title": "产品设计文档 + 接口对齐 + 会议室",
  "summary": "需在下周一前完成产品设计文档、与张三对齐接口,并预订下午会议室",
  "tags": ["工作", "产品", "会议"],
  "todos": [
    {
      "content": "完成产品设计文档",
      "priority": "high",
      "due_date": "2026-10-19"
    },
    {
      "content": "跟张三对接口",
      "priority": "high",
      "due_date": null
    },
    {
      "content": "订会议室(最好是下午)",
      "priority": "medium",
      "due_date": null
    }
  ],
  "category": "work"
}

完美符合 Schema。

前端拿到这个结构,直接渲染就行------完全不用写"容错逻辑"。

6. 前端 React 渲染示例

jsx 复制代码
function NoteCard({ text }) {
  const [note, setNote] = useState(null);

  const extract = async () => {
    const res = await fetch("/api/notes/extract", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ text }),
    });
    setNote(await res.json());
  };

  if (!note) return <button onClick={extract}>抽取</button>;

  return (
    <div className="note-card">
      <h2>{note.title}</h2>
      <p>{note.summary}</p>
      <div>
        {note.tags.map(tag => <span key={tag} className="tag">{tag}</span>)}
      </div>
      <ul>
        {note.todos.map((todo, i) => (
          <li key={i} data-priority={todo.priority}>
            {todo.content}
            {todo.due_date && <small>截止 {todo.due_date}</small>}
          </li>
        ))}
      </ul>
    </div>
  );
}

为什么能这么写?

因为 API 契约稳定了。 字段名、类型都保证对。

对比没有结构化输出时:

jsx 复制代码
// 得写一堆容错
const todos = note.todos || [];
const tags = Array.isArray(note.tags) ? note.tags : [];
if (typeof note.priority === "string" && ["high", "medium", "low"].includes(note.priority)) {
  // ...
}

现在一行 note.todos.map(...) 就够了。

这就是结构化输出的价值------把"容错"从代码里赶出去。


七、结构化输出的高级技巧

1. 用 Field 加语义约束

Pydantic 的 Field 能定义:

python 复制代码
class Review(BaseModel):
    rating: int = Field(ge=1, le=5, description="评分 1-5")
    comment: str = Field(min_length=10, max_length=500, description="评论,至少 10 字")
    sentiment: Literal["positive", "neutral", "negative"]
    tags: list[str] = Field(default_factory=list, max_length=3)
    is_verified_purchase: bool = Field(description="是否已验证购买")

这些约束会进入 JSON Schema,严格模式下模型会遵守。

2. 处理"可选"字段

问题: 严格模式要求所有字段 required,但业务上有"可选"字段。

解法: 用默认值:

python 复制代码
class Booking(BaseModel):
    from_city: str
    to_city: str
    notes: str = Field(default="", description="备注,没有就空字符串")
    #                       ↑ 默认值让它 required 但可为空

模型看到默认值,就知道"没内容的话给空值"。

比 Optional[str] 更适合严格模式。

3. 多模型输出(异构)

有时候模型需要"输出 A 或 B",不是同一结构:

python 复制代码
from typing import Union
from pydantic import BaseModel, Field


class SuccessResult(BaseModel):
    status: Literal["success"]
    data: dict
    execution_time: float


class ErrorResult(BaseModel):
    status: Literal["error"]
    error_code: str
    error_message: str


class Result(BaseModel):
    """用 Union 表达"二选一" """
    result: SuccessResult | ErrorResult = Field(discriminator="status")

discriminator="status" 是关键 ------告诉 Pydantic / LLM 用 status 字段区分类型。

严格模式支持 Union,但有坑:

  • 所有可能结构都要列出来
  • 用 discriminator 明确区分字段
  • 不要用 Optional[X] 表达"二选一"

4. 敏感字段的脱敏

模型输出可能包含"敏感信息"------比如用户原文里的手机号。

Pydantic 有 Field(exclude=True):

python 复制代码
class User(BaseModel):
    name: str
    email: str
    raw_input: str = Field(exclude=True)   # ← 输出 JSON 时不带

但严格模式下不推荐 ------模型不理解 exclude,可能会填 null。

更好的做法: 分两步------先让模型输出,再 Pydantic 处理后过滤。

5. 复杂 JSON:用"中间结构"

太复杂的 Schema 会让模型很吃力。

比如: "电商订单"有商品列表、收货地址、支付信息、优惠券。

拆法:

python 复制代码
# 别一口气定义巨大模型
class GiantOrder(BaseModel):
    ...

# 拆成"两步调用"
class OrderBasic(BaseModel):
    """第一步:抽取基本信息"""
    user_id: str
    total: float

class OrderItems(BaseModel):
    """第二步:抽取商品列表"""
    items: list[dict]

# 或者用"工具调用"做多步(Day 34)

经验:单次 JSON Schema 不要超过 20 个字段、不要嵌套超过 3 层。


八、前端视角:结构化输出 vs 前端"组件 Props"

做过前端的你,可以把结构化输出理解成"LLM 版的 Props 校验"。

React 组件:

jsx 复制代码
function UserCard({ name, age, avatar }) {
  PropTypes.checkPropTypes({
    name: PropTypes.string.isRequired,
    age: PropTypes.number.isRequired,
    avatar: PropTypes.string,
  });
  ...
}

结构化输出:

python 复制代码
class UserCard(BaseModel):
    name: str
    age: int
    avatar: str | None = None

核心一样:给"数据"定义"契约",输入方必须遵守。

对照表:

前端 后端(Python)
PropTypes Pydantic
zod Pydantic
TS interface Pydantic BaseModel
required props Field()
枚举 props Literal
defaultProps Field 默认值
运行时校验 model_validate
生成文档 model_json_schema()

你在前端养成的"类型驱动开发"习惯,直接用到 LLM 上。

这就是"结构化输出"最迷人的地方------

它把"从前端到后端"的契约思维,扩展到了"从前端到 LLM"。


九、今日踩坑

  1. 严格模式要求 additionalProperties: false → Pydantic 默认不生成,要手动处理
  2. 严格模式要求所有字段 required → 可选字段要加默认值
  3. JSON Mode 要求 prompt 提"JSON" → OpenAI 的规定,容易忘
  4. model_json_schema() 生成的 $defs 不被所有厂商支持 → 有的要展开引用
  5. 嵌套太深模型会出错 → 控制在 3 层内
  6. 字段太多会失败 → 控制在 20 个以内
  7. 枚举值拼错模型也"顺从" → 严格模式才会真正拦住
  8. Field(exclude=True) 在严格模式下会填 null → 别指望模型理解
  9. Union 类型没加 discriminator → 模型分不清哪种
  10. response_format 各厂商 API 名字不同 → 通义叫 response_format,智谱可能不一样,查文档
  11. 忘了检查 finish_reason → 如果是 length,JSON 可能被截断
  12. strict: True 但模型不支持 → 报错或降级成普通模式
  13. Pydantic 校验失败没兜底 → 建议加 try/except 或二次重试
  14. 流式 + 结构化输出 → 部分厂商不支持,要提前测
  15. 生产环境一定要 strict=True → 别信"差不多就行"

十、今日总结

今天核心收获三件事:

  1. 结构化输出三层次 :Prompt 要求 → JSON Mode → JSON Schema,生产用第三层
  2. Pydantic 是核心:定义模型、生成 Schema、二次校验,一站式搞定
  3. strict: True 是保障:保证字段、类型、枚举都符合,比"祈祷模型听话"稳一万倍

一个感受:

LLM 从"文本生成器"变成"结构化数据生成器",这是一个质变。

有了它,你可以:

  • 从文本抽取结构化数据(简历解析、新闻结构化)
  • 生成 API 请求参数(自然语言转 API 调用)
  • 分类和标注(给内容打标签)
  • 生成配置("帮我生成一个 nginx 配置")

LLM 不再只是"聊天玩具",而是"数据处理工具"。

前端开发的本质是"和接口打交道"------接口要结构、要类型、要契约。

结构化输出让 LLM 输出的东西也能"契约化" ------这才是它真正融入工程体系的开始。

Day 35 完成度:✅

明日预告(Day 36)

  • Prompt 工程入门 :
    • 什么是好的 Prompt
    • System / User / Assistant 角色的用法
    • 指令的清晰度、具体性
    • 输出格式的约束
    • 常见的 Prompt 反模式
  • 用一套方法论写 Prompt,而不是"感觉对了就行"

最后

如果这篇对你有用,点个赞让我知道有人在看。

评论区互动: 你之前在什么场景下需要"结构化输出"?解析简历?抽取文章元数据?欢迎分享。

有问题评论区见,我们第 36 天继续。

100 天,第 36 天见。


附录:系列目录

第一阶段(已完成 ✅)

阶段 天数 内容
Python 基础 01-15 语法 + 命令行助手
后端工程化 16-30 数据库/测试/FastAPI/Docker/上线

第二阶段:大模型应用(进行中 🎯)

天数 内容
31-34 LLM 入门 + 客户端 + 流式 + Function Calling ✅
35 结构化输出(JSON Schema) ✅
36-40 Prompt 工程 + 多模型 + 成本优化
41-45 本地模型 + 阶段复盘

第三阶段:RAG(Day 46-60)

第四阶段:Agent(Day 61-80)

第五阶段:完整项目(Day 81-100)

35 天已过,65 天继续。

一起从切图仔变成 Agent 开发者。

相关推荐
在世修行1 小时前
智学文枢:数据管理服务
python·审计日志·pyside·数据清理·孤儿文件
日日大王学编程1 小时前
DeepSeekHarness开源Agent底座详解
python·开源·deep learning
Eider19981 小时前
python基础语法之并发基础练习
开发语言·python
长和信泰光伏储能1 小时前
探索绿色能源:光伏发电的渠道与应用
python·能源
郝学胜-神的一滴2 小时前
AI 编程智能体 20:技术栈选型与架构落地全攻略
人工智能·python·架构·langchain·1024程序员节·智能体
云和数据.ChenGuang2 小时前
python和java的try
java·开发语言·python
小小张说故事2 小时前
Python 的 list 为什么能装下任何类型?拆开 PyObject* 数组,以及它让你多付了多少内存
后端·python
chi11302 小时前
【无标题】
前端·javascript·python
编程见习者2 小时前
Deep Agents介绍
人工智能·python·langchain