在上一篇中我们讲述了工具的使用,这一篇我们说一下结构化输出。什么是结构化输出呢?LangChain的结构化输出(Structured Output) 指的是:要求模型最终返回一个符合预定义结构的数据对象,例如固定字段的JSON、Pydantic 模型、TypedDict,而不再是无格式的自然语言文本。
目前LangChain 1.x 支持多种Schema与结构化输出方式:
模型对象可以调用 with_structured_output() 绑定输出模式(schema)。
只有 Pydantic 返回的是Schema类实例,其余三种方式返回的都是 字典 ;也只有 Pydantic 在类型不匹配时会抛出异常。
它通过在运行时强制执行类型提示,确保数据的正确性和一致性,是 生产场景首选 。
需要满足的几个要素:
str 、int、float、List[xxx]、Optional[xxx]等示例:
pythonimport os
from dotenv import load_dotenv
from langchain.chat_models import init_chat_model
from pydantic import BaseModel, Field
load_dotenv()
model = init_chat_model(
model="qwen3.7-plus",
model_provider="openai", # 关键:指定使用openai兼容协议
api_key=os.getenv("DASHSCOPE_API_KEY"),
base_url=os.getenv("DASHSCOPE_API_BASE_URL")
)
class Person(BaseModel):
"""人物信息"""
name: str = Field(description="姓名")
age: int = Field(description="年龄")
occupation: str = Field(description="职业")
# class MovieModel(BaseModel):
# """
# 电影的详细信息
# """
# title: str = Field(description="电影标题")
# year: int = Field(description="电影上映年份")
# director: str = Field(description="导演")
# rating: float = Field(description="电影评分,满分十分")
# 创建结构化输出的 LLM
structured_llm = model.with_structured_output(Person)
# 调用
result = structured_llm.invoke("张三是一名 30 岁的软件工程师")
print(result)
print(type(result))
# result 是 Person 实例
print(result.name) # "张三"
print(result.age) # 30
print(result.occupation) # "软件工程师"
问题:LLM 未填充某些字段怎么办? 使用 Optional 指定字段为可选的。
pythonfrom pydantic import BaseModel, Field
from typing import Optional
class Person(BaseModel):
"""人物信息"""
name: str = Field(description="姓名")
age: int = Field(description="年龄")
occupation: str = Field(description="职业")
structured_llm = model_with_closeai.with_structured_output(Person)
structured_llm.invoke("张三是一名医生")
# Person(name='张三', age=0, occupation='医生')
上面案例可见,如果模型分析不出age,输出的信息中age是0,如果我们的需求是如果缺少该字段,该字段的值是None。这时候可以将age设置为可选的。
pythonfrom typing import Optional
from pydantic import BaseModel, Field
class Person(BaseModel):
"""人物信息"""
name: str = Field(description="姓名")
age: Optional[int] = Field(description="年龄")
occupation: str = Field(description="职业")
structured_llm = model_with_closeai.with_structured_output(Person)
structured_llm.invoke("张三是一名医生")
# Person(name='张三', age=None, occupation='医生')
LLM 未提供的信息会使用默认值。格式如下:
Field(default="默认值", description="描述")
注
注意:不同模型提供商对default字段的支持是不同的。
pythonclass Person(BaseModel):
"""人物信息"""
name: str = Field(description="姓名")
age: int = Field(1,description="年龄") # age 默认值为 1
occupation: str = Field(description="职业")
问题:如何限制字段的可选值?
回答:使用枚举。
pythonfrom enum import Enum
class Priority(str, Enum):
LOW = "低"
MEDIUM = "中"
HIGH = "高"
class Task(BaseModel):
title: str
priority: Priority # 只能是 LOW/MEDIUM/HIGH
如果嫌单独定义一个 Enum 类太麻烦,也可以直接导入 typing 中的 Literal ,直接在字段里把允许的值写死。
pythonfrom typing import Optional, Literal
from pydantic import BaseModel, Field
class CustomerInfo(BaseModel):
"""客户信息"""
name: str = Field(description="客户姓名")
phone: str = Field(description="电话号码")
email: Optional[str] = Field("未提供", description="邮箱")
issue: str = Field(description="问题描述")
# 使用 Literal 直接限定字面量值
urgency: Literal["低", "中", "高"] = Field(description="紧急程度")
# 测试
structured_llm = model.with_structured_output(CustomerInfo)
conversation = """
客服: 您好,请问有什么可以帮助您?
客户: 我是王小明,电话 138-1234-5678,我的订单一直没发货,很着急!
客服: 好的,我帮您查一下
"""
result = structured_llm.invoke(f"从以下客服对话中提取客户信息:\n{conversation}")
print(result)
# name='王小明' phone='138-1234-5678' email=None issue='订单未发货' urgency='高'
pythonfrom typing import List
class Person(BaseModel):
"""人物信息"""
name: str
age: int
class PersonList(BaseModel):
"""人物列表信息"""
people: List[Person] # 多个 Person 对象
structured_llm = model.with_structured_output(PersonList)
result = structured_llm.invoke("张三 30岁,李四 25岁")
print(result)
# people=[Person(name='张三', age=30), Person(name='李四', age=25)]
案例2:
pythonclass Review(BaseModel):
"""产品评论"""
product: str
rating: int = Field(description="评分 1-5")
pros: List[str] = Field(description="优点列表")
cons: List[str] = Field(description="缺点列表")
structured_llm = model.with_structured_output(Review)
review = structured_llm.invoke("""
iPhone 17 很棒!摄像头强大,手感好。但是价格贵,没有充电器。4分。
""")
print(review)
# product='iPhone 17' rating=4 pros=['摄像头强大', '手感好'] cons=['价格贵', '没有充电器']
应用场景:
pythonfrom pydantic import BaseModel
class Address(BaseModel):
"""地点描述"""
city: str
district: str
class Company(BaseModel):
"""公司信息"""
name: str
address: Address # 嵌套模型
structured_llm = model.with_structured_output(Company)
说明:LLM 能力有限,复杂嵌套结构可能会出错。所以建议:
pythonfrom pydantic import ValidationError
class User(BaseModel):
name: str = Field(min_length=2, max_length=20)
age: int = Field(ge=0, le=150)
email: str
print("\n有效数据:")
try:
user = User(name="张三", age=30, email="zhang@example.com")
print(f"[OK] {user.name}, {user.age}, {user.email}")
except ValidationError as e:
print(f"[FAIL] {e}")
print("\n无效数据(年龄超出范围):")
try:
user = User(name="李四", age=200, email="li@example.com")
print(f"[OK] {user}")
except ValidationError as e:
print(f"[FAIL] 验证失败(符合预期): {e.errors()[0]['msg']}")
#有效数据:
# [OK] 张三, 30, zhang@example.com
# 无效数据(年龄超出范围):
# [FAIL] 验证失败(符合预期): Input should be less than or equal to 150
TypedDict 是 Python 3.8+ 引入的一种类型提示工具,即带有类型声明的字典结构。适合需要快速定义字典结构且无需 Pydantic 重量级功能的场景。
TypedDict 可以进一步说明:
pythonfrom typing_extensions import TypedDict
class MovieDict(TypedDict):
title: str
year: int
director: str
rating: float
movie: MovieDict = {
"title1": "盗梦空间",
"year": 2010,
"director": "克里斯托弗·诺兰",
"rating": 8.8,
}
print(movie)
Annotated 用来在“类型”之外,再附加一些额外信息,即元数据。类似于 Pydantic 的 Field
基本形式:
Annotated[类型, 附加信息1, 附加信息2, ...]
示例:
pythonclass MovieTypedDict(TypedDict):
"""
电影的详细信息
"""
title: Annotated[str, "电影的正式名称,例如《盗梦空间》"]
year: Annotated[int, "电影的公映年份,使用四位数字表示"]
director: Annotated[str, "电影导演的全名"]
rating: Annotated[float, "电影在10分制下的评分,可包含一位小数"]
# 设置模型结构化输出
structured_llm = model.with_structured_output(MovieTypedDict)
# 调用模型并获取结构化输出
response = structured_llm.invoke("给我介绍下电影《星际穿越》")
print(type(response))
print(response)
# <class 'dict'>
# {'title': '星际穿越', 'year': 2014, 'director': '克里斯托弗·诺兰', 'rating': 9.2}
TypedDict也支持类型嵌套
pythonfrom typing import TypedDict, List, Annotated
# 使用TypedDict定义嵌套结构
class Actor(TypedDict):
"""演员情况"""
name: Annotated[str, "演员姓名"]
role: Annotated[str, "饰演的角色"]
class Movie(TypedDict):
"""电影情况"""
title: Annotated[str, "电影标题"]
year: Annotated[int, "上映年份"]
director: Annotated[str, "导演"]
cast: Annotated[List[Actor], "演员列表"] # 嵌套列表定义
rating: Annotated[float, "评分"]
...的使用不同的模型提供商效果不一样
pythonfrom typing_extensions import TypedDict, Annotated
class MovieDict(TypedDict):
"""
电影的详细信息
"""
title: Annotated[str, ..., "电影标题"]
year: Annotated[int, ..., "电影上映年份"]
director: Annotated[str, ..., "导演"]
rating: Annotated[float, ..., "电影评分,满分十分"]
model_with_structure = model.with_structured_output(MovieDict)
response = model_with_structure.invoke("根据这段话抽取盗梦空间的信息,不包含的信息可以留空:盗梦空间在2010年上映,导演是克里斯托弗·诺兰。")
print(response)
print(type(response))
# {'title': '盗梦空间', 'year': 2010, 'director': '克里斯托弗·诺兰', 'rating': 0}
# <class 'dict'>
说明:上述代码的 ... 是Python的字面量,等价于 Ellipsis ,可以理解为占位符。下游框架(如LangChain)可以对 ... 作定制化处理,如LangChain中Annotated的 ... 表示当前字段是必须存在的,不可省略,用来指示模型的输出。
这种方式需要按照JSON Schema规范拼接JSON字符串,比较繁琐,并且缺少校验机制。不推荐。
pythonjson_schema = {
"title": "Movie",
"description": "A movie with details",
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "The title of the movie"
},
"year": {
"type": "integer",
"description": "The year the movie was released"
},
"director": {
"type": "string",
"description": "The director of the movie"
},
"rating": {
"type": "number",
"description": "The movie's rating out of 10"
}
},
"required": ["title", "year", "director", "rating"]
}
structured_model = model.with_structured_output(
json_schema,
method="json_schema"
)
response = structured_model.invoke("给出盗梦空间的信息")
print(response)
# {'title': '盗梦空间', 'year': 2010, 'director': '克里斯托弗·诺兰', 'rating': 9.3}
# <class 'dict'>
说明:
1、method:结构化输出的方式,但是否可用,依赖于模型供应商及Langchain适配器的具体实现。比 如,DeepSeek模型服务不支持json_shema模式。
2、以上代码中定义json_schema的时候指定的title、description、type、properties、required是遵循JSON Schema 规范的标准关键字,是固定写法。几个关键字的解释如下:
@dataclass模式@dataclass是 Python 标准库 dataclasses 提供的类装饰器,用于简化“以字段为核心”的数据类定义。 给类加上 @dataclass 后,Python 会根据字段声明自动生成常用方法,例如:
__init____repr____eq__pythonfrom pydantic import Field
from dataclasses import dataclass
@dataclass
class Movie():
"""
电影的详细信息
"""
title: str = Field(description="电影标题")
year: int = Field(description="电影上映年份")
director: str = Field(description="导演")
rating: float = Field(description="电影评分,满分十分")
structured_model = model.with_structured_output(Movie)
response = structured_model.invoke("给出盗梦空间的信息")
print(response)
print(type(response))
这里以pydantic模式为例
pythonfrom pydantic import BaseModel, Field
class MovieModel(BaseModel):
"""
电影的详细信息
"""
title: str = Field(description="电影标题")
year: int = Field(description="电影上映年份")
director: str = Field(description="导演")
rating: float = Field(description="电影评分,满分十分")
model_with_structure = model.with_structured_output(MovieModel)
response = model_with_structure.invoke("给出盗梦空间的信息")
print(response)
print(type(response))
模拟大模型的响应信息和pydantic定义的数据格式不一致

只有pydantic模式,如果格式不一致会报错

以上定义输出结构的四种模式中,我们都是通过调用“with_structured_output”来获取结构化输出结果,除了这种方式外,还可以通过使用输出解释器来获取结构化输出结果。下面介绍这两种获取结构化结果的方式。
这种方式是 最新 、 最简洁 的API,直接让模型“理解”你需要的数据结构,并返回解析好的对象。 此外,我们可以在with_structured_output方法中传入 include_raw=True 参数,表示返回解析前的 原始AIMessage ,从而访问令牌用量等元数据。
pythonclass Movie(BaseModel):
"""电影信息"""
title: str = Field(description="电影标题")
year: int = Field(description="上映年份")
director: str = Field(description="导演")
rating: float = Field(description="评分(10分制)")
# 设置模型结构化输出
model_with_structure = model.with_structured_output(Movie, include_raw=True)
# 调用模型并获取结构化输出
resp = model_with_structure.invoke("给我介绍下电影《星际穿越》")
输出包含了完整的输出响应,包含三个字段
这种方法更传统,依赖于在提示词中明确指示模型输出特定格式的文本,然后使用解析器进行转换。
其流程是: 提示词指导 (引导生成指定类型) → 模型生成文本 → 解析器转换 。
python
from langchain_core.output_parsers import JsonOutputParser
from langchain_core.prompts import ChatPromptTemplate
# 1. 创建提示词模板
prompt_template = ChatPromptTemplate.from_messages([
("system", "回答用户问题,必须始终输出一个包含title(电影标题)和year(上映年份)的JSON 对象"),
("human", "问题:{question}")
])
# 3. 定义结构
class Movie(BaseModel):
"""电影信息"""
title: str = Field(description="电影标题")
year: int = Field(description="上映年份")
# 4. 创建输出解析器
parser = JsonOutputParser(pydantic_object=Movie)
# 5. 创建链
chain = prompt_template | model | parser
# 6. 调用(返回字典)
response = chain.invoke({"question": "介绍电影《盗梦空间》"})
print(response)


本文作者:繁星
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!