> ## Documentation Index
> Fetch the complete documentation index at: https://www.yuan111.asia/doc/llms.txt
> Use this file to discover all available pages before exploring further.

# 05 · 结构化输出：让模型交出"保证格式"的数据

# 05 · 结构化输出：让模型交出"保证格式"的数据

> **本片目标**：让模型不只输出文本，而是输出**符合你定义的 schema 的对象**——客服工单、抽取字段、分类结果。核心是 `with_structured_output` + Pydantic 蓝图。
> **新增规定性：5**（输出契约：定义 `BaseModel` 蓝图，模型输出必须匹配）
> **数据字典**：`with_structured_output` 参数、Pydantic 蓝图、`method` 三种模式。
> **进程线程模型**：同 invoke（同步阻塞）。
> **网络模型**：**tool\_calling 模式** = 模型走"工具调用"通道返回结构化 JSON（不是靠提示词"请输出 JSON"）。

***

## 1. 上集回顾

第 04 篇让模型输出文本和流式。但业务真正要的是**结构化数据**：

* 客服：从用户吐槽里抽出 `intent`、`order_id`、`urgency`；
* RAG：回答完还要返回**引用来源列表**；
* 报表：输出字段名必须对，方便程序直接读。

**靠提示词"请输出 JSON"是脆弱的**：模型可能输出多余说明文字、字段名拼错、JSON 语法错。你需要一个**契约**——模型必须输出符合你定义结构的对象，格式错了框架就重试或报错。

这就是 `with_structured_output`。

***

## 2. 数据字典：Pydantic 蓝图（规定性 5 的核心）

### 2.1 蓝图定义

```python theme={null}
from pydantic import BaseModel, Field

class SupportTicket(BaseModel):
    intent: str                              # 必填字段
    order_id: str | None = Field(default=None, description="订单号，没有则为 None")
    urgency: int = Field(description="紧急程度 1-5")
```

| 元素                          | 类型                                | 说明                       |                                  |
| --------------------------- | --------------------------------- | ------------------------ | -------------------------------- |
| `intent: str`               | 类型注解                              | 必填字符串                    |                                  |
| \`order\_id: str            | None = Field(default=None, ...)\` | 可选字段                     | 用 `Field(description=...)` 给模型提示 |
| `urgency: int = Field(...)` | 带描述必填                             | description 会进提示词，指导模型填值 |                                  |

**关键**：Pydantic 蓝图既是**提示词的一部分**（描述指导模型），又是**输出校验器**（解析结果必须符合）。

### 2.2 `with_structured_output` 签名

```python theme={null}
structured_model = model.with_structured_output(
    schema,              # Pydantic 类（或 TypedDict / JsonSchema dict）
    method="function_calling",   # 见下表
)
result = structured_model.invoke("我的订单 20260701001 一直没发货，很生气！")
# result: SupportTicket(intent='投诉', order_id='20260701001', urgency=5)
```

| `method`                 | 底层机制                            | 适用          |
| ------------------------ | ------------------------------- | ----------- |
| `"function_calling"`（默认） | 把 schema 变成工具定义，模型"调用工具"返回 JSON | 最稳，几乎所有模型支持 |
| `"json_mode"`            | 强制模型输出合法 JSON                   | 不支持工具调用的模型  |
| `"json_schema"`          | 用 JSON Schema 约束（各家支持不一）        | 部分新模型       |

**返回值**：`invoke` 后**直接返回 Pydantic 实例**（不是 AIMessage！）。`type(result)` 就是你的蓝图类。

### 2.3 它内部是什么？（衔接 04 篇的 parser 家族）

`with_structured_output` 不是"魔法 API"——它内部就是**一个模型 + 一个解析器**组成的管道（实测源码确认）：

```python theme={null}
# with_structured_output 内部等价于：
llm = model.bind_tools([schema], tool_choice="any")      # ① 模型：按工具 schema 生成 JSON
output_parser = PydanticToolsParser(tools=[schema])       # ② 解析器：tool_calls → Pydantic 实例
return llm | output_parser                                # ③ 串成管道 → RunnableSequence
```

**关键认知：这就是你在 04 篇学的 parser，只是"剥的壳"不同**：

|      | 04 篇 `StrOutputParser`    | 本篇 `PydanticToolsParser`   |
| ---- | ------------------------- | -------------------------- |
| 属于   | `output_parsers` 模块       | `output_parsers` 模块（同一家族）  |
| 剥什么壳 | `AIMessage.content` → str | `tool_calls` → Pydantic 实例 |
| 共同祖先 | `BaseOutputParser`        | `BaseOutputParser`         |

> 04 篇说过"`StrOutputParser` 实现了通用接口 `BaseOutputParser`，以后自定义解析器只需实现 `parse` 方法"——**`PydanticToolsParser` 就是这个家族里"最重"的成员**。它把模型返回的 `tool_calls`（JSON）解析成你的蓝图对象。
>
> 所以 `with_structured_output` 返回 `RunnableSequence` 而不是普通模型，就是因为它是 `bind_tools 的模型 | PydanticToolsParser` 两步管道。完整 parser 家族全景（`StrOutputParser` / `JsonOutputParser` / `PydanticToolsParser` 等）见第 13 篇全地图的 `output_parsers` 模块。

***

## 3. 关键方法

| 方法                               | 签名                                                                                | 说明            |
| -------------------------------- | --------------------------------------------------------------------------------- | ------------- |
| `with_structured_output`         | `with_structured_output(schema, method="function_calling", **kwargs) -> Runnable` | 包一层，返回结构化版模型  |
| `structured_model.invoke(input)` | `-> Pydantic 实例`                                                                  | 直接得对象         |
| `structured_model.stream(input)` | `-> Iterator[AIMessageChunk]`                                                     | 流式（部分模型支持）    |
| `result.model_dump()`            | `-> dict`                                                                         | 对象 → dict     |
| `result.model_dump_json()`       | `-> str`                                                                          | 对象 → JSON 字符串 |

### 3.1 嵌套蓝图：字段里带 `list`

蓝图可以嵌套——字段类型是 `list[另一个蓝图]`，框架递归解析。典型场景：一次返回"一批工单"：

```python theme={null}
class TicketList(BaseModel):
    tickets: list[SupportTicket] = Field(description="一批工单")

batch = model.with_structured_output(TicketList).invoke(
    "订单 20260701001 没发货（投诉，紧急）；订单 20260701002 想退货（退货，一般）。")
batch.tickets
# → [SupportTicket(intent='投诉', order_id='20260701001', urgency=5),
#    SupportTicket(intent='退货', order_id='20260701002', urgency=3)]
```

**嵌套蓝图的用处**：一次模型调用拿到"一批结构化结果"。第 09 篇 RAG 组装、第 15 篇完整后端项目会用这个能力返回"答案 + 引用来源列表"（`list[str]` 类型的 `sources` 字段）——**那个蓝图结构在 05 篇你已经会写了**，只是还没接上检索。

***

## 4. 进程线程模型

```
structured_model.invoke(...)
  ├─ 内部: 模型收到(原消息 + 工具定义[你的schema])
  ├─ 模型返回 tool_calls=[{name:'SupportTicket', args:{...}}]   ← 网络往返 1 次
  └─ 框架把 tool_calls[0].args 解析成 SupportTicket 实例       ← 纯内存
```

**网络往返仍只有 1 次**——结构化输出不是"先生成再校验重试"（除非解析失败）。它是一次调用，模型直接通过工具调用通道吐出 JSON。

***

## 5. 网络模型

```
invoke → POST .../v1/messages
  Body 里带 tools=[{...schema 转成的 JSON Schema 工具定义...}]
  模型响应 tool_calls=[{"name": "SupportTicket", "args": {"intent": "投诉", "order_id": "20260701001", "urgency": 5}}]
```

**关键洞察**：`with_structured_output` 底层用的是**工具调用协议**——模型被"要求调用一个返回 JSON 的工具"。这是各家模型厂商都稳定支持的能力，比"提示词里写请输出 JSON"可靠得多。

***

## 6. 验证：跑起来

配套代码 `code/05_structured.py`：

1. 定义 `SupportTicket` 蓝图；
2. `with_structured_output` 包模型；
3. 传一段吐槽，打印返回对象的类型、字段值、`model_dump()`；
4. 对比普通 `invoke` 的字符串输出；
5. **嵌套蓝图**：一次返回一批工单。

```powershell theme={null}
cd enterprise-rag-course\code
python 05_structured.py
```

**预期输出（节选，实测）**：

```
===== 普通 invoke（自由文本） =====
你的订单 20260701001 已查到，正在加急处理...

===== with_structured_output（契约输出） =====
返回类型: <class '__main__.SupportTicket'>
intent  : 投诉
order_id: 20260701001
urgency : 4
model_dump(): {'intent': '投诉', 'order_id': '20260701001', 'urgency': 4}

===== 嵌套蓝图 =====
intent=投诉, order_id=20260701001, urgency=5
intent=退货, order_id=20260701002, urgency=3
```

***

## 7. 边界

* **字段越多越容易错**——蓝图字段太多、描述不清时模型可能填错。保持字段少而清晰，`description` 写清楚。
* **method 选择**：默认 `function_calling` 优先；你的模型不支持工具调用再降级 `json_mode`。
* **嵌套结构**：蓝图可以嵌套（`BaseModel` 里有 `list[AnotherModel]`），框架递归解析。
* **枚举/校验**：Pydantic 的 `Field(ge=1, le=5)` 等约束会在解析时校验，超出会报错。

***

## 推荐资料（延伸阅读）

* [LangChain 官方文档 · 结构化输出](https://docs.langchain.com/oss/python/langchain/structured-output) —— with\_structured\_output 权威指南
* [Pydantic 官方文档 · 模型](https://docs.pydantic.dev/latest/concepts/models/) —— Pydantic 蓝图的定义规范

***

## 8. 未完待续

现在"输入"（模板）和"输出"（解析/结构化）都齐了。但有一个致命问题第 01 篇就埋下了：**模型没有记忆**。

```
你：我叫小林。
模型：你好小林！
你：我叫什么名字？   ← 模型：？？？刚才你说过吗？
```

每次调用模型都是"金鱼脑"——不记得上次说了什么。怎么让对话连贯？这就是第 06 篇：记忆与会话。

→ [06 · 记忆与会话](/doc/doc/enterprise-rag-course/06-记忆与会话)
