目录
1.从 invoke 到 with_structured_output
with_structured_output
2.结构化输出方式
2.1. 返回 Pydantic 对象(推荐)
Field 的本质:它是一个“元数据工厂函数”
2.2. 返回 TypedDict
Annotated
2.3. 返回 JSON Schema
2.4. 选择输出
3.示例
3.1. 信息提取器
3.2. 与工具(Tools)结合
我们常常面临一个尴尬的场景:模型返回了一段流畅自然的文本,但我们需要从中提取出精确的字段,存入数据库或用于后续逻辑。于是,正则表达式、字符串切割、异常处理……一套“手工解析”组合拳下来,确实难绷
LangChain 的 with_structured_output 方法应运而生,它让聊天模型直接返回结构化的数据对象(如 JSON、Pydantic 模型或 TypedDict)
1.从 invoke 到 with_structured_output
from langchain_openai import ChatOpenAI model = ChatOpenAI(model='deepseek-v4-flash') response = model.invoke('告诉我关于苹果公司的最新消息') print(response.content) #输出:你好!关于苹果公司的最新消息....这段文本对人类友好,对程序却是“灾难”。要提取“公司名”和“股价变化”,只能写复杂的解析逻辑。
with_structured_output允许我们预先定义数据结构,并强制模型按此结构返回。使用起来分三步:
- 定义输出结构(Pydantic、TypedDict 或 JSON Schema)
- 绑定结构到模型,生成新的 Runnable
- 调用该 Runnable,获得结构化结果
with_structured_output
with_structured_output是一个包装器(Wrapper),接收一个 Pydantic 模型(或 JSON Schema),重构模型的 invoke / ainvoke 方法,强制 LLM 的输出必须符合该 Schema,并直接返回解析好的 Python 对象。
model.with_structured_output( schema=Data, # 必填:Pydantic 类或 JSON Schema 字典 include_raw=False, # 关键!下文详解 )返回⼀个类似model的 Runnable。不同之处在于执⾏ Runnable 后的输出结果,输出的不是字符串或消息 ,⽽是输出与给定输出结构相对应的对象
include_raw默认 False,直接返回 Pydantic 对象(如 Data 实例)。如果你设为 True,返回的是一个字典,包含三个键:
- raw:LLM 返回的原始 AIMessage 对象(包含 Token 消耗、响应头等元数据)
- parsed:解析成功的 Pydantic 对象(如果验证通过)
- parsing_error:如果 Pydantic 验证失败,这里会存放异常信息(None 表示成功)
# 调试神器:既拿数据,又看 Token 消耗 structured_model = model.with_structured_output(Data, include_raw=True) result = structured_model.invoke("讲个笑话") print(result['parsed']) # 正常业务数据 print(result['raw'].response_metadata['token_usage']) # 计费详情2.结构化输出方式
2.1. 返回 Pydantic 对象(推荐)
我们可以设置执⾏ Runnable 后的输出结果指定为 Pydantic 类,这将返回⼀个 Pydantic 对象。
当收到模型的响应后,LangChain 会提取出代表 Pydantic 参数的 JSON 对象,并⽤ Pydantic 模型
对其进⾏解析和验证,将这个验证后的 JSON 转换为⼀个可⽤的 Pydantic 对象实例返回。如
from langchain_openai import ChatOpenAI from pydantic import BaseModel, Field from typing import Optional model = ChatOpenAI(model="gpt-4o-mini") class Joke(BaseModel): """给用户讲一个笑话。""" setup: str = Field(description="这个笑话的开头") punchline: str = Field(description="这个笑话的妙语") rating: Optional[int] = Field(default=None, description="从1到10分给这个笑话评分") structured_model = model.with_structured_output(Joke) result = structured_model.invoke("给我讲一个关于唱歌的笑话") print(result) # 输出: setup='为什么歌手总是喜欢在洗手间里唱歌?' punchline='因为那里有很好的回音和灵感!' rating=7BaseModel 是 Pydantic 库的基类。继承它意味着 Joke 变成了数据模型类,而不仅仅是普通的 Python 类。Pydantic 在运行时会对类属性进行类型校验(Type Validation)。如果 LLM 返回的 rating 是字符串 "ten",Pydantic 会抛出验证错误(或尝试转换,取决于配置)
返回的 result 是一个 Joke 实例,可直接访问属性。Pydantic 还支持嵌套结构:
class Data(BaseModel): jokes: List[Joke] structured_model = model.with_structured_output(Data) result = structured_model.invoke("分别讲一个关于唱歌和跳舞的笑话") # result.jokes 是一个列表Field 的本质:它是一个“元数据工厂函数”
Field是一个普通函数。它返回一个 FieldInfo 对象。当你写 setup: str = Field(description='开头') 时,Pydantic 并不会立刻把它赋值给 setup,而是把这个 FieldInfo 对象存储在类的内部属性 __fields__(或 model_fields)中。这个对象包含了该字段所有的校验规则和描述
Field 参数详解
(1) 校验约束
如果大模型(或用户)传入了非法数据,Pydantic 会直接抛出ValidationError,避免脏数据进入业务逻辑
| 参数名 | 作用 | 你的场景示例 |
|---|---|---|
gt/ge | 大于(>) / 大于等于(>=) | rating: int = Field(ge=1, le=10)(强制1-10分) |
lt/le | 小于(<) / 小于等于(<=) | 同上 |
min_length/max_length | 字符串/列表的最小/最大长度 | setup: str = Field(min_length=5)(笑话开头至少5个字) |
pattern | 正则表达式匹配 | code: str = Field(pattern=r'^A\d{3}$') |
multiple_of | 数值的倍数 | score: int = Field(multiple_of=2) |
(2) 文档与描述(给 AI 和开发者看,直接影响 LLM 的输出质量)
| 参数名 | 作用 |
|---|---|
description | 最核心。生成 JSON Schema 时填入description字段,OpenAI 会把它当做人设提示(System Prompt)来理解字段含义。 |
title | 字段的显示名称,若未填写则默认使用变量名(如setup)。 |
examples | 极具价值。给大模型提供“满分示例”。LLM 看到例子后,会模仿格式返回。 |
deprecated | 布尔值。标记该字段是否已弃用,会在 Schema 中生成提示,告诉 AI 尽量别用。 |
(3) 行为控制(默认值与别名)
| 参数名 | 作用 |
|---|---|
default | 设定固定默认值(如rating: int = Field(default=5))。若 LLM 没给,就用 5。 |
default_factory | 传入一个无参函数,用于生成动态默认值(如datetime.now)。注意:default和default_factory只能二选一。 |
alias | 设置字段的“别名”。例如前端传参叫"妙语",但 Python 变量想叫punchline,用alias="妙语"做映射。 |
frozen | 布尔值。设为True后,该字段在实例化后禁止修改,相当于只读属性。 |
2.2. 返回 TypedDict
TypedDict⽤于为字典对象提供精确的、结构化的类型提⽰。它允许我们指定⼀个字典中应该有哪些键,以及每个键对应的值的类型。定义方式与 Pydantic 类似,但返回的是普通字典,并捕捉键名拼写错误与类型错误
from typing import Optional from typing_extensions import Annotated, TypedDict class Joke(TypedDict): setup: Annotated[str, ..., "这个笑话的开头"] punchline: Annotated[str, ..., "这个笑话的妙语"] rating: Annotated[Optional[int], None, "从1到10分给这个笑话评分"] structured_model = model.with_structured_output(Joke) result = structured_model.invoke("给我讲一个关于唱歌的笑话") print(result) # {'setup': '...', 'punchline': '...', 'rating': 7}若希望获取原始响应(包含 token 用量、finish_reason 等元数据),可开启 include_raw=True:
structured_model = model.with_structured_output(Joke, include_raw=True) result = structured_model.invoke("给我讲一个关于唱歌的笑话") # result 是一个字典,包含 'raw'(原始消息)、'parsed'(解析后的对象)和 'parsing_error'Annotated
| 知识点 | 一句话总结 |
|---|---|
Annotated是什么 | 类型注解的“附加信息容器”,解释器忽略,第三方库读取。 |
| 为什么用 | 解决类型检查器误报、实现校验规则复用、叠加多个校验器。 |
| Pydantic 中怎么用 | Annotated[类型, Field(...)]代替直接Field。 |
| LangChain 场景 | 必须用Field(description=...)包裹,不能只用字符串。 |
| 核心记忆 | Annotated不改变类型,只附加元数据;必填性由default决定,与Optional无关。 |
它的语法是 Annotated[类型, 元数据1, 元数据2, ...]
- 第一个参数(类型):告诉类型检查器(如 Pyright、Mypy)这个变量是什么类型(如 str、int)
- 后续参数(元数据):可以放任意对象(如 Field(...)、校验器、或者你自己写的类),仅供库(如 Pydantic)在运行时读取,Python 解释器本身会完全忽略它们
关键区别:
# 旧写法(直接绑定 Field) setup: str = Field(description="开头") # 新写法(用 Annotated 包裹 Field) setup: Annotated[str, Field(description="开头")]直接绑定 Field 会“污染”类属性(赋值),而 Annotated 把元数据剥离到类型注解区域,让类型检查器和数据校验器各司其职,互不干扰
2.3. 返回 JSON Schema
当不想引入 Pydantic 或 TypedDict 时,可以直接提供 JSON Schema 字典:
json_schema = { "title": "joke", "type": "object", "properties": { "setup": {"type": "string", "description": "笑话开头"}, "punchline": {"type": "string", "description": "笑话妙语"}, "rating": {"type": "integer", "description": "评分", "default": None} }, "required": ["setup", "punchline"] } structured_model = model.with_structured_output(json_schema) result = structured_model.invoke("给我讲一个关于唱歌的笑话") # 返回字典2.4. 选择输出
有时,模型需要根据用户输入返回不同类型的响应(比如询问笑话则返回 Joke,普通问候则返回 ConversationalResponse)。可以使用 Union 类型:
from pydantic import BaseModel, Field from typing import Union class Joke(BaseModel): setup: str punchline: str rating: Optional[int] = None class ConversationalResponse(BaseModel): response: str = Field(description="对用户查询的会话响应") class FinalResponse(BaseModel): final_output: Union[Joke, ConversationalResponse] structured_model = model.with_structured_output(FinalResponse) result1 = structured_model.invoke("给我讲一个笑话") # result1.final_output 是 Joke 实例 result2 = structured_model.invoke("你好") # result2.final_output 是 ConversationalResponse 实例3.示例
3.1. 信息提取器
结构化输出最常见的应用是信息提取。比如从一段描述中提取人物属性:
class Person(BaseModel): name: Optional[str] = Field(default=None, description="名字") hair_color: Optional[str] = Field(default=None, description="头发颜色") height_in_meters: Optional[str] = Field(default=None, description="身高(米)") structured_model = model.with_structured_output(Person) messages = [ SystemMessage("你是提取信息专家,不知道的属性返回 null"), HumanMessage("史密斯身高6英尺,金发。") ] result = structured_model.invoke(messages) print(result) # name='史密斯' hair_color='金发' height_in_meters='1.83'关键点:
- 所有字段设为 Optional,让模型在不确定时返回 None,避免强行编造。
- 为每个字段提供详细的 description,这对提取准确率影响很大。
3.2. 与工具(Tools)结合
(分两步走):
- 第一轮:绑定工具,让模型决定是否调用,并手动执行工具,将结果添加到消息历史
- 第二轮:将包含工具结果的消息列表传给带有结构化输出的模型,得到最终结构化结果
# 1. 绑定工具,获得工具调用请求 model_with_search = model.bind_tools([web_search]) messages = [HumanMessage("搜索西安天气")] ai_msg = model_with_search.invoke(messages) messages.append(ai_msg) # 2. 手动执行工具 for tool_call in ai_msg.tool_calls: tool_msg = web_search.invoke(tool_call) messages.append(tool_msg) # 3. 现在用结构化输出模型处理完整消息历史 structured_model = model.with_structured_output(SearchResult) result = structured_model.invoke(messages)