with_structured_output)大模型天生只能输出自由的自然语言(人话),而下游业务代码只接受严格的数据格式(机器话)。with_structured_output 的核心使命就是作为连接两者的“模具”与“桥梁”。

如果不使用结构化输出的痛点:
json.loads() 直接崩溃。KeyError 报错。在 LangChain 中,要让大模型按规矩办事,核心方法就是调用模型实例上的 with_structured_output(schema) 方法。你需要预先定义一个“结构说明书(Schema)”并传给它。
定义这份“说明书”,目前主流有以下 4 种方式:
min_length、gt=0 等限制条件),生态集成最好,报错信息精准。在实际业务中,我们几乎总是将 with_structured_output 与 Pydantic 结合使用。下面按照从基础到进阶的顺序,演示所有的核心用法:
这是最常见的场景:从一段自然语言文本中提取固定的几个字段。核心在于利用 Field(description="...") 给大模型下达明确的指令。
复制代码from pydantic import BaseModel, Field# 1. 定义数据结构
class Person(BaseModel):
# Field(description="...") 是专门写给大模型看的“提示词”
# 大模型会严格根据这里的描述,去自然语言中寻找并提取对应的答案
name: str = Field(description="姓名")
age: int = Field(description="年龄")
occupation: str = Field(description="职业")# 2. 绑定结构化输出
structured_model = model.with_structured_output(Person)# 3. 调用模型
result = structured_model.invoke("张三是一名30岁的软件工程师")# 4. 直接像使用普通 Python 对象一样调用属性
print(type(result)) # <class '__main__.Person'>
print(f"姓名:{result.name}, 职业:{result.occupation}")
Optional)如果一段文本中不包含某个字段的信息,直接提取可能导致大模型“瞎编乱造”。通过引入 Optional,可以明确告诉大模型:“这个字段是选填的,如果没有请填 None”。
复制代码from typing import Optionalclass Person(BaseModel):
name: str = Field(description="姓名")
# 如果用户的文本里没提年龄,大模型会乖乖返回 None,而不会自己瞎猜
age: Optional[int] = Field(description="年龄")
Enum / Literal)在做文本分类任务时,我们希望大模型的输出结果被严格限定在几个特定的词以内。Pydantic 支持两种实现方式:轻量级的 Literal 和正规的 Enum 类。
方式一:使用 Literal (轻量级,最常用)
直接把可选项写在方括号里,代码极其简洁。
复制代码from typing import Literalclass CustomerFeedback(BaseModel):
issue: str = Field(description="问题描述")
# 强制大模型做单选题,只能从“低”、“中”、“高”这三个词里面选一个输出
urgency: Literal["低", "中", "高"] = Field(description="紧急程度")
方式二:使用 Enum 类 (企业级,更规范)
需要通过 Python 内置的 enum 模块单独定义一个枚举类。
复制代码from enum import Enum
from pydantic import BaseModel, Field# 独立定义枚举类(注意:强烈建议继承 str,写成 str, Enum,这样才能与大模型生成的文本无缝对齐)
class UrgencyEnum(str, Enum):
LOW = "低"
MEDIUM = "中"
HIGH = "高"class CustomerFeedback(BaseModel):
issue: str = Field(description="问题描述")
# 将枚举类作为类型传入
urgency: UrgencyEnum = Field(description="紧急程度")
当业务需求比较复杂,需要提取“多个同类事物(列表)”或“有层级包含关系”的数据时,可以直接嵌套不同的 Pydantic 类。
复制代码from typing import List# 先定义子结构
class Actor(BaseModel):
name: str = Field(description="演员姓名")
role: str = Field(description="饰演的角色")# 再定义主结构
class Movie(BaseModel):
title: str = Field(description="电影标题")
director: str = Field(description="导演")
# 嵌套结构:提取多个演员作为一个列表
cast: List[Actor] = Field(description="演员列表") # 模型执行后,result.cast 就会是一个标准的 Python List,里面全是 Actor 对象
利用 Pydantic 强大的数值和字符串限制规则,可以防止大模型产生“幻觉”输出不合理的数据(比如负数的价格)。
复制代码class Product(BaseModel):
# 限制最小长度为2
name: str = Field(description="产品名称", min_length=2)
# gt (greater than) 限制价格必须严格大于 0
price: float = Field(description="价格", gt=0)
# ge (greater than or equal) 限制库存必须大于等于 0
stock: int = Field(description="库存", ge=0)
在查阅许多旧教程或官方文档时,你可能会频繁看到 TypedDict。但强烈建议在实战中尽量避开它。在了解它为什么坑之前,我们先看看它的基本用法。
它的核心逻辑依然是定义数据格式,然后传给 with_structured_output。但由于它本身没法写描述,所以必须借助 Annotated 来补充提示词。
复制代码from typing_extensions import TypedDict, Annotated# 1. 定义数据结构:使用 Annotated[类型, "描述"] 强行绑定提示词
class MovieTypedDict(TypedDict):
title: Annotated[str, "电影的名称"]
year: Annotated[int, "上映年份"]# 2. 绑定模型并调用
structured_model = model.with_structured_output(MovieTypedDict)
result = structured_model.invoke("介绍一下星际穿越,是2014年上映的")# 3. ️ 注意返回值类型:它返回的不是对象,而是一个纯字典!
print(type(result)) # <class 'dict'>
print(result["title"]) # 只能用字典的 [] 语法取值,绝对不能用 result.title
为什么说实战中要尽量避开它?因为它充满了误导性,且缺乏运行时的安全保障机制。
虽然语法上你写了 class MovieTypedDict(TypedDict):,但这完全是 Python 强加类型提示时的历史包袱。在代码运行阶段,实例化出来的东西根本不是类的对象,依然是一个最普通、最光秃秃的 Python 字典 (dict)。这导致你没法像 Pydantic 那样优雅地使用 movie.title 取值,严重割裂了代码体验。
AnnotatedTypedDict 天生只能写类型,没有地方写给大模型看的提示词。为了解决这个残疾问题,被迫引入了 Annotated 这个补丁。
Annotated(读音:/ˈænəteɪtɪd/,意为“带有注解的”)的固定格式必须是:Annotated[真正的类型, "给大模型看的描述"]。这种写法极其啰嗦,完全不如 Pydantic 的 Field(description="...") 来得优雅直观。
... 的表现差异TypedDict 在收到 JSON 数据后没有任何强制校验能力。如果你在定义时使用了 ...(省略号,代表该字段是必填项,没有默认值):
复制代码rating: Annotated[float, ..., "电影评分"]
当遇到原文中找不到该信息时,不同大模型的反应会完全不受控:
KeyError 崩溃。0)塞进去。终极结论:TypedDict 语法啰嗦、概念误导、缺乏安全拦截,甚至会逼迫大模型产生幻觉。请将它封印在理论知识库中,日常工程实战一律无脑拥抱 Pydantic。
除了使用 Python 代码(Pydantic、TypedDict)来间接定义格式外,LangChain 也允许你直接手写原生的 JSON Schema 字典来规范大模型的输出。
正如前面多次提到的,大模型的 API 实际上只认识 JSON Schema,这是底层唯一的通信协议。无论是 Pydantic 还是 TypedDict,在交给 LangChain 底层时,最终都会被翻译成这种格式。
你需要手写一个完全符合 JSON Schema 规范的 Python 字典,并在绑定模型时显式指定 method="json_schema"。
复制代码# 1. 纯手写一个 JSON Schema 字典(包含极其啰嗦的嵌套结构)
project_schema = {
"title": "MovieInfo",
"description": "包含电影标题、导演和演员列表",
"type": "object",
"properties": {
"title": {"type": "string", "description": "电影标题"},
"director": {"type": "string", "description": "导演"},
"cast": { # 嵌套结构:定义一个演员数组
"type": "array",
"description": "演员列表",
"items": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "演员姓名"},
"role": {"type": "string", "description": "演员角色"}
},
"required": ["name", "role"]
}
}
},
"required": ["title", "director", "cast"] # 指定最外层的必填项
}# 2. 绑定到模型,注意必须显式传入 method="json_schema"
structured_model = model.with_structured_output(project_schema, method="json_schema")# 3. 调用模型
response = structured_model.invoke("介绍一下星际穿越,包含导演和主要演员")# 4. ️ 返回值类型:与 TypedDict 一样,返回的是纯字典,依然缺乏运行时对象的强校验
print(type(response)) # <class 'dict'>
print(response["title"])
properties 拼成 property)。虽然 LangChain 的 with_structured_output 也支持 Python 标准库的 @dataclass,但它在实战中的表现非常“鸡肋”。我们可以通过一个简单的例子来看清它的真面目:
复制代码from dataclasses import dataclass
from pydantic import Field # 痛点1:依然要白嫖 Pydantic@dataclass
class Movie():
# 虽然是 dataclass,但为了写提示词,还得用 Pydantic 的 Field
title: str = Field(description="电影标题")
year: int = Field(description="电影上映年份")structured_model = model.with_structured_output(Movie)
response = structured_model.invoke("给出盗梦空间的信息")# 痛点2与3:返回的并非 Movie 对象,而是字典,且缺乏强校验
print(type(response)) # 输出:<class 'dict'>
print(response["title"])
从上面的代码中,我们可以总结出 @dataclass 在这里的三个致命问题:
@dataclass 没法写给大模型看的描述词,必须去借用 Pydantic 的 Field 来打补丁。既然语法写出来已经和 Pydantic 一模一样了,为什么不直接用真正的 BaseModel 呢?@dataclass 也不会像真正的 Pydantic 那样立刻报错拦截。dict),你还是得老老实实拿 ["title"] 去取值,享受不到面向对象编程的便利。终极结论:在这个场景下,@dataclass 完全是一个**“阉割版的 Pydantic”。由于它形同虚设的校验和缝合怪般的语法,实战中请直接无视它,所有场景一律无脑使用 Pydantic** 即可。
include_raw=True)在绝大多数情况下,with_structured_output 默认只会返回最终解析好的对象(比如一个 Person 或 Movie 实例)。
但是,如果在开发调试阶段,你不仅想拿到结构化数据,还想偷窥大模型的原始返回文本,或者想查看本次调用的 Token 消耗花销,只需要在绑定时传入 include_raw=True 参数即可。
复制代码from pydantic import BaseModel, Field
from rich import print as rprintclass Movie(BaseModel):
"""电影信息"""
title: str = Field(description="电影标题")
year: int = Field(description="上映年份")
director: str = Field(description="导演")
rating: float = Field(description="评分(10分制)")# ️ 关键点:传入 include_raw=True 参数
structured_model = model.with_structured_output(Movie, include_raw=True)
response = structured_model.invoke("帮我介绍一下星际穿越这个电影")print(type(response)) # 此时返回值不再是 Movie 对象,而是 <class 'dict'>
rprint(response) # 打印这个包含了大量细节的“超级大礼包字典”
当你加了 include_raw=True 后,返回值会变成一个包含三大核心字段的字典:
复制代码{
# 1. raw: 大模型最原始的返回对象 (AIMessage),里面包含了原始字符串、完整的 Token 消耗统计等元数据
'raw': AIMessage(
content='{"title":"星际穿越","year":2014,"director":"克里斯托弗·诺兰","rating":8.6}',
additional_kwargs={'refusal': None, ...},
response_metadata={
'token_usage': {
'completion_tokens': 37,
'prompt_tokens': 170,
'total_tokens': 207 # 在这里可以清楚地看到 Token 花销
},
'model_name': 'gpt-5.4-mini-2026-03-17',
# ... 其他底层通信元数据 ...
},
id='lc_run--019e8795-992d-7920-a06c-ff6738ca1ff0-0',
usage_metadata={'input_tokens': 170, 'output_tokens': 37, 'total_tokens': 207}
),
# 2. parsed: 成功解析出来的 Pydantic 对象,业务代码中实际需要使用的数据
'parsed': Movie(title='星际穿越', year=2014, director='克里斯托弗·诺兰', rating=8.6),
# 3. parsing_error: 如果解析过程中大模型返回了无法被 Pydantic 识别的错误格式,报错信息会放在这里
'parsing_error': None
}